@cotal-ai/pi 0.94.7 → 0.95.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 +11 -4
- package/dist/standalone.js +11 -4
- package/package.json +3 -3
package/dist/standalone.js
CHANGED
|
@@ -65196,10 +65196,10 @@ function date4(params) {
|
|
|
65196
65196
|
config(en_default());
|
|
65197
65197
|
|
|
65198
65198
|
// ../connector-core/dist/docs-bundle.generated.js
|
|
65199
|
-
var DOCS_VERSION = "0.
|
|
65199
|
+
var DOCS_VERSION = "0.95.0";
|
|
65200
65200
|
function loadDocsBundle() {
|
|
65201
65201
|
return {
|
|
65202
|
-
"version": "0.
|
|
65202
|
+
"version": "0.95.0",
|
|
65203
65203
|
"generatedFrom": "docs/*.md + SPEC.md + spec/cotal-lang.md + spec/cotal.schema.json",
|
|
65204
65204
|
"pages": [
|
|
65205
65205
|
{
|
|
@@ -65270,14 +65270,14 @@ function loadDocsBundle() {
|
|
|
65270
65270
|
"title": "`cotal` CLI reference",
|
|
65271
65271
|
"kind": "Reference: describes the TypeScript reference implementation (the `cotal` CLI), not the wire contract.",
|
|
65272
65272
|
"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.",
|
|
65273
|
-
"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 whether it can connect to 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 | [`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| `--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 system-account seed is\nnever persisted, so they are renewed by issuing a **new system account** under the same broker\noperator and minting fresh creds against it. 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## 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]\n```\n\nThis release serves a local endpoint for status, sign-in and a connection check only: it does not\nregister with a space, start a Manager, connect to a mesh or Cloud, or install a system service. It\nsupports 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## 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## 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"
|
|
65273
|
+
"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 whether it can connect to 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| `--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 system-account seed is\nnever persisted, so they are renewed by issuing a **new system account** under the same broker\noperator and minting fresh creds against it. 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## 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]\n```\n\nThis release serves a local endpoint for status, sign-in and a connection check only: it does not\nregister with a space, start a Manager, connect to a mesh or Cloud, or install a system service. It\nsupports 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## 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"
|
|
65274
65274
|
},
|
|
65275
65275
|
{
|
|
65276
65276
|
"slug": "config",
|
|
65277
65277
|
"title": "Configuration",
|
|
65278
65278
|
"kind": "Reference: describes the TypeScript reference implementation (the `cotal` CLI and connectors), not the wire contract.",
|
|
65279
65279
|
"summary": "Four things configure a Cotal workstation: the config file (per-connector settings, notably which of your MCP servers get shared with spawned agents), your preferences (the defaults your own comman\u2026",
|
|
65280
|
-
"body": '# Configuration\n\n> **Reference**: describes the TypeScript reference implementation (the `cotal` CLI and connectors), not the wire contract. \xB7 **For:** operators \xB7 **Wire contract:** [SPEC](../SPEC.md)\n\nFour things configure a Cotal workstation: the **config file** (per-connector settings, notably\nwhich of your MCP servers get shared with spawned agents), your **preferences** (the defaults your\nown commands use, set with `cotal config`), a set of **`COTAL_*` environment variables**, and the\n**on-disk layout** under a project\'s `.cotal/` and your machine\'s `~/.cotal`. None of these are part\nof the wire contract; they configure the reference implementation only.\n\n## The config file\n\nThe cotal config file carries per-connector launch settings. It is layered from two locations,\nmost-specific-wins:\n\n| Layer | Path | Scope |\n|---|---|---|\n| Base | `$XDG_CONFIG_HOME/cotal/config.json` (else `~/.config/cotal/config.json`; `%APPDATA%\\Cotal\\config.json` on Windows) | Operator-level, every space |\n| Override | `<project-root>/.cotal/config.json` | Space-local |\n\nThey merge per connector and per server name: a server in the space-local file replaces the\nsame-named server in the operator-level file; connectors or servers present in only one side are\nkept. A missing file is empty (valid); malformed JSON, a non-object top level, or a shared server\nthat cannot launch as written is a loud error.\n\nIt carries three things: which of your personal MCP servers a connector should **share** with the\nagents it spawns, optional `spawn.env` names that deliberately add environment capability to a\nspawned agent (see [Environment variables](#environment-variables) below), and an optional\n`modelPolicy` that limits which models a role may launch on (see [Model policy](#model-policy)).\n\nClaude Code loads its native user, local, project, plugin and connected-account MCP sources by\ndefault, alongside Cotal and this file\'s declared sharing overrides. Claude retains its approval,\ndisabled-server and managed-policy rules. An empty Cotal sharing map does not disable native\nsources. Use `--share-tools none` for Cotal alone, or name declared servers to restrict one spawn.\nCarried sessions retain their private home\'s isolated MCP configuration.\n\nOn its first run, `cotal setup` with Claude as the application still seeds eligible user-scope declarations into this file and\nkeeps an existing list. Native discovery does not depend on that snapshot. Every enabled server\nmay start a process per seat, so use an explicit sharing selection for a lighter seat.\n\n```json\n{\n "connectors": {\n "claude": {\n "mcpServers": {\n "github": {\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-github"],\n "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }\n }\n }\n }\n }\n}\n```\n\nEach server is written in the de-facto `.mcp.json` shape, so you can copy an entry straight out of\nyour own Claude / VS Code / Cursor config. Secrets ride as **`${VAR}` references** (also\n`${VAR:-default}`), resolved from your environment at launch and forwarded to the child **by name**\n(never as literals) so the file stays safe to keep in `~/.config` or a gitignored `.cotal/`. Only\n`command`, `args`, `env`, `url`, and `headers` are expanded; any other key passes through verbatim.\n\nA server that cannot launch as written is refused when the file is read, with its path in the file\n(`connectors.<name>.mcpServers.<server>.<field>`). `command`, `type` and `url` must be strings, `args`\na list of strings, and `env` and `headers` objects of strings. A server must also name a transport to\nstart: a non-empty `command` when `type` is absent or `stdio`, or a non-empty `url` when it is `http`,\n`sse` or `ws`. Any other `type` is refused. Every spawn reads the whole file, so one such entry\nrefuses every spawn until it is fixed, `--share-tools none` included.\n\n**`--share-tools` interplay**. The per-spawn selection narrows what this config declares:\n\n| `--share-tools` | Result |\n|---|---|\n| (flag absent) | Every server declared for the connector |\n| `none` or empty | Nothing |\n| `a,b` | Only those named: each **must** be declared, or the spawn fails (no silent drop) |\n\nA `supervise --roster` entry selects the same way with a `share-tools:` list: an absent key\nshares every declared server, `[]` shares none, and `[a, b]` shares only those named. Each list\nentry is a server name as written, so `[none]` shares a server declared as `none`.\n\nToday only the `claude` connector consumes shared MCP servers; OpenCode inherits config through its\nown merge layer and Hermes has no MCP. See [Connect Claude Code](connect-claude.md) for the full\nsharing model.\n\n### Model policy\n\n`modelPolicy` names, per role, the models a seat in that role may launch on. Each key is a role.\nIts `models` list holds the allowed model ids, and an optional `variants` list holds the allowed\nvariants.\n\n```json\n{\n "modelPolicy": {\n "reviewer": { "models": ["vendor/model-B"], "variants": ["high"] }\n }\n}\n```\n\n`cotal spawn` and the manager behind `cotal spawn --detach` check it before anything is minted or\nlaunched. They judge the effective role (a `--role` override counts) and the effective model and\nvariant (`--model` and `--variant` win over the persona\'s fields, as always). The spawn is refused\nwhen the role has an entry and:\n\n- no model resolves at all, since the harness would then pick one and nothing would record which;\n- the model is not in `models`. Ids compare whole, so `vendor/model-B-fast` does not match\n `vendor/model-B`;\n- `variants` is set and the variant is absent or not in it;\n- the launch carries any launch option (`launchOptions:` in the persona or manifest, or `--opt`).\n The connector applies launch options unread, after the model and variant, and one can select\n another model (an OpenCode `model`, a Claude `--model`), so a role under the policy launches\n without them.\n\nThe refusal names the persona, whether the value came from its own field or from the flag, the\nvalue, and the allowed ids. Roles with no entry, and personas with no role, are not constrained.\nA seat launches on the model and variant that were checked, even if its persona file changes after\nthe check.\n\nA space-local entry for a role replaces the operator-level entry for that role, and a role named in\nonly one file keeps its entry. A policy that cannot be read as written (a `models` list that is\nempty or holds a non-string, or an unknown field) fails every spawn with an error naming the file.\nThe policy is read at each spawn, so an edit applies to the next spawn without a restart. A spawn\nreads the file once, so the seat launches with the shared servers and `spawn.env` names of the same\ncontents its policy check read. Seats that are already running are not re-checked, including a\nsupervised restart or a preserved seat resumed after maintenance.\n\n## Preferences\n\nPreferences are the defaults your own commands use when nothing more specific names a value. You set\nthem with [`cotal config`](cli.md#config), for yourself or for one space, and they are saved in\n`preferences.json` in the configuration directory (see [Configuration files](#configuration-files)).\n`cotal setup` saves the application it sets up as the space\'s `harness`, and as yours when you have\nnone, so a space you prepare later starts with it.\n\n| Key | Values | Per space | Environment | Default | Read by |\n|---|---|---|---|---|---|\n| `harness` | a connector name, such as `claude` or `opencode` | yes | `COTAL_DEFAULT_AGENT` | `claude` | `spawn` |\n| `persona` | a catalog name or a path | yes | `COTAL_DEFAULT_PERSONA` | `default` | `spawn` |\n| `detach-key` | `ctrl-<char>` or `^<char>` | no | `COTAL_DETACH_KEY` | `ctrl-]` | `attach` |\n| `color` | `auto`, `always` or `never` | no | `FORCE_COLOR`, then `NO_COLOR` | `auto` | every command |\n| `editor` | an editor command, such as `code --wait` | no | `VISUAL`, then `EDITOR` | none | `personas edit` |\n\nOne resolver decides each key, in this order: an explicit flag, the persona\'s pin, the environment\nvariable, your value for the space the command acts on, your own value, then the default. For a\ndetached `spawn` the manager\'s own `COTAL_DEFAULT_AGENT` sits between your values and the product\ndefault. `--no-color` and `--plain` beat the `color` key. `FORCE_COLOR` turns color on, or off when it\nis `0` or `false`, and a non-empty `NO_COLOR` turns it off; both beat a saved value. `cotal config`\nshows the value that wins and every other value it outranks, so a shell export that hides a saved\nvalue is visible.\n\n`harness` is checked against the official connectors and the ones installed extensions provide. A\nvalue is checked when it is written, and an unknown key or an invalid value is refused with the valid\nones. Preferences hold no credentials. They change future commands, not agents already running.\n\nThe file is private to its owner and versioned:\n\n```json\n{ "schemaVersion": 1, "revision": 3, "defaults": { "harness": "opencode" }, "spaces": { "demo": { "persona": "reviewer" } } }\n```\n\n`revision` counts the writes. A write holds a lock, refuses with `CONFLICT` if the file changed after\nthe command read it, and replaces the file atomically, so two writers never lose an edit. A file that\ndoes not validate (not JSON, a newer `schemaVersion`, an unknown key or an invalid value) stops every\ncommand except help and shell completion, with an error that names the file and the problem.\nNothing resets it: fix or remove the file.\n\n## Environment variables\n\nThese are the operator-facing variables. Most of the connector-session ones (space, name, role, \u2026)\nare set **for you** by `cotal spawn` / the manager when they launch an agent; you set them by hand\nonly when you drive a connector session yourself (e.g. your own `claude` with the plugin) or a custom\nlauncher. Comma-separated lists are trimmed. The connector on/off flags (`COTAL_CHANNEL`,\n`COTAL_EVENTS`, `COTAL_EVENTS_REQUIRED`, `COTAL_TLS`, `COTAL_CODEX_TUI`, `COTAL_JCODE_TUI`) take\n`1`, `true`, `yes` or `on` and `0`, `false`, `no` or `off` in any case. The connector refuses any\nother value when it starts, naming the variable.\n\n| Variable | Consumed by | Meaning | Default |\n|---|---|---|---|\n| `COTAL_SPACE` | connector session | Space to join | `demo` (or the join link\'s) |\n| `COTAL_NAME` | connector session | Presence name / identity | required (or via `COTAL_AGENT_FILE` / `COTAL_LINK`) |\n| `COTAL_ROLE` | connector session | Role | agent file\'s `role:`, else none |\n| `COTAL_SERVERS` | connector session | Broker URL(s). Hand-driven sessions only: a launcher-spawned seat gets this in its launch material instead (see below) | the default local broker (or the link\'s) |\n| `COTAL_CREDS` | connector session | Path to a NATS creds file (auth mode). Hand-driven sessions only, same as above | none (open mode) |\n| `COTAL_LINK` | connector session | `cotal://token@host/space` join link: supplies server, auth, space | none |\n| `COTAL_AGENT_FILE` | connector session | Path to a persona file: supplies name, role, kind, channels | none |\n| `COTAL_SUBSCRIBE` | connector session | Active channel read set. Set but empty means no channels | agent file / link, else no channels |\n| `COTAL_ALLOW_SUBSCRIBE` | connector session | Read ACL (channels the agent *may* read). Set but empty means the read set alone | agent file\'s, else = `COTAL_SUBSCRIBE` |\n| `COTAL_ALLOW_PUBLISH` | connector session | Post ACL (channels the agent *may* post to). Set but empty means deny | agent file\'s, else deny (empty) |\n| `COTAL_MODEL` | connector session | Model label (display metadata), set by the launcher | none |\n| `COTAL_KIND` | connector session | Endpoint kind | `agent` |\n| `COTAL_TLS` | connector session | Connect over TLS (`1`); off keeps the join link\'s choice | off (or the link\'s) |\n| `COTAL_TOKEN` | connector session | Auth token (token / open modes) | none |\n| `COTAL_CAPABILITIES` | connector session | Control-plane capabilities (e.g. `spawn`) that gate manager tools | agent file\'s `capabilities:` |\n| `COTAL_QUIET` / `COTAL_MUTED` | connector session | Per-channel attention defaults (never-wake / drop-on-receive) | agent file\'s, else none |\n| `COTAL_CHANNEL` | Claude connector | Force channel wake-nudges on (`1`) / off; set to `1` by the Claude launcher | on when the MCP client declares the `claude/channel` capability in `initialize` |\n| `COTAL_EVENTS` | connector session | Arm this session\'s event plane (`1`); set by the launcher unless the launch used `--no-events` | launcher-managed |\n| `COTAL_EVENTS_REQUIRED` | hand-driven user-mode connector | Trusted registration says events are mandatory; arms the plane and refuses if the session grant omits its event channel. Launcher-managed sessions carry this in launch material instead | off |\n| `COTAL_DEFAULT_AGENT` | `cotal spawn` | Default connector type for a bare spawn (below an explicit `--agent` and the persona\'s `agent:` pin, above a saved [`harness` preference](#preferences)) | `claude` |\n| `COTAL_DEFAULT_PERSONA` | `cotal spawn` | Default persona for a bare spawn (above a saved [`persona` preference](#preferences)) | `default` |\n| `COTAL_SKIP_CONNECTOR_SEED` | boot gate | Skip the automatic built-in-connector seed/refresh on a command (`1`); `cotal ext seed` still works. `agent-bearer` skips the gate by name, no flag needed | off |\n| `COTAL_ALLOW_CHECKOUT_SEED` | seed store | Permit a source-checkout CLI to write the operator-global seed store (`1`) after isolating `$XDG_CONFIG_HOME`. Used by in-tree seed smokes that spawn the checkout-shaped `bin/` CLI into a scratch config. Any other value is ignored. The checkout refusal does not name this variable. | off |\n| `COTAL_DETACH_KEY` | `cotal attach` | Detach escape key (ctrl-<char> / ^<char>), matched as the control byte or its kitty / modifyOtherKeys encoding (above a saved [`detach-key` preference](#preferences)) | `ctrl-]` |\n| `COTAL_FEEDBACK_KEY` | `feedback`, connector | Beta feedback key \u2192 keyed intake | none (public intake) |\n| `COTAL_FEEDBACK_EMAIL` | `feedback`, connector | Contact email for the keyless public intake | your git email |\n| `COTAL_FEEDBACK_URL` | `feedback`, connector | Intake URL override (self-hosted) | keyed / public intake |\n| `COTAL_SKIP_ASSIST` | `setup` | Disable the connector debug handoff on a failed step (`1`; for CI) | off |\n| `COTAL_COMPLETE_DEBUG` | `completion` | Print completion-resolution errors to stderr | off |\n| `COTAL_DEBUG` | any `cotal` command | Print the stack trace under a command\'s one-line error | off |\n| `COTAL_CODEX_BIN` / `COTAL_OPENCODE_BIN` / `COTAL_ORCA_BIN` | Codex and OpenCode connectors, Orca runtime | Pin the `codex`, `opencode` or Orca CLI binary | found on `PATH` |\n| `COTAL_IDP_TIMEOUT_MS` | `cotal login`, IdP requests | Per-request IdP timeout in milliseconds, for a slow IdP; a malformed value fails loud | `30000` |\n| `COTAL_ENROLLMENT_FILE` | foreground `spawn` | Private `0600` file containing one remote enrollment URL; preferred over the environment form | none |\n| `COTAL_MANAGED_HANDOFF_FILE` | `cotal` entry, foreground `spawn` | Private `0600` file holding one managed lifecycle handoff from a delegating runtime; taken and deleted before anything else runs | none |\n| `COTAL_ENROLLMENT_URL` | foreground `spawn` | One remote enrollment URL when a secret file cannot be mounted; conflicts with `COTAL_ENROLLMENT_FILE` | none |\n| `COTAL_SERVE_HEADLESS` | OpenCode runtime | Run the OpenCode server without a foreground TUI (`1`). Stdout gets one `[cotal-serve]` line with the server\'s port and session and no password; a host that drives the server passes its own `OPENCODE_SERVER_PASSWORD` to the launcher | off |\n| `COTAL_HOME` | workspace | Override the machine-home dir for the **mesh registry only** (`meshes/`, `current-mesh`, onboard marker). Does **not** redirect project-root paths (`findCotalRoot` / `.cotal/broker-policy.json`, NATS store, manager/delivery state, auth). Tests that run `cotal up` must also use a temp project root with its own `.cotal/` as `cwd` | `~/.cotal` |\n\n> `--console-port` is a `cotal supervise` flag, not an environment variable; there is no\n> `COTAL_CONSOLE_PORT`.\n\n### Launcher variables\n\nThese are wired into a spawned child\'s environment by the connector / launcher and read back inside\nthe session. They are not operator knobs; listed so you recognize them in a process listing.\n\n| Variable | Purpose |\n|---|---|\n| `COTAL_ID` | Stable agent id chosen by the launcher (static meshes) |\n| `COTAL_MANAGER_INSTANCE` | Stable instance id of the launching manager. User-auth managed calls request a separate control view for this instance: the seat\'s tools always, and `cotal` commands in its shell unless `--on` names another. The issuer authorizes the selection. It carries no credential or grant. An unbound session uses the issuer\'s unique authorized selection |\n| `COTAL_ENVIRONMENT` | Opaque provider-issued environment reference published in presence. Read once when the endpoint is constructed; omitted when the launcher sets none |\n| `COTAL_LIFECYCLE_UID` | The incarnation\'s lifecycle UID, minted once per spawn; the session binds its lifecycle-keyed DM/delivery/history consumers by it (its credential pins the same names). Required for an authed launch (`COTAL_CREDS` or user-mode); config parsing fails loud without it. Open mode omits it (the endpoint self-mints per session) |\n| `COTAL_BACKFILL_FLOOR` | The CHAT stream sequence a resumed seat\'s prior incarnation had reached before its preservation cut; the boot backfill reads only what came after it. Set by the manager on a preserved resume, absent on a fresh spawn. Must parse as a non-negative integer; a broken launcher\'s malformed value fails loud rather than silently falling back to a full replay |\n| `COTAL_OWNER` / `COTAL_ACTOR` / `COTAL_SENTINEL_CREDS` / `COTAL_BEARER_CMD` | User-auth launch identity: the agent\'s principal, its sentinel creds path, and the exec-able bearer command; all four together, mutually exclusive with `COTAL_CREDS`. A launcher-spawned seat carries them in its launch material instead of its environment. A remote enrollment\'s bearer argv uses `agent-bearer --exchange-url <https://base>`; the token never falls back to a local service file |\n| `COTAL_LAUNCH_MATERIAL` | Path to this launch\'s private 0600 material file (see [Launch material](#launch-material) below). Carries the broker URL, the creds path, the auth token, the user-auth identity, the required-events flag, and the control token. A PATH, never a secret |\n| `COTAL_CONTROL_SOCKET` | The session\'s local control endpoint path. The MCP server listens on it and the lifecycle hooks connect to it. A launcher-spawned session carries the token that authenticates the first frame in its launch material. A hand-driven Claude Code or jcode session sets this path and `COTAL_CONTROL_TOKEN` itself (see [Launch material](#launch-material)) |\n| `COTAL_BRIDGE_SOCKET` / `COTAL_TOOLS_FILE` / `COTAL_PARENT_PID` | Hermes sidecar plumbing (bridge socket, generated tool descriptors, launcher pid to watch). The bridge socket\'s first frame carries the control token from the launch material (or `COTAL_CONTROL_TOKEN` in standalone mode) |\n| `OPENCODE_CONFIG_CONTENT` | Inline OpenCode config (the injected cotal plugin, highest merge layer) |\n| `OPENCODE_DB` / `COTAL_OPENCODE_*` | OpenCode server plumbing (the per-agent DB, and the connector\'s home, port and server URL) |\n\nA spawned agent receives a fixed OS execution allow-list (PATH, HOME, TERM, locale, and\nXDG/Windows config directories), the machine-wide `COTAL_*` operator knobs (`COTAL_HOME`, the\nfeedback set, the default-agent pair, the `*_BIN` overrides, the timing knobs), the provider inputs\nits connector declares, and `${VAR}` names an explicitly shared MCP server requires. It does not\ninherit the manager\'s ambient environment. This keeps host-session markers such as\n`CLAUDE_CODE_CHILD_SESSION` / `CLAUDECODE` (and the analogous names other hosts use to mark a nested\nsession), unrelated service secrets, and environment-only capabilities out of seats unless\ndeliberately supplied. A seat\'s transcript/resume behaviour is a property of the seat, never of how\nmany layers up someone once ran `cotal up` inside an agent. Connection material is not in the\nenvironment at all (see [identity & auth](identity-and-auth.md)).\n\nEnrollment inputs are launcher-only secrets. `spawn` removes both enrollment variable names from the\nconnector\'s child environment, and `spawn.env` cannot list them.\n\nPATH is forwarded whole, including entries such as `~/.local/bin` where connector binaries live, so\na seat can still launch after the strip. There is no inherit mode and no opt-in-to-containment flag:\nthe allow-list is the only path.\n\nTo deliberately add an environment name for a spawned agent, declare `spawn.env` in the config file:\n\n```json\n{ "spawn": { "env": ["MY_PROVIDER_API_KEY"] } }\n```\n\nThe listed names are added to the fixed boundary. That is also the opt-in for a host-session marker\na persona has chosen to receive (`CLAUDE_CODE_CHILD_SESSION` and friends). A `COTAL_` name other\nthan the machine-wide knobs above is refused and the seat is not launched: the launcher sets those\nnames for each seat, so a value from the spawning process would become the seat\'s role, model or\nread set. An empty array adds nothing. A space-local `spawn` block replaces the operator-level one\noutright rather than merging, so a local list stays local. No `spawn` block, `"spawn": { "env": [] }`, and `"spawn": {}`\nall add no names. A `spawn` that is not an object, or an `env` that is not a list of non-empty names,\nis refused when the file is read, with the file and the field named, so every spawn and resume is\nrefused until it is fixed.\n\nBe honest with yourself about what this buys: `HOME` is forwarded, so an agent with a shell reads\n`~/.aws`, `~/.ssh` and `~/.config` regardless. The boundary protects what a file on disk cannot hand\nover anyway, and that is more than a list of secret values. Some variables are **capability\nhandles**: they do not contain a secret, they name a live process that will act on your behalf.\n`SSH_AUTH_SOCK` is the sharp one. Inherit it and the agent can ask your `ssh-agent` to sign, which\nmeans it can reach any host or sign any commit that key authorises, and it keeps that power even\nif the private key file is not on disk at all. Nothing under `~/.ssh` has to exist for it to work,\nso "a shell reads `~/.ssh` regardless" does not cover this case. The same shape covers a\n`gpg-agent` socket and the desktop and cloud credential brokers. So the default boundary protects:\nsecrets that live **only** in the environment, such as an `aws-vault exec` or `op run` shell or\nCI-injected values, and the capability handles above, which it removes along with everything else\nit does not name. Real containment is still a sandbox or a VM.\n\nModel discovery is the exception, and it is deliberate rather than an oversight. When the `codex` or\n`opencode` connector enumerates a model catalog (`cotal models`, and the manager\'s selector), it runs\nthat harness with your environment minus Cotal\'s own `COTAL_*`, and it does **not** consult\n`spawn.env`. Those probes are short-lived catalog reads rather than agent seats, so an allow-list\nthat confines a seat does not confine them.\n\n### Launch material\n\nA process environment is inherited by every descendant. A seat launched with its credential, its\nbroker URL and its control token in the environment hands all three to the build it runs, the linter,\nthe third-party CLI, the test suite that reads its broker from the environment. Nothing in that chain\nasked for any of it.\n\nSo a launcher-spawned seat does not get them in its environment. The launcher writes them to a single\n**0600 file inside a 0700 private directory** and exports only its path, as `COTAL_LAUNCH_MATERIAL`.\nThe session reads the launch-material file once at startup. For a managed creds path, it reads the\ncredential once to pin the seat\'s nkey, then keeps the path as a renewal source. A re-signed file is\nread by renewal and by reconnect after the cached credential expires, and a file for a different\nnkey is refused. An unbounded credential has no renewal point and remains a static boot-time value.\nThis is the same shape\n`cotal agent-bearer` already uses for its spawn-time secret: the material rides a file, never argv\n(which is visible in a process listing) and never the ambient environment (which is inherited).\n\nThree connectors drop the path once they have read it, so the shells and tools those seats run\ninherit no reference at all: **pi** and **codex**, whose sessions run in the seat process, and\n**OpenCode**, whose seat process is a shim that starts `opencode serve` (the plugin runs in that\nserver, which is also what executes the session\'s tool calls). Those three also **delete the file**\nat the same moment, along with the private directory that held it. Nothing reads it again, so leaving\nit on disk would only extend how long a copy of the material exists. The directory is only removed\nwhen it is provably the one the launcher wrote: the right filename inside, the launcher\'s prefix on\nthe directory, the directory sitting directly in the OS temp root, and a non-recursive removal that\nfails rather than deletes if anything else is in there.\n\nTwo keep it, and for the same reason in both cases: a process that starts LATER has to read it.\n**Claude**\'s readers are short-lived children, the MCP server and one process per lifecycle hook,\nwhich begin after the session is already running. **Hermes**\' launcher starts a gateway child that\nneeds the control token. For those two, a shell the seat runs still inherits a path to the material\nfile, though not the material itself.\n\nThe material\'s private directory is also a launch artifact (see\n[Private launch files](authoring-a-connector.md#private-launch-files)), so the launcher removes it\nwhen the seat\'s process exits, the same way it removes a persona file. For Claude that is when the\nfile goes. Hermes\' launcher merges the file it was given into a second file of its own for the\ngateway child, and that second file is not a launch artifact. The launcher writes the file after\nevery check that can refuse the launch, so a refused launch leaves none behind. A file can still\noutlive its seat when the launcher and the child\'s watcher were both killed, or when a spawn failed\nafter the file was written but before any process started. The OS temp reaper removes those.\n\nWhat this does: the values are out of every descendant\'s environment, so an `env` dump, a CI log, a\nsuite that defaults its broker from the environment, or a tool handed a credential it never asked\nfor, all stop seeing them. What it does not do: hide the material from a process running as the same\nuser that deliberately opens the file. No environment-level control can, and the same is already true\nof `~/.cotal/auth/creds`. What changes is that reaching the material is a deliberate act rather than\nan inheritance nobody chose.\n\nDriving a connector session **by hand** still works the documented way: set `COTAL_CREDS` /\n`COTAL_SERVERS` (and the user-auth quartet) yourself, and no material file is involved. Setting both\na material file and any of them is refused rather than resolved by precedence: one launch carries one\nidentity plane. `COTAL_LINK` counts as one of them, because a join link carries the server, the auth\nand the space in a single string.\n\nA Claude Code or jcode session also serves a local control endpoint and refuses to start without\none, so a hand-driven one sets `COTAL_CONTROL_SOCKET` to a socket path you choose and\n`COTAL_CONTROL_TOKEN` to a random secret. OpenCode, Codex and pi sessions start without them.\n\n`eventsRequired` is an additive boolean in launch material. The launcher derives it from the selected\nuser-auth registration. A Claude, OpenCode, Codex, Jcode or Pi launch that carries it also sets\n`COTAL_EVENTS` and the workspace root, even when the launcher passed `events: false`. Connector config\nexposes it and the Claude, OpenCode 1.x, Codex, Jcode and Pi startup gates arm on it even when\n`COTAL_EVENTS` is absent. A direct env launch may use `COTAL_EVENTS_REQUIRED=1` only with the complete\nuser-auth quartet. The session refuses if its post ACL does not cover its own `events.<owner>.<actor>`\nchannel.\n\nThe control endpoint is a pair, and **half a pair is refused**. A launch with a control socket path\nand no resolvable token, or a token and no socket path, does not fall back to running without a\ncontrol plane: it fails with a sentence naming which half is missing. The one exception is the\nlifecycle hook relay, which catches that refusal, writes a single warning to stderr naming no values,\nand then does nothing, because a hook that throws is a hook that blocked the session. Failing open is\ndeliberate; failing open silently is not.\n\n## On-disk layout\n\n### Project files\n\nA project\'s state lives in `.cotal/` at the mesh root (found by walking up from the cwd, like `.git`).\n**It is gitignored**; it holds secrets and machine-local process state.\n\n| Path | What it is |\n|---|---|\n| `auth/broker.json` | Broker trust material: the operator seed and the system account (secret; the system-account signing seed is stripped before writing). One per broker, shared by every space on it |\n| `auth/account.<key>.json` | One space\'s own NATS data account and signing seed (secret). One file per space, all signed by the broker above; `<key>` is a stable, case-safe hex encoding of the space name (never the raw name, so two case-differing spaces can\'t collide) |\n| `auth/space.<key>/` | One space\'s user-auth state (IdP pin, issuer keys, owner secret, callout account, client sentinel credentials), present only when that space enables per-user auth. Keyed by the same case-safe hex encoding; pre-hex layouts (`auth/<space>/`) are renamed here on first touch |\n| `auth/creds/space.<key>/<name>.creds` | Per-agent minted NATS credentials, under the segment of the space they belong to - same case-safe hex encoding as the rows above. Pre-segment layouts (`auth/creds/<name>.creds`) are moved here on first touch. The `creds` directory itself stays shared, so a root\'s tenants keep their agent material in sibling segments rather than sibling roots |\n| `auth/seat-generation.<key>.<n>.json` | Writer generation `<n>` of one seat, claimed by exclusive create when `cotal up` resumes the seat from a checkpoint ([Backups](cli.md#backups)). `<key>` is the space\'s case-safe hex key, `00`, then the seat name in the same encoding, so no two seats share a key. A space or seat name the encoding refuses, or one containing NUL, claims nothing |\n| `auth/server.conf` | Generated nats-server config for the broker (`# Generated by \\`cotal up\\` - do not edit by hand.`). Path is `<projectRoot>/.cotal/auth/server.conf`, not `~/.cotal` unless that is the mesh root. Default bind is loopback (`host: 127.0.0.1`); `--host` on a **stopped** `cotal up` regenerates it. A live refresh does not rewrite this file. The core renderer accepts every space on the broker; `cotal up` currently orchestrates one space per root, so it renders that one space\'s account |\n| `broker-policy.json` | Durable broker **launch** policy (TLS-required cert/key path references, or plaintext). Survives `cotal down` so a bare re-`up` cannot silently drop TLS. Under the project root: **not** under `COTAL_HOME` |\n| `agents/<name>.md` | Persona / agent files ([Agent files](agent-files.md)) |\n| `manifests/<runId>.json` | Additive ownership ledger written by the retired `spawn -f` deploy; `cotal down -f <file> --run <id>` tears one down |\n| `config.json` | Space-local connector config (the override layer above) |\n| `nats.pid` \xB7 `nats.log` | Background nats-server pid + log |\n| `manager.<key>.pid` \xB7 `manager.<key>.log` | Manager (supervisor) pid + log for one space; every line of the log starts with the UTC time it was written (ISO 8601), so a reap can be placed in time without another file; `manager.<key>.delivery-aware` marks a delivery-aware build and is written owner-only (0600) whatever the umask, never through a symlink and only into a file this user owns. `<key>` is the same case-safe hex space key as the rows above, so one root can run a manager per space. A pre-segmentation root-scoped `manager.pid` is still read while it is the only spelling present, and is removed as the new record is written. A start that finds it already removed, by its exiting owner or by a concurrent start, continues. Both spellings present is reported as ambiguous rather than guessed. A start that refuses over this record names the file it read, even if the record moves to the other spelling before the refusal prints. The manager writes the pid itself, whatever started it, and removes it on a clean stop only while it still names that process. A reader treats the record as a running manager only if the pid is alive **and** the process is a supervisor: a recycled pid belonging to something else is reported as a stale record, never signalled |\n| `delivery.<key>.pid` \xB7 `delivery.<key>.log` \xB7 `space.<key>/delivery.creds` | Delivery daemon pid and log for one space, and its scoped cred (auth mode). `cotal down` and the teardown of a foreground `cotal up` remove the cred once the daemon is confirmed stopped. Per-space and compatible with a pre-segmentation `delivery.pid` on the same terms as the manager row |\n| `web.pid` \xB7 `web.log` | Web dashboard pid + log |\n| `membership.json` \xB7 `membership-*.creds` | Membership feed state + its scoped creds |\n| `setup.log` | `cotal setup` log, one section appended per run. Each entry is one timestamped line: a control character or Unicode line separator in a path or error message is written as a `\\uXXXX` escape |\n\nA command that acts on the whole folder without being told a space reads one off these runtime\nrecords: `<key>` decodes back to the space name, and a space whose record is running wins over\nresidue from a stopped one. Two spaces running under one root is reported rather than arbitrated.\nThis is what lets `cotal status` and `cotal down` work in a folder whose mesh runs with\n`broker: { auth: false }`, where there is no `auth/account.<key>.json` to name the space.\nA record, or a `.cotal` listing, that exists but cannot be read is reported as that read error\nrather than read as absent. A process record\'s read error names the record\'s path.\n\n### Machine files\n\nCross-project machine state, so a `cotal spawn` from any directory can find a running mesh. Location:\n`~/.cotal` on POSIX, `%LOCALAPPDATA%\\Cotal` on Windows; overridable with `COTAL_HOME`.\n\n`COTAL_HOME` overrides **this tree only** (registry + current pointer + onboard marker). It is not a\nfull workstation sandbox. Broker launch policy, the JetStream store, pidfiles, and auth live under\nthe **project** `.cotal/` found by walking up from the cwd ([Project: `.cotal/`](#project-files)\nabove, including `broker-policy.json` on TLS meshes). A probe that sets `COTAL_HOME` alone and runs\n`cotal up --tls-cert \u2026` from a directory whose walked root is the operator home still writes those\nproject paths on the live machine.\n\n| Path | What it is |\n|---|---|\n| `meshes/space.<key>.json` | Registry of running meshes: one file per broker `cotal up` started (server URL, root path, mode, TLS-required client intent when recorded, attach bind host and live-session ceiling when the operator set them); `<key>` is the same case-safe hex encoding of the space name, and the record\'s own `space` field is authoritative |\n| `current-mesh` | Default space a bare `cotal spawn` joins (set by `cotal use`) |\n| `onboarded.json` | Marker (with `ONBOARD_VERSION`) that `cotal setup` writes once; setup shows its splash until then |\n| the Claude plugin marketplace | The installed `cotal-mesh` plugin assets |\n\n### Configuration files\n\nDistinct from `~/.cotal`. Location: `$XDG_CONFIG_HOME/cotal`, else `~/.config/cotal` on POSIX, or\n`%APPDATA%\\Cotal` on Windows.\n\n| Path | What it is |\n|---|---|\n| `config.json` | Operator-level connector config (the base layer above) |\n| `preferences.json` | Your [preferences](#preferences), private to you (`0600`), written by `cotal config` |\n| `extensions/` | `cotal ext` install prefix: its own npm root (`node_modules`) plus an `extensions.json` provider/command-display cache. Built-in connectors install here too, seeded on first run |\n| `seed/` | Built-in-connector seeding state: the `ever-seeded` authority (+ durable backup), the init witness, the version stamp, the crash cursor, and `store/<version>/<name>` (the stable payloads `ext add --install-links` reifies each seeded connector from) |\n\nBoth `extensions/` and `seed/store/` are operator-global: shared by every space, project directory, and\ncheckout on the machine, and moved only by `$XDG_CONFIG_HOME` (a fresh project dir isolates `.cotal/`,\nnot these). `COTAL_HOME` does not relocate them. A CLI running from a source checkout (`pnpm cotal`,\n`tsx bin/cotal.ts`, `node bin/cotal.ts`, or a suite child of those, identified by a `bin/` package\nroot next to `implementations/` or `pnpm-workspace.yaml`) refuses to write, stamp, or\ngarbage-collect that store: the refusal names the store path, the generation it declined, and\n`COTAL_SKIP_CONNECTOR_SEED=1` as the way to run other commands from a checkout. An entry that\ncannot be proven as a released `cotal-ai` install is refused the same way. Isolating\n`$XDG_CONFIG_HOME` (on Windows, `%APPDATA%`) keeps a checkout off the installed config but does not\nlift the refusal by itself. A\nreleased install or an `npx` unpack still seeds as before. The in-tree seed smokes that must seed\nfrom a checkout-shaped `bin/` set `COTAL_ALLOW_CHECKOUT_SEED=1` against an isolated config; an\nopt-in write still records that checkout path in `seed/stamp.json` as `writtenBy`. The reconcile\nnames on stderr both the store payloads it writes and any old generation it removes, so a\nmachine-wide re-seed or cleanup is visible when it happens. Those lines are provenance output. When a\nstderr write fails, at once or after waiting in a full pipe, the line is printed on stdout with the\nerror and the reconcile still completes. If stdout fails too, the reconcile still completes and the\nrun exits 1 instead of 0. A line still waiting in a full stderr pipe when the run exits, as when the\nCLI exits on a closed stdout, is lost and also makes the run exit 1. Node does not say which stderr\nbytes are still waiting, so a line that had to wait and got through just before the exit also makes\nthe run exit 1 when later stderr output is still waiting. A run whose stderr is closed or redirected\naway at launch keeps the write and loses the line.\n\nFor how `cotal setup` populates the machine state and the plugin, and how the built-in connectors are\nseeded as removable extensions, see [setup internals](setup-internals.md).\n'
|
|
65280
|
+
"body": '# Configuration\n\n> **Reference**: describes the TypeScript reference implementation (the `cotal` CLI and connectors), not the wire contract. \xB7 **For:** operators \xB7 **Wire contract:** [SPEC](../SPEC.md)\n\nFour things configure a Cotal workstation: the **config file** (per-connector settings, notably\nwhich of your MCP servers get shared with spawned agents), your **preferences** (the defaults your\nown commands use, set with `cotal config`), a set of **`COTAL_*` environment variables**, and the\n**on-disk layout** under a project\'s `.cotal/` and your machine\'s `~/.cotal`. None of these are part\nof the wire contract; they configure the reference implementation only.\n\n## The config file\n\nThe cotal config file carries per-connector launch settings. It is layered from two locations,\nmost-specific-wins:\n\n| Layer | Path | Scope |\n|---|---|---|\n| Base | `$XDG_CONFIG_HOME/cotal/config.json` (else `~/.config/cotal/config.json`; `%APPDATA%\\Cotal\\config.json` on Windows) | Operator-level, every space |\n| Override | `<project-root>/.cotal/config.json` | Space-local |\n\nThey merge per connector and per server name: a server in the space-local file replaces the\nsame-named server in the operator-level file; connectors or servers present in only one side are\nkept. A missing file is empty (valid); malformed JSON, a non-object top level, or a shared server\nthat cannot launch as written is a loud error.\n\nIt carries three things: which of your personal MCP servers a connector should **share** with the\nagents it spawns, optional `spawn.env` names that deliberately add environment capability to a\nspawned agent (see [Environment variables](#environment-variables) below), and an optional\n`modelPolicy` that limits which models a role may launch on (see [Model policy](#model-policy)).\n\nClaude Code loads its native user, local, project, plugin and connected-account MCP sources by\ndefault, alongside Cotal and this file\'s declared sharing overrides. Claude retains its approval,\ndisabled-server and managed-policy rules. An empty Cotal sharing map does not disable native\nsources. Use `--share-tools none` for Cotal alone, or name declared servers to restrict one spawn.\nCarried sessions retain their private home\'s isolated MCP configuration.\n\nOn its first run, `cotal setup` with Claude as the application still seeds eligible user-scope declarations into this file and\nkeeps an existing list. Native discovery does not depend on that snapshot. Every enabled server\nmay start a process per seat, so use an explicit sharing selection for a lighter seat.\n\n```json\n{\n "connectors": {\n "claude": {\n "mcpServers": {\n "github": {\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-github"],\n "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" }\n }\n }\n }\n }\n}\n```\n\nEach server is written in the de-facto `.mcp.json` shape, so you can copy an entry straight out of\nyour own Claude / VS Code / Cursor config. Secrets ride as **`${VAR}` references** (also\n`${VAR:-default}`), resolved from your environment at launch and forwarded to the child **by name**\n(never as literals) so the file stays safe to keep in `~/.config` or a gitignored `.cotal/`. Only\n`command`, `args`, `env`, `url`, and `headers` are expanded; any other key passes through verbatim.\n\nA server that cannot launch as written is refused when the file is read, with its path in the file\n(`connectors.<name>.mcpServers.<server>.<field>`). `command`, `type` and `url` must be strings, `args`\na list of strings, and `env` and `headers` objects of strings. A server must also name a transport to\nstart: a non-empty `command` when `type` is absent or `stdio`, or a non-empty `url` when it is `http`,\n`sse` or `ws`. Any other `type` is refused. Every spawn reads the whole file, so one such entry\nrefuses every spawn until it is fixed, `--share-tools none` included.\n\n**`--share-tools` interplay**. The per-spawn selection narrows what this config declares:\n\n| `--share-tools` | Result |\n|---|---|\n| (flag absent) | Every server declared for the connector |\n| `none` or empty | Nothing |\n| `a,b` | Only those named: each **must** be declared, or the spawn fails (no silent drop) |\n\nA `supervise --roster` entry selects the same way with a `share-tools:` list: an absent key\nshares every declared server, `[]` shares none, and `[a, b]` shares only those named. Each list\nentry is a server name as written, so `[none]` shares a server declared as `none`.\n\nToday only the `claude` connector consumes shared MCP servers; OpenCode inherits config through its\nown merge layer and Hermes has no MCP. See [Connect Claude Code](connect-claude.md) for the full\nsharing model.\n\n### Model policy\n\n`modelPolicy` names, per role, the models a seat in that role may launch on. Each key is a role.\nIts `models` list holds the allowed model ids, and an optional `variants` list holds the allowed\nvariants.\n\n```json\n{\n "modelPolicy": {\n "reviewer": { "models": ["vendor/model-B"], "variants": ["high"] }\n }\n}\n```\n\n`cotal spawn` and the manager behind `cotal spawn --detach` check it before anything is minted or\nlaunched. They judge the effective role (a `--role` override counts) and the effective model and\nvariant (`--model` and `--variant` win over the persona\'s fields, as always). The spawn is refused\nwhen the role has an entry and:\n\n- no model resolves at all, since the harness would then pick one and nothing would record which;\n- the model is not in `models`. Ids compare whole, so `vendor/model-B-fast` does not match\n `vendor/model-B`;\n- `variants` is set and the variant is absent or not in it;\n- the launch carries any launch option (`launchOptions:` in the persona or manifest, or `--opt`).\n The connector applies launch options unread, after the model and variant, and one can select\n another model (an OpenCode `model`, a Claude `--model`), so a role under the policy launches\n without them.\n\nThe refusal names the persona, whether the value came from its own field or from the flag, the\nvalue, and the allowed ids. Roles with no entry, and personas with no role, are not constrained.\nA seat launches on the model and variant that were checked, even if its persona file changes after\nthe check.\n\nA space-local entry for a role replaces the operator-level entry for that role, and a role named in\nonly one file keeps its entry. A policy that cannot be read as written (a `models` list that is\nempty or holds a non-string, or an unknown field) fails every spawn with an error naming the file.\nThe policy is read at each spawn, so an edit applies to the next spawn without a restart. A spawn\nreads the file once, so the seat launches with the shared servers and `spawn.env` names of the same\ncontents its policy check read. Seats that are already running are not re-checked, including a\nsupervised restart or a preserved seat resumed after maintenance.\n\n## Preferences\n\nPreferences are the defaults your own commands use when nothing more specific names a value. You set\nthem with [`cotal config`](cli.md#config), for yourself or for one space, and they are saved in\n`preferences.json` in the configuration directory (see [Configuration files](#configuration-files)).\n`cotal setup` saves the application it sets up as the space\'s `harness`, and as yours when you have\nnone, so a space you prepare later starts with it.\n\n| Key | Values | Per space | Environment | Default | Read by |\n|---|---|---|---|---|---|\n| `harness` | a connector name, such as `claude` or `opencode` | yes | `COTAL_DEFAULT_AGENT` | `claude` | `spawn` |\n| `persona` | a catalog name or a path | yes | `COTAL_DEFAULT_PERSONA` | `default` | `spawn` |\n| `detach-key` | `ctrl-<char>` or `^<char>` | no | `COTAL_DETACH_KEY` | `ctrl-]` | `attach` |\n| `color` | `auto`, `always` or `never` | no | `FORCE_COLOR`, then `NO_COLOR` | `auto` | every command |\n| `editor` | an editor command, such as `code --wait` | no | `VISUAL`, then `EDITOR` | none | `personas edit` |\n\nOne resolver decides each key, in this order: an explicit flag, the persona\'s pin, the environment\nvariable, your value for the space the command acts on, your own value, then the default. For a\ndetached `spawn` the manager\'s own `COTAL_DEFAULT_AGENT` sits between your values and the product\ndefault. `--no-color` and `--plain` beat the `color` key. `FORCE_COLOR` turns color on, or off when it\nis `0` or `false`, and a non-empty `NO_COLOR` turns it off; both beat a saved value. `cotal config`\nshows the value that wins and every other value it outranks, so a shell export that hides a saved\nvalue is visible.\n\n`harness` is checked against the official connectors and the ones installed extensions provide. A\nvalue is checked when it is written, and an unknown key or an invalid value is refused with the valid\nones. Preferences hold no credentials. They change future commands, not agents already running.\n\nThe file is private to its owner and versioned:\n\n```json\n{ "schemaVersion": 1, "revision": 3, "defaults": { "harness": "opencode" }, "spaces": { "demo": { "persona": "reviewer" } } }\n```\n\n`revision` counts the writes. A write holds a lock, refuses with `CONFLICT` if the file changed after\nthe command read it, and replaces the file atomically, so two writers never lose an edit. A file that\ndoes not validate (not JSON, a newer `schemaVersion`, an unknown key or an invalid value) stops every\ncommand except help and shell completion, with an error that names the file and the problem.\nNothing resets it: fix or remove the file.\n\n## Environment variables\n\nThese are the operator-facing variables. Most of the connector-session ones (space, name, role, \u2026)\nare set **for you** by `cotal spawn` / the manager when they launch an agent; you set them by hand\nonly when you drive a connector session yourself (e.g. your own `claude` with the plugin) or a custom\nlauncher. Comma-separated lists are trimmed. The connector on/off flags (`COTAL_CHANNEL`,\n`COTAL_EVENTS`, `COTAL_EVENTS_REQUIRED`, `COTAL_TLS`, `COTAL_CODEX_TUI`, `COTAL_JCODE_TUI`) take\n`1`, `true`, `yes` or `on` and `0`, `false`, `no` or `off` in any case. The connector refuses any\nother value when it starts, naming the variable.\n\n| Variable | Consumed by | Meaning | Default |\n|---|---|---|---|\n| `COTAL_SPACE` | connector session | Space to join | `demo` (or the join link\'s) |\n| `COTAL_NAME` | connector session | Presence name / identity | required (or via `COTAL_AGENT_FILE` / `COTAL_LINK`) |\n| `COTAL_ROLE` | connector session | Role | agent file\'s `role:`, else none |\n| `COTAL_SERVERS` | connector session | Broker URL(s). Hand-driven sessions only: a launcher-spawned seat gets this in its launch material instead (see below) | the default local broker (or the link\'s) |\n| `COTAL_CREDS` | connector session | Path to a NATS creds file (auth mode). Hand-driven sessions only, same as above | none (open mode) |\n| `COTAL_LINK` | connector session | `cotal://token@host/space` join link: supplies server, auth, space | none |\n| `COTAL_AGENT_FILE` | connector session | Path to a persona file: supplies name, role, kind, channels | none |\n| `COTAL_SUBSCRIBE` | connector session | Active channel read set. Set but empty means no channels | agent file / link, else no channels |\n| `COTAL_ALLOW_SUBSCRIBE` | connector session | Read ACL (channels the agent *may* read). Set but empty means the read set alone | agent file\'s, else = `COTAL_SUBSCRIBE` |\n| `COTAL_ALLOW_PUBLISH` | connector session | Post ACL (channels the agent *may* post to). Set but empty means deny | agent file\'s, else deny (empty) |\n| `COTAL_MODEL` | connector session | Model label (display metadata), set by the launcher | none |\n| `COTAL_KIND` | connector session | Endpoint kind | `agent` |\n| `COTAL_TLS` | connector session | Connect over TLS (`1`); off keeps the join link\'s choice | off (or the link\'s) |\n| `COTAL_TOKEN` | connector session | Auth token (token / open modes) | none |\n| `COTAL_CAPABILITIES` | connector session | Control-plane capabilities (e.g. `spawn`) that gate manager tools | agent file\'s `capabilities:` |\n| `COTAL_QUIET` / `COTAL_MUTED` | connector session | Per-channel attention defaults (never-wake / drop-on-receive) | agent file\'s, else none |\n| `COTAL_CHANNEL` | Claude connector | Force channel wake-nudges on (`1`) / off; set to `1` by the Claude launcher | on when the MCP client declares the `claude/channel` capability in `initialize` |\n| `COTAL_EVENTS` | connector session | Arm this session\'s event plane (`1`); set by the launcher unless the launch used `--no-events` | launcher-managed |\n| `COTAL_EVENTS_REQUIRED` | hand-driven user-mode connector | Trusted registration says events are mandatory; arms the plane and refuses if the session grant omits its event channel. Launcher-managed sessions carry this in launch material instead | off |\n| `COTAL_DEFAULT_AGENT` | `cotal spawn` | Default connector type for a bare spawn (below an explicit `--agent` and the persona\'s `agent:` pin, above a saved [`harness` preference](#preferences)) | `claude` |\n| `COTAL_DEFAULT_PERSONA` | `cotal spawn` | Default persona for a bare spawn (above a saved [`persona` preference](#preferences)) | `default` |\n| `COTAL_SKIP_CONNECTOR_SEED` | boot gate | Skip the automatic built-in-connector seed/refresh on a command (`1`); `cotal ext seed` still works. `agent-bearer` skips the gate by name, no flag needed | off |\n| `COTAL_ALLOW_CHECKOUT_SEED` | seed store | Permit a source-checkout CLI to write the operator-global seed store (`1`) after isolating `$XDG_CONFIG_HOME`. Used by in-tree seed smokes that spawn the checkout-shaped `bin/` CLI into a scratch config. Any other value is ignored. The checkout refusal does not name this variable. | off |\n| `COTAL_DETACH_KEY` | `cotal attach` | Detach escape key (ctrl-<char> / ^<char>), matched as the control byte or its kitty / modifyOtherKeys encoding (above a saved [`detach-key` preference](#preferences)) | `ctrl-]` |\n| `COTAL_FEEDBACK_KEY` | `feedback`, connector | Beta feedback key \u2192 keyed intake | none (public intake) |\n| `COTAL_FEEDBACK_EMAIL` | `feedback`, connector | Contact email for the keyless public intake | your git email |\n| `COTAL_FEEDBACK_URL` | `feedback`, connector | Intake URL override (self-hosted) | keyed / public intake |\n| `COTAL_TELEMETRY` | any `cotal` command | The [usage telemetry](telemetry.md) level (`off`, `minimal`, `regular`), above the settings file; any other value refuses | the file\'s level, `regular` at first |\n| `COTAL_TELEMETRY_DISABLED` / `DO_NOT_TRACK` | any `cotal` command | Force usage telemetry off (`1` or `true`) | off |\n| `COTAL_TELEMETRY_URL` | any `cotal` command | Usage telemetry intake override (self-hosted) | `https://cotal.ai/v1/telemetry` |\n| `COTAL_SKIP_ASSIST` | `setup` | Disable the connector debug handoff on a failed step (`1`; for CI) | off |\n| `COTAL_COMPLETE_DEBUG` | `completion` | Print completion-resolution errors to stderr | off |\n| `COTAL_DEBUG` | any `cotal` command | Print the stack trace under a command\'s one-line error | off |\n| `COTAL_CODEX_BIN` / `COTAL_OPENCODE_BIN` / `COTAL_ORCA_BIN` | Codex and OpenCode connectors, Orca runtime | Pin the `codex`, `opencode` or Orca CLI binary | found on `PATH` |\n| `COTAL_IDP_TIMEOUT_MS` | `cotal login`, IdP requests | Per-request IdP timeout in milliseconds, for a slow IdP; a malformed value fails loud | `30000` |\n| `COTAL_ENROLLMENT_FILE` | foreground `spawn` | Private `0600` file containing one remote enrollment URL; preferred over the environment form | none |\n| `COTAL_MANAGED_HANDOFF_FILE` | `cotal` entry, foreground `spawn` | Private `0600` file holding one managed lifecycle handoff from a delegating runtime; taken and deleted before anything else runs | none |\n| `COTAL_ENROLLMENT_URL` | foreground `spawn` | One remote enrollment URL when a secret file cannot be mounted; conflicts with `COTAL_ENROLLMENT_FILE` | none |\n| `COTAL_SERVE_HEADLESS` | OpenCode runtime | Run the OpenCode server without a foreground TUI (`1`). Stdout gets one `[cotal-serve]` line with the server\'s port and session and no password; a host that drives the server passes its own `OPENCODE_SERVER_PASSWORD` to the launcher | off |\n| `COTAL_HOME` | workspace | Override the machine-home dir for the **mesh registry only** (`meshes/`, `current-mesh`, onboard marker). Does **not** redirect project-root paths (`findCotalRoot` / `.cotal/broker-policy.json`, NATS store, manager/delivery state, auth). Tests that run `cotal up` must also use a temp project root with its own `.cotal/` as `cwd` | `~/.cotal` |\n\n> `--console-port` is a `cotal supervise` flag, not an environment variable; there is no\n> `COTAL_CONSOLE_PORT`.\n\n### Launcher variables\n\nThese are wired into a spawned child\'s environment by the connector / launcher and read back inside\nthe session. They are not operator knobs; listed so you recognize them in a process listing.\n\n| Variable | Purpose |\n|---|---|\n| `COTAL_ID` | Stable agent id chosen by the launcher (static meshes) |\n| `COTAL_MANAGER_INSTANCE` | Stable instance id of the launching manager. User-auth managed calls request a separate control view for this instance: the seat\'s tools always, and `cotal` commands in its shell unless `--on` names another. The issuer authorizes the selection. It carries no credential or grant. An unbound session uses the issuer\'s unique authorized selection |\n| `COTAL_ENVIRONMENT` | Opaque provider-issued environment reference published in presence. Read once when the endpoint is constructed; omitted when the launcher sets none |\n| `COTAL_LIFECYCLE_UID` | The incarnation\'s lifecycle UID, minted once per spawn; the session binds its lifecycle-keyed DM/delivery/history consumers by it (its credential pins the same names). Required for an authed launch (`COTAL_CREDS` or user-mode); config parsing fails loud without it. Open mode omits it (the endpoint self-mints per session) |\n| `COTAL_BACKFILL_FLOOR` | The CHAT stream sequence a resumed seat\'s prior incarnation had reached before its preservation cut; the boot backfill reads only what came after it. Set by the manager on a preserved resume, absent on a fresh spawn. Must parse as a non-negative integer; a broken launcher\'s malformed value fails loud rather than silently falling back to a full replay |\n| `COTAL_OWNER` / `COTAL_ACTOR` / `COTAL_SENTINEL_CREDS` / `COTAL_BEARER_CMD` | User-auth launch identity: the agent\'s principal, its sentinel creds path, and the exec-able bearer command; all four together, mutually exclusive with `COTAL_CREDS`. A launcher-spawned seat carries them in its launch material instead of its environment. A remote enrollment\'s bearer argv uses `agent-bearer --exchange-url <https://base>`; the token never falls back to a local service file |\n| `COTAL_LAUNCH_MATERIAL` | Path to this launch\'s private 0600 material file (see [Launch material](#launch-material) below). Carries the broker URL, the creds path, the auth token, the user-auth identity, the required-events flag, and the control token. A PATH, never a secret |\n| `COTAL_CONTROL_SOCKET` | The session\'s local control endpoint path. The MCP server listens on it and the lifecycle hooks connect to it. A launcher-spawned session carries the token that authenticates the first frame in its launch material. A hand-driven Claude Code or jcode session sets this path and `COTAL_CONTROL_TOKEN` itself (see [Launch material](#launch-material)) |\n| `COTAL_BRIDGE_SOCKET` / `COTAL_TOOLS_FILE` / `COTAL_PARENT_PID` | Hermes sidecar plumbing (bridge socket, generated tool descriptors, launcher pid to watch). The bridge socket\'s first frame carries the control token from the launch material (or `COTAL_CONTROL_TOKEN` in standalone mode) |\n| `OPENCODE_CONFIG_CONTENT` | Inline OpenCode config (the injected cotal plugin, highest merge layer) |\n| `OPENCODE_DB` / `COTAL_OPENCODE_*` | OpenCode server plumbing (the per-agent DB, and the connector\'s home, port and server URL) |\n\nA spawned agent receives a fixed OS execution allow-list (PATH, HOME, TERM, locale, and\nXDG/Windows config directories), the machine-wide `COTAL_*` operator knobs (`COTAL_HOME`, the\nfeedback set, the default-agent pair, the `*_BIN` overrides, the timing knobs), the provider inputs\nits connector declares, and `${VAR}` names an explicitly shared MCP server requires. It does not\ninherit the manager\'s ambient environment. This keeps host-session markers such as\n`CLAUDE_CODE_CHILD_SESSION` / `CLAUDECODE` (and the analogous names other hosts use to mark a nested\nsession), unrelated service secrets, and environment-only capabilities out of seats unless\ndeliberately supplied. A seat\'s transcript/resume behaviour is a property of the seat, never of how\nmany layers up someone once ran `cotal up` inside an agent. Connection material is not in the\nenvironment at all (see [identity & auth](identity-and-auth.md)).\n\nEnrollment inputs are launcher-only secrets. `spawn` removes both enrollment variable names from the\nconnector\'s child environment, and `spawn.env` cannot list them.\n\nPATH is forwarded whole, including entries such as `~/.local/bin` where connector binaries live, so\na seat can still launch after the strip. There is no inherit mode and no opt-in-to-containment flag:\nthe allow-list is the only path.\n\nTo deliberately add an environment name for a spawned agent, declare `spawn.env` in the config file:\n\n```json\n{ "spawn": { "env": ["MY_PROVIDER_API_KEY"] } }\n```\n\nThe listed names are added to the fixed boundary. That is also the opt-in for a host-session marker\na persona has chosen to receive (`CLAUDE_CODE_CHILD_SESSION` and friends). A `COTAL_` name other\nthan the machine-wide knobs above is refused and the seat is not launched: the launcher sets those\nnames for each seat, so a value from the spawning process would become the seat\'s role, model or\nread set. An empty array adds nothing. A space-local `spawn` block replaces the operator-level one\noutright rather than merging, so a local list stays local. No `spawn` block, `"spawn": { "env": [] }`, and `"spawn": {}`\nall add no names. A `spawn` that is not an object, or an `env` that is not a list of non-empty names,\nis refused when the file is read, with the file and the field named, so every spawn and resume is\nrefused until it is fixed.\n\nBe honest with yourself about what this buys: `HOME` is forwarded, so an agent with a shell reads\n`~/.aws`, `~/.ssh` and `~/.config` regardless. The boundary protects what a file on disk cannot hand\nover anyway, and that is more than a list of secret values. Some variables are **capability\nhandles**: they do not contain a secret, they name a live process that will act on your behalf.\n`SSH_AUTH_SOCK` is the sharp one. Inherit it and the agent can ask your `ssh-agent` to sign, which\nmeans it can reach any host or sign any commit that key authorises, and it keeps that power even\nif the private key file is not on disk at all. Nothing under `~/.ssh` has to exist for it to work,\nso "a shell reads `~/.ssh` regardless" does not cover this case. The same shape covers a\n`gpg-agent` socket and the desktop and cloud credential brokers. So the default boundary protects:\nsecrets that live **only** in the environment, such as an `aws-vault exec` or `op run` shell or\nCI-injected values, and the capability handles above, which it removes along with everything else\nit does not name. Real containment is still a sandbox or a VM.\n\nModel discovery is the exception, and it is deliberate rather than an oversight. When the `codex` or\n`opencode` connector enumerates a model catalog (`cotal models`, and the manager\'s selector), it runs\nthat harness with your environment minus Cotal\'s own `COTAL_*`, and it does **not** consult\n`spawn.env`. Those probes are short-lived catalog reads rather than agent seats, so an allow-list\nthat confines a seat does not confine them.\n\n### Launch material\n\nA process environment is inherited by every descendant. A seat launched with its credential, its\nbroker URL and its control token in the environment hands all three to the build it runs, the linter,\nthe third-party CLI, the test suite that reads its broker from the environment. Nothing in that chain\nasked for any of it.\n\nSo a launcher-spawned seat does not get them in its environment. The launcher writes them to a single\n**0600 file inside a 0700 private directory** and exports only its path, as `COTAL_LAUNCH_MATERIAL`.\nThe session reads the launch-material file once at startup. For a managed creds path, it reads the\ncredential once to pin the seat\'s nkey, then keeps the path as a renewal source. A re-signed file is\nread by renewal and by reconnect after the cached credential expires, and a file for a different\nnkey is refused. An unbounded credential has no renewal point and remains a static boot-time value.\nThis is the same shape\n`cotal agent-bearer` already uses for its spawn-time secret: the material rides a file, never argv\n(which is visible in a process listing) and never the ambient environment (which is inherited).\n\nThree connectors drop the path once they have read it, so the shells and tools those seats run\ninherit no reference at all: **pi** and **codex**, whose sessions run in the seat process, and\n**OpenCode**, whose seat process is a shim that starts `opencode serve` (the plugin runs in that\nserver, which is also what executes the session\'s tool calls). Those three also **delete the file**\nat the same moment, along with the private directory that held it. Nothing reads it again, so leaving\nit on disk would only extend how long a copy of the material exists. The directory is only removed\nwhen it is provably the one the launcher wrote: the right filename inside, the launcher\'s prefix on\nthe directory, the directory sitting directly in the OS temp root, and a non-recursive removal that\nfails rather than deletes if anything else is in there.\n\nTwo keep it, and for the same reason in both cases: a process that starts LATER has to read it.\n**Claude**\'s readers are short-lived children, the MCP server and one process per lifecycle hook,\nwhich begin after the session is already running. **Hermes**\' launcher starts a gateway child that\nneeds the control token. For those two, a shell the seat runs still inherits a path to the material\nfile, though not the material itself.\n\nThe material\'s private directory is also a launch artifact (see\n[Private launch files](authoring-a-connector.md#private-launch-files)), so the launcher removes it\nwhen the seat\'s process exits, the same way it removes a persona file. For Claude that is when the\nfile goes. Hermes\' launcher merges the file it was given into a second file of its own for the\ngateway child, and that second file is not a launch artifact. The launcher writes the file after\nevery check that can refuse the launch, so a refused launch leaves none behind. A file can still\noutlive its seat when the launcher and the child\'s watcher were both killed, or when a spawn failed\nafter the file was written but before any process started. The OS temp reaper removes those.\n\nWhat this does: the values are out of every descendant\'s environment, so an `env` dump, a CI log, a\nsuite that defaults its broker from the environment, or a tool handed a credential it never asked\nfor, all stop seeing them. What it does not do: hide the material from a process running as the same\nuser that deliberately opens the file. No environment-level control can, and the same is already true\nof `~/.cotal/auth/creds`. What changes is that reaching the material is a deliberate act rather than\nan inheritance nobody chose.\n\nDriving a connector session **by hand** still works the documented way: set `COTAL_CREDS` /\n`COTAL_SERVERS` (and the user-auth quartet) yourself, and no material file is involved. Setting both\na material file and any of them is refused rather than resolved by precedence: one launch carries one\nidentity plane. `COTAL_LINK` counts as one of them, because a join link carries the server, the auth\nand the space in a single string.\n\nA Claude Code or jcode session also serves a local control endpoint and refuses to start without\none, so a hand-driven one sets `COTAL_CONTROL_SOCKET` to a socket path you choose and\n`COTAL_CONTROL_TOKEN` to a random secret. OpenCode, Codex and pi sessions start without them.\n\n`eventsRequired` is an additive boolean in launch material. The launcher derives it from the selected\nuser-auth registration. A Claude, OpenCode, Codex, Jcode or Pi launch that carries it also sets\n`COTAL_EVENTS` and the workspace root, even when the launcher passed `events: false`. Connector config\nexposes it and the Claude, OpenCode 1.x, Codex, Jcode and Pi startup gates arm on it even when\n`COTAL_EVENTS` is absent. A direct env launch may use `COTAL_EVENTS_REQUIRED=1` only with the complete\nuser-auth quartet. The session refuses if its post ACL does not cover its own `events.<owner>.<actor>`\nchannel.\n\nThe control endpoint is a pair, and **half a pair is refused**. A launch with a control socket path\nand no resolvable token, or a token and no socket path, does not fall back to running without a\ncontrol plane: it fails with a sentence naming which half is missing. The one exception is the\nlifecycle hook relay, which catches that refusal, writes a single warning to stderr naming no values,\nand then does nothing, because a hook that throws is a hook that blocked the session. Failing open is\ndeliberate; failing open silently is not.\n\n## On-disk layout\n\n### Project files\n\nA project\'s state lives in `.cotal/` at the mesh root (found by walking up from the cwd, like `.git`).\n**It is gitignored**; it holds secrets and machine-local process state.\n\n| Path | What it is |\n|---|---|\n| `auth/broker.json` | Broker trust material: the operator seed and the system account (secret; the system-account signing seed is stripped before writing). One per broker, shared by every space on it |\n| `auth/account.<key>.json` | One space\'s own NATS data account and signing seed (secret). One file per space, all signed by the broker above; `<key>` is a stable, case-safe hex encoding of the space name (never the raw name, so two case-differing spaces can\'t collide) |\n| `auth/space.<key>/` | One space\'s user-auth state (IdP pin, issuer keys, owner secret, callout account, client sentinel credentials), present only when that space enables per-user auth. Keyed by the same case-safe hex encoding; pre-hex layouts (`auth/<space>/`) are renamed here on first touch |\n| `auth/creds/space.<key>/<name>.creds` | Per-agent minted NATS credentials, under the segment of the space they belong to - same case-safe hex encoding as the rows above. Pre-segment layouts (`auth/creds/<name>.creds`) are moved here on first touch. The `creds` directory itself stays shared, so a root\'s tenants keep their agent material in sibling segments rather than sibling roots |\n| `auth/seat-generation.<key>.<n>.json` | Writer generation `<n>` of one seat, claimed by exclusive create when `cotal up` resumes the seat from a checkpoint ([Backups](cli.md#backups)). `<key>` is the space\'s case-safe hex key, `00`, then the seat name in the same encoding, so no two seats share a key. A space or seat name the encoding refuses, or one containing NUL, claims nothing |\n| `auth/server.conf` | Generated nats-server config for the broker (`# Generated by \\`cotal up\\` - do not edit by hand.`). Path is `<projectRoot>/.cotal/auth/server.conf`, not `~/.cotal` unless that is the mesh root. Default bind is loopback (`host: 127.0.0.1`); `--host` on a **stopped** `cotal up` regenerates it. A live refresh does not rewrite this file. The core renderer accepts every space on the broker; `cotal up` currently orchestrates one space per root, so it renders that one space\'s account |\n| `broker-policy.json` | Durable broker **launch** policy (TLS-required cert/key path references, or plaintext). Survives `cotal down` so a bare re-`up` cannot silently drop TLS. Under the project root: **not** under `COTAL_HOME` |\n| `agents/<name>.md` | Persona / agent files ([Agent files](agent-files.md)) |\n| `manifests/<runId>.json` | Additive ownership ledger written by the retired `spawn -f` deploy; `cotal down -f <file> --run <id>` tears one down |\n| `config.json` | Space-local connector config (the override layer above) |\n| `nats.pid` \xB7 `nats.log` | Background nats-server pid + log |\n| `manager.<key>.pid` \xB7 `manager.<key>.log` | Manager (supervisor) pid + log for one space; every line of the log starts with the UTC time it was written (ISO 8601), so a reap can be placed in time without another file; `manager.<key>.delivery-aware` marks a delivery-aware build and is written owner-only (0600) whatever the umask, never through a symlink and only into a file this user owns. `<key>` is the same case-safe hex space key as the rows above, so one root can run a manager per space. A pre-segmentation root-scoped `manager.pid` is still read while it is the only spelling present, and is removed as the new record is written. A start that finds it already removed, by its exiting owner or by a concurrent start, continues. Both spellings present is reported as ambiguous rather than guessed. A start that refuses over this record names the file it read, even if the record moves to the other spelling before the refusal prints. The manager writes the pid itself, whatever started it, and removes it on a clean stop only while it still names that process. A reader treats the record as a running manager only if the pid is alive **and** the process is a supervisor: a recycled pid belonging to something else is reported as a stale record, never signalled |\n| `delivery.<key>.pid` \xB7 `delivery.<key>.log` \xB7 `space.<key>/delivery.creds` | Delivery daemon pid and log for one space, and its scoped cred (auth mode). `cotal down` and the teardown of a foreground `cotal up` remove the cred once the daemon is confirmed stopped. Per-space and compatible with a pre-segmentation `delivery.pid` on the same terms as the manager row |\n| `web.pid` \xB7 `web.log` | Web dashboard pid + log |\n| `membership.json` \xB7 `membership-*.creds` | Membership feed state + its scoped creds |\n| `setup.log` | `cotal setup` log, one section appended per run. Each entry is one timestamped line: a control character or Unicode line separator in a path or error message is written as a `\\uXXXX` escape |\n\nA command that acts on the whole folder without being told a space reads one off these runtime\nrecords: `<key>` decodes back to the space name, and a space whose record is running wins over\nresidue from a stopped one. Two spaces running under one root is reported rather than arbitrated.\nThis is what lets `cotal status` and `cotal down` work in a folder whose mesh runs with\n`broker: { auth: false }`, where there is no `auth/account.<key>.json` to name the space.\nA record, or a `.cotal` listing, that exists but cannot be read is reported as that read error\nrather than read as absent. A process record\'s read error names the record\'s path.\n\n### Machine files\n\nCross-project machine state, so a `cotal spawn` from any directory can find a running mesh. Location:\n`~/.cotal` on POSIX, `%LOCALAPPDATA%\\Cotal` on Windows; overridable with `COTAL_HOME`.\n\n`COTAL_HOME` overrides **this tree only** (registry + current pointer + onboard marker). It is not a\nfull workstation sandbox. Broker launch policy, the JetStream store, pidfiles, and auth live under\nthe **project** `.cotal/` found by walking up from the cwd ([Project: `.cotal/`](#project-files)\nabove, including `broker-policy.json` on TLS meshes). A probe that sets `COTAL_HOME` alone and runs\n`cotal up --tls-cert \u2026` from a directory whose walked root is the operator home still writes those\nproject paths on the live machine.\n\n| Path | What it is |\n|---|---|\n| `meshes/space.<key>.json` | Registry of running meshes: one file per broker `cotal up` started (server URL, root path, mode, TLS-required client intent when recorded, attach bind host and live-session ceiling when the operator set them); `<key>` is the same case-safe hex encoding of the space name, and the record\'s own `space` field is authoritative |\n| `current-mesh` | Default space a bare `cotal spawn` joins (set by `cotal use`) |\n| `onboarded.json` | Marker (with `ONBOARD_VERSION`) that `cotal setup` writes once; setup shows its splash until then |\n| the Claude plugin marketplace | The installed `cotal-mesh` plugin assets |\n\n### Configuration files\n\nDistinct from `~/.cotal`. Location: `$XDG_CONFIG_HOME/cotal`, else `~/.config/cotal` on POSIX, or\n`%APPDATA%\\Cotal` on Windows.\n\n| Path | What it is |\n|---|---|\n| `config.json` | Operator-level connector config (the base layer above) |\n| `preferences.json` | Your [preferences](#preferences), private to you (`0600`), written by `cotal config` |\n| `extensions/` | `cotal ext` install prefix: its own npm root (`node_modules`) plus an `extensions.json` provider/command-display cache. Built-in connectors install here too, seeded on first run |\n| `seed/` | Built-in-connector seeding state: the `ever-seeded` authority (+ durable backup), the init witness, the version stamp, the crash cursor, and `store/<version>/<name>` (the stable payloads `ext add --install-links` reifies each seeded connector from) |\n\nBoth `extensions/` and `seed/store/` are operator-global: shared by every space, project directory, and\ncheckout on the machine, and moved only by `$XDG_CONFIG_HOME` (a fresh project dir isolates `.cotal/`,\nnot these). `COTAL_HOME` does not relocate them. A CLI running from a source checkout (`pnpm cotal`,\n`tsx bin/cotal.ts`, `node bin/cotal.ts`, or a suite child of those, identified by a `bin/` package\nroot next to `implementations/` or `pnpm-workspace.yaml`) refuses to write, stamp, or\ngarbage-collect that store: the refusal names the store path, the generation it declined, and\n`COTAL_SKIP_CONNECTOR_SEED=1` as the way to run other commands from a checkout. An entry that\ncannot be proven as a released `cotal-ai` install is refused the same way. Isolating\n`$XDG_CONFIG_HOME` (on Windows, `%APPDATA%`) keeps a checkout off the installed config but does not\nlift the refusal by itself. A\nreleased install or an `npx` unpack still seeds as before. The in-tree seed smokes that must seed\nfrom a checkout-shaped `bin/` set `COTAL_ALLOW_CHECKOUT_SEED=1` against an isolated config; an\nopt-in write still records that checkout path in `seed/stamp.json` as `writtenBy`. The reconcile\nnames on stderr both the store payloads it writes and any old generation it removes, so a\nmachine-wide re-seed or cleanup is visible when it happens. Those lines are provenance output. When a\nstderr write fails, at once or after waiting in a full pipe, the line is printed on stdout with the\nerror and the reconcile still completes. If stdout fails too, the reconcile still completes and the\nrun exits 1 instead of 0. A line still waiting in a full stderr pipe when the run exits, as when the\nCLI exits on a closed stdout, is lost and also makes the run exit 1. Node does not say which stderr\nbytes are still waiting, so a line that had to wait and got through just before the exit also makes\nthe run exit 1 when later stderr output is still waiting. A run whose stderr is closed or redirected\naway at launch keeps the write and loses the line.\n\nFor how `cotal setup` populates the machine state and the plugin, and how the built-in connectors are\nseeded as removable extensions, see [setup internals](setup-internals.md).\n'
|
|
65281
65281
|
},
|
|
65282
65282
|
{
|
|
65283
65283
|
"slug": "connect-claude",
|
|
@@ -65454,6 +65454,13 @@ function loadDocsBundle() {
|
|
|
65454
65454
|
"summary": "If you build on the @cotal-ai/ packages, this page is what you can rely on and what will change.",
|
|
65455
65455
|
"body": '# Substrate stability\n\n> **Project** (informative) \xB7 **For:** anyone building a product on the published packages \xB7 **See also:** [Embedding Cotal](embedding.md), [Release](release.md), the spec\'s [versioning rules](../SPEC.md#11-versioning-and-extensibility)\n\nIf you build on the `@cotal-ai/*` packages, this page is what you can rely on and what will change.\nTwo versions matter and they move independently: the **wire** version (`protocolVersion`) and the\n**package** versions (npm semver).\n\n## What is stable today\n\n- **Wire shape: v0.3, version marker unset.** The implemented and documented binding shape is v0.3:\n both binding revisions are merged, owner+actor identity (which *supersedes* the old single-id\n grammar and re-keys every subject) and manager-free channel live delivery (which *replaced* the\n mediated live-tail; the implementation staged it as an additive overlay, but the resulting wire\n revision is breaking). The `protocolVersion` field is **optional and the reference implementation\n does not populate it**: normative [SPEC section 11](../SPEC.md#11-versioning-and-extensibility)\n still names the contract version v0.2, and cards carry no marker (a receiver treats an omitted\n marker as the v0.x line). So the shape you build against is v0.3 while nothing on the wire announces\n it, and old and new channel clients do not interoperate (core CHANGELOG). The wire is pre-1.0 and\n may still change.\n- **Packages: pre-1.0 (the 0.x line).** core, workspace, auth, delivery, manager, web, and the\n connectors publish in lockstep. The exact version is whatever the packages\' `package.json` (and\n `cotal_docs`) report, so any number on this page is illustrative.\n These are the surfaces [Embedding Cotal](embedding.md) documents.\n\nBuildable on today: the owner+actor identity grammar, the lateral chat/DM/task envelopes and\nsubjects, presence and discovery, channels, the three delivery modes, and the Plane-3 durable\nbackstop. The auth callout and delivery daemon are the two hardest server-side pieces and both are\nstandalone and merged.\n\n## The npm semver caveat\n\nThe packages are **pre-1.0**. Under semver, a pre-1.0 line makes no compatibility promise across\nminor bumps: a `0.13.x` to `0.14.0` change may break an API. So a product that embeds these packages\nmust **pin exact versions** and upgrade deliberately, reading the changelog and the diff, not float a\ncaret range. The support and deprecation policy for the embedding surface is\n[below](#compatibility-policy).\n\n## The wire compatibility signal\n\nPer [SPEC section 11](../SPEC.md#11-versioning-and-extensibility):\n\n- `AgentCard.protocolVersion` is the one-way compatibility signal, but it is optional and the\n reference implementation currently leaves it unset (an omitted marker means "assume the v0.x\n line"). v0 has **no** in-band capability negotiation; deployments agree on the binding and version\n out of band.\n- Additive changes (a new optional field, a namespaced `Part.kind`, a new subject) are\n backward-compatible and ship as a minor bump; receivers ignore what they do not recognize.\n- Changing the meaning of an existing field or subject, or removing or renaming one, is breaking and\n ships under a new version marker.\n\nBecause the field is optional and the reference implementation leaves it unset, a broker cannot gate\non it today: there is no marker to read, and a v0.2-shaped and a v0.3-shaped card look the same. So a\nhosted broker that must refuse wrong-wire clients gates out of band on the package or build version,\nor another agreed signal. Whether `protocolVersion` becomes populated and sufficient at the v0.4 hard\ncut, or a dedicated gate is added, is open operator-readiness work.\n\n## The coming v0.4 cut\n\nThe one currently planned hard cut ahead of the substrate is the control surface\'s **v0.4**. It is a\ndeliberate pre-1.0 breaking hard cut (no dual-serving, no translation shims); old subjects,\nenvelopes, handlers, and credential grants are removed after the cut, and `protocolVersion` targets\n`0.4` at completion.\n\n**Projected break families.** The control-surface campaign is\nmid-flight (its later phases are not complete), so the exact set of deleted subjects and changed\nsignatures is generated from the integrated diff at cutover, not knowable precisely today. The\nfamilies that are in scope to break:\n\n- **Control grammar.** The authority-tier service taxonomy, the old control envelope, control\n subjects and handlers, and their credential grants are deleted and replaced by the typed\n endpoint/class/instance rails.\n- **Lifecycle-UID API changes.** Public durable, history, ACL, member, provision, and deprovision\n APIs change to require a lifecycle UID; `dinbox`/`dlv` subjects, ACL/member keys, and\n presence/membership schemas become lifecycle-scoped.\n- **Daemon control absorbed.** The manager control plane (attach moves from a loopback endpoint to\n sessions; spawn becomes an action) and the delivery control shapes are migrated onto the surface;\n the old delivery-specific control protocol is deleted.\n- **Broker floor.** v0.4 raises the minimum NATS server to 2.12.\n\nWhat **stays** across v0.4: the lateral chat/DM message envelopes and the owner+actor identity\ngrammar. What breaks alongside the control grammar is the presence/membership and durable-delivery\n**backing** grammar and API, so "only the control surface breaks" understates it.\n\n## Building around the cut\n\n- **Pin an exact patch** (for example `0.13.1`, not a `0.13.x` range) and treat the embedding surface as pre-1.0.\n- **Shim every control-plane, lifecycle, durable-delivery, and presence/membership call** behind an\n internal client, so the v0.4 swap is one contained change rather than a rewrite. This is broader\n than "control subjects."\n- **Do not ship a public API on v0.3 shapes that v0.4 deletes** until the control surface reaches its\n consolidation phase and the final v0.4 inventory exists.\n\n## Compatibility policy\n\nThe substrate packages stay **pre-1.0 (0.x)** for now. The project does **not** declare a 1.0 line\nfor them yet, because a known breaking change is still ahead (the [v0.4 cut](#the-coming-v04-cut)),\nand a 1.0 promise made right before a deliberate break would be hollow. A 1.0 line is revisited once\nv0.4 has landed and the hosted-composition gaps [Embedding Cotal](embedding.md) documents (the secret\nseam, multi-space) have closed.\n\nWhat a product embedding the packages can rely on in the meantime:\n\n- **Pin an exact patch.** A caret or tilde range can pull in a breaking minor. Pin `0.N.P`, not\n `^0.N.P` or `~0.N`.\n- **Patch is bug-fix only.** A `0.N.x` patch bump carries no intended breaking change. A **minor**\n bump (`0.N` to `0.N+1`) may break an API; read the changeset and the diff before taking one.\n- **Every break is written down.** A breaking change ships with a changeset entry and a changelog\n note that names what changed, so an upgrade is never a silent surprise.\n- **One minor of deprecation notice, where practical.** A symbol slated for removal is marked\n deprecated for one minor line before it is removed (soft-deprecate in `0.N`, remove in `0.N+1`).\n The v0.4 hard cut is the explicit exception: it is a coordinated break with no dual-serving,\n signalled in advance rather than soft-deprecated.\n- **Supported line.** The latest minor is supported; the previous minor gets patch-level fixes until\n the next minor ships (a one-minor overlap). This is deliberately light-touch while the only\n consumer is the project\'s own hosted repo; it tightens (a longer window, a firmer deprecation\n period) when there are external embedders.\n\nUntil the 1.0 line exists, the [build-around guidance](#building-around-the-cut) above (pin exact,\nshim the breaking families) is the safe posture.\n'
|
|
65456
65456
|
},
|
|
65457
|
+
{
|
|
65458
|
+
"slug": "telemetry",
|
|
65459
|
+
"title": "Telemetry",
|
|
65460
|
+
"kind": "",
|
|
65461
|
+
"summary": "The cotal CLI sends anonymous usage data so we can see which commands people use, which fail, and on which platforms.",
|
|
65462
|
+
"body": '# Telemetry\n\nThe `cotal` CLI sends anonymous usage data so we can see which commands people use, which fail,\nand on which platforms. It is on by default, it works with no mesh, no Cloud and no sign-in, and\nyou can turn it off in one command. This page is the contract: the intake accepts the bodies below\nand nothing else.\n\n## Levels\n\n| Level | What is sent |\n|---|---|\n| `off` | Nothing. Under the environment opt-outs the settings file is neither read nor created. |\n| `minimal` | One `command` event per command with the command name, outcome, exit code, duration, Cotal version, OS, architecture, Node major and CI flag. |\n| `regular` (default) | The `command` event with the registered subcommand, flag names, typed error code, whether stdout is a terminal, and the shell and terminal program names; plus one `install` event per UTC day with the installed connectors, extensions, runtimes and mesh count. |\n\n```bash\ncotal telemetry # the level in force, where it comes from, the install id and the URL\ncotal telemetry off\ncotal telemetry minimal\ncotal telemetry regular # the default; `on` is an alias\n```\n\nThe environment outranks the file: `DO_NOT_TRACK=1` or `COTAL_TELEMETRY_DISABLED=1` forces `off`,\nand `COTAL_TELEMETRY=off|minimal|regular` names a level. Any other `COTAL_TELEMETRY` value refuses\nwith `USAGE_INVALID`. `cotal telemetry` names the source of the level in force.\n\nThe settings live in `<home>/telemetry.json` (`~/.cotal/telemetry.json`, or under `COTAL_HOME`),\nmode `0600`: the level, a random install id (uuid v4), the file\'s creation time, and the times of\nthe one-time notice and the last install event. A file that does not validate refuses every\ncommand with `SCHEMA_INVALID` and is never replaced.\n\n## Never sent\n\nArguments, flag values, paths, mesh, space, agent, user and host names, environment values, error\ntext, prompts, messages and any other content. The install id identifies the settings file, not a\nperson or machine. The daemon, manager, connectors and workflow runtime send nothing; neither do\ninternal entries, hidden daemon runners, `agent-bearer` and `cotal telemetry` itself.\n\n## Delivery\n\nOne `command` event is posted as the CLI process exits, whatever the exit path. At `regular`, the\nfirst command of each UTC day also posts an `install` event first. Each is one `POST` of JSON,\n`content-type: application/json`, to `https://cotal.ai/v1/telemetry`, with a 1.5 second timeout, no\nretry, and every failure ignored: telemetry never changes a command\'s output, exit code or timing\nbeyond that timeout. `COTAL_TELEMETRY_URL` points a self-hosted intake.\n\nThe first interactive run at `regular` prints one notice to stderr and records it; a run whose\nstderr is not a terminal (CI logs) sends but prints nothing, so a later terminal run still shows it.\n\n## The `command` event\n\n| Field | Type | Meaning |\n|---|---|---|\n| `v` | `1` | Body version. |\n| `installId` | uuid string | The install id. |\n| `level` | `"minimal"` or `"regular"` | The level the event was sent at. |\n| `event` | `"command"` | |\n| `command` | string | The registered top-level command name (`up`, `spawn`, `ext`), never a user word. |\n| `outcome` | `"ok"`, `"error"` or `"refused"` | Exit 0 is `ok`; a typed refusal, or exit 2 without one, is `refused`; anything else is `error`. |\n| `exitCode` | integer | The process exit code. |\n| `durationMs` | integer | Milliseconds from dispatch to exit. |\n| `version` | string | The `cotal-ai` version. |\n| `os` | string | `process.platform`. |\n| `arch` | string | `process.arch`. |\n| `node` | integer | The Node major version. |\n| `ci` | boolean | Whether `CI` is set. |\n\nAt `regular` the event also carries:\n\n| Field | Type | Meaning |\n|---|---|---|\n| `subcommand` | string or `null` | The registered subcommand the first positional names (`ext add`, `daemon status`), never a user value. |\n| `flags` | string array, at most 32 | Flag names as typed (`--json`, `-j`, `--server`), values stripped; an unknown flag counts by name. |\n| `errorCode` | string or `null` | The typed code of a refusal or failure (`USAGE_INVALID`, `AUTH_REQUIRED`, `OPERATION_PARTIAL`); `null` on success or when the exit carried no code. |\n| `interactive` | boolean | Whether stdout is a terminal. |\n| `shell` | string or `null` | The basename of `SHELL`. |\n| `terminal` | string or `null` | `TERM_PROGRAM` as set. |\n\n## The `install` event\n\nSent at `regular` only, at most once per UTC day, before that day\'s first `command` event. It\ncarries `v`, `installId`, `level: "regular"`, `event: "install"`, `version`, `os`, `arch`, `node`\nand `ci` as above, and:\n\n| Field | Type | Meaning |\n|---|---|---|\n| `osRelease` | string | `os.release()`. |\n| `packageManager` | string or `null` | The agent in `npm_config_user_agent` as `name@major` (`npm@10`, `pnpm@9`), when a package manager ran the command. |\n| `installMethod` | `"installer"`, `"npm"` or `"unknown"` | `installer` under the launcher `get.cotal.ai` writes, `npm` when the binary runs from a `node_modules` tree. |\n| `connectors` | `{ kind, version }` array | The installed connectors by kind (`claude`, `opencode`, `codex`, `hermes`, `jcode`, `pi`), from the extensions manifest. |\n| `extensions` | `{ name, version }` array | The other installed `@cotal-ai/*` packages. Third-party packages are not listed. |\n| `runtimes` | string array | The registered runtime names (`pty`, `tmux`, `cmux`, `orca`, `herdr`). |\n| `meshes` | integer | How many meshes the mesh registry holds. |\n| `hostedMesh` | boolean | Whether any registered mesh uses user sign-in. |\n| `uptimeDays` | integer | Whole days since `telemetry.json` was created. |\n'
|
|
65463
|
+
},
|
|
65457
65464
|
{
|
|
65458
65465
|
"slug": "transport",
|
|
65459
65466
|
"title": "Transport vs protocol",
|