@cotal-ai/pi 0.99.3 → 0.100.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 +15 -15
- package/dist/standalone.js +131 -88
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -27209,10 +27209,10 @@ import { isConcreteChannel as isConcreteChannel3, channelInAllow as channelInAll
|
|
|
27209
27209
|
import { parsePrincipalKey as parsePrincipalKey2, peerLabel as peerLabel3 } from "@cotal-ai/core";
|
|
27210
27210
|
|
|
27211
27211
|
// ../connector-core/dist/docs-bundle.generated.js
|
|
27212
|
-
var DOCS_VERSION = "0.
|
|
27212
|
+
var DOCS_VERSION = "0.100.0";
|
|
27213
27213
|
function loadDocsBundle() {
|
|
27214
27214
|
return {
|
|
27215
|
-
"version": "0.
|
|
27215
|
+
"version": "0.100.0",
|
|
27216
27216
|
"generatedFrom": "docs/*.md + SPEC.md + spec/cotal-lang.md + spec/cotal.schema.json",
|
|
27217
27217
|
"pages": [
|
|
27218
27218
|
{
|
|
@@ -27234,7 +27234,7 @@ function loadDocsBundle() {
|
|
|
27234
27234
|
"title": "Architecture",
|
|
27235
27235
|
"kind": "Concept (informative)",
|
|
27236
27236
|
"summary": "Cotal is built as a thin waist: the normative wire contract (subjects, message schemas, presence/discovery, delivery semantics, the auth grammar) is the standard (SPEC), and everything else is a pl\u2026",
|
|
27237
|
-
"body": "# Architecture\n\n> **Concept** (informative) \xB7 **For:** anyone who wants to know how Cotal is built, and why \xB7 **Normative:** [SPEC](../SPEC.md)\n\nCotal is built as a thin waist: the normative wire contract (subjects, message schemas,\npresence/discovery, delivery semantics, the auth grammar) is the standard\n([SPEC](../SPEC.md)), and everything else is a pluggable edge over existing building\nblocks. Identity, transport, storage, and discovery compose from proven pieces (NATS,\nJetStream, JWT/nkeys) rather than being reinvented. Adapters stay thin and swappable, and\nnothing adapter-specific leaks into the core.\n\n## A2A influence\n\nCotal reuses A2A's vocabulary and shapes so it stays interoperable rather than siloed, and\nimplements them over NATS/JetStream.\n\n**From A2A** come the *data shapes*: `AgentCard` (identity / role / tags / skills),\n`Message` / `Part` (text and data), and correlation ids (`contextId`). We do not adopt\nA2A's HTTP/JSON-RPC transport, `Task` RPCs, or its request/response server model, none of\nwhich fit lateral pub/sub.\n\nThe *addressing model* is Cotal's own: the hierarchical address `space / service / instance`\nand three delivery modes, multicast, unicast, anycast\n([presence & delivery](presence-and-delivery.md)). **Mentions** are a priority hint on a\nmulticast, not a routing target. NATS/JetStream is the data plane, adding the durability and\npresence a bare pub/sub layer leaves to the app.\n\nIdentity is an A2A `AgentCard` whose instance id is shaped to later become a **DID**\n(`did:key`) so authenticity can survive an untrusted relay ([roadmap](roadmap.md)).\n\n## NATS mapping\n\nThe messaging plane rides three subject kinds, with the sender encoded in the subject\nitself, where the server can police it, rather than in a self-asserted payload field\n([SPEC \xA73](../SPEC.md#3-subject-layout)); the endpoint control surface adds its own rails\n([SPEC \xA713](../SPEC.md#13-endpoint-control-surface-v04)):\n\n| Delivery | Subject |\n|---|---|\n| multicast | `cotal.<space>.chat.<owner>.<actor>.<channel\u2026>` |\n| unicast | `cotal.<space>.inst.<toOwner>.<toActor>.<owner>.<actor>` |\n| anycast | `cotal.<space>.svc.<role>.<owner>.<actor>` |\n| endpoint (control) | `cotal.<space>.ep.<one\\|all\\|inst\\|reply>.\u2026` ([\xA713.2](../SPEC.md#132-grammar)) |\n\nThe sender is a **principal**, an `owner.actor` pair: the account the agent acts on behalf\nof, then the agent's own handle under it ([identity & auth](identity-and-auth.md)). Two\ntokens instead of one means the broker can deny cross-owner *and* same-owner cross-actor\nforgery in the subject grammar itself.\n\nBehind the subjects, each space gets three **JetStream streams** (chat / DM / task, for\nstorage, per-reader bookmarks, and history), **KV buckets** for presence and the channel\nregistry, and the endpoint control surface on its own rails and streams\n([SPEC \xA713](../SPEC.md#13-endpoint-control-surface-v04)). Rather than re-implementing delivery\nguarantees, Cotal uses the native NATS mechanisms: streams for at-least-once and late\njoin, queue groups for anycast load-balancing, KV TTL for liveness ([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding);\nthe reasoning: [presence & delivery](presence-and-delivery.md)). Isolation is one NATS\n**account per space** ([spaces & channels](spaces.md)); authorization is per-agent JWT\nACLs ([identity & auth](identity-and-auth.md)). Bytes too large for a message are referenced by an\n`artifact` part ([SPEC \xA75](../SPEC.md#5-envelopes)), and setup creates a per-space Object Store\nfor them. Nothing in Cotal stores or fetches those bytes yet ([roadmap](roadmap.md)).\n\nWhether any of this *requires* NATS is answered in\n[transport vs protocol](transport.md): the contract is transport-agnostic; NATS/JetStream\nis the reference binding.\n\n## Package layout\n\n```\nexamples \u2500\u2500\u2192 implementations \u2500\u2500\u2192 workspace \u2500\u2500\u2192 core \u2190(peer)\u2500\u2500 extensions\n (interoperate at runtime over NATS, not via imports)\n```\n\n- **`@cotal-ai/core`**, the protocol: subjects, schemas, the NATS client layer, and the\n extension contracts (`Connector`, `Command`, `Runtime`) with the `Registry` they\n self-register into. Depends on nothing else in the repo.\n- **`@cotal-ai/workspace`**, the machine-local operator layer over `~/.cotal`: mesh\n registry, target resolution, auth-path helpers. Not part of the wire standard, so a\n third party can embed core without inheriting workstation plumbing.\n- **`@cotal-ai/lang`**, the workflow language: validator, interpreter, journal. Depends on\n nothing else in the repo.\n- **`@cotal-ai/seat`**, local PTY seat custody: one detached custodian process per seat, the\n authenticated local protocol a manager worker uses to adopt that handle, and the identity-pinned\n reap a successor manager uses on a seat nobody adopted. Depends on PTY libraries, not on core or\n NATS. Linux is the production transport; other platforms throw from this package.\n- **`extensions/*`**: pluggable adapters (connectors, runtimes). Each **peer-depends** on\n core (binding to the host's single core instance) and self-registers on import; an\n unknown agent type **throws**, no silent fallback.\n- **`implementations/*`**, opinionated surfaces over core: the CLI, the manager, the\n delivery daemon, the web dashboard. Implementations never import each other; they meet\n at runtime, in a shared space over NATS. A composition root (the `cotal` binary, or an\n example) wires the pieces it wants.\n- **`examples/*`**: use-cases and composition roots, never published\n ([examples](examples.md)). An example only configures and orchestrates; new message\n kinds or subjects go into core, generalized, never into an example.\n\nThe published binary also loads **operator-installed extensions**: `cotal ext add\n<npm-package>` installs into a cotal-owned prefix, imports once so the package\nself-registers, then caches every contributed `kind:name`. Command metadata is cached for\n`--help`/completion; running a command or requesting a provider imports its owner lazily and\nuses the live object. Before that first import, the loader rebinds shared peers to the current\nhost under the extension-prefix lock; version skew or an unbindable peer fails loudly.\nThe repo's `@cotal-ai/web` dashboard and optional tmux/cmux/Orca/Herdr runtimes use this mechanism.\nRuntime resolution stays registry-driven and open-ended: a name with no registered/installed\nprovider fails loud (never a fallback), and a third-party runtime installs under its own package\nname. The CLI does carry a small, non-authoritative map of the first-party runtime names\n(`orca`/`tmux`/`cmux`/`herdr`) to their `@cotal-ai/*` packages, used only to print an exact `cotal ext add`\nhint for a known-but-uninstalled runtime and to list them in `cotal runtimes`; it never resolves or\nregisters a provider.\n\nMachine-local processes use the same registry. The CLI and its composed native packages contribute broker/control-plane\n`local-process` descriptors, while an installed package contributes its own (for example `web`).\nThat keeps `cotal down <component>` and `cotal status` extensible without teaching the base CLI\npackage-specific pidfiles. A provider process claims its declared pidfile with exclusive create;\nextension removal reserves that same path so startup cannot cross uninstall. A descriptor may set\n`cooperativeStop` to use the workspace's process-bound local cleanup capability on Windows.\nManager and delivery declare it; injected hosted services do not use this workstation channel.\n\nThe workstation service controller also uses `local-process` declarations. A provider can declare\nits closed service option schema, lifecycle capabilities and launch/readiness hooks. Metadata is\ncached at extension installation, with declaration and package-metadata fingerprints. Saving a\nservice definition does not import provider code. An explicit launch loads through the normal\nversion-locked registry path and pins its adapter provenance beside the direct process identity.\nNative readiness remains the adapter's attributed observation, not a new wire authority. Service\nlaunch intents participate in extension removal guards. Delivery registers its own adapter over\nshared workspace custody metadata; CLI stop clients do not import the delivery implementation.\nNative startup receipts are private, request-specific local observations without credentials.\nA trusted embedding captures the manager's selected public identity through `onServiceIdentity`\nbefore endpoint construction. A protected file binds that selection to the service instance,\nlaunch request and real process creation identity. It supplies no epoch, readiness or grant.\nDotted endpoint classes retain their native grammar; encoding is validation, not replacement.\n\nThe manager child exclusively claims its real creation-pinned native PID slot before endpoint\ninitialization; that starting record is not serving evidence. Existing custody is not adopted or\nreclaimed by the child. A record whose pid is dead, as a killed child leaves it, is removed by the\nmanager adapter before the next launch, under the controller's definition lock. It settles that same process only after native start, and release remains owner-bound.\nThe manager adapter correlates that binding with an attributed current instance/epoch response\nand its process lifecycle UID. It preserves the invoking caller, selected root/broker and required\nTLS. The native status policy uses the same fresh owner/admin visibility and process-bound seat\npredicate as native shutdown. Owner-filtered visibility, pending work and maintenance block stop.\nA preserved manager, and one launched for a resume that has not committed, still answers `status`\nthrough the same membership gate, as a counted read that writes nothing; every other command stays\nfenced in those states. A preserving or stopping manager answers nothing.\nThe confirmed full scope is rechecked under the native stop reservation and again in the child.\n\nNative stop hooks bind service, launch and stop IDs plus both process creation identities. Phase\nfiles are create-only, and accepted/returned evidence plus the original process exit is required\nfor a stopped record. A missing terminal remains unknown; generic signal escalation is not a\nnative stop substitute. Reservations are not reclaimed. The adapter releases one only after its own\ncompleted receipt with the original process exit, or a refused receipt as the only one, settles the\nrequest, and only once the process that took it has exited. A request with neither is cleared only\nwhen an operator abandons it by its ID, after the original process and the process that took the\nreservation have both exited, and the record is then recorded failed. Lease, registration and family cleanup\nstill remain not checked in the receipt. The service layer stays workstation-local and adds no\nwire authority. See [CLI services](cli.md#owned-process-services).\n\nThe auth-service adapter loads its existing prepared native projection and claims its per-space\nPID custody in the real child. Its startup phase follows native plane initialization and signal\nhandlers, without exposing the private loopback capability. Daemon input validation and flags are\nshared with the legacy entry. The current adapter refuses remote or TLS-required broker targets\nbecause that native transport has not propagated the required TLS policy through every connection.\nIt does not create account/issuer material or start dependencies.\n\nThe Linear adapter similarly calls its native account/inventory/registration/renewal implementation\nthrough a trusted root and initialization callback. It adds no issuer or generic caller authority.\nIts private initialization phase is process evidence only, not a broker flush or a fresh endpoint\nresponse. User-auth/open targets and OS-service modes remain unsupported. Auth and Linear presets declare\nforeground lifetime through the shared process controller and their native signal handlers. The\nterminal lifetime is printed before launch; output is inherited, not captured in a background log.\nNative service child boot skips connector repair/seed. Native bounded close/deregistration remains\ndistinct from confirmed cleanup. Manager foreground mode still requires its agent-impact flow.\n\nSupervisor receipts are separate protected workstation records bound to a full instance,\ndefinition revision, machine and OS user. Their read view checks recorded unit-file bytes only;\nloaded OS state and autostart remain unchecked. It does not read another execution host's unit\npath, and missing/modified/unreadable files do not authorize direct process fallback or removal.\nThe native install/uninstall and OS control path is not implemented by those receipts.\nAn explicit status read can query only the unchanged recorded systemd user unit on its execution\nhost. The query requests fixed public properties, checks its physical fragment and attributes its\nmain PID against creation-pinned instance/launcher records. Native state, enablement, reload need\nand unverified drop-ins stay distinct; none is endpoint readiness or an authorization to mutate.\nInventories and offline previews do not run the OS query.\nRecorded systemd logs select the original invocation, boot, unit and OS UID through native\njournal fields, without querying or adopting the current invocation. Finite reads and streaming\nrecords are bounded; native cursors/timestamps remain intact and omitted records are gaps.\nLeaving a stream closes its journal reader, not the service. Retention/access completeness and\nnative cleanup remain unestablished. No old direct-child file is substituted for a missing journal.\nThe private direct-process unit entry bypasses extension overlays, seed/reconcile and provider\nmaterialization. Its validated controller identity and recorded native invocation precede process\nreplacement. It does not fork a detached wrapper, and its starting record is not evidence that the\nexecutable ran. Unit installation, native actions and private-store generation changes are not\nsupported.\n\nBare status reads selected-space owned definitions and instance records separately from legacy\npidfiles and remote registrations. It shares the controller's public projections, preserving\nfull instance/host identities and historical readiness times without a fresh serving claim.\nUnreadable families stay unavailable and make the report partial. The read imports no provider,\ncontrols no service and runs no OS unit query; local records grant no remote process ownership.\n\nNamed multi-service stops resolve one selected space and all full owned instances before effects.\nThey reject duplicate names, shared process/native identities, foreign or unresolved custody,\nrecorded OS supervisors and unsupported process roles. Metadata-only previews import no provider\nand take no reservation. Confirmed actions reserve definitions in stable order and recheck every\nrecord and complete native impact before the first stop. Each per-effect native guard remains in\nforce. A later failure leaves subsequent members unattempted; one receipt preserves the known and\nunknown outcomes without rollback, automatic retry or a cleanup claim. Failed/unattempted rows\nretain labeled pre-stop snapshots rather than inferred current state.\n\nThe schema catalog collects native creation inputs for message/control streams, durable registries,\nTTL and membership buckets, contract/authority/session stores and artifacts. Bootstrap uses the same\nbuilders. Its native retention, shadow, immutability, leader-read and artifact checks remain in place.\nThe independent owned-stream and backup inventories are not derived from this catalog.\nA separate desired-creation configuration shares the first-party stream baseline, native subjects,\nvalidated public scalar KV defaults, explicit options and hardening. It rejects unexpanded inputs.\nHardening is included in the create request rather than applied after adopting an existing bucket.\nThis value does not open a connection, create a store, repair drift or authorize apply.\n\nA read-only facade exposes stream-info without connection creation or mutating methods. Its observer\nkeeps structured permission denial, reported not-found, target mismatch and failed reads distinct.\nA shared five-second budget labels unvisited resources without claiming request cancellation.\nIt does not retry with broader credentials. Server/account/mode binding and authoritative absence\nare not established by the supplied port or a claimed authority label.\n\nThe shared diff compares declared stream fields and post-create hardening. It also calls shared\npure checks for backup-stream, contract, authority/session, issued/admission and artifact invariants.\nNative creators use the same checks. The lifecycle binder and schema inspection also share the\npure primary/Limits/no-global-eviction authority-store check. Schema rows name the first failing\ninvariant's fields without copying native diagnostic text or supplied config values. The native\nbind error keeps its existing class, message and check order. This moves no lifecycle operation\nor store access into core. Migration routines and TTL write canaries are not read-only checks\nand are never called by the diff. Fields outside declarative comparison remain visible. For direct\nstreams, extra observed fields are compared with the existing first-party baseline. Nondefault\nundeclared or unmodeled settings block the row without copying their values. Reserved NATS\nmetadata follows the existing backup policy. Missing fields are not filled from desired values,\nand unspecified SDK defaults are not expanded by this comparison. Native KV/object-store\nsubjects and explicit public options map to their documented wire fields, including TTL units,\nmarker TTL and post-create hardening. An unsupported option blocks rather than disappearing;\nthis mapping is not a complete effective SDK configuration or permission to reconcile. The extra-field\npolicy also covers observed KV/object backing-stream fields. It shares the first-party stream\nbaseline and reads only scalar public KV input defaults. Unknown or nondefault fields remain\nblocked. It preserves the native artifact policy that permits either direct-read setting and\nthe native TTL policy that permits a smaller existing duplicate window. It fills no missing\nobserved field and does not call an SDK constructor or claim complete effective defaults.\nThe reconciliation policy names only existing native guard families: TTL enforcement with its\ncanary, the stock legacy artifact cap and contract hardening with its shadow check. It shares\nthe native legacy-cap constant and hardening fields. Other changed fields require migration.\nA required guard is not an accepted action. Inspection calls no migration or canary and cannot\nprove a contract stream clean from configuration alone. Apply still needs a fresh privileged\nbarrier, native guards and readback.\nA native invariant match is not complete schema, target authority or permission to reconcile.\nSDK/server defaults, runtime checks and complete field policy remain outside this partial schema. Separate\ninitialization definitions share native seed ordering, field-merge inputs and profile-default\nprecedence. An explicit existing delivery class wins. Those definitions do not validate patches,\nobserve revisions or make legacy seeding revision guarded. Patch text stays out of public operation\ndiagnostics. A separate bounded registry observer validates native patch inputs before key reads,\nretains observed revisions and tombstones, and distinguishes missing, unavailable, denied,\nmismatched, malformed and unvisited rows. It checks the selected native handle names without\nopening a bucket. A deadline does not prove request cancellation. Existing values are private\nplanning data, and successful native field validation is not complete field policy. Neither a\nnull read nor a tombstone grants creation or write authority. A pure initialization comparison\nuses those observations and the same native seed-before-profile definitions. Known fields are\nclosed over the native channel/default types and validated without changing legacy validators.\nUnmodeled fields block instead of being dropped. An existing valid row can yield a private\ncandidate with its observed revision; a missing, deleted or unreadable row cannot. Explicit\nseed fields overwrite existing fields before the profile fills an absent delivery class.\nThe public projection shows state, changed field names and revisions, never candidate values.\nNo revision guard has run and no candidate is an accepted write plan.\n\nThe prepared CLI inspection adapter resolves an existing open/static registration. It validates\nnative profile/patch inputs before authority preparation. Static mode loads the full current trust\nchain and uses one native temporary provisioner connection with its own inbox. Explicit open mode\nloads no signer, mints no identity and uses no credentials. Both retain the registered TLS policy.\nThe branches are selected before connection, never retried after an auth refusal. User-auth,\noff-registry targets, stripped static trust and reported broker-mode mismatch remain refused.\nBefore static schema reads, a bounded native `$SYS.REQ.USER.INFO` query must match the minted\nuser, selected account and connected server. This uses the provisioner's own connection and\nscoped inbox, not system credentials or an arbitrary-user query. Unavailable, denied or mismatched\nidentity refuses without fallback. The service requires a configured system account. Returned\npermissions/expiry are not adopted as an issued ceiling or lifecycle proof. Explicit open mode\ndoes not invent an account identity or require this system service.\nIt uses bind-only open/get and stream-info without create or repair. A reported server ID alone\nis not account proof; local reader closure does not prove credential revocation. The native\nidentity observation does not authorize apply or establish definitive resource absence.\nNo public command is connected to this adapter yet. Its prepared public projection and shared\nterminal view omit raw configs, registry values, private patches, unknown extra-field names and\nnative diagnostic bodies. They label non-atomic reads and incomplete policy without a readiness\nor successful-apply claim.\n\nThe catalog, observers and diff cannot authorize writes. Revision-bound initialization, privileged\napply and stack/update integration are not supported.\n\nStandalone endpoint caller minting uses the same static-root/account policy as fixed-profile\nminting. A native control instrument resolves and digest-verifies the live surface before issuance.\nThe supported subset is untargeted synchronous ephemeral commands; a surface requiring target,\njournal or action authority refuses in full. Core's agent provisioner supplies the lifecycle\nfootprint and exact additional endpoint rows, not a hand-written grant profile. A create-only\nnon-secret local intent precedes footprint work and prevents blind replay after interruption.\nPrivate output, provisioner close and unconfirmed cleanup remain separate observations. The paired\nfile consumer checks the full selected static trust chain, native signature/current signer,\nmatching user key and time window. It reconstructs permissions with the native builder and checks\nprincipal tags before the broker connection. A local intent selects the comparison, never authority.\nOnly fresh synchronous untargeted methods within that native scope can be called; changed shapes\nand broader flags refuse without repair or caller fallback. No arbitrary raw-file triple, accepted\ngeneration, live lifecycle currency, managed launch or user-auth issuer is inferred.\n\nThe dashboard service takes one explicit protected static credential, not a cached operator or\nseat identity. Its service mode is explicitly read-only and retains the registered TLS requirement.\nThe protected file must bind to the root's current validated static issuer and matching user key\nbefore native initialization; no signer is forwarded to a handler or used for replacement minting.\nIt never mints a purger credential or enters the native channel-delete transport. Browser bootstrap\nand readiness secrets stay in a create-only owner-private full-launch record. Native HTTP listen\nand installed signal handlers precede its process-initialization phase. That phase is not a fresh\nauthenticated endpoint response. User-auth caller handoff and OS supervision remain unsupported.\nOld cached web commands refuse before package loading; the replacement is the declarative service\npreset, not an implicit reinstall or a second launcher.\n\nBeyond the app-bound connectors, `@cotal-ai/pi` is a **host-native plugin**: a pi extension\nloaded into the user's own pi (CLI or SDK-embedded), placing a Cotal endpoint inside the\nsession's process and driving its run loop off the inbox. See\n[connect-pi](connect-pi.md).\n\n## Connector runtime\n\nEvery coding-agent integration exposes the same four surfaces:\n\n| Surface | Carries |\n|---|---|\n| Outbound, ambient | lifecycle \u2192 presence and activity, automatically |\n| Outbound, deliberate | the messaging tools (`cotal_send` / `cotal_dm` / `cotal_anycast`) |\n| Inbound, pull | `cotal_inbox` |\n| Inbound, push | wake-and-inject into the live session |\n\nThe shared runtime lives in [`@cotal-ai/connector-core`](../extensions/connector-core):\nthe mesh agent, the [`cotal_*` tool surface](mcp-tools.md) (defined once in its tool\nspecs, so it cannot drift across hosts), and the delivery buffer with its attention\npolicy. Each adapter is a thin client\nover it that binds to its host's native mechanism: an installed plugin + MCP server for\n[Claude Code](connect-claude.md), an in-process plugin for\n[OpenCode](connect-opencode.md) (beta), a host-mode peer driving `codex app-server` for\n[Codex](connect-codex.md) (beta), a Python sidecar for\n[Hermes](connect-hermes.md) (alpha), a host-mode peer driving the Jcode Harness API for\n[Jcode](connect-jcode.md) (beta), a host-native extension for\n[pi](connect-pi.md) (alpha). The [connectors matrix](connectors.md) compares them\nfeature-by-feature.\n\nReplies carry their correlation ([SPEC \xA75](../SPEC.md#5-envelopes)). A `cotal_dm` to a peer\nanswers the message its `replyTo` argument names, which must be one that peer sent you and no DM\nback has answered yet. The argument wins over a message the connector bound the call to. Without\nthe argument it answers the oldest such message, but only while all\nof that peer's waiting messages belong to one conversation (`contextId`). When they belong to more\nthan one, the DM is refused with their ids, so an answer is never put in a conversation by guess.\nThe reply names the message in `replyTo` and copies its `contextId`, which belongs to the asker.\nThe message counts as answered once the reply is published, so a failed send leaves it for the\nretry. Until then it is still waiting, and a DM without `replyTo` sent meanwhile is refused if\nanother conversation is waiting too. With nothing to answer, a DM carries the connector's own\n`contextId`, if it sets one. A connector that runs several host sessions on one seat (Hermes)\nstamps each question with a `contextId` of its own, and routes an answer back to the session that\nasked only when the answer copies it and comes from the peer the question went to.\n\nThe endpoint underneath self-heals: when the transport connection dies terminally, a\nsupervisor rebuilds it (rebuilds are serialized and coalesced), and unacked in-flight\nmessages redeliver on the rebound durables, so nothing is lost across the gap. A failed\npost-connect bind closes its partial transport before the retry loop starts another attempt.\nA manual `/reconnect` is the human-invoked counterpart.\n\n## Manager supervision\n\nThe CLI does not spawn agents itself; a long-lived **manager** owns their lifecycle,\nasked over the mesh. The manager is not a privileged control plane: it is an ordinary\nservice endpoint on the same `ep` rails as any other daemon\n([\xA713](../SPEC.md#13-endpoint-control-surface-v04)), holding only the capability rows its\ncallers grant it. It owns process lifecycle and config binding (start / stop / restart,\nbinding env and policy) and has no say in what work the agents do. Agents coordinate\nlaterally; the manager only births and configures them.\n\n- **Off the message hot path.** Each agent self-connects to the mesh through its own\n connector. The manager owns processes in order to control them, but observes everything\n through presence, so a bring-your-own-terminal agent it never spawned still shows up in\n `ps`.\n- **Pluggable runtimes.** Spawning is abstracted behind a `Runtime` contract (like pm2 or\n docker for agent TUIs): **`pty`** ships built-in (the manager owns the pseudo-terminal\n in-process on every platform; watch or type via `cotal attach`; on Linux it still adopts\n seats an earlier manager left under a detached custodian, and elsewhere `adopt` throws); **`tmux`**, **`cmux`**, **`orca`**, and **`herdr`** are\n extensions that put each teammate in its own native terminal surface (explicit opt-ins\n that throw when the extension isn't loaded, never a silent fallback); **byo** is the\n floor (a human's own terminal, tracked via presence); **host** (Agent SDK, true mid-turn\n interrupt) is the documented upgrade path ([roadmap](roadmap.md)).\n- **Served commands.** `spawn` (an action, below), `stop`, `ps`, `status`, `attach`,\n `models`, and `define-persona` are among its endpoint commands\n ([\xA713.5](../SPEC.md#135-verbs)) any authorized node can send, policy-gated\n ([identity & auth](identity-and-auth.md)). A caller learns them off the wire with `cotal\n describe manager`; nothing is compiled in.\n- **Spawn is an action.** Asking for an agent no longer blocks the caller while the process\n comes up. The manager accepts a spawn **goal** ([\xA713.6](../SPEC.md#136-composites)) and\n immediately returns the allocated identity (the agent's name, its `owner`/`actor`/`uid`\n triple, a `goalId`, and the executor coordinate `{lifecycleUid, epoch}`); progress events\n then report the launch until a terminal outcome. Presence within the readiness window is\n `succeeded`, an early exit is `failed`, and the window passing with neither is\n `uncertain`: a bounded, reconcilable outcome a later `ps` settles against the live roster,\n never a silent hang.\n- **Bounded spawn.** A gate caps concurrent and in-flight agents and a minimum-lifetime\n floor bounds spawn/despawn churn, so a capability-holding but compromised peer cannot\n fork-bomb the host. The gate runs at goal acceptance, before any identity is minted or\n process launched, so a refused spawn leaves nothing behind. A refusal states what holds the\n slots and whether waiting can free one.\n- **Declared environment boundary.** A spawned agent receives a fixed OS allow-list (PATH/HOME/\n locale, including PATH entries connector binaries live in), the machine-wide `COTAL_*` operator\n knobs, connector-declared provider inputs, explicitly shared MCP references, and names\n deliberately added through `spawn.env`. It never inherits the manager's ambient environment, so\n host-session markers (`CLAUDE_CODE_CHILD_SESSION` and the analogous names other hosts use) and\n unrelated capabilities cannot become properties of every seat. Connection material rides a private\n file instead of the environment.\n- **Instance addressing.** One space can hold more than one manager. Each keeps a stable\n logical instance id across restarts and advances its process epoch when it comes back, so\n peers address a specific manager without caring which process currently serves it. `cotal\n spawn <persona> --detach --on <instance>` pins one instance (`ps`, `stop`, `attach`, `models`\n and the hosted `run` verbs take the same flag), and MCP `cotal_spawn`, `cotal_run` and\n `cotal_personas` accept the equivalent `instance` argument. An untargeted spawn rides class anycast and the acceptance records which\n instance took it. `ps` and `status` scatter across every registered instance and label a\n non-answering one as registered with no answer within the deadline, never dropping it.\n- **A manager holds a liveness lease, and only losing it to another process ends the process.**\n Each instance keeps its own key in the space's manager bucket and refreshes it several times\n over inside the key's TTL. A refresh that fails is a question, not a verdict, so the manager\n re-reads the key before deciding what to do. If the key is still its own it adopts the broker's\n revision and carries on. If the key is gone (it expired during a stall) it puts it back. If\n another process holds it, that process took the instance over during the stall, so this one\n says so and exits without releasing the lease or the registration, which are the successor's\n now. If the broker cannot be asked at all it keeps serving and asks again, for as long as that\n takes. A manager that cannot reach its broker gains nothing by ending itself, and the seats it\n holds lose everything. Each change of state is one line in the manager's log, not one line per\n tick.\n A clean stop waits for a renew already in flight and then releases the key at the broker's own\n revision, so a same-root restart never waits out the bucket TTL.\n- **Attach is a mesh session.** The console and dashboard discover agents over the **mesh**\n (presence, `ps`). `cotal attach` no longer hands back a `127.0.0.1` URL: it redeems a\n one-use, holder-bound session offer, and the terminal bytes stream over the mesh on\n core-NATS session subjects scoped to the two parties, with backpressure surfaced as an\n explicit drop notice rather than silent loss. Attach reaches managers on other machines through\n the broker. The manager's own socket stays private. A late\n attach still repaints the full screen from a replayed snapshot of a headless terminal\n mirror (including alternate-screen TUIs). If the manager restarts, its successor refuses\n the old session. `cotal attach` reconnects when its link dies by asking the manager for a new\n session ([CLI](cli.md#managed-seats)).\n- **The manager's console face is a separate, credentialed surface.** The manager still\n serves the browser console over local HTTP: the static page plus the roster, the live feed,\n and the route that mints the browser's own session. It binds loopback unless the operator\n says otherwise (`cotal supervise --console-host`), and every route that carries mesh data\n or mints a credential requires the manager's console token.\n\nThe result is that an agent can grow and shape its own team: ask for a teammate\n(`cotal_spawn`), mint a persona on the fly (`cotal_persona`), or tear one down\n(`cotal_despawn`). Every newcomer joins as a peer, not as a child of whoever requested\nit. Each managed agent runs under a durable **lifecycle**: a despawn retires it (settling\nand evicting the old incarnation) before its name frees for reuse, and a supervised restart\nrecovers the same lifecycle rather than minting a new one, so durables and credentials key\non the lifecycle, not the reusable name ([SPEC \xA713.1](../SPEC.md#131-lifecycle-identity);\n[identity & auth](identity-and-auth.md)). Destructive space-wide operations (history purge)\nstay operator-only.\n\n\n## Observers\n\nA watch surface is a read-only observer: an endpoint that consumes without registering\npresence (invisible to peers) while watching everyone else's. All three surfaces\n(terminal console, plain stream, web dashboard) derive from that one observer through a\nshared render-agnostic model, so no surface re-implements wire semantics. The guide is\n[watch a mesh](watch-a-mesh.md); the model is [MeshView](mesh-view.md).\n\n## Addressing\n\nThree identity layers, in increasing permanence\n([SPEC \xA72](../SPEC.md#2-identity), [\xA76](../SPEC.md#6-presence-and-discovery)):\n\n- **`name`** is a cosmetic, reusable human handle. Addressing by name is best-effort\n convenience, with deterministic and fail-loud resolution: a unique live name resolves,\n and a collision among live peers throws with the candidate ids rather than silently\n picking one. The manager auto-numbers its own spawns (`reviewer` \u2192 `reviewer_2`).\n The connector and the CLI print a peer with one label (in the roster, message attribution,\n `cotal endpoints` and the console): `name/role`, or the bare name when it has no role. A\n control character, Unicode line or paragraph separator, or bracket in either part prints as a\n space. That label is not an address, because `/` is reserved for `owner/name` handles: a target\n written that way is refused, and the refusal gives the bare name.\n- **`role`** is the addressable service, which makes it the anycast address:\n `svc.reviewer` reaches \"whoever is a reviewer\", so the label carries routing meaning.\n- **The instance id** is the authoritative address: the presence key, the unicast target,\n the credential subject.\n\n**Instance continuity:** the id tracks *context* continuity, not the label. A resumed\nsession (same context window) keeps its id; presence, thread correlation, and in-flight\nDMs stay continuous. A fresh context, even reusing the name, is a **new** instance with a\nnew id: reusing an id across a discontinuous context would tell peers \"same agent, same\nmemory\" when the new session has none. One deliberate exception: OpenCode's `/new` inside\nthe same managed process keeps the mesh identity and advances only the thread correlation\nid: process continuity, not credential reuse.\n\n## Deferred\n\nSessions/moderator, signed envelopes + DID identity, instant offline, artifact delivery,\njoin-link bootstrap tokens for static meshes, and federation are designed for but not built yet;\neach is tracked, with its direction, in the [roadmap](roadmap.md).\n"
|
|
27237
|
+
"body": "# Architecture\n\n> **Concept** (informative) \xB7 **For:** anyone who wants to know how Cotal is built, and why \xB7 **Normative:** [SPEC](../SPEC.md)\n\nCotal is built as a thin waist: the normative wire contract (subjects, message schemas,\npresence/discovery, delivery semantics, the auth grammar) is the standard\n([SPEC](../SPEC.md)), and everything else is a pluggable edge over existing building\nblocks. Identity, transport, storage, and discovery compose from proven pieces (NATS,\nJetStream, JWT/nkeys) rather than being reinvented. Adapters stay thin and swappable, and\nnothing adapter-specific leaks into the core.\n\n## A2A influence\n\nCotal reuses A2A's vocabulary and shapes so it stays interoperable rather than siloed, and\nimplements them over NATS/JetStream.\n\n**From A2A** come the *data shapes*: `AgentCard` (identity / role / tags / skills),\n`Message` / `Part` (text and data), and correlation ids (`contextId`). We do not adopt\nA2A's HTTP/JSON-RPC transport, `Task` RPCs, or its request/response server model, none of\nwhich fit lateral pub/sub.\n\nThe *addressing model* is Cotal's own: the hierarchical address `space / service / instance`\nand three delivery modes, multicast, unicast, anycast\n([presence & delivery](presence-and-delivery.md)). **Mentions** are a priority hint on a\nmulticast, not a routing target. NATS/JetStream is the data plane, adding the durability and\npresence a bare pub/sub layer leaves to the app.\n\nIdentity is an A2A `AgentCard` whose instance id is shaped to later become a **DID**\n(`did:key`) so authenticity can survive an untrusted relay ([roadmap](roadmap.md)).\n\n## NATS mapping\n\nThe messaging plane rides three subject kinds, with the sender encoded in the subject\nitself, where the server can police it, rather than in a self-asserted payload field\n([SPEC \xA73](../SPEC.md#3-subject-layout)); the endpoint control surface adds its own rails\n([SPEC \xA713](../SPEC.md#13-endpoint-control-surface-v04)):\n\n| Delivery | Subject |\n|---|---|\n| multicast | `cotal.<space>.chat.<owner>.<actor>.<channel\u2026>` |\n| unicast | `cotal.<space>.inst.<toOwner>.<toActor>.<owner>.<actor>` |\n| anycast | `cotal.<space>.svc.<role>.<owner>.<actor>` |\n| endpoint (control) | `cotal.<space>.ep.<one\\|all\\|inst\\|reply>.\u2026` ([\xA713.2](../SPEC.md#132-grammar)) |\n\nThe sender is a **principal**, an `owner.actor` pair: the account the agent acts on behalf\nof, then the agent's own handle under it ([identity & auth](identity-and-auth.md)). Two\ntokens instead of one means the broker can deny cross-owner *and* same-owner cross-actor\nforgery in the subject grammar itself.\n\nBehind the subjects, each space gets three **JetStream streams** (chat / DM / task, for\nstorage, per-reader bookmarks, and history), **KV buckets** for presence and the channel\nregistry, and the endpoint control surface on its own rails and streams\n([SPEC \xA713](../SPEC.md#13-endpoint-control-surface-v04)). Rather than re-implementing delivery\nguarantees, Cotal uses the native NATS mechanisms: streams for at-least-once and late\njoin, queue groups for anycast load-balancing, KV TTL for liveness ([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding);\nthe reasoning: [presence & delivery](presence-and-delivery.md)). Isolation is one NATS\n**account per space** ([spaces & channels](spaces.md)); authorization is per-agent JWT\nACLs ([identity & auth](identity-and-auth.md)). Bytes too large for a message are referenced by an\n`artifact` part ([SPEC \xA75](../SPEC.md#5-envelopes)), and setup creates a per-space Object Store\nfor them. Nothing in Cotal stores or fetches those bytes yet ([roadmap](roadmap.md)).\n\nWhether any of this *requires* NATS is answered in\n[transport vs protocol](transport.md): the contract is transport-agnostic; NATS/JetStream\nis the reference binding.\n\n## Package layout\n\n```\nexamples \u2500\u2500\u2192 implementations \u2500\u2500\u2192 workspace \u2500\u2500\u2192 core \u2190(peer)\u2500\u2500 extensions\n (interoperate at runtime over NATS, not via imports)\n```\n\n- **`@cotal-ai/core`**, the protocol: subjects, schemas, the NATS client layer, and the\n extension contracts (`Connector`, `Command`, `Runtime`) with the `Registry` they\n self-register into. Depends on nothing else in the repo.\n- **`@cotal-ai/workspace`**, the machine-local operator layer over `~/.cotal`: mesh\n registry, target resolution, auth-path helpers. Not part of the wire standard, so a\n third party can embed core without inheriting workstation plumbing.\n- **`@cotal-ai/lang`**, the workflow language: validator, interpreter, journal. Depends on\n nothing else in the repo.\n- **`@cotal-ai/seat`**, local PTY seat custody: one detached custodian process per seat, the\n authenticated local protocol a manager worker uses to adopt that handle, and the identity-pinned\n reap a successor manager uses on a seat nobody adopted. Depends on PTY libraries, not on core or\n NATS. Linux is the production transport; other platforms throw from this package.\n- **`extensions/*`**: pluggable adapters (connectors, runtimes). Each **peer-depends** on\n core (binding to the host's single core instance) and self-registers on import; an\n unknown agent type **throws**, no silent fallback.\n- **`implementations/*`**, opinionated surfaces over core: the CLI, the manager, the\n delivery daemon, the web dashboard. Implementations never import each other; they meet\n at runtime, in a shared space over NATS. A composition root (the `cotal` binary, or an\n example) wires the pieces it wants.\n- **`examples/*`**: use-cases and composition roots, never published\n ([examples](examples.md)). An example only configures and orchestrates; new message\n kinds or subjects go into core, generalized, never into an example.\n\nThe published binary also loads **operator-installed extensions**: `cotal ext add\n<npm-package>` installs into a cotal-owned prefix, imports once so the package\nself-registers, then caches every contributed `kind:name`. Command metadata is cached for\n`--help`/completion; running a command or requesting a provider imports its owner lazily and\nuses the live object. Before that first import, the loader rebinds shared peers to the current\nhost under the extension-prefix lock; version skew or an unbindable peer fails loudly.\nThe repo's `@cotal-ai/web` dashboard and optional tmux/cmux/Orca/Herdr runtimes use this mechanism.\nRuntime resolution stays registry-driven and open-ended: a name with no registered/installed\nprovider fails loud (never a fallback), and a third-party runtime installs under its own package\nname. The CLI does carry a small, non-authoritative map of the first-party runtime names\n(`orca`/`tmux`/`cmux`/`herdr`) to their `@cotal-ai/*` packages, used only to print an exact `cotal ext add`\nhint for a known-but-uninstalled runtime and to list them in `cotal runtimes`; it never resolves or\nregisters a provider.\n\nMachine-local processes use the same registry. The CLI and its composed native packages contribute broker/control-plane\n`local-process` descriptors, while an installed package contributes its own (for example `web`).\nThat keeps `cotal down <component>` and `cotal status` extensible without teaching the base CLI\npackage-specific pidfiles. A provider process claims its declared pidfile with exclusive create;\nextension removal reserves that same path so startup cannot cross uninstall. A descriptor may set\n`cooperativeStop` to use the workspace's process-bound local cleanup capability on Windows.\nManager and delivery declare it; injected hosted services do not use this workstation channel.\n\nThe workstation service controller also uses `local-process` declarations. A provider can declare\nits closed service option schema, lifecycle capabilities and launch/readiness hooks. Metadata is\ncached at extension installation, with declaration and package-metadata fingerprints. Saving a\nservice definition does not import provider code. An explicit launch loads through the normal\nversion-locked registry path and pins its adapter provenance beside the direct process identity.\nNative readiness remains the adapter's attributed observation, not a new wire authority. Service\nlaunch intents participate in extension removal guards. Delivery registers its own adapter over\nshared workspace custody metadata; CLI stop clients do not import the delivery implementation.\nNative startup receipts are private, request-specific local observations without credentials.\nA trusted embedding captures the manager's selected public identity through `onServiceIdentity`\nbefore endpoint construction. A protected file binds that selection to the service instance,\nlaunch request and real process creation identity. It supplies no epoch, readiness or grant.\nDotted endpoint classes retain their native grammar; encoding is validation, not replacement.\n\nThe manager child exclusively claims its real creation-pinned native PID slot before endpoint\ninitialization; that starting record is not serving evidence. Existing custody is not adopted or\nreclaimed by the child. A record whose pid is dead, as a killed child leaves it, is removed by the\nmanager adapter before the next launch, under the controller's definition lock. It settles that same process only after native start, and release remains owner-bound.\nThe manager adapter correlates that binding with an attributed current instance/epoch response\nand its process lifecycle UID. It preserves the invoking caller, selected root/broker and required\nTLS. The native status policy uses the same fresh owner/admin visibility and process-bound seat\npredicate as native shutdown. Owner-filtered visibility, pending work and maintenance block stop.\nA preserved manager, and one launched for a resume that has not committed, still answers `status`\nthrough the same membership gate, as a counted read that writes nothing; every other command stays\nfenced in those states. A preserving or stopping manager answers nothing.\nThe confirmed full scope is rechecked under the native stop reservation and again in the child.\n\nNative stop hooks bind service, launch and stop IDs plus both process creation identities. Phase\nfiles are create-only, and accepted/returned evidence plus the original process exit is required\nfor a stopped record. A missing terminal remains unknown; generic signal escalation is not a\nnative stop substitute. A reservation is reclaimed only when the process that took it has exited\nbefore it published a request, so it handed the child nothing. Otherwise the adapter releases one only after its own\ncompleted receipt with the original process exit, or a refused receipt as the only one, settles the\nrequest, and only once the process that took it has exited. A request with neither is cleared only\nwhen an operator abandons it by its ID, after the original process and the process that took the\nreservation have both exited, and the record is then recorded failed. Lease, registration and family cleanup\nstill remain not checked in the receipt. The service layer stays workstation-local and adds no\nwire authority. See [CLI services](cli.md#owned-process-services).\n\nThe auth-service adapter loads its existing prepared native projection and claims its per-space\nPID custody in the real child. Its startup phase follows native plane initialization and signal\nhandlers, without exposing the private loopback capability. Daemon input validation and flags are\nshared with the legacy entry. The current adapter refuses remote or TLS-required broker targets\nbecause that native transport has not propagated the required TLS policy through every connection.\nIt does not create account/issuer material or start dependencies.\n\nThe Linear adapter similarly calls its native account/inventory/registration/renewal implementation\nthrough a trusted root and initialization callback. It adds no issuer or generic caller authority.\nIts private initialization phase is process evidence only, not a broker flush or a fresh endpoint\nresponse. User-auth/open targets and OS-service modes remain unsupported. Auth and Linear presets declare\nforeground lifetime through the shared process controller and their native signal handlers. The\nterminal lifetime is printed before launch; output is inherited, not captured in a background log.\nNative service child boot skips connector repair/seed. Native bounded close/deregistration remains\ndistinct from confirmed cleanup. Manager foreground mode still requires its agent-impact flow.\n\nSupervisor receipts are separate protected workstation records bound to a full instance,\ndefinition revision, machine and OS user. Their read view checks recorded unit-file bytes only;\nloaded OS state and autostart remain unchecked. It does not read another execution host's unit\npath, and missing/modified/unreadable files do not authorize direct process fallback or removal.\nThe native install/uninstall and OS control path is not implemented by those receipts.\nAn explicit status read can query only the unchanged recorded systemd user unit on its execution\nhost. The query requests fixed public properties, checks its physical fragment and attributes its\nmain PID against creation-pinned instance/launcher records. Native state, enablement, reload need\nand unverified drop-ins stay distinct; none is endpoint readiness or an authorization to mutate.\nInventories and offline previews do not run the OS query.\nRecorded systemd logs select the original invocation, boot, unit and OS UID through native\njournal fields, without querying or adopting the current invocation. Finite reads and streaming\nrecords are bounded; native cursors/timestamps remain intact and omitted records are gaps.\nLeaving a stream closes its journal reader, not the service. Retention/access completeness and\nnative cleanup remain unestablished. No old direct-child file is substituted for a missing journal.\nThe private direct-process unit entry bypasses extension overlays, seed/reconcile and provider\nmaterialization. Its validated controller identity and recorded native invocation precede process\nreplacement. It does not fork a detached wrapper, and its starting record is not evidence that the\nexecutable ran. Unit installation, native actions and private-store generation changes are not\nsupported.\n\nBare status reads selected-space owned definitions and instance records separately from legacy\npidfiles and remote registrations. It shares the controller's public projections, preserving\nfull instance/host identities and historical readiness times without a fresh serving claim.\nUnreadable families stay unavailable and make the report partial. The read imports no provider,\ncontrols no service and runs no OS unit query; local records grant no remote process ownership.\n\nNamed multi-service stops resolve one selected space and all full owned instances before effects.\nThey reject duplicate names, shared process/native identities, foreign or unresolved custody,\nrecorded OS supervisors and unsupported process roles. Metadata-only previews import no provider\nand take no reservation. Confirmed actions reserve definitions in stable order and recheck every\nrecord and complete native impact before the first stop. Each per-effect native guard remains in\nforce. A later failure leaves subsequent members unattempted; one receipt preserves the known and\nunknown outcomes without rollback, automatic retry or a cleanup claim. Failed/unattempted rows\nretain labeled pre-stop snapshots rather than inferred current state.\n\nThe schema catalog collects native creation inputs for message/control streams, durable registries,\nTTL and membership buckets, contract/authority/session stores and artifacts. Bootstrap uses the same\nbuilders. Its native retention, shadow, immutability, leader-read and artifact checks remain in place.\nThe independent owned-stream and backup inventories are not derived from this catalog.\nA separate desired-creation configuration shares the first-party stream baseline, native subjects,\nvalidated public scalar KV defaults, explicit options and hardening. It rejects unexpanded inputs.\nHardening is included in the create request rather than applied after adopting an existing bucket.\nThis value does not open a connection, create a store, repair drift or authorize apply.\n\nA read-only facade exposes stream-info without connection creation or mutating methods. Its observer\nkeeps structured permission denial, reported not-found, target mismatch and failed reads distinct.\nA shared five-second budget labels unvisited resources without claiming request cancellation.\nIt does not retry with broader credentials. Server/account/mode binding and authoritative absence\nare not established by the supplied port or a claimed authority label.\n\nThe shared diff compares declared stream fields and post-create hardening. It also calls shared\npure checks for backup-stream, contract, authority/session, issued/admission and artifact invariants.\nNative creators use the same checks. The lifecycle binder and schema inspection also share the\npure primary/Limits/no-global-eviction authority-store check. Schema rows name the first failing\ninvariant's fields without copying native diagnostic text or supplied config values. The native\nbind error keeps its existing class, message and check order. This moves no lifecycle operation\nor store access into core. Migration routines and TTL write canaries are not read-only checks\nand are never called by the diff. Fields outside declarative comparison remain visible. For direct\nstreams, extra observed fields are compared with the existing first-party baseline. Nondefault\nundeclared or unmodeled settings block the row without copying their values. Reserved NATS\nmetadata follows the existing backup policy. Missing fields are not filled from desired values,\nand unspecified SDK defaults are not expanded by this comparison. Native KV/object-store\nsubjects and explicit public options map to their documented wire fields, including TTL units,\nmarker TTL and post-create hardening. An unsupported option blocks rather than disappearing;\nthis mapping is not a complete effective SDK configuration or permission to reconcile. The extra-field\npolicy also covers observed KV/object backing-stream fields. It shares the first-party stream\nbaseline and reads only scalar public KV input defaults. Unknown or nondefault fields remain\nblocked. It preserves the native artifact policy that permits either direct-read setting and\nthe native TTL policy that permits a smaller existing duplicate window. It fills no missing\nobserved field and does not call an SDK constructor or claim complete effective defaults.\nThe reconciliation policy names only existing native guard families: TTL enforcement with its\ncanary, the stock legacy artifact cap and contract hardening with its shadow check. It shares\nthe native legacy-cap constant and hardening fields. Other changed fields require migration.\nA required guard is not an accepted action. Inspection calls no migration or canary and cannot\nprove a contract stream clean from configuration alone. Apply still needs a fresh privileged\nbarrier, native guards and readback.\nA native invariant match is not complete schema, target authority or permission to reconcile.\nSDK/server defaults, runtime checks and complete field policy remain outside this partial schema. Separate\ninitialization definitions share native seed ordering, field-merge inputs and profile-default\nprecedence. An explicit existing delivery class wins. Those definitions do not validate patches,\nobserve revisions or make legacy seeding revision guarded. Patch text stays out of public operation\ndiagnostics. A separate bounded registry observer validates native patch inputs before key reads,\nretains observed revisions and tombstones, and distinguishes missing, unavailable, denied,\nmismatched, malformed and unvisited rows. It checks the selected native handle names without\nopening a bucket. A deadline does not prove request cancellation. Existing values are private\nplanning data, and successful native field validation is not complete field policy. Neither a\nnull read nor a tombstone grants creation or write authority. A pure initialization comparison\nuses those observations and the same native seed-before-profile definitions. Known fields are\nclosed over the native channel/default types and validated without changing legacy validators.\nUnmodeled fields block instead of being dropped. An existing valid row can yield a private\ncandidate with its observed revision; a missing, deleted or unreadable row cannot. Explicit\nseed fields overwrite existing fields before the profile fills an absent delivery class.\nThe public projection shows state, changed field names and revisions, never candidate values.\nNo revision guard has run and no candidate is an accepted write plan.\n\nThe prepared CLI inspection adapter resolves an existing open/static registration. It validates\nnative profile/patch inputs before authority preparation. Static mode loads the full current trust\nchain and uses one native temporary provisioner connection with its own inbox. Explicit open mode\nloads no signer, mints no identity and uses no credentials. Both retain the registered TLS policy.\nThe branches are selected before connection, never retried after an auth refusal. User-auth,\noff-registry targets, stripped static trust and reported broker-mode mismatch remain refused.\nBefore static schema reads, a bounded native `$SYS.REQ.USER.INFO` query must match the minted\nuser, selected account and connected server. This uses the provisioner's own connection and\nscoped inbox, not system credentials or an arbitrary-user query. Unavailable, denied or mismatched\nidentity refuses without fallback. The service requires a configured system account. Returned\npermissions/expiry are not adopted as an issued ceiling or lifecycle proof. Explicit open mode\ndoes not invent an account identity or require this system service.\nIt uses bind-only open/get and stream-info without create or repair. A reported server ID alone\nis not account proof; local reader closure does not prove credential revocation. The native\nidentity observation does not authorize apply or establish definitive resource absence.\nNo public command is connected to this adapter yet. Its prepared public projection and shared\nterminal view omit raw configs, registry values, private patches, unknown extra-field names and\nnative diagnostic bodies. They label non-atomic reads and incomplete policy without a readiness\nor successful-apply claim.\n\nThe catalog, observers and diff cannot authorize writes. Revision-bound initialization, privileged\napply and stack/update integration are not supported.\n\nStandalone endpoint caller minting uses the same static-root/account policy as fixed-profile\nminting. A native control instrument resolves and digest-verifies the live surface before issuance.\nThe supported subset is untargeted synchronous ephemeral commands; a surface requiring target,\njournal or action authority refuses in full. Core's agent provisioner supplies the lifecycle\nfootprint and exact additional endpoint rows, not a hand-written grant profile. A create-only\nnon-secret local intent precedes footprint work and prevents blind replay after interruption.\nPrivate output, provisioner close and unconfirmed cleanup remain separate observations. The paired\nfile consumer checks the full selected static trust chain, native signature/current signer,\nmatching user key and time window. It reconstructs permissions with the native builder and checks\nprincipal tags before the broker connection. A local intent selects the comparison, never authority.\nOnly fresh synchronous untargeted methods within that native scope can be called; changed shapes\nand broader flags refuse without repair or caller fallback. No arbitrary raw-file triple, accepted\ngeneration, live lifecycle currency, managed launch or user-auth issuer is inferred.\n\nThe dashboard service takes one explicit protected static credential, not a cached operator or\nseat identity. Its service mode is explicitly read-only and retains the registered TLS requirement.\nThe protected file must bind to the root's current validated static issuer and matching user key\nbefore native initialization; no signer is forwarded to a handler or used for replacement minting.\nIt never mints a purger credential or enters the native channel-delete transport. Browser bootstrap\nand readiness secrets stay in a create-only owner-private full-launch record. Native HTTP listen\nand installed signal handlers precede its process-initialization phase. That phase is not a fresh\nauthenticated endpoint response. User-auth caller handoff and OS supervision remain unsupported.\nOld cached web commands refuse before package loading; the replacement is the declarative service\npreset, not an implicit reinstall or a second launcher.\n\nBeyond the app-bound connectors, `@cotal-ai/pi` is a **host-native plugin**: a pi extension\nloaded into the user's own pi (CLI or SDK-embedded), placing a Cotal endpoint inside the\nsession's process and driving its run loop off the inbox. See\n[connect-pi](connect-pi.md).\n\n## Connector runtime\n\nEvery coding-agent integration exposes the same four surfaces:\n\n| Surface | Carries |\n|---|---|\n| Outbound, ambient | lifecycle \u2192 presence and activity, automatically |\n| Outbound, deliberate | the messaging tools (`cotal_send` / `cotal_dm` / `cotal_anycast`) |\n| Inbound, pull | `cotal_inbox` |\n| Inbound, push | wake-and-inject into the live session |\n\nThe shared runtime lives in [`@cotal-ai/connector-core`](../extensions/connector-core):\nthe mesh agent, the [`cotal_*` tool surface](mcp-tools.md) (defined once in its tool\nspecs, so it cannot drift across hosts), and the delivery buffer with its attention\npolicy. Each adapter is a thin client\nover it that binds to its host's native mechanism: an installed plugin + MCP server for\n[Claude Code](connect-claude.md), an in-process plugin for\n[OpenCode](connect-opencode.md) (beta), a host-mode peer driving `codex app-server` for\n[Codex](connect-codex.md) (beta), a Python sidecar for\n[Hermes](connect-hermes.md) (alpha), a host-mode peer driving the Jcode Harness API for\n[Jcode](connect-jcode.md) (beta), a host-native extension for\n[pi](connect-pi.md) (alpha). The [connectors matrix](connectors.md) compares them\nfeature-by-feature.\n\nReplies carry their correlation ([SPEC \xA75](../SPEC.md#5-envelopes)). A `cotal_dm` to a peer\nanswers the message its `replyTo` argument names, which must be one that peer sent you and no DM\nback has answered yet. The argument wins over a message the connector bound the call to. Without\nthe argument it answers the oldest such message, but only while all\nof that peer's waiting messages belong to one conversation (`contextId`). When they belong to more\nthan one, the DM is refused with their ids, so an answer is never put in a conversation by guess.\nThe reply names the message in `replyTo` and copies its `contextId`, which belongs to the asker.\nThe message counts as answered once the reply is published, so a failed send leaves it for the\nretry. Until then it is still waiting, and a DM without `replyTo` sent meanwhile is refused if\nanother conversation is waiting too. With nothing to answer, a DM carries the connector's own\n`contextId`, if it sets one. A connector that runs several host sessions on one seat (Hermes)\nstamps each question with a `contextId` of its own, and routes an answer back to the session that\nasked only when the answer copies it and comes from the peer the question went to.\n\nThe endpoint underneath self-heals: when the transport connection dies terminally, a\nsupervisor rebuilds it (rebuilds are serialized and coalesced), and unacked in-flight\nmessages redeliver on the rebound durables, so nothing is lost across the gap. A failed\npost-connect bind closes its partial transport before the retry loop starts another attempt.\nA manual `/reconnect` is the human-invoked counterpart.\n\n## Manager supervision\n\nThe CLI does not spawn agents itself; a long-lived **manager** owns their lifecycle,\nasked over the mesh. The manager is not a privileged control plane: it is an ordinary\nservice endpoint on the same `ep` rails as any other daemon\n([\xA713](../SPEC.md#13-endpoint-control-surface-v04)), holding only the capability rows its\ncallers grant it. It owns process lifecycle and config binding (start / stop / restart,\nbinding env and policy) and has no say in what work the agents do. Agents coordinate\nlaterally; the manager only births and configures them.\n\n- **Off the message hot path.** Each agent self-connects to the mesh through its own\n connector. The manager owns processes in order to control them, but observes everything\n through presence, so a bring-your-own-terminal agent it never spawned still shows up in\n `ps`.\n- **Pluggable runtimes.** Spawning is abstracted behind a `Runtime` contract (like pm2 or\n docker for agent TUIs): **`pty`** ships built-in (the manager owns the pseudo-terminal\n in-process on every platform; watch or type via `cotal attach`; on Linux it still adopts\n seats an earlier manager left under a detached custodian, and elsewhere `adopt` throws); **`tmux`**, **`cmux`**, **`orca`**, and **`herdr`** are\n extensions that put each teammate in its own native terminal surface (explicit opt-ins\n that throw when the extension isn't loaded, never a silent fallback); **byo** is the\n floor (a human's own terminal, tracked via presence); **host** (Agent SDK, true mid-turn\n interrupt) is the documented upgrade path ([roadmap](roadmap.md)).\n- **Served commands.** `spawn` (an action, below), `stop`, `ps`, `status`, `attach`,\n `models`, and `define-persona` are among its endpoint commands\n ([\xA713.5](../SPEC.md#135-verbs)) any authorized node can send, policy-gated\n ([identity & auth](identity-and-auth.md)). A caller learns them off the wire with `cotal\n describe manager`; nothing is compiled in.\n- **Spawn is an action.** Asking for an agent no longer blocks the caller while the process\n comes up. The manager accepts a spawn **goal** ([\xA713.6](../SPEC.md#136-composites)) and\n immediately returns the allocated identity (the agent's name, its `owner`/`actor`/`uid`\n triple, a `goalId`, and the executor coordinate `{lifecycleUid, epoch}`); progress events\n then report the launch until a terminal outcome. Presence within the readiness window is\n `succeeded`, an early exit is `failed`, and the window passing with neither is\n `uncertain`: a bounded, reconcilable outcome a later `ps` settles against the live roster,\n never a silent hang.\n- **Bounded spawn.** A gate caps concurrent and in-flight agents and a minimum-lifetime\n floor bounds spawn/despawn churn, so a capability-holding but compromised peer cannot\n fork-bomb the host. The gate runs at goal acceptance, before any identity is minted or\n process launched, so a refused spawn leaves nothing behind. A refusal states what holds the\n slots and whether waiting can free one.\n- **Declared environment boundary.** A spawned agent receives a fixed OS allow-list (PATH/HOME/\n locale, including PATH entries connector binaries live in), the machine-wide `COTAL_*` operator\n knobs, connector-declared provider inputs, explicitly shared MCP references, and names\n deliberately added through `spawn.env`. It never inherits the manager's ambient environment, so\n host-session markers (`CLAUDE_CODE_CHILD_SESSION` and the analogous names other hosts use) and\n unrelated capabilities cannot become properties of every seat. Connection material rides a private\n file instead of the environment.\n- **Instance addressing.** One space can hold more than one manager. Each keeps a stable\n logical instance id across restarts and advances its process epoch when it comes back, so\n peers address a specific manager without caring which process currently serves it. `cotal\n spawn <persona> --detach --on <instance>` pins one instance (`ps`, `stop`, `attach`, `models`\n and the hosted `run` verbs take the same flag), and MCP `cotal_spawn`, `cotal_run` and\n `cotal_personas` accept the equivalent `instance` argument. An untargeted spawn rides class anycast and the acceptance records which\n instance took it. `ps` and `status` scatter across every registered instance and label a\n non-answering one as registered with no answer within the deadline, never dropping it.\n- **A manager holds a liveness lease, and only losing it to another process ends the process.**\n Each instance keeps its own key in the space's manager bucket and refreshes it several times\n over inside the key's TTL. A refresh that fails is a question, not a verdict, so the manager\n re-reads the key before deciding what to do. If the key is still its own it adopts the broker's\n revision and carries on. If the key is gone (it expired during a stall) it puts it back. If\n another process holds it, that process took the instance over during the stall, so this one\n says so and exits without releasing the lease or the registration, which are the successor's\n now. If the broker cannot be asked at all it keeps serving and asks again, for as long as that\n takes. A manager that cannot reach its broker gains nothing by ending itself, and the seats it\n holds lose everything. Each change of state is one line in the manager's log, not one line per\n tick.\n A clean stop waits for a renew already in flight and then releases the key at the broker's own\n revision, so a same-root restart never waits out the bucket TTL.\n- **Attach is a mesh session.** The console and dashboard discover agents over the **mesh**\n (presence, `ps`). `cotal attach` no longer hands back a `127.0.0.1` URL: it redeems a\n one-use, holder-bound session offer, and the terminal bytes stream over the mesh on\n core-NATS session subjects scoped to the two parties, with backpressure surfaced as an\n explicit drop notice rather than silent loss. Attach reaches managers on other machines through\n the broker. The manager's own socket stays private. A late\n attach still repaints the full screen from a replayed snapshot of a headless terminal\n mirror (including alternate-screen TUIs). If the manager restarts, its successor refuses\n the old session. `cotal attach` reconnects when its link dies by asking the manager for a new\n session ([CLI](cli.md#managed-seats)).\n- **The manager's console face is a separate, credentialed surface.** The manager still\n serves the browser console over local HTTP: the static page plus the roster, the live feed,\n and the route that mints the browser's own session. It binds loopback unless the operator\n says otherwise (`cotal supervise --console-host`), and every route that carries mesh data\n or mints a credential requires the manager's console token.\n\nThe result is that an agent can grow and shape its own team: ask for a teammate\n(`cotal_spawn`), mint a persona on the fly (`cotal_persona`), or tear one down\n(`cotal_despawn`). Every newcomer joins as a peer, not as a child of whoever requested\nit. Each managed agent runs under a durable **lifecycle**: a despawn retires it (settling\nand evicting the old incarnation) before its name frees for reuse, and a supervised restart\nrecovers the same lifecycle rather than minting a new one, so durables and credentials key\non the lifecycle, not the reusable name ([SPEC \xA713.1](../SPEC.md#131-lifecycle-identity);\n[identity & auth](identity-and-auth.md)). Destructive space-wide operations (history purge)\nstay operator-only.\n\n\n## Observers\n\nA watch surface is a read-only observer: an endpoint that consumes without registering\npresence (invisible to peers) while watching everyone else's. All three surfaces\n(terminal console, plain stream, web dashboard) derive from that one observer through a\nshared render-agnostic model, so no surface re-implements wire semantics. The guide is\n[watch a mesh](watch-a-mesh.md); the model is [MeshView](mesh-view.md).\n\n## Addressing\n\nThree identity layers, in increasing permanence\n([SPEC \xA72](../SPEC.md#2-identity), [\xA76](../SPEC.md#6-presence-and-discovery)):\n\n- **`name`** is a cosmetic, reusable human handle. Addressing by name is best-effort\n convenience, with deterministic and fail-loud resolution: a unique live name resolves,\n and a collision among live peers throws with the candidate ids rather than silently\n picking one. The manager auto-numbers its own spawns (`reviewer` \u2192 `reviewer_2`).\n The connector and the CLI print a peer with one label (in the roster, message attribution,\n `cotal endpoints` and the console): `name/role`, or the bare name when it has no role. A\n control character, Unicode line or paragraph separator, or bracket in either part prints as a\n space. That label is not an address, because `/` is reserved for `owner/name` handles: a target\n written that way is refused, and the refusal gives the bare name.\n- **`role`** is the addressable service, which makes it the anycast address:\n `svc.reviewer` reaches \"whoever is a reviewer\", so the label carries routing meaning.\n- **The instance id** is the authoritative address: the presence key, the unicast target,\n the credential subject.\n\n**Instance continuity:** the id tracks *context* continuity, not the label. A resumed\nsession (same context window) keeps its id; presence, thread correlation, and in-flight\nDMs stay continuous. A fresh context, even reusing the name, is a **new** instance with a\nnew id: reusing an id across a discontinuous context would tell peers \"same agent, same\nmemory\" when the new session has none. One deliberate exception: OpenCode's `/new` inside\nthe same managed process keeps the mesh identity and advances only the thread correlation\nid: process continuity, not credential reuse.\n\n## Deferred\n\nSessions/moderator, signed envelopes + DID identity, instant offline, artifact delivery,\njoin-link bootstrap tokens for static meshes, and federation are designed for but not built yet;\neach is tracked, with its direction, in the [roadmap](roadmap.md).\n"
|
|
27238
27238
|
},
|
|
27239
27239
|
{
|
|
27240
27240
|
"slug": "mcp-tools",
|
|
@@ -27262,7 +27262,7 @@ function loadDocsBundle() {
|
|
|
27262
27262
|
"title": "Agent files",
|
|
27263
27263
|
"kind": "Reference (the persisted form of an agent's identity + persona, read by every launcher)",
|
|
27264
27264
|
"summary": "An agent's identity and persona live in one Markdown file instead of being passed flag-by-flag, the same shape Claude Code uses for subagents:",
|
|
27265
|
-
"body": "# Agent files\n\n> **Reference** (the persisted form of an agent's identity + persona, read by every launcher) \xB7 **For:** operators \xB7 **ACL semantics:** [SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization), [Appendix B](../SPEC.md#appendix-b-profile-acls)\n\nAn agent's identity and persona live in one Markdown file instead of being passed\nflag-by-flag, the same shape Claude Code uses for subagents:\n\n```markdown\n.cotal/agents/<name>.md\n---\nname: dave # \u2192 COTAL_NAME / card.name\nrole: builder # \u2192 COTAL_ROLE / card.role (presence + anycast address)\ndescription: \u2026 # \u2192 card.description\ntags: [edit, test] # \u2192 card.tags (\"what it can do\")\nsubscribe: [general, team.backend] # channels it reads at boot (omit = none)\nallowSubscribe: [general, team.>] # read ACL (omit = same as subscribe)\nallowPublish: [general, team.backend] # post ACL (omit = none, default-deny)\nmodel: opus # optional model override\nvariant: high # optional connector-defined model variant\ncapabilities: [spawn, run] # may manage teammates and start workflow runs\n---\nYou are a builder on a shared mesh of peer agents\u2026 \u2190 the body is the persona\n```\n\n**Frontmatter is identity** (an A2A-style `AgentCard`,\n[SPEC \xA76](../SPEC.md#6-presence-and-discovery)); **the body is the persona**, appended to\nthe session's system prompt at launch: the one field that *must* be applied at launch,\nbecause a session cannot change its system prompt afterward. Connectors that use an external\nprompt file write an owner-private temporary copy and pass only its path, so the persona body is not\npublished in the agent process argv. Owner-private means OS-user isolation: any process running as\nthe same user can read that copy while it exists, as it can this agent file. The launcher removes the\ncopy once it has proved the agent process gone. On every runtime, the default pty included, and in\nthe foreground `cotal spawn`, a watcher started beside the agent also removes the copy once the agent\nprocess is gone, even when the launcher was killed. A launch refused before any process started\nremoves it at once. A launch that failed before its agent started, and on Windows a killed launcher,\nleaves the copy for the OS temp reaper.\n\n## Fields\n\nAuthoritative shape: [`agent-file.ts`](../packages/core/src/agent-file.ts).\n\n| Field | Type | Meaning |\n|---|---|---|\n| `name` | string, required | Display name \u2192 `card.name`. A launcher resolves a bare name to `.cotal/agents/<name>.md`. At most 128 characters; the validator refuses longer names rather than minting a credential that would overflow the broker's CONNECT line. |\n| `role` | string | The addressable **service**: presence label *and* the anycast address ([SPEC \xA73](../SPEC.md#3-subject-layout)). One `[A-Za-z0-9_-]` token; any other spelling is refused when the agent joins. |\n| `kind` | `agent` \\| `endpoint` | Participation class; default `agent`. |\n| `description` | string | One-line summary \u2192 `card.description`. |\n| `tags` | string[] | Capability tags \u2192 `card.tags`. |\n| `subscribe` | string[] | The **active read set**: channels subscribed at boot (mutable at runtime via join/leave). Must be \u2286 `allowSubscribe`. **Omitted \u21D2 no channels**: an agent reads what it lists, and one that lists none joins none (still reachable by DM, anycast and presence). List `general` if you want it. |\n| `allowSubscribe` | string[] | The **read ACL**: channels it *may* read. Wildcard subtrees allowed (`team.>`). Omitted or empty \u21D2 same as `subscribe`. |\n| `allowPublish` | string[] | The **post ACL**: channels it may publish to. **Omitted \u21D2 deny**; posting is the dangerous capability, declare it explicitly. |\n| `quiet` | string[] | Per-channel attention *default*: ambient stays buffered and pull-only until `cotal_inbox`; `@mention`s remain automatic. Concrete channels within the read ACL. |\n| `muted` | string[] | Per-channel attention *default*: dropped on receive, `@mentions` included. |\n| `model` | string | Model override handed to the agent CLI (Claude: `opus` / full id; OpenCode: `provider/model`). When the cotal config has a `modelPolicy` entry for the persona's role, the model must be one it allows, and a persona with no model is refused ([Model policy](config.md#model-policy)). A whitespace-only value is refused when the file loads. |\n| `variant` | string | Connector-defined model variant (e.g. an OpenCode variant, see `cotal models`). Checked against the role's `modelPolicy` variants when that entry lists any. A whitespace-only value is refused when the file loads. |\n| `agent` | string | The connector/harness this persona pins (`claude`, `jcode`, and so on). Precedence: explicit `--agent` > this field > `COTAL_DEFAULT_AGENT` > the saved [`harness` preference](config.md#preferences) > the product default, the same shape as `model`/`variant`, so the env var stays a *default* and cannot beat a deliberate per-persona pin. A value naming an unregistered connector fails the spawn loudly (no silent fallback). |\n| `launchOptions` | map | Opaque per-connector launch options forwarded **raw** to the harness (Claude flags, OpenCode agent config; Hermes and pi have no option surface and fail loud). A CLI `--opt key=value` overrides a key set here. A role with a `modelPolicy` entry launches without launch options ([Model policy](config.md#model-policy)). See [run a mesh](run-a-mesh.md#spawning-agents). |\n| `capabilities` | string[] | Control-plane capabilities minted into the cred. `spawn` grants the privileged control subject (spawn / named stop / persona definition), default-deny when absent, enforced by the broker, not a handler. `run` grants the manager's workflow-run commands (start, resume, answer, status, list) plus the spawn set a program's own spawns need, and injects the `cotal_run` tool. On a per-user-auth mesh, `role:<r>` additionally lets the agent delegate role `r` when spawning ([identity & auth](identity-and-auth.md)); `admin` is never a persona capability. |\n| `owner` | string | **Policy, not content**: set once by `definePersona` (owner = creator); only the owner (or admin) may redefine the file over the wire. Never write it by hand. |\n| *(any other key)* | string | Kept verbatim in `meta` so a connector can read its own launcher hints without core knowing them. Saving a definition whose `meta` names a key this table models, or the renamed `channels` or `publish`, is refused before anything is written. The connector-owned keys are the exception: `connector`, `model`, `variant`, and `host` (the machine the session runs on) are overlaid from the live session, so a file cannot declare a harness or a host it is not on. |\n\nThe three channel verbs on one card, with the common recipes:\n[Channels & permissions](channels-and-permissions.md). Attention semantics (`quiet` /\n`muted` are one-way *defaults*; the runtime toggle is per-instance and resets on restart):\n[Connect Claude](connect-claude.md#attention).\n\n## Persona lookup\n\n- **By name.** A launcher resolves a bare name to `<target-root>/.cotal/agents/<name>.md`. The\n target root comes from the selected mesh, including `cotal use`, `--space`, and `--server`, so\n launchers, `cotal persona`, setup, status, and agent-profile minting use one catalog. This is a\n directory convention, not an HTTP well-known; mesh discovery stays NATS presence. The card built\n from the file is what gets broadcast.\n- **One ref.** The launcher sets `COTAL_AGENT_FILE=<abs path>` (the *who*) the way\n `COTAL_LINK` carries the *where*; the joined session reads its card straight from the\n file. Individual `COTAL_*` vars still override it ([config](config.md)).\n- **Defaults.** A bare `cotal spawn` uses the `default` persona (`COTAL_DEFAULT_PERSONA` or the\n saved [`persona` preference](config.md#preferences) changes the fallback); the harness comes from\n `--agent` > the persona's `agent:` pin > the invoking CLI's `COTAL_DEFAULT_AGENT` > its saved\n `harness` preference > the manager's own environment, else Claude. An explicit flag always wins over the file, and the file wins over either environment\n default, including detached spawns ([run a mesh](run-a-mesh.md)).\n\nEvery launcher consumes the file the same way; they differ only in how they run the spec:\n\n| Launcher | How to point at a file |\n|---|---|\n| Manager (`cotal spawn --detach dave`) | auto-discovers `.cotal/agents/dave.md` in the manager's workspace, or `--config <persona-or-path>`; same grammar as foreground (`--model`, `--variant`, `--cwd`, `--prompt`, ACL overrides, `--share-tools`). |\n| Foreground (`cotal spawn dave`) | same resolution; the real agent TUI takes over this terminal. Works from any directory via the mesh registry. |\n\n`.cotal/` is gitignored (user-local, like `.claude/`); commit persona files you want\nshared some other way. The demo ships committed examples under\n[`examples/01-lateral-coordination/agents/`](../examples/01-lateral-coordination/agents/).\n\n## Persona purpose\n\nExpert-persona prompts (\"you are a world-class\u2026\") do not reliably improve accuracy. Keep\nthe body to what the agent *does* and how it *coordinates*; a persona that needs facts\nshould point at the source (the repo's docs, a URL), not assert them.\n\n## Defining one at runtime\n\n`cotal_persona(name, prompt, model?, role?, agent?, subscribe?, allowSubscribe?, allowPublish?, announce?)` sends a persona to the manager, which\nwrites the same file; a later `cotal_spawn(name, role?, agent?, model?, variant?)` brings\nit online, so a peer can mint a teammate with no hand-written file\n([tool catalog](mcp-tools.md)). The write path takes **content** (`model` /\n`persona`, plus optional role, agent, and channel grants). A prompt that is already a\ncomplete agent file (its own `---` frontmatter) is **merged** into one block: grants,\nrole, and agent from that block survive, and explicit tool arguments such as `model` win.\nA malformed leading frontmatter block is refused (`prompt-frontmatter`) rather than\nwrapped. `capabilities` and `owner` remain policy and have no slot, so a peer cannot\ngrant itself spawn or claim ownership. A persona with no `capabilities:` line therefore\nspawns **without** `spawn`, and a spawn whose effective role is `manager` is **refused at\nspawn time** rather than joining as a labelled manager that silently cannot seat workers:\neither put `capabilities: [spawn]` on the file (an operator edit) or spawn it under\nanother role.\n\n**Defining is silent.** Nothing goes out on the mesh unless you pass `announce: <channel>`,\nand then it goes to that channel only. A peer that did not ask for the persona has no way\nto judge whether spawning it is wanted, and a broadcast soliciting spawns from an\nunfamiliar principal is a thing a peer should be suspicious of, so announcing belongs on\nthe channel your team is working on rather than `general`. The old announcement carried limited discovery. Peers already listening saw the bare name, but\nno prompt, model, or role. Peers joining later saw nothing. No path a peer can\ndeliberately consult is affected: `cotal_personas` lists and shows the catalog over the\nwire (spawn-capability, same ownership as the write), `cotal personas list` reads the\ncatalog within a workspace, and `cotal_spawn` on a name that does not exist fails loud.\n\nThe operator-side counterpart is `cotal persona` (bare list, show, create, edit and remove). It reads\nand writes the selected space root's files offline. Complete file input preserves every field and\nsource byte. An editor draft validates before promotion and refuses concurrent local CLI changes\n([CLI](cli.md#persona)).\n\nOn a user-auth mesh, spawning owned persona content requires its creator principal or a\ncurrent admin grant. A hosted run uses its admitted caller for that decision. A foreign\ncard refuses as not-found before connector loading or enrollment, including an explicit\nconfig path. Unowned operator files remain launchable. Manifest launches retain their\nseparate owner authorization.\n"
|
|
27265
|
+
"body": "# Agent files\n\n> **Reference** (the persisted form of an agent's identity + persona, read by every launcher) \xB7 **For:** operators \xB7 **ACL semantics:** [SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization), [Appendix B](../SPEC.md#appendix-b-profile-acls)\n\nAn agent's identity and persona live in one Markdown file instead of being passed\nflag-by-flag, the same shape Claude Code uses for subagents:\n\n```markdown\n.cotal/agents/<name>.md\n---\nname: dave # \u2192 COTAL_NAME / card.name\nrole: builder # \u2192 COTAL_ROLE / card.role (presence + anycast address)\ndescription: \u2026 # \u2192 card.description\ntags: [edit, test] # \u2192 card.tags (\"what it can do\")\nsubscribe: [general, team.backend] # channels it reads at boot (omit = none)\nallowSubscribe: [general, team.>] # read ACL (omit = same as subscribe)\nallowPublish: [general, team.backend] # post ACL (omit = none, default-deny)\nmodel: opus # optional model override\nvariant: high # optional connector-defined model variant\ncapabilities: [spawn, run] # may manage teammates and start workflow runs\n---\nYou are a builder on a shared mesh of peer agents\u2026 \u2190 the body is the persona\n```\n\nThe file may use LF or CRLF line endings and may start with a UTF-8 byte order mark, so a\ncatalog checked out on Windows reads the same.\n\n**Frontmatter is identity** (an A2A-style `AgentCard`,\n[SPEC \xA76](../SPEC.md#6-presence-and-discovery)); **the body is the persona**, appended to\nthe session's system prompt at launch: the one field that *must* be applied at launch,\nbecause a session cannot change its system prompt afterward. Connectors that use an external\nprompt file write an owner-private temporary copy and pass only its path, so the persona body is not\npublished in the agent process argv. Owner-private means OS-user isolation: any process running as\nthe same user can read that copy while it exists, as it can this agent file. The launcher removes the\ncopy once it has proved the agent process gone. On every runtime, the default pty included, and in\nthe foreground `cotal spawn`, a watcher started beside the agent also removes the copy once the agent\nprocess is gone, even when the launcher was killed. A launch refused before any process started\nremoves it at once. A launch that failed before its agent started, and on Windows a killed launcher,\nleaves the copy for the OS temp reaper.\n\n## Fields\n\nAuthoritative shape: [`agent-file.ts`](../packages/core/src/agent-file.ts).\n\n| Field | Type | Meaning |\n|---|---|---|\n| `name` | string, required | Display name \u2192 `card.name`. A launcher resolves a bare name to `.cotal/agents/<name>.md`. At most 128 characters; the validator refuses longer names rather than minting a credential that would overflow the broker's CONNECT line. |\n| `role` | string | The addressable **service**: presence label *and* the anycast address ([SPEC \xA73](../SPEC.md#3-subject-layout)). One `[A-Za-z0-9_-]` token; any other spelling is refused when the agent joins. |\n| `kind` | `agent` \\| `endpoint` | Participation class; default `agent`. |\n| `description` | string | One-line summary \u2192 `card.description`. |\n| `tags` | string[] | Capability tags \u2192 `card.tags`. |\n| `subscribe` | string[] | The **active read set**: channels subscribed at boot (mutable at runtime via join/leave). Must be \u2286 `allowSubscribe`. **Omitted \u21D2 no channels**: an agent reads what it lists, and one that lists none joins none (still reachable by DM, anycast and presence). List `general` if you want it. |\n| `allowSubscribe` | string[] | The **read ACL**: channels it *may* read. Wildcard subtrees allowed (`team.>`). Omitted or empty \u21D2 same as `subscribe`. |\n| `allowPublish` | string[] | The **post ACL**: channels it may publish to. **Omitted \u21D2 deny**; posting is the dangerous capability, declare it explicitly. |\n| `quiet` | string[] | Per-channel attention *default*: ambient stays buffered and pull-only until `cotal_inbox`; `@mention`s remain automatic. Concrete channels within the read ACL. |\n| `muted` | string[] | Per-channel attention *default*: dropped on receive, `@mentions` included. |\n| `model` | string | Model override handed to the agent CLI (Claude: `opus` / full id; OpenCode: `provider/model`). When the cotal config has a `modelPolicy` entry for the persona's role, the model must be one it allows, and a persona with no model is refused ([Model policy](config.md#model-policy)). A whitespace-only value is refused when the file loads. |\n| `variant` | string | Connector-defined model variant (e.g. an OpenCode variant, see `cotal models`). Checked against the role's `modelPolicy` variants when that entry lists any. A whitespace-only value is refused when the file loads. |\n| `agent` | string | The connector/harness this persona pins (`claude`, `jcode`, and so on). Precedence: explicit `--agent` > this field > `COTAL_DEFAULT_AGENT` > the saved [`harness` preference](config.md#preferences) > the product default, the same shape as `model`/`variant`, so the env var stays a *default* and cannot beat a deliberate per-persona pin. A value naming an unregistered connector fails the spawn loudly (no silent fallback). |\n| `launchOptions` | map | Opaque per-connector launch options forwarded **raw** to the harness (Claude flags, OpenCode agent config; Hermes and pi have no option surface and fail loud). A CLI `--opt key=value` overrides a key set here. A role with a `modelPolicy` entry launches without launch options ([Model policy](config.md#model-policy)). See [run a mesh](run-a-mesh.md#spawning-agents). |\n| `capabilities` | string[] | Control-plane capabilities minted into the cred. `spawn` grants the privileged control subject (spawn / named stop / persona definition), default-deny when absent, enforced by the broker, not a handler. `run` grants the manager's workflow-run commands (start, resume, answer, status, list) plus the spawn set a program's own spawns need, and injects the `cotal_run` tool. On a per-user-auth mesh, `role:<r>` additionally lets the agent delegate role `r` when spawning ([identity & auth](identity-and-auth.md)); `admin` is never a persona capability. |\n| `owner` | string | **Policy, not content**: set once by `definePersona` (owner = creator); only the owner (or admin) may redefine the file over the wire. Never write it by hand. |\n| *(any other key)* | string | Kept verbatim in `meta` so a connector can read its own launcher hints without core knowing them. Saving a definition whose `meta` names a key this table models, or the renamed `channels` or `publish`, is refused before anything is written. The connector-owned keys are the exception: `connector`, `model`, `variant`, and `host` (the machine the session runs on) are overlaid from the live session, so a file cannot declare a harness or a host it is not on. |\n\nThe three channel verbs on one card, with the common recipes:\n[Channels & permissions](channels-and-permissions.md). Attention semantics (`quiet` /\n`muted` are one-way *defaults*; the runtime toggle is per-instance and resets on restart):\n[Connect Claude](connect-claude.md#attention).\n\n## Persona lookup\n\n- **By name.** A launcher resolves a bare name to `<target-root>/.cotal/agents/<name>.md`. The\n target root comes from the selected mesh, including `cotal use`, `--space`, and `--server`, so\n launchers, `cotal persona`, setup, status, and agent-profile minting use one catalog. This is a\n directory convention, not an HTTP well-known; mesh discovery stays NATS presence. The card built\n from the file is what gets broadcast.\n- **One ref.** The launcher sets `COTAL_AGENT_FILE=<abs path>` (the *who*) the way\n `COTAL_LINK` carries the *where*; the joined session reads its card straight from the\n file. Individual `COTAL_*` vars still override it ([config](config.md)).\n- **Defaults.** A bare `cotal spawn` uses the `default` persona (`COTAL_DEFAULT_PERSONA` or the\n saved [`persona` preference](config.md#preferences) changes the fallback); the harness comes from\n `--agent` > the persona's `agent:` pin > the invoking CLI's `COTAL_DEFAULT_AGENT` > its saved\n `harness` preference > the manager's own environment, else Claude. An explicit flag always wins over the file, and the file wins over either environment\n default, including detached spawns ([run a mesh](run-a-mesh.md)).\n\nEvery launcher consumes the file the same way; they differ only in how they run the spec:\n\n| Launcher | How to point at a file |\n|---|---|\n| Manager (`cotal spawn --detach dave`) | auto-discovers `.cotal/agents/dave.md` in the manager's workspace, or `--config <persona-or-path>`; same grammar as foreground (`--model`, `--variant`, `--cwd`, `--prompt`, ACL overrides, `--share-tools`). |\n| Foreground (`cotal spawn dave`) | same resolution; the real agent TUI takes over this terminal. Works from any directory via the mesh registry. |\n\n`.cotal/` is gitignored (user-local, like `.claude/`); commit persona files you want\nshared some other way. The demo ships committed examples under\n[`examples/01-lateral-coordination/agents/`](../examples/01-lateral-coordination/agents/).\n\n## Persona purpose\n\nExpert-persona prompts (\"you are a world-class\u2026\") do not reliably improve accuracy. Keep\nthe body to what the agent *does* and how it *coordinates*; a persona that needs facts\nshould point at the source (the repo's docs, a URL), not assert them.\n\n## Defining one at runtime\n\n`cotal_persona(name, prompt, model?, role?, agent?, subscribe?, allowSubscribe?, allowPublish?, announce?)` sends a persona to the manager, which\nwrites the same file; a later `cotal_spawn(name, role?, agent?, model?, variant?)` brings\nit online, so a peer can mint a teammate with no hand-written file\n([tool catalog](mcp-tools.md)). The write path takes **content** (`model` /\n`persona`, plus optional role, agent, and channel grants). A prompt that is already a\ncomplete agent file (its own `---` frontmatter) is **merged** into one block: grants,\nrole, and agent from that block survive, and explicit tool arguments such as `model` win.\nA malformed leading frontmatter block is refused (`prompt-frontmatter`) rather than\nwrapped. `capabilities` and `owner` remain policy and have no slot, so a peer cannot\ngrant itself spawn or claim ownership. A persona with no `capabilities:` line therefore\nspawns **without** `spawn`, and a spawn whose effective role is `manager` is **refused at\nspawn time** rather than joining as a labelled manager that silently cannot seat workers:\neither put `capabilities: [spawn]` on the file (an operator edit) or spawn it under\nanother role.\n\n**Defining is silent.** Nothing goes out on the mesh unless you pass `announce: <channel>`,\nand then it goes to that channel only. A peer that did not ask for the persona has no way\nto judge whether spawning it is wanted, and a broadcast soliciting spawns from an\nunfamiliar principal is a thing a peer should be suspicious of, so announcing belongs on\nthe channel your team is working on rather than `general`. The old announcement carried limited discovery. Peers already listening saw the bare name, but\nno prompt, model, or role. Peers joining later saw nothing. No path a peer can\ndeliberately consult is affected: `cotal_personas` lists and shows the catalog over the\nwire (spawn-capability, same ownership as the write), `cotal personas list` reads the\ncatalog within a workspace, and `cotal_spawn` on a name that does not exist fails loud.\n\nThe operator-side counterpart is `cotal persona` (bare list, show, create, edit and remove). It reads\nand writes the selected space root's files offline. Complete file input preserves every field and\nsource byte. An editor draft validates before promotion and refuses concurrent local CLI changes\n([CLI](cli.md#persona)).\n\nOn a user-auth mesh, spawning owned persona content requires its creator principal or a\ncurrent admin grant. A hosted run uses its admitted caller for that decision. A foreign\ncard refuses as not-found before connector loading or enrollment, including an explicit\nconfig path. Unowned operator files remain launchable. Manifest launches retain their\nseparate owner authorization.\n"
|
|
27266
27266
|
},
|
|
27267
27267
|
{
|
|
27268
27268
|
"slug": "authoring-a-connector",
|
|
@@ -27283,21 +27283,21 @@ function loadDocsBundle() {
|
|
|
27283
27283
|
"title": "`cotal` CLI reference",
|
|
27284
27284
|
"kind": "Reference: describes the TypeScript reference implementation (the `cotal` CLI), not the wire contract.",
|
|
27285
27285
|
"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.",
|
|
27286
|
-
"body": "# `cotal` CLI reference\n\n> **Reference**: describes the TypeScript reference implementation (the `cotal` CLI), not the wire contract. \xB7 **For:** operators \xB7 **Wire contract:** [SPEC](../SPEC.md)\n\n`cotal` is the operator command line for the reference implementation: bring a mesh up, mint\nidentities, launch agents, watch what they do, and tear it all down. It is a thin client over the\nwire contract: the normative subjects and schemas live in the [SPEC](../SPEC.md); this page is\nlookup material for the commands, not a walkthrough; if you are new, start with\n[Getting started](getting-started.md).\n\n## Running it\n\n```bash\nnpm install -g cotal-ai # puts `cotal` on your PATH (needs Node 22+)\ncotal # the selected space, common commands, and where to start\ncotal help # compact actions and selected context (also `cotal --help`)\ncotal help --all # every command group, including hidden commands\ncotal help <command> [<subcommand>] # one command's or subcommand's usage and flags\ncotal version # cotal-ai version + each installed extension's (also `cotal --version`, `cotal -V`)\ncotal version --json # the same as one JSON document (also `-j`)\n```\n\nThe shell installer prints its absolute launcher path. Use it when an older installation still\nwins plain `cotal`. Having `~/.local/bin` somewhere on PATH does not establish precedence, and a\nchild installer cannot change the parent shell's command cache, aliases or functions. Inspect\n`type -a cotal` in that shell. After putting the new directory first, refresh its command cache\nwith `hash -r` (or `rehash` in zsh). An alias or function must be corrected separately. To restore\nconnectors after an old executable rebuilt them, run the verified newer absolute executable with\n`ext seed`, without `--force`.\n\n`npx cotal-ai <command>` runs it without a global install. Bare `cotal` starts nothing: it names\nthe selected space, lists common commands, and points a machine with no selected space to\n`cotal setup`. Every command generates its own help, usage, and shell completion from its declared\nflags, and `cotal <command> --help` prints the same help as `cotal help <command>`. `-v` is not a\nversion flag.\n\nSome commands have short aliases, listed in `cotal help --all`: `sp` (spawn), `a` (attach),\n`s` (status), `r` (run), `x` (ext), `h` (history), `cfg` (config) and `p` (persona). Subcommands can have their own, such as\n`cotal ext rm` for `cotal ext remove`. An alias behaves the same as its command. A command or\nsubcommand is never selected by a prefix, so `cotal spaw` is an unknown command. A command, alias,\nor extension command that would claim a word another one already claims is refused when it\nregisters or installs.\n\nIn a dev clone, `pnpm cotal <command>` runs `bin/cotal.ts` through `tsx`, but `bin/` loads the other\npackages from their built `dist/`. Run `pnpm build` after `pnpm install`, and again after you edit a\npackage, or the command fails with `ERR_MODULE_NOT_FOUND` for `@cotal-ai/cli/dist/index.js` or runs\nthe old code. A Windows source build needs the matching MSVC Developer Command Prompt to compile\nits first-party process identity binding. Published packages already contain the x64 and arm64\nbinaries and need no compiler or native download.\n\nCommands that change the installation run the automatic connector seed and stop when the\noperator-global seed store is not current for the checkout's version, which includes a fresh config\nand a store another version wrote. The error includes\n`this would migrate the machine-wide store and can take an installed cotal down`. A\ncheckout never writes the connector store an installed `cotal` uses. For day-to-day\nwork, set `COTAL_SKIP_CONNECTOR_SEED=1` to skip that seed step:\n\n```bash\npnpm install\npnpm build\nCOTAL_SKIP_CONNECTOR_SEED=1 pnpm cotal status\n```\n\nPointing `XDG_CONFIG_HOME` at a scratch dir is not enough by itself. Isolated release tests\nthat must seed from a checkout also set `COTAL_ALLOW_CHECKOUT_SEED=1`, as described under\n[Built-in connectors are seeded extensions](#built-in-connectors-are-seeded-extensions).\n\nBefore publication, the release workflow installs the packaged candidate into fresh homes and\ncaches on Linux, macOS and Windows, on x64 and arm64 with Node 22, 24 and 26. Each setup seeds\nconnectors, starts the broker, delivery daemon and Manager, checks native process identity and\nManager control, then stops and restarts them. Every platform must pass before normal or snapshot\npublication can proceed.\n\nAn undeclared flag is a usage error, and so is a flag given more than once unless it is\nrepeatable, as `--opt` and `down --session-store` are. So is an unknown command, and a first word\nthat is not one of the command's subcommands when it declares them. So is input a command checks\nbefore it acts, such as a missing argument, an invalid flag value or two flags that conflict, and\n`cotal help` with a subcommand that does not exist. The command prints the error, then any remedy,\nthen its help on stderr, writes nothing on stdout, exits 1, and does not run. A usage error is found before any effect, so it never seeds\nconnectors or writes configuration. Help, version and completion never do either.\n\nCommand output, including error lines on stderr, the guided `setup` and `meshes add` prompts and\nthe `console` screen, is colored only when stdout is a terminal, so piped or redirected output is\nplain text. A non-empty `NO_COLOR` turns color off on a terminal too. `FORCE_COLOR` turns color on even when output is\npiped, unless it is `0` or `false`, and it takes precedence over `NO_COLOR`. `--no-color` turns\ncolor off for that run, whatever the environment says. It sets no `NO_COLOR` for the programs the\nrun starts, but they are no longer forced into color by `FORCE_COLOR`.\n\nAt any question the CLI asks, Ctrl-D on an empty line or an input that closes cancels the question\nas Ctrl-C does, so the command reports its own cancelled outcome.\n`--no-input` means the run never prompts, opens an editor or opens a browser. A command that would\nprompt behaves as it does without a terminal, `web` prints its link without opening it, and\n`personas edit` is refused. `--json` implies `--no-input`. `--no-color` and `--no-input` work before\nthe command word on every command, and after it on every command except one that takes its words\nraw. After `--` they are ordinary words. No command may declare a flag with either name. The output\nflags `--json` (`-j`) and `--plain` may also come before the command word, as in\n`cotal --json status`; they are read as the command's own flags, so a command without that output\nrefuses them as it does after the command word. A command that takes its words raw, such as the\nhidden completion entry, has no flags to read them as and refuses them before its word with a usage\nerror. A `--` before the command word ends these leading flags. An installed extension\nwhose command declares one is an internal error, and `cotal` exits 1 before running any command.\nShell completion treats the current `--` word as a flag prefix until it is committed. After `-- `,\nit completes positional values without offering options. A string option's value is completed\nfrom its own candidates or file hint, without inserting other option names.\n\nCommands come from the surfaces the binary composes: the base mesh CLI, the manager and the\ndelivery daemon, plus any operator-installed extensions. Hidden entries such as the deprecated\n`supervise` and `deliver` are not completed.\n`cotal ext add <npm-package>` installs any registry providers a package contributes: commands,\nruntimes, and local process lifecycle descriptors. The `web` dashboard and optional manager\nruntimes ship this way. A `@cotal-ai/*` package named without a version installs this cotal's\nversion, as `setup` and `update` do; a version or tag you name is installed as given.\n\n## Structured output\n\nA command whose help has a `structured output` line naming `json` takes `--json` (`-j`) and writes\none JSON document on stdout. Most commands have one, among them `cotal status`, `cotal space`,\n`cotal service`, `cotal up`, `cotal down`, `cotal ps`, `cotal history` and `cotal config`.\n`cotal help <command>` shows whether a command and each of its subcommands has one.\n`cotal version --json` writes:\n\n```json\n{\"schemaVersion\":1,\"command\":\"version\",\"target\":null,\"outcome\":\"ok\",\"completeness\":\"complete\",\"result\":{\"version\":\"<version>\",\"extensions\":[{\"package\":\"@cotal-ai/web\",\"version\":\"<version>\"}]},\"warnings\":[]}\n```\n\n| Field | Meaning |\n|---|---|\n| `schemaVersion` | `1` |\n| `command` | The command that ran |\n| `target` | What it acted on, or `null` for a command that reads only local metadata |\n| `outcome` | `ok`, `no-op`, `refused`, `failed`, `partial` or `unknown` |\n| `completeness` | How much of the read scope the result covers: `complete`, `partial` or `not-checked`. It says nothing about whether the work succeeded |\n| `result` | The command's result. A `partial` read keeps what it read; a refused, failed or unknown run has `null` |\n| `warnings` | One `{code, target, message}` for each thing it could not read or check |\n| `error` | Present when the outcome is not `ok` or `no-op`: `{code, message, remedy, cause}`, where `cause` keeps an endpoint's own code, outcome and safe detail |\n| `changed` | What a command that did not succeed changed anyway |\n\nDiagnostics stay on stderr. A usage error under `--json` is a `refused` envelope with code\n`USAGE_INVALID`, and the help it names stays off stdout. A command without a JSON output refuses\n`--json` as an unknown option.\n\nA subcommand's help has its own `structured output` line, and only the forms that show one write a\ndocument. Bare `cotal ext` and `cotal ext list` do. `ext add`, `ext remove`, `ext root` and\n`ext seed` report as text, refuse `--json` and `--plain` with a usage error, and exit 1 on any\nfailure.\n\nA command whose `structured output` line also names `plain` takes `--plain`, which writes the same\nreport as plain lines: no color, no marks, ASCII labels. Plain writes these punctuation marks in\nASCII wherever they appear: `\xB7` and long dashes as `-`, `\u2026` as `...`, `\u2192` and `\u21B3` as `->`, and `\xA7`\nas `section`. Other characters, such as the letters of a name or a message, are written as they are.\nPass one of `--json` and `--plain`; both is a `USAGE_INVALID` refusal. A table that does not fit the\nterminal is written as one labeled record per row, so no value is cut off.\n\nShared human reports for a resolved space begin with `Mesh: <space>`, including\nempty `ps` results, manager inventories, the impact a command shows before it\nstops something, and classified failures. A failure raised after the command\nresolved its space carries that space in `target.space`, even when the failing\nstep named no space. The label comes from the operation's target, not a new read\nof the current selection. Global reports do not invent a selected mesh. JSON\nretains `target.space` without a text header. Plain watch feeds and followed\nhistory prefix each new row with the mesh name so the context remains visible as\noutput scrolls.\n\nOn a terminal, a long value, line, warning or failure wraps at spaces, and each continuation line\nstarts under the text it continues. A word wider than the line, such as a path, keeps a line of its\nown and is never split. Help wraps the same way, including the title of `cotal help --all`.\n\nA usage line breaks only between options, never between an option and its argument. A ` | ` stays\non the line before it whenever it fits there. A compact `|`, such as the one in\n`<persona.md>|--prompt`, is a break point too: the line ends right after it, the next line starts\nwith the following option, and no space is added or lost. A usage too wide for the line breaks after\nits own ` | ` or compact `|` alternatives first. A group too wide for the line, such as\n`(--to/-t <agent> | --channel/-c <channel> | --role/-r <role>)`, takes lines of its own and breaks\nthe same way inside, and so does a group nested in it. A group that fits, such as\n`[--json|--plain]`, stays whole. Only a single option with its argument that is wider than the line\nbreaks at its spaces. Examples are not wrapped, so each stays one line to copy.\n\nWhen the flag names leave their descriptions fewer than 30 columns, each description goes on the\nlines under its flag. A report's list of commands follows the same 30-column rule. Below 48 columns,\na record puts each label on a line of its own with its value under it. Output to a pipe or a file is\nnever wrapped.\n\nA read that could not read part of what it was asked for is `partial`: `result` holds what it read,\n`completeness` is `partial`, each part it could not read is a warning, and it exits 4. A list is\nnever presented as complete when part of it is missing.\n\nA target the command cannot resolve or reach is refused before any request, with the code for why:\n`TARGET_NOT_FOUND` (no such mesh), `TARGET_AMBIGUOUS` (several match), `TARGET_UNAVAILABLE` (its\nbroker or manager did not answer), `AUTH_REQUIRED` (it needs a sign-in or a fresh credential),\n`PERMISSION_DENIED` (it refused the credential), `DEPENDENCY_UNAVAILABLE` (trust material, the\nuser-auth service or a space catalog it needs could not be used), or `UNSUPPORTED`.\n\nA command with a JSON output exits by its outcome, with or without `--json`:\n\n| Outcome | Exit | Codes |\n|---|---|---|\n| `ok`, `no-op` | 0 | none |\n| `refused` before any effect, for invalid input | 1 | `USAGE_INVALID`, `SCHEMA_INVALID` |\n| `refused` at a known boundary before any effect | 2 | `PERMISSION_DENIED`, `AUTH_REQUIRED`, `TARGET_UNAVAILABLE`, `TARGET_NOT_FOUND`, `TARGET_AMBIGUOUS`, `DEPENDENCY_UNAVAILABLE`, `UNSUPPORTED`, `CONFLICT`, `LIFECYCLE_BLOCKED` |\n| `unknown`: it may have had an effect that cannot be attributed. Do not retry blindly | 3 | `OPERATION_UNKNOWN` |\n| `partial`: some of the work completed, or cleanup remains | 4 | `OPERATION_PARTIAL` |\n| `failed`: a confirmed failure with no cleanup left | 5 | `OPERATION_FAILED` |\n\nAn error such a command does not classify is `unknown`, exit 3. Other commands still exit 1 on any\nfailure.\n\n## Commands\n\n| Area | Command | Purpose |\n|---|---|---|\n| Set up & lifecycle | [`setup`](#setup) | Prepare a space, application and runtimes; opt into startup separately |\n| Set up & lifecycle | [`update`](#update) | Reconcile first-party extensions and check or opt into a coherent CLI upgrade |\n| Set up & lifecycle | [`up`](#up) | Start a local mesh (nats-server + JetStream), or boot a whole manifest with `-f` |\n| Set up & lifecycle | [`down`](#down) | Stop the whole stack, selected registered components, or a manifest deploy |\n| Set up & lifecycle | [`backup`](#backups) | Create an offline full-space or registry-only artifact from a preserved cut |\n| Set up & lifecycle | [`clean`](#clean) | Configurable cleanup: purge history (live), or wipe the local store / identity (stopped) |\n| Set up & lifecycle | [`reset`](#reset) | Preview or reset one stopped local store or space identity |\n| Set up & lifecycle | [`space`](#selecting-a-space) | List the spaces this machine can reach, or show one in full |\n| Set up & lifecycle | [`use`](#selecting-a-space) | Select the space commands target when none is named, or show the selection |\n| Set up & lifecycle | [`meshes`](#mesh-registry) | List, register and drop meshes; `space` replaces its listing |\n| Set up & lifecycle | [`sync`](#mesh-registry) | Refresh the signed-in account's advertised spaces |\n| Set up & lifecycle | [`status`](#mesh-registry) | Read-only diagnostics for setup, processes, and the selected mesh |\n| Agents & personas | [`spawn`](#spawn) | Launch an agent from a persona (foreground, or `--detach` via the manager) |\n| Agents & personas | [`models`](#models) | List connector model catalogs and variants from the manager |\n| Agents & personas | [`ps`](#managed-seats) | List managed agents and their mesh status |\n| Agents & personas | [`stop`](#managed-seats) | Ask the manager to stop a managed agent |\n| Agents & personas | [`attach`](#managed-seats) | Stream and drive a managed agent's terminal (pty runtime) |\n| Agents & personas | [`input`](#input) | Type one line into a managed agent's terminal without attaching |\n| Agents & personas | [`persona`](#persona) | List, show, edit, create, or remove local personas |\n| Agents & personas | [`supervise`](#supervise) | Deprecated and hidden from help: run a manager daemon; use the `manager` service preset |\n| Agents & personas | [`service`](#service) | Save, start, stop and inspect owned local services, from a preset or a definition file |\n| Agents & personas | [`daemon`](#daemon) | Linux: serve the local machine daemon's control endpoint, read its status, sign it in, check and request its own registration with a space |\n| Agents & personas | [`runtimes`](#runtimes) | List the agent runtimes the manager can spawn through and whether each is reachable |\n| Agents & personas | [`seats`](#seats) | List the pty seat custodians an earlier Linux manager left, and drain the ones whose agent has exited |\n| Agents & personas | [`endpoint deregister`](#endpoint-deregister) | Remove the service registration of an instance whose host is gone, when the broker affirms nothing is subscribed on its rail |\n| Agents & personas | [`reconcile-gate`](#reconcile-gate) | Unfreeze an issuance gate left frozen by a crashed restart when the successor cannot boot-heal it (holder gone, complete CONNZ sweep) |\n| Messaging & watching | [`who`](#who) | The live roster of the selected space, and whether a manager runs each seat |\n| Messaging & watching | [`whoami`](#whoami) | The identity this CLI acts under, and what it may read, post and do |\n| Messaging & watching | [`endpoints`](#endpoints) | List every endpoint in the live presence roster, including infrastructure |\n| Messaging & watching | [`describe` / `invoke`](#endpoint-control) | Resolve a v0.4 service's command surface off the wire; invoke one command by name |\n| Messaging & watching | [`send`](#send) | Send one message, then exit: DM a peer, post a channel, or ask a role |\n| Messaging & watching | [`chat`](#chat) | Open a live session under its own identity: terminal view, `--plain` lines, or `--stdio` for programs |\n| Messaging & watching | [`channels`](#channels) | Inspect or set the channel registry |\n| Messaging & watching | [`history`](#history) | Read retained messages, or clear them |\n| Messaging & watching | [`pending`](#pending) | Inspect one DM durable's native counts and bounded candidate IDs |\n| Messaging & watching | [`console`](#console) | Live protocol view for a space (TUI, or `--plain` line stream) |\n| Messaging & watching | [Web dashboard](#web) | Browser dashboard, run as the `web` service preset (installed as the `@cotal-ai/web` extension) |\n| Extensions & misc | [`linear`](#linear) | Serve the official Linear MCP server as an endpoint agents can call (installed as the `@cotal-ai/linear` extension) |\n| Auth & meshes | [`mint`](#mint) | Mint a creds file for a space (static auth mode) |\n| Auth & meshes | [`login`](#login) | Sign in to a per-user-auth mesh's IdP (once per machine) |\n| Auth & meshes | [`logout`](#login) | Revoke the IdP session and clear the cached login |\n| Auth & meshes | [`actor`](#actor) | Manage a user-auth space's actor ledger (grant / revoke / list) |\n| Auth & meshes | [`doctor`](#doctor) | Credential-health diagnosis and repair (`doctor auth`) |\n| Auth & meshes | [`join`](#join) | Join a space as your own presence (interactive) |\n| Manifest | [`topology`](#manifest-deploys) | Validate and view a mesh manifest's access graph (read-only) |\n| Extensions & misc | [`ext`](#ext) | List, install and remove operator CLI extensions |\n| Extensions & misc | [`docs`](#docs) | Read this version's protocol and CLI docs, offline |\n| Extensions & misc | [`config`](#config) | Show and set your preferences: the defaults your commands use |\n| Extensions & misc | [`completion`](#completion) | Report, print or install shell completion |\n| Extensions & misc | [`feedback`](#feedback) | Send feedback to the Cotal developers |\n| Extensions & misc | [`telemetry`](#telemetry) | Show or set the anonymous usage telemetry level (`off`, `minimal`, `regular`) |\n| Extensions & misc | [`deliver`](#server-daemons) | Deprecated and hidden from help: run the delivery daemon; use the `delivery` service preset |\n| Workflow runs | [`run`](#run) | Operate durable workflow runs: start, resume, list, inspect, answer a checkpoint, check an edited program with migrate |\n| Extensions & misc | [`feedback-intake`](#server-daemons) | Deprecated and hidden from help: run a feedback intake server; use the `feedback-intake` service preset |\n| Extensions & misc | [`auth-service`](#server-daemons) | Deprecated and hidden from help: run a user-auth space's auth service by hand; use the `auth-service` service preset |\n\nThe manifest modes of `up`, `spawn`, and `down` (`-f <cotal.yaml>`) plus `topology` are covered\ntogether under [Manifest deploys](#manifest-deploys).\n\n## setup\n\n```bash\ncotal setup [--destination <local|self-hosted>] [-H|--harness <connector>] [--runtime <name>]... [--project <dir>]\n [--space <name>] [--connection-file <bundle.json>] [--configure-only | --start [-d|--detach]]\n [--on <manager-instance>]\n [--demo|--full] [-y|--yes] [--json|--plain]\ncotal setup --skills\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--destination` | asked | `local`: a space this machine starts. `self-hosted`: a space registered here that runs on a server, including a Cloud space `cotal login` discovered. `cloud` is refused before anything is written or contacted, and the refusal names that route: `cotal login`, then `--destination self-hosted --space <name>`. |\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 but cannot be chosen. Its hint says to sign\nin with `cotal login` and choose Self-hosted, because a Cloud space is registered here when you sign in.\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. The refusal prints the\nscripted form below and not the full command help:\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\nAfter an upgrade, `up`, `down` and the `service` verbs still control the stack members and\nfirst-party service instances an earlier release recorded, as long as their adapter's declaration\ndid not change. A third-party adapter's instances stay pinned to the package version that started\nthem.\n\nWithout `--self`, `update` keeps the installed first-party surfaces coherent with the running\nbinary: it force-reconciles the four built-in connectors, brings what a connector installed in its\nharness to this release (the `cotal` plugin copy Claude Code sessions load, and its Claude Desktop entry), then reinstalls other `@cotal-ai/*`\noperator extensions at the binary's exact version. Each extension runs in an isolated child, so one\nfailure cannot poison later replays. It then checks npm; a newer binary is an informational notice\nwith `cotal update --self` as the next command, not an automatic install.\n\nBefore disk reconciliation, `update` reads the selected running manager. A machine with no recorded\nmesh has no running manager to observe, so that read is skipped and the command completes. The same\nholds when every recorded mesh is down and none is selected. A remote user mesh, registered with\n`cotal meshes add --mode user`, is named and skipped: its manager runs under another install, so\nthere is no custody on this machine to preserve, and a `legacy` verdict still comes only from a\nmanager this machine read. A stopped mesh that this machine runs, chosen with `--space` or by running\ninside its project, has no running manager either when no `cotal supervise` process on this machine\nmay be serving its space and nothing listens at its recorded broker address, so the read is skipped\nthere too. That is the cold path past a `legacy` verdict: `cotal down`, then `cotal update`. Any\nother selected mesh whose manager cannot be read is still a refusal: a named space that is not\nrecorded, a mesh registered with `cotal meshes add`, a `--server` override, a `cotal supervise`\nprocess still running from any folder whose command line names that space or no space, or a broker\naddress where anything accepts the connection or the dial does not finish in time. With several\nmeshes running and no `--space`, `--server` or `--creds`, the install is machine-wide, so every\nrunning manager is reported in turn, each under its space name, before anything is written; a\n`legacy` verdict on any of them makes the whole run not a hot update. A selector flag still reports\none manager. A manager without a\ncustody generation is reported as `legacy`: it cannot preserve its manager-owned PTYs, so the\ncommand says that this is not a hot update and prints `exact`, `fork`, `fresh`, or `drain-only`\nfor every seat. This report sends no stop, preservation-commit, or replacement command.\nIt does not preserve a running PTY on a legacy manager. The built-in pty runtime spawns\nin-process on every platform and reports `legacy`. On Linux it still adopts seats that an earlier\nmanager left under a detached custodian, but it starts no new custodian. An incompatible native\n`@lydell/node-pty` or ConPTY ABI break remains an explicit per-seat maintenance cut.\n\nWith `--self`, the selected running manager is reported before any global install. When a newer\nrelease exists, Cotal then installs the exact version it validated, resolves and verifies that\npackage in npm's global root, then launches that binary with the same `--space` / `--server` /\n`--creds` selection to reconcile connectors and first-party extensions to the new generation. An npx\nor dev-clone invocation therefore installs and continues through a separate global copy; it never\nclaims the already-running process changed. From a source checkout, the `update --self` hint in\n`update` and `update --check` says so. If the binary is current, `--self` performs the normal\nlocal reconcile without reinstalling it. Each npm install it runs starts with 022 added to your\numask, so a umask of 0002 does not leave the installed tree group-writable.\n\nThird-party extensions are listed with their installed version and recorded spec but are not\nauto-updated in v1. Floating third-party updates require `@cotal-ai/*` peer-range validation and are\na future follow-up. A failed connector/extension install, npm metadata check, or requested global\ninstall is reported and makes the command exit nonzero. Independent extension attempts continue so\nthe output includes every failure; an unavailable npm registry does not undo a completed local\nreconcile, but the command still exits nonzero because it could not establish that the install is\ncurrent.\n\n## up\n\n```bash\ncotal up [--foreground|--detach] [--open] [--space <s>] [--root <prepared-dir>] [--server <url>] [--channels <path>] [--runtime <name>]\ncotal up --user-auth --idp <url> [--exchange-public-port <n> --exchange-public-url <https://\u2026> [--exchange-trusted-proxy]]\n [--advertised-server <url>] [--agent-provisioning-url <https://\u2026>]\ncotal up --tls-cert <cert.pem> --tls-key <key.pem> # serve broker TLS (both, or neither)\ncotal up --restore <dir> [--restore-only registry] [--accept-missing-source]\ncotal up -f <cotal.yaml> [--dry-run] [--runtime <name>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--server <url>` | auto (free local port) | Listen URL override: one `nats://` or `tls://` URL with a host, without credentials, path, query or fragment. Any other value refuses `USAGE_INVALID` before anything is written. A controller stack's broker also needs one IP literal that is not a wildcard, which `up` checks before it writes the stack journal. With `-f`, it overrides the manifest's `broker.servers` and follows that field's rules instead: a valid URL with no embedded credentials |\n| `--host <host>` | none | Bind host override for a **fresh** broker boot: an IP or hostname only, never a URL (that is `--server`) and never `host:port` (the port comes from `--server` or its default); a URL or port-bearing value is refused pointing at the right flag. With no `--server`, the broker URL is derived from it, so `--host <addr>` alone is enough to make a mesh reachable at that address; a `--host`/`--server` pair naming different addresses is refused. A wildcard bind (`0.0.0.0`, `::`) keeps a dialable loopback URL. Recorded on the mesh and reused by every later manager launch, so a repair or resume keeps remote [`attach`](#managed-seats) working. A live refresh (`\u2713 mesh already running`) does not rewrite `.cotal/auth/server.conf` or rebind nats; stop the broker, then re-run `up --host` |\n| `--space <s>` | the folder's space | Space name |\n| `--root <prepared-dir>` | current folder's workspace | Use an existing local project's own `.cotal` root rather than inheriting one from the invoking folder. An absent, inherited or symlinked `.cotal`, or the machine's home store, refuses before startup. Prepare a new folder with `space create <space> --root <dir>` first. Relative file, restore, channels, TLS and store paths remain relative to the invoking folder. The native `up` selection rule still applies after startup. |\n| `--store-dir <dir>` | none | JetStream store directory (recorded; a repair up reuses it) |\n| `--max-file-store <bytes>` | nats-server's dynamic cap | JetStream file storage cap in bytes (a positive integer, no unit suffix). Without it nats-server sizes the store at start as three quarters of the free space on its filesystem. The cap is fixed at broker start: a running broker cannot change it (`cotal down` first), `down --preserve-state` keeps it for the resume, and a resume with a different value is refused. Not accepted with `-f` |\n| `--channels <path>` | `.cotal/channels.json` if present | Channel-registry seed file (JSON). An explicit path that is missing is an error |\n| `--restore <dir>` | none | Restore a completed offline backup before exposing the normal listener |\n| `--restore-only registry` | artifact selection | Restore only the registry component |\n| `--accept-missing-source` | off | Explicit disaster consent when the inode-bound preserved source is absent |\n| `--accept-stale-checkpoint` | off | Explicit consent to resume a seat whose checkpoint was captured outside its recorded recency horizon |\n| `--open` | off (auth) | Unauthenticated dev mesh: no JWT, no ACLs |\n| `--user-auth` | off | Per-user auth: people `cotal login`; connects are authorized against the actor ledger |\n| `--idp <url>` | none | With `--user-auth`: the IdP auth base URL to pin on first enable. A later run may omit it or pass any spelling that normalizes to the pinned URL, such as one with a trailing slash; a different IdP is refused. With `-f`, overrides the manifest's `broker.idp` under the same rules: https (or http on a loopback IP literal), with no credentials, query or fragment |\n| `--exchange-public-port <n>` | none | With `--user-auth`: add the public exchange face on this loopback port, for an HTTPS reverse proxy to forward to |\n| `--exchange-public-url <https://\u2026>` | none | With `--exchange-public-port`: advertise the reverse proxy's HTTPS URL in discovery |\n| `--exchange-trusted-proxy` | off | With `--exchange-public-port`: attribute public failure buckets to the last `X-Forwarded-For` hop. Enable only when the listener is reachable solely through a trusted proxy; otherwise the socket address is used |\n| `--advertised-server <url>` | `--server` | With `--exchange-public-port`: the broker address the public discovery bundle tells participants to dial, such as a `wss://` address behind your proxy. Must be a `nats://`, `tls://`, `ws://` or `wss://` URL |\n| `--agent-provisioning-url <https://\u2026>` | none | With `--exchange-public-port`: the deployment's agent-provisioning endpoint the public bundle advertises. A participant's `cotal spawn` on a remote user-auth mesh sends its login bearer there to provision the agent; without one, that spawn needs a one-time enrollment ([Run a mesh](run-a-mesh.md#remote-supervised-seats-by-enrollment)). HTTPS only |\n| `--service-endpoints` | off | With `--user-auth`: the auth service also hosts service endpoints: the public registration door and the authenticated host route that `cotal service-endpoint` drives |\n| `--detach` | on | Run in the background, which is also the no-flag default |\n| `--foreground` | off | Keep native stack lifetime in this terminal. Required when a systemd/launchd unit supervises `up` itself. Cannot combine with `--detach` |\n| `--tls-cert <path>` | none | PEM certificate to serve TLS with. Must be given together with `--tls-key`. Before starting the broker, Cotal checks readability, private-key mode, key/certificate match, the validity window, and host coverage. `nats-server` accepts an expired certificate and leaves the failure to clients, so Cotal performs these checks first. The decision is recorded; a later bare `cotal up` keeps serving TLS |\n| `--tls-key <path>` | none | PEM private key for `--tls-cert`. Refused if group- or other-readable (tighten to `600`) |\n| `--file <cotal.yaml>`, `-f` | none | Launch a whole mesh from a manifest |\n| `--dry-run` | off | With `-f`: print the plan, mutate nothing. A preview from an unprepared folder under the machine home reads that folder's empty policy rather than inheriting the home store; applying there requires `space create` first. |\n| `--runtime <name>` | `pty` (or the manifest's, with `-f`) | Agent runtime for the mesh manager (`pty` built in; others are installed extensions, explicit-only). Resolved + probed before the broker starts; an uninstalled/unreachable runtime fails loud. Without `-f`, that refusal is `DEPENDENCY_UNAVAILABLE` (exit 2) before anything is written, and for a known runtime the remedy names the `cotal ext add <pkg>` to run. With `-f`, overrides the manifest's runtime |\n| `--max-sessions <n>` | 64 | Live-session ceiling for the mesh manager. Each console pane and each `cotal attach` is one session, so size for agents \xD7 panes, not agent count. Recorded on the mesh and reused by every later manager launch, so a repair or resume does not silently drop back to 64. A running manager cannot change it: `cotal down` first, then `cotal up --max-sessions <n>` |\n| `--no-manager` | off | Broker-only boot: start the broker and, in auth mode, the delivery daemon, and no local manager. A refresh under the flag of a mesh whose manager is live refuses rather than keeping or stopping it. For a controller stack, use `cotal service stop stack-manager --space <s> --yes` first. A legacy stack has no command that stops only its manager: `cotal down` stops the whole stack, then `cotal up --no-manager --space <s>` starts it broker-only. Cannot be combined with `--runtime`, `--max-sessions`, or an agent-declaring manifest |\n| `--rotate-sys` | off | Rotate the space's system account and re-mint its two `$SYS` creds. Needs a stopped mesh; refused with `--open` |\n\n`cotal up` boots a local nats-server with JetStream and, in auth mode (the default), JWT auth and\nper-agent ACLs. It runs in the background by default and records the mesh so `cotal spawn` from\nany directory can find it; `--detach` explicitly names the same mode. `--foreground` keeps the\nnative stack in this terminal. A contradictory mode pair refuses before setup or seed work. An\nunqualified `up` from a folder that inherits the machine's home `.cotal` refuses before seeding:\nprepare that folder with `space create <space> --root .`, then run `up --root .`, or move to the\nhome folder if the home stack was intended. When `--root` names another prepared project, run bare\n`cotal down` from that project folder: bare down\nstops the folder's stack, and `down --space` is not a remote replacement for that scope. With no\n`--space`, `up` runs the folder's space: the one its auth account names, else the one its stack or\nrunning mesh records, else the one prepared there, else `main`. A second `up` in the same folder\nruns the same space again. A folder whose stack and registrations name more than one space refuses\nwith `CONFLICT` and asks for `--space`. With no\n`--server`, it auto-selects a free port if the default address is taken, and a later `cotal up` of\nthe same space from the same folder starts from the URL that mesh recorded; an explicit `--server`\nstays fail-loud on collision. A background boot also brings up the control plane (delivery daemon in auth\nmode, then the manager). `--no-manager` is the broker-only mode: it boots\nthe broker (and the delivery daemon in auth mode) and starts no manager, so there is no manager\npidfile to leave stale. A refresh under the flag of a mesh whose manager is live refuses rather\nthan keeping or stopping it. Stop a controller stack's manager first with\n`cotal service stop stack-manager --space <s> --yes`. A legacy stack has no command that stops only\nits manager: `cotal down` stops the whole stack, then `cotal up --no-manager --space <s>` starts it\nbroker-only. For a split topology with a manager, wait for `.cotal/manager.<spaceKey>.log` to contain `\u2713 manager up`, then stop that manager the same way on that\nhost and run [`supervise`](#supervise) against the remote broker; see\n[Run a mesh](run-a-mesh.md). `cotal up --detach` prints `\u2713 running in the background:` with\n`manager` listed (pidfile liveness, not a teardown boundary); with `--no-manager` the line lists\nonly what actually started. Started from the machine home, the line names `cotal down --preserve-state`,\nbecause bare `down` refuses the home stack. With `--no-manager` it names `cotal up` first, because\n`--preserve-state` needs a manager. On POSIX, a foreground broker has its own session and process group.\nTerminal Ctrl-C reaches the CLI owner, which stops the manager through the same stop as bare\n`cotal down` (see [`down`](#down)), then the rest of the stack, and reports managed agents under\nthe same rule. When the manager stop is refused, Ctrl-C prints the refusal with the reap route and\nleaves the stack running, including the manager, broker, delivery and any auth service. A second\ninterrupt during teardown does not start another stop. Windows keeps its existing console signal\nbehavior. The `-f` form is a\n[manifest deploy](#manifest-deploys).\n\nAn `up` that starts or refreshes a space on this machine writes the `default` persona into\n`<root>/.cotal/agents` when the root has none, so a bare `cotal spawn` has a persona to launch. A\nspace prepared or started before it had one gets it on its next `up`, and a persona you edited is\nleft as it is. A controller stack lists the write under `result.written` and in its report, and the\nretained launcher prints the path it wrote. If the write fails, the stack stays up and the failure\nis a warning. A controller recovery or restore leaves the persona to the next `up`.\n\nA space name belongs to one registered mesh. `up` refuses a name that another live mesh holds. It\ntakes over a name whose broker is not running, and when that registration belonged to another root\nit prints that root and its broker address. It refuses instead when that root holds a restore or a\npreserved cut in its maintenance journal: finish that attempt from its root, or start this stack\nunder another `--space`. It also keeps the name, and names that root, when the root or its journal\ncannot be read, for example when access to a parent folder is denied, or when the recorded root is\nnot an absolute path. A recorded root matches this one only by the folder it resolves to on disk, so\na path such as `<this root>/link/..` whose link leads through an unreadable folder keeps its name\nlike any root that cannot be read. A root counts as gone only when it no longer exists or is not a\ndirectory. A readable root with no maintenance journal also gives up its name. Each of these\nrefusals ends `refused` with exit 2, on a controller stack and on the retained launcher, and shows\nits reason and the next step, such as `cotal meshes rm <space>` for a root that is not an absolute\npath.\n\nA foreground `up` holds the root's maintenance lock only while it starts the stack. A bare\n`cotal down` from another terminal can then stop the stack; the foreground `up` sees the completed\nstop or preserve, prints `Foreground stack released` and exits 0. A systemd unit that runs\n`up --foreground` should set `KillMode=mixed`, so the stop signal reaches only `cotal up`, which runs\nthe guarded stack stop, and a `TimeoutStopSec` long enough for that stop. The default\n`control-group` mode signals every member at the same time, and a manager signalled directly does\nnot stop through its request-bound stop.\n\nThe generated broker configuration is owner-private (`0600`) in open and authenticated modes,\neven with a group-writable umask. `up` creates `.cotal` and the broker store owner-private (`0700`)\ntoo. An existing directory keeps its mode. The broker log `.cotal/nats.log` and the auth service,\ndelivery and manager logs are created owner-private as well, and the process records `up` writes, such as\n`.cotal/nats.pid` and its identity pin, are created `0600` under a fresh name that is never opened\nthrough a symlink, then renamed into place. The pid record and identity pin of a native service\ninstance are published the same way. A file Cotal writes in place under `~/.cotal` or a\nproject's `.cotal`, such as a generated configuration, a credential, a process marker, a persona,\na seat record or one of those logs, is never written through a symlink.\nA file you own is set to `0600` through the open file. A symlink, a file another user owns or a name\nthat is not a regular file is refused and left unchanged. A private directory is set to `0700`\nthrough its path, which follows a symlink; this step does not refuse a symlinked `~/.cotal` or\n`.cotal`.\nOn Windows the owner and symlink checks do not\napply, because POSIX modes are not that platform's protection model; the file is opened with mode\n`0600` and private secrets are locked down by their access list. Local records under `~/.cotal`\nand a project's `.cotal`, such as the stack journal, service records and deployment attempts, use\nthe same model there. A store directory Cotal creates gets the private access list, and a record is\nwritten like a private secret. Reading a record or checking its path does not check an owner, a mode\nor an access list, and does not refuse a link: a symlink or junction is followed. Size limits, the\nrecord schema, the regular-file check and the refusal of a link inside a record store's own\ndirectories still apply. When a service path check refuses a path, the refusal names the option,\nthe path that failed, and its owner and mode. A whole-stack failure shows that sentence as its\nnative detail.\n\nA fresh prepared static-auth, open or plain-loopback user-auth root starts through the service\ncontroller. It records its mode, definitions and full instance IDs in the private\n`.cotal/stack-controller.json` journal. Static-auth startup prepares trust and configuration, starts\nthe broker, initializes native streams, then starts delivery and the optional manager. Open startup\nprepares an unauthenticated broker and streams, then the optional manager; it creates no delivery\nservice or credential. Each member reports its own readiness; the result does not prove agent or\ntask readiness. `--json` returns one\nstack receipt and `--plain` returns a line-oriented report. Partial startup keeps confirmed members\nand reports them; unknown operations are retained and never retried automatically. A failure while\ninitializing streams or recording the broker registration is attributed to the broker, not the next\nunstarted member. The failure target names that stage. Typed JetStream API errors retain their\nnumeric code and status without copying the server's description or changing the unknown outcome.\n\nA journal root always keeps that controller route and its recorded mode and broker address, even\nwhen a completed stop removed its mesh registration. Bare `up` reuses that address; an explicit\nconflicting `--server` or `--open` refuses before effects. Legacy-only options refuse rather than switching launchers.\n`up -f`, recovery and existing legacy roots retain their native paths; those paths refuse the\nnew JSON/plain flags. A legacy root is one with no stack journal that is neither prepared nor\nregistered for the controller, such as a folder an earlier release started and a normal `down` then\nunregistered. Plain `cotal up` there starts it again on its retained launcher, with its identity,\nstore and history. `space create` in such a folder refuses `CONFLICT` and names that `cotal up`.\nA controller `up` checks that the space is prepared or locally started at its root before it writes\ntrust, configuration, a definition, an instance or the journal; a refusal there leaves the folder as\nit was. When a completed stop and `space forget` removed that record,\n`cotal space create <space> --root <root>` prepares the same space at its root again, and `up`\nstarts it from its journal. Schema apply is not supported. Fresh `team boot` uses the controller\nroute described below. Recovery re-entry keeps its recorded mode.\n\nA whole-stack `up` or `down` takes the root's maintenance lock before it reads the journal. While\nanother whole-stack or maintenance command holds it, the command refuses `LIFECYCLE_BLOCKED` and\nnames that command's pid. A crashed member, or an `up` or `down` killed before it recorded its\noutcome, is settled by the next bare `up` or `down` from process evidence. A member whose recorded\nprocess (pid and start time) is gone is recorded as failed, the earlier operation is recorded as\nfailed, and the command continues with an `OPERATION_SETTLED` warning. A member that is still\nrunning is kept: `up` reuses it and `down` stops it. A member whose process cannot be established,\nor one with a stop that was requested and never confirmed, refuses `LIFECYCLE_BLOCKED` and names\n`cotal service status <definition> --instance <id> --space <space>`. A manager stop whose request\nwas published and not confirmed before `down` stopped waiting keeps the manager's record `stopping`\nunder that request, also after its process exits. The next bare `up` or `down` reads that request's\nown receipts first. When the manager recorded the stop as completed and its exact process is gone,\nthe record becomes `stopped` with that receipt, the request's reservation is released once the\n`down` that took it has exited, and the command continues. When the manager's only receipt is a\nrefusal, which it writes when it started no stop, for example because that `down` had already\nexited, the request is removed, its reservation is released, and the record returns to what its\nprocess shows, so `down` stops a running manager and `up` reuses it. Otherwise it refuses\n`LIFECYCLE_BLOCKED` and names the instance and the original request. `down --dry-run` writes\nnothing, so it does not settle such a stop and refuses the same way. `service start`, `restart` and\n`stop` refuse it `LIFECYCLE_BLOCKED`, exit 2, before any readiness probe, stop request or record write.\nThe earlier stop's outcome stays unknown. When the launch's\nrequest file does not read as its own, the refusal reports that it is unknown whether a stop was\npublished. A manager record that an earlier release left `unknown` while its launch still holds a\nstop request is recorded `stopping` under that request when its process is gone. Its exit is never\nrecorded as a crash, and no `up`, `down` or `service` command relaunches it, removes its record or\nremoves the stack's registration. A stop that has neither a completed nor a refused receipt, such\nas one whose manager was killed before it read the request, is cleared only on request:\n`cotal down --abandon-stop <request-id> --yes`, with the request ID the refusal names. It refuses\n`LIFECYCLE_BLOCKED` while the manager or the `down` that sent the request still runs, and\n`TARGET_NOT_FOUND` when no member retains that request; either way nothing changes. Otherwise it\nremoves the request, the stop reservation and a `--with-agents` shutdown intent that the same stop\narmed, records the manager `failed`, and stops the rest of the stack with a `STOP_ABANDONED` warning.\nWhether that manager stopped or released its agents' seats is not confirmed, and a seat that runs\noutside the manager process may still run. It needs `--yes` and refuses `--dry-run` and\n`--with-agents`. When `up`\nredefines a member because its launch options changed, the journal records the\nreplacement's instance and launch request before the old instance is retired, and the new\ndefinition's revision before the replacement launches. After an `up` cut off in between, the next\nbare `up` or `down` adopts the replacement only when its record carries that launch request and\nrevision for the same operation; `down` stops it and `up` reuses it. Any other instance record\nrefuses `CONFLICT`, and nothing is adopted. A preserve or resume that\nnever recorded its outcome stays with its own maintenance attempt. A member that exits before\nreadiness is recorded as failed with its exit code, and the refusal names its log with\n`cotal service logs <definition> --instance <id> --space <space>`.\n\nBefore it launches a broker, the controller binds the recorded broker address once. When another\nprocess holds it, `up` refuses `CONFLICT`, names the address and the bind error, and launches\nnothing; the stack keeps its recorded address. A text `up` prints one `Starting <definition>` line\non stderr before each member it starts, with the readiness timeout it waits for. A started stack's\nNext offers `cotal spawn`, or `cotal up` to add the manager after `--no-manager`, and `cotal down`.\n\nOn a controller root, `down --preserve-state` refuses `UNSUPPORTED` (exit 2) in this release, in\nevery stack mode. It refuses while validating its arguments, before the confirmation and before any\nstop, write or manager request. Plain `down` stops the stack and keeps its store, channels and\nhistory for the next `up`. Preserving a controller-owned stack returns in a later release.\n`backup create`, `backup restore` and `up --restore` need a preserved cut, so they refuse the same\nway on a controller root (see [Backups](#backups)).\n\nUser-auth controller startup requires a plain loopback IP and an auth provider with read-only\nserver preflight. A first enable checks the required IdP and its JWKS before creating maintenance\nstate, a stack journal, trust or credentials. Missing or invalid inputs are usage refusals; an\nunavailable JWKS is a dependency refusal. Malformed IdP inputs are not echoed. An explicit `--idp`\nis also compared with the recorded pin before maintenance state changes, even while the broker is\nrunning. A different IdP refuses without refreshing members or rewriting their journal. The controller\nchecks again under its lock, and the provider repeats its authoritative checks during preparation,\nbefore any member launches. Later races or failures retain their records and are not rolled back.\nIt starts the broker, initializes streams, records provisional public trust metadata, then starts\nthe `stack-auth` service. Only that exact process's provider readiness supplies the final endpoints.\nDelivery uses the same hosting-root authority as native startup. No user caller is replaced by a\nstatic or borrowed caller.\n\nThe manager requires an existing signed-in, granted CLI login. First enable can therefore start\nthe other members and return partial `OPERATION_PARTIAL` with native cause `AUTH_REQUIRED`, while\nleaving the manager unattempted. Sign in with `cotal login`, obtain the required grant, then run\nbare `cotal up` to continue that known partial operation. `--no-manager` omits that member and its\nlogin requirement. Unknown outcomes are never continued or replayed. Public exchange faces and\nTLS-required or remote user-auth brokers still require the retained native route.\n\nThe journal reads earlier three-member auth/open records with an unattempted auth-service row.\nIts next write records all four roles. It neither adopts legacy processes nor converts a mode.\n\nA repair `up` on a mesh whose broker died reopens the store its record names, and refuses a\ndifferent `--store-dir` rather than silently opening a second store.\n\nThe generated `.cotal/auth/server.conf` is written on a real broker boot and is not an\noperator-owned config. `--host` changes that file only when nats is actually started. A unit\nrestart that leaves an answering listener in place is a refresh, not a rebind.\n\nOn an existing mesh, `cotal up` reconciles the presence and lease bucket TTLs. It writes a reserved\ncanary and waits for the bucket to expire it before reporting success. If the broker accepts the\nstream update but the backing store does not persist or enforce it, `up` exits nonzero with a TTL\npersistence error instead of trusting the value returned by stream info. A refresh that restores a\nmissing manager waits for that process to come up, then says so with its pid (`\u2713 restored in the\nbackground: manager (pid N)`). A manager that `up` starts and that exits at boot instead is reported\nas `! control plane degraded:` with the path of its log, and `up` then exits nonzero, whether it was a\nrefresh, a first `--detach` boot or `up -f`. The same happens when another\n`cotal supervise` takes over the manager pidfile before the started process comes up. A legacy\nrefresh that finds everything already running prints only the `\u2713 mesh \"<space>\" already running`\nline. A controller refresh prints its members under `Controller stack already running`, and its JSON\nresult is a no-op unless it wrote the default persona. Neither lists a role the stack's mode does not use, such as auth on a\nstatic-auth stack. A first\nboot starts its manager without the restore line.\n\nA manager or delivery daemon that is killed, or a daemon that exits because its broker went away,\ncannot release its lease. The row stays until the bucket TTL removes it: 10s for the manager and\n30s for delivery. A repair `up` inside that window waits out the row when it can show the holder is\ngone, then starts the replacement, so one run restores the control plane. For the manager, the row\nmust name this workspace root and a pid that is not running on this host; the new manager logs\n`which is not running on this host; waiting` and the seconds left. For delivery, no daemon may be\nrecorded running in this folder and the row must be held by the credential this folder gave its\nprevious daemon; `up` prints `shard 0 is still leased to this folder's previous delivery daemon`.\nThis holds on a controller stack too, both for an `up` right after the crash and for one after a\n`down` that recorded the members as already stopped, because that `down` keeps the credential.\nThe wait never writes the lease. A row that is written again while `up` waits has a live holder, so\nthe start is refused and `up` exits nonzero. A row held by any other process is refused at once,\nas before.\n\nA delivery daemon that `up` launched and that exits before it is ready fails the start. The error\nquotes the failure the daemon logged during this launch and names its log,\n`.cotal/delivery.<spaceKey>.log`. It describes a lease held elsewhere only when the daemon logged\nno failure. A daemon start that fails on a root whose maintenance journal holds a restore or a\npreserved cut never suggests `clean store`, because that store holds the restored or preserved\ndata. Check the root with `cotal status` first.\n\nA full restore recreates the delivery daemon's durables from their checkpoints, at their recorded\nstart sequence. The daemon binds such a restored durable when it otherwise matches its own\nconfiguration, so a restored store starts. A durable that differs in any other setting still\nrefuses.\n\n\n`--user-auth --idp <url>` starts the space's auth service alongside the broker: the NATS\nauth callout plus its capability-gated local exchange, and optionally the closed public exchange\nface configured by the three `--exchange-*` flags above. The service is torn down with `cotal down`,\nand a re-run of `cotal up` heals a dead service on a running broker. `up` waits for the service to\nfinish binding: while the daemon it launched (or found running) stays alive, the wait extends past\nthe base 15s up to 60s; a daemon that exits is refused at once with \"exited before becoming ready\",\nand one alive past 60s is refused as \"alive and still starting\" (wedged), naming the pid record and\nthe service log. `--user-auth` and `--open`\ncontradict each other and are refused loudly; a running broker cannot change auth mode\nwithout a `cotal down` first. See [identity & auth](identity-and-auth.md).\n\n`--rotate-sys` renews the two `$SYS` credentials (`membership-observer`, `connection-evictor`).\nThey carry a 30-day expiry and nothing re-signs them in place, because the trust record never\ncarries the system-account seed, so they are renewed by issuing a **new system account** under the\nsame broker operator and minting fresh creds against it. The root keeps that seed in its own secret\nstore, written at space creation and replaced here, so an auth service started with\n`--service-endpoints` can mint the short-lived observer credentials service caller issuance needs. A plain re-`up` does **not** do this: it reuses the\nexisting trust record, and its `$SYS` creds along with it.\n\nThe rotation is safe to run on a real space, with one operational cost. The data account, the account\nsigning key, every agent credential minted from it, and the JetStream store are all untouched; what\ndies is the retired system account, and with it any out-of-band copy of the old `$SYS` creds, on every\nbroker that loads the rotated config. The cost is that **earlier full backups stop being restorable**\n(see below), so this is not a no-consequence operation. It needs the broker to restart on the rewritten\nconfig, so it runs as part of a boot:\n\n```bash\ncotal down\ncotal up --rotate-sys --detach # agents reconnect; nothing is re-provisioned\ncotal doctor auth # both $SYS creds healthy again, 30 days out\n```\n\nA rotation is a stopped, fresh boot, and anything that is not one refuses it, all for the same reason\n(the on-disk material and the broker it runs on must never end up on different generations):\n\n- a live mesh, because the running broker would keep serving the retired account;\n- an open mesh, whether that comes from `--open` or from `broker.auth: false` in a manifest, which\n has no system account at all;\n- `--restore`, because reinstating a trust root and superseding it in one command leaves no way to\n say which authority the mesh came up on;\n- an unfinished restore or resume attempt on this root, including one `cotal up` would recover on\n its own, because those paths can adopt a live listener and return without booting a broker;\n- a root that hosts more than one space, because the system account lives in the shared broker\n record and a rotation would retire every tenant's, while the root holds one `$SYS` cred pair\n pinned to one data account.\n\nTwo things to know before you run it:\n\n- **The retirement is config-load-bound.** Old `$SYS` creds are refused by any broker that loads the\n rotated config. A stale `nats-server` still running the *previous* config in memory would keep\n honouring them, so stop every broker for this root first. `--rotate-sys` refuses if this root's\n mesh is recorded as running, if anything unidentified is answering at the address it was given, or\n if the root's pid file names a live (or unreadable) process. Those are Cotal's own ownership\n records, not a scan of the process table: a `nats-server` you started by hand against this root's\n `server.conf` on some other port writes none of them and will not be seen. Do not run one.\n- **It invalidates earlier full backups.** A full artifact binds to the trust chain it was taken\n against, and that commitment covers the operator JWT and the system account. Every full backup\n taken before a rotation refuses to restore afterwards, so take a fresh `cotal backup` once the\n rotated mesh is up. `cotal up --restore` names this case when the data account still matches.\n\nThe commit is not atomic (a trust-record write plus two credential writes), so an interrupted\nrotation leaves the record ahead of the creds. That split is detected rather than silent: every\n`cotal up` on an auth mesh, and every `cotal doctor auth`, compares each `$SYS` cred's issuer against\nthe persisted record and names the retired account. `up` warns rather than refusing, because these\ncreds power the membership graph and live eviction, both of which degrade fail-soft; the mesh is not\nworth taking down over them. Re-running the rotation heals it, at the cost of one generation.\n\nWhile those creds are expired the mesh keeps delivering messages, but the\n[membership feed](delivery-daemon.md) and live connection eviction stay down; `cotal doctor auth`\nand the manager's log both name the credential and this repair.\n\n## down\n\n`down <service-name>` routes one saved owned definition through the\nsame selection, confirmation, reservation, native manager-impact and stop controller as\n`service stop`. Use `--instance <full-id>` when selection needs a full instance pin. `--yes`,\n`--dry-run`, `--space`, `--json` and `--plain` keep that controller's meanings; structured receipts\nname `down` while retaining the service-stop result and actual identity. Unknown definitions do not\nfall back to legacy component pidfiles. A controller stack names its members `stack-broker`,\n`stack-auth`, `stack-delivery` and `stack-manager`; a legacy name such as `manager` refuses\n`TARGET_NOT_FOUND` there and names the `stack-` member to use. Completion offers saved definitions without imports or a\nnetwork request. No hidden connector repair or shared catalog preparation precedes this route.\n\nMultiple names resolve as one selected-space set. All owned targets and process roles are checked\nbefore effects. After confirmation, the controller reserves every definition in stable order and\nrechecks all records and native impact before the first stop. Stops follow the displayed input\norder, with each native guard rechecking at effect. A later failure stops the sequence; one receipt\nretains completed, failed/unknown and unattempted members. Reservation/preflight refusals also\nretain the full selected set. Release is attempted for every acquired definition hold; a reported\nrelease error makes an otherwise known outcome partial without erasing known stop results. An\nalready unknown stop stays unknown. Lock cleanup is not independently proved. This is not atomic and no rollback or cleanup is claimed. Dry-run reads metadata without reservations, provider imports or native impact\nqueries. Duplicate names and `--instance` with multiple names are usage errors. Named `--with-agents` refuses and points to\n`cotal service stop <manager-name> --instance <full-id> --with-agents`; it is never dropped.\n\nBare `down --dry-run` reads local process records for one registered space at the current\nproject root. A root with no `.cotal/` directory, or with no stack registered at it, refuses\n`TARGET_NOT_FOUND`, exit 2, because there is no stack there to preview. A root whose stack is\nalready stopped shows the no-op described below instead. Add `--space <name>` to\nconfirm that exact space belongs to this root;\n`--json` and `--plain` produce a partial, finite inspection receipt. It lists pidfile\nliveness, readable additive deployment IDs, retained additive attempts and\nunreadable paths under this root, and saved service definitions, instance IDs, recorded states and\nhosts, supervisor receipts, and retained install, uninstall and unit-action\nattempts in the selected space's workstation store. Saved records are not attributed to this project root, and their recorded\nstates do not prove live process or supervisor status. A submitted native attempt may have\nchanged state; the preview neither reconciles nor retries it. For each saved service instance it\nalso shows what bare `down` would do with it: stop it, skip it as already stopped, or refuse, with\nthe reason. That plan observes processes on this host and changes nothing. Other local registrations with\nthe exact recorded broker address appear as possible sharing. No match does not exclude\naddress aliases, remote tenants or live attachments. The preview does not establish process\nownership, the complete service set, exclusive broker use or agent impact.\n`--with-agents` only marks the requested policy. It does not reserve or signal\na process, and the receipt is not an authorized stop plan. A fresh `team show`\nsummary points to this read-only command from the recorded root.\n\nOn a root with a controller journal, bare `down` reads only its recorded full instance IDs. It\nshows the member set and the live manager's native agent impact before confirmation, then stops\nmanager, delivery and broker in that order. `--yes` acknowledges that scope in scripts; `--with-agents`\nrequests the manager's native retirement policy. The broker scope is checked again after its\ndependants stop. A failure stops the sequence and preserves completed and unattempted members.\nOnly a complete confirmed stop removes the delivery credential and local registration. When the\ndelivery daemon was already gone, its credential stays: that daemon never released its lease, and\nthe next `up` uses the credential to recognise the row and wait it out before it mints a new one.\nThe JSON `cleanup` field then reads `registration-removed-delivery-credential-kept`. 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. The additive ledger refusal names each ledger with the command\nthat finishes it: `cotal down --run <id>` for a readable one, and for an unreadable one the reason it\ncould not be read and the same command once it is repaired. 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. A registry record that cannot be\nread refuses both with `SCHEMA_INVALID`, exit 1, naming its file.\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 and continue. They warn once when an account's catalog starts failing and stay quiet while\nit keeps failing; the next failure after a successful refresh warns again. `cotal status` refreshes every account,\nnever refuses on a refresh failure, and lists each account as `fresh`, `updated`, `not-modified`,\n`no-catalog`, or `failed` with its error. With no cached sign-in, catalog preparation reads without\ncreating a control directory or lock. An explicit IdP without a cached login refuses with\n`AUTH_REQUIRED`. An unreadable sign-in or catalog cache refuses with `DEPENDENCY_UNAVAILABLE`\nbefore refresh or registry application. Both exit 2 and leave the cache untouched; diagnostics\nname the path without printing cache contents or parser excerpts.\n`cotal sync` bypasses freshness and reports added, changed, removed, unchanged, and name collisions. `--idp` limits it to one signed-in account. It never connects\nto a broker. `--json` gives each account under `accounts` with its `idp`, `state`, `catalog`, the\nfive name lists, `selectionCleared` and `error`. An account whose catalog could not be refreshed keeps\nits last snapshot and is named in a `CATALOG_UNAVAILABLE` warning, so the result is partial, exit 4;\nwhen no account could be refreshed, `sync` refuses with `DEPENDENCY_UNAVAILABLE`, exit 2.\n\nThe registry is updated under the same lock that guards the catalog cache, so a command never lists\na discovered space set that another command is still writing. The cache records a fetched snapshot\nas not yet applied before the first registry write and as applied after the last. If a command dies\nor is stopped in between, the next command applies that snapshot again before it can use it, with\nno request inside the freshness window.\n\nThe shared dispatcher applies this preparation to every command that declares both `--space` and\n`--server` as mesh-target flags, including commands registered by other packages and commands that\ndeclare their own equivalent flag objects. Daemon and startup commands that use those names only as\nconfiguration explicitly opt out. Registry-local `meshes add` and `meshes rm` never refresh a catalog.\nWhile the registry holds a record this build cannot use, the preparation neither refreshes nor\napplies a catalog, so the command's own checks run first. A snapshot left unapplied is applied by the\nnext preparation after the record is restored or removed. A command that resolves its target through\nthe registry still refuses the record by name.\n\nRun on a terminal with the space or `--server` missing, **`meshes add` is guided**: it asks for the\none thing that cannot be derived (the broker URL), probes it, and tells you what answered - open or\nrequiring credentials. It then offers the spaces your `--root` already holds credentials for, states\nthe mode as a fact about that broker rather than asking, and shows the exact record before writing\nanything. A broker that does not answer, or a space name already registered, becomes a choice rather\nthan an error. Anything you pass on the command line is taken as given and not asked again. Without\na terminal - a script, an agent, CI - nothing prompts and the flag form's errors stand\n(`COTAL_NO_PROMPT=1` forces that too).\n\n`cotal up` and `cotal down` maintain their own records. `meshes add` registers a mesh they cannot\nspeak for: one running on another machine, a shared broker, a hosted space. `--root` is the folder\nwhose `.cotal/auth` holds that mesh's credentials and whose `.cotal/agents` holds its personas.\nThe default is the project you run it in. The registry stores that path, never a secret. `--mode`\ndefaults to `auth` when the root holds the space's account record and to `open` otherwise. The\nbroker is probed before anything is recorded, so a wrong address, or credentials that mesh will\nnot accept, fails here instead of at the first `spawn`; `--force` records without verifying (and\nreplaces an existing record).\n\nA hostname or public address is registrable only when the connection will **require TLS**. Pass\n`--tls`, or use a `tls://` URL. The scheme is recorded as enforced intent, so every later dial\nthrough the record demands the handshake (and `meshes add tls://\u2026` against a plaintext broker is\nrefused at registration). Without required TLS the fence admits loopback literals, and\nprivate-overlay literals only with `--allow-unencrypted-overlay`, which records your acceptance that\nthe overlay is protected only while its tunnel is up ([Run a mesh](run-a-mesh.md#which-addresses-you-may-register)). RFC1918 addresses are refused in both modes because a cafe LAN is private but does not belong to you.\n\nA **user-auth** mesh registers from supplied pinned trust, never guessed: `--user-auth-file`\ntakes the bundle exported where the mesh runs; `--from` asks before it dials the address at all,\nthen fetches the `/.well-known/cotal-mesh` discovery document under that address (HTTPS only; a URL\nthat already ends in that path is fetched as given), displays the pins, and asks again before\nadopting them. The document must parse as JSON under any content type the host serves it with; an\nanswer that does not is refused with the URL and the content type that came back. Neither fetch follows redirects: a 302 can move a pinned fetch\nonto plaintext or onto another host, so it is refused rather than followed, and the pinned\nexchange must itself be an `https://` URL, except for an exchange on this machine, where plain\n`http://` is accepted for a loopback *literal* (`127.0.0.1`, `::1`, any spelling of them) but not\nfor `localhost`, which is a name rather than an address. Registration verifies that the exchange\nanswers `/health` and `/jwks` as the pinned issuer. It also verifies that the broker refuses a bare\nconnect; that auth-required refusal is the pass. The sentinel credentials land in a 0600 file under\nthe entry's root; the registry records only the path.\n\n`meshes rm` drops records. It never stops a mesh. For a mesh running on this machine `cotal down`\nis the right verb, and `rm` says so unless you pass `--force`. A broker record that cannot be proven\nstopped counts as running, as it does for `clean`. A hand-added record is removed by\n`meshes rm`, by an `add --force` replacement, or by a `cotal up` that actually starts the broker for that same space, server and root, which becomes that\nmesh and so takes the record over (a `cotal up` for that space anywhere else refuses instead).\nNothing that merely *infers* a record is stale from a dead broker touches it: an\nunreachable broker is listed `offline` and stays, whether `cotal up` or `cotal meshes add`\nwrote the record; a foreground `up` whose broker exits unexpectedly keeps its record the same way.\nA bare command does not treat that offline record as a running mesh;\nname it with `--space` to restart it. When a bare control command resolves its target, it probes the\nrecorded brokers once, and its mode check and its connect both read that one result. `cotal down` / `cotal clean all` still drop an `up` record for the project\nthey tear down; a hand-added one they leave alone even when it shares a root, because nothing\non this machine could write it back.\n\nA discovered entry belongs to the normalized IdP origin and proved subject that supplied it. Local\nteardown, cleanup, and liveness pruning do not remove it. A manual or locally started entry with the\nsame name wins and remains untouched; that discovered name is reported as a collision. Logging out\nremoves only the discovered entries owned by that account.\n\n`cotal meshes` and `cotal status` print `events: required` for a registration carrying\n`policy: { events: \"required\" }`. On that space, foreground spawn, detached spawn and manager\nstarts that host a native harness cannot opt out or launch without an event plane. `--no-events` is refused with\nthe space named. A connector without an event plane is refused with both the space and connector\nnamed. A session whose own grant omits `events.<owner>.<actor>` is refused before joining and the\nmessage names a full-row `actor grant` repair. A running seat whose event plane stops keeps running\nand keeps its mesh connection on that space as on any other; the connector re-establishes the plane\nitself and reports every failure with the delay before the next attempt. A seat the manager resumes\nfrom a preserved cut meets the same rules as a fresh\nspawn: a seat retained with `--no-events`, or on a connector without an event plane, is refused, and\na resumed seat carries the requirement in its launch material.\n\n`join`, one-shot sends and passive protocol views host no native harness turns. They publish no\nAG-UI runs, regardless of who drives their input. Required events do not turn terminal messages\ninto agent runs or copy channel and DM text into an event stream. This launch policy is not an\nadditional broker authorization boundary. The broker still enforces each endpoint's grants.\n\nThe selection `cotal use` sets applies from every directory, including inside another mesh's\nproject. `status` is a read-only report in this order: this machine\n(the installed `cotal-ai` version, NATS and each named integration), the installed extensions and\ntheir versions, this folder's `.cotal/`, the recorded meshes, the selected mesh (its connection, the\ndelivery responder, its channels and membership feed), then the hints. It lists no roster; `cotal ps`\nlists managed seats. Stale Claude skills and out-of-date `.agents` skills recommend\n`cotal setup --skills`, not unscoped `cotal setup`. `status` takes `--space` / `--server` to pick the\nmesh to inspect; it starts nothing.\n\nStatus displays broker addresses without URL user information, query parameters or fragments,\nincluding component facts. Saved addresses and connection inputs are unchanged. An address that\ncannot be displayed safely is `null` in JSON, `unavailable` in text, and names a\n`BROKER_ADDRESS_INVALID` warning with a partial result. Its broker component is refused.\n\nOnly the selected mesh is checked. Its row in the recorded meshes dials its address with its\nrecorded TLS requirement, and every other row reads `not checked`. `--all` checks every recorded\nmesh, including discovered ones, each with its own recorded TLS requirement.\n\n`status --json` writes one envelope whose `target` is `{space, scope}`, where `space` is the selected\nspace or `null` and `scope` is `selected` or `all`. Its `result` is the report the text draws:\n\n| Field | Holds |\n|---|---|\n| `kind` | `infrastructure` |\n| `machine` | `version`, `install` (`kind` and `root`), `nats` (`path`, `bundled` or `missing`), `integrations` (each `{name, state, text}` a connector's setup reports), `connectors` (each `{name, requires, missing}`), `skills` (`state`: `current`, `not-installed`, `behind` or `unreadable`), and `web` (`installed`, `url` and the error of each when it could not be read) |\n| `extensions` | `root` and `items`, each `{package, label, version}` |\n| `folder` | `root`, `auth` (`state`: `space`, `spaces`, `none` or `unreadable`), `personas` (`root`, `default`: `valid`, `invalid`, `missing` or `unreadable`, `demo` and `error`), `launches` (whether a bare spawn launches from this folder's catalog; `null` when no mesh resolves), `processes` (each `{name, state, pid, detail}`, `state` one of `running`, `stopped`, `not-serving`, `unknown` or `unreadable`) and `storedState` |\n| `meshes` | `current`, `unreadable` (the registry directory, a record or the current-mesh pointer that could not be read, or a pointer that is empty, each as `{file, error}`), `items` (each with `space`, `server`, `mode`, `root`, `origin`, `tlsRequired`, `eventsRequired`, `current` and `check`: `reachable`, `unreachable` or `not-checked`) and `catalogs` (each `{idp, state, error}`) |\n| `selected` | `{resolved: false, code, message}` when no mesh resolves (`code` is `registry-unreadable` when the registry directory or a record could not be read); otherwise the target's fields, `connection` (`ok` or the reason it failed; on a user-auth mesh `reachable` or `unreachable`), `login`, `deliveryResponder` (`bound`, `unbound`, `stale` or `unknown`), `snapshot`, `channels` and `membershipFeed` |\n| `services` | Selected-space local owned records: `space`, `state` (`read`, `partial` or `not-checked`), non-atomic `observedAt`, public definition/instance projections and explicit scope. An unreadable family is `null`; stored readiness is historical. Older serialized snapshots may omit this observation, which is not an empty inventory. |\n| `hints` | The hints, in the order the report prints them |\n\nAn unreachable broker, a stopped process or an unbound responder is a state the report names, and\n`status` exits 0. A part it cannot read is named on its row and as a warning, and the run is\n`partial` with exit 4: a PID record, the folder's trust material, the delivery-aware marker, the\nconnectors, the skills bundle, the web records, a persona catalog, the mesh\nregistry or one of its records, the current-mesh pointer, a user-auth login, or the selected mesh's\nchannels or its owned service definition/instance records. Only an absent catalog, registry or pointer reads as empty or unset; an empty pointer is\nnamed too. A registry directory or record that cannot be read also leaves the selected mesh\nunresolved, because resolution refuses it, and the readable records are still listed. A corrupt or\nunreadable extensions manifest is refused while loading the command surface, before this report\ncan run. The manager\nrow asks the service endpoint once: a live process that does\nnot answer is `not serving`, and a probe that could not be made leaves the row\n`running \xB7 service unchecked`.\n\nThe local auth reader uses a stripped signer only when both split trust records are absent.\nThe load refuses dangling and looping links at either record even when an older stripped signer is present.\nA process row whose PID record exists but cannot be read, or exists under both its current and a\npre-upgrade name, reads `pidfile unreadable` with the error, and the other rows still print. A live\nmanager whose delivery-aware marker cannot be read keeps its row and names the failure as\n`delivery-aware marker unreadable` with the error. The `Web process` row\nprints the address the selected mesh's dashboard recorded once it was listening, while the PID in\nits `web.pid` is alive. The dashboard that holds `web.pid` picks the record. A dashboard started with\n`cotal service start` writes its session record beside its service startup receipt, and status finds\nthat record through the web service instance whose recorded process is that PID. Any other holder is\nread from `web.session`. Neither record is read in place of the other. A `web.pid`, session record or\nservice instance record that exists but cannot be read is named on the row with the error.\nOtherwise it reads `down`, or `not installed` without the web\nextension. Status and the `cotal setup` card count the web extension as installed only when a\npackage in the extensions manifest provides `web` and that package is on disk. A web package record\nthat cannot be read, is not JSON or has no `version` is named on the `Web extension` row and on the\ncard's web row, after the dashboard's address when it is listening and beside an unreadable\n`web.pid` or session record. The `Web process` row still prints, and reads `down` unless the\ndashboard is listening.\n\nIf a refresh fails, `status` may still show the kept catalog bytes for diagnosis. It labels them\nstale with the last successful snapshot timestamp and the refresh error. It never calls that state\nsynchronized or online. If a selected discovered space vanishes from a successful snapshot, the\nselection is cleared and the command reports that no default is selected.\n\nFor a user-auth mesh the selected-mesh section reports the login `status` works as: the signed-in\nsubject when this machine holds a cached session for the entry's pinned IdP, or the exact `cotal\nlogin --idp <url>` line when it does not, with no network round trip either way. A locally\nprovisioned space also shows the actor grant row; a discovered or registered remote entry reports\nthe grant as not checkable on this machine, because the ledger runs where the space was\nprovisioned. `--components` on a user-mode target probes as that same signed-in login (`ps`'s\ncredential), never a static mint; when the login cannot supply a credential, the row says why\ninstead of printing the broker's refusal of an unauthenticated probe.\n\nPersona rows name the catalog they describe. If this folder and the selected mesh use different\ncatalogs, status names both and marks which one spawn launches from. A green `default` means the file\npasses the same agent-file loader spawn uses; a present but invalid file is reported as invalid.\n\n`cotal status --components` prints the report, then a fail-loud per-component health pass. It takes\nno `--json`, `--plain` or `--all`. It reads **each\ncomponent's own control surface**, rather than treating a PID, a lease, or a successful probe of a\nsibling as proof that the component serves. It prints one of `serving`, `absent`, `not-serving`, or\n`refused` for each component and exits `0`, `1`, `2`, or `3` respectively (the highest observed\nstate wins). That exit replaces the report's own, so a partial report followed by a clean pass exits\n0:\n\n- **manager**: local PID record, its liveness-lease holder and PID, then the manager's own typed\n `status` service reachability from this host. Manager builds that do not report static\n reconciliation say `static reconciliation not reported by this manager build`; the line stays\n visible even when the manager is otherwise `serving`.\n- **delivery**: local PID record, its ready lease (`ready` is the daemon's own bound-control\n signal), and the latest `renewal.<spaceKey>.json` adoption verdict, the record of the space the\n command was asked about, keyed per space the way the pidfiles are. A re-signed credential and a\n broker-accepted adoption stay distinct facts. A root-only `renewal.json` left by an older build\n names no space and is never read as any space's verdict (`doctor auth` names it as a leftover).\n- **web**: local PID record, then the `/api/meta` response at the address the dashboard recorded\n once it was listening, which must name the same PID. The holder of `web.pid` picks the record, as\n on the `Web process` row: a service-run dashboard's own session record, found through its web\n service instance and named on the row as `service instance <id>`, or `web.session` for any other\n holder. The probe presents the readiness nonce recorded beside that address, the one credential\n the dashboard accepts on `/api/meta`. The probe follows no redirect. An answer with a status\n outside 2xx, a 3xx included, is `not-serving`, and the row names that status, such as `HTTP 401`.\n A live PID with no recorded address (the dashboard is still writing it, or an earlier build\n started it), or an unrecognizable process record, is `refused`, not a green default-port guess. A\n session record or service instance record that exists but cannot be read is also `refused`, and\n the row names the read error.\n- **broker**: the registered mesh URL dialed from this host with its recorded TLS requirement.\n\n`absent` means Cotal has no live local component record (or has a stale record); `not-serving`\nmeans the component record is live but its service/readiness surface did not answer or is not ready.\nThose are intentionally separate exit cases. A failed or unreadable probe is `refused`, never an\nabsent component or a clean zero. A PID record that exists but cannot be read, or exists under both\nits current and a pre-upgrade name, refuses only its own row. A record that its component removes\nwhile the pass runs reads as `absent`.\n\nEach component prints on one line. A control character inside a reported reason, such as a newline\nin a stored row's field name, prints as its `\\uXXXX` escape.\n\n## spawn\n\n```bash\ncotal spawn [<persona>] [--file <persona-file>] [-d | --foreground] [--dry-run] [options]\n```\n\nA bare `spawn` launches through the running manager, waits until the agent joins, and attaches this\nterminal to it (see [Managed launch view](#managed-launch-view)). It needs usable input and output\nterminals and refuses `--json` and `--plain`, before an identity is allocated. `--detach` (`-d`)\nreturns once the agent has joined, without attaching. `--foreground` runs the application directly\nin this process. `--file` (`-f`) names a persona file, never a manifest: manifests deploy with\n`cotal team deploy <file>`, and a manifest passed to `--file` is refused with that command.\n\nOn a user-auth mesh, an ordinary `--foreground` spawn without `--name` allocates the first free actor\nin the persona's series: `default_agent`, `default_agent_2`, `default_agent_3`, and so on. Held\nlocal material and existing grants are occupied, even when no presence is visible. Allocation\npreserves the existing session and its credentials. An explicit `--name`, a signed enrollment,\nand a managed handoff pin an identity and refuse a collision instead of renaming it. Authorization,\nnetwork and malformed-provisioning failures are not reasons to try another name.\n\nFor a foreground spawn onto a remote user-auth mesh, a one-time enrollment may stand in for a cached\nhuman login. Prefer a private file:\n\n```bash\ncotal spawn --foreground --file ./seat.md --space main \\\n --enrollment-file /run/secrets/cotal-enrollment\n```\n\nThe file contains only the enrollment URL, ending with at most one line terminator, and must be\nmode `0600` on POSIX. A launcher may name the same file in `COTAL_ENROLLMENT_FILE` instead, and run\n`cotal spawn --config ./seat.md --space main`. An orchestrator that cannot mount a file may set\n`COTAL_ENROLLMENT_URL`; that value is redeemed byte for byte, so a trailing newline in it is refused.\nNaming more than one source is refused, and so is `--enrollment-file` with `--detach`, `--creds` or\n`--on`. Enrollment input requires `--space` and applies only to a foreground persona spawn. If the\nmesh is not registered yet, the enrollment response must carry the stock user-bundle fields and the\ncommand needs a persona file (`--file`, or `--config` from a launcher) because there is no local\nremote-mesh persona catalog to read. The client\nredeems the URL once, registers the returned mesh material, exchanges the returned actor token at the\npinned auth service, and removes both enrollment variables before starting any child process. A\nregistered mesh whose record pins no exchange URL is refused before the URL is redeemed, so the\nenrollment stays unspent; re-register the mesh with `cotal meshes add <space> --from <url> --mode user`.\n\nA cached login for the same IdP and an enrollment are conflicting proofs, so the command refuses\nrather than choosing one. An invalid enrollment never falls back to login provisioning. Unknown,\nexpired, revoked, and already-used enrollments all produce one response: ask the owner for a fresh\none. See [Enrollment redeem](identity-and-auth.md#enrollment-redeem) for the HTTP contract.\n\nA runtime that starts a managed seat outside the manager's filesystem hands the child a managed\nhandoff instead: one `0600` file named by `COTAL_MANAGED_HANDOFF_FILE`, carrying the lifecycle the\nmanager already enrolled. The runtime builds the command with `delegatedSeatCommand`:\n\n```bash\nCOTAL_MANAGED_HANDOFF_FILE=/run/seat/handoff.json \\\n cotal spawn --config ./seat.md --space main --name <actor> --agent claude \\\n --expect-owner <owner> --expect-lifecycle-uid <uid>\n```\n\nThe `cotal` entry reads the file, deletes it and drops the variable before it parses flags, prints\nhelp or loads extensions, so every outcome leaves no file. The variable is read under any letter\ncase; spellings that name different files are refused after every one of them was deleted. The\nspawn then refuses a malformed handoff, or one whose space, owner, actor or lifecycle UID differs\nfrom `--space`, `--expect-owner`, `--name` and `--expect-lifecycle-uid`, before any broker\nconnection or exchange request. Every refusal on this path names the field and never a value from\nthe handoff. The registration's server, exchange and enforcement checks, the local state this\nmachine keeps for the space (its mesh record, user-auth state and agent secret files), target\nresolution, the policy refresh, the broker preflight and the agent auth preflight quote the space,\nthe server, the exchange URL, the actor or a path named for one of them in their own diagnostics and\nin the filesystem errors under them. For a handoff each prints one fixed sentence that names the\nfield and the phase instead, whether its check fails or an error is thrown. When the agent auth\npreflight's rollback then fails to remove a secret or file, that sentence is followed by the names of\nthe cleanup steps that failed, without their errors. The event-plane policy\nrefusals name the handoff's space field. An actor outside `[A-Za-z0-9_]` and a space that cannot\nname local state, such as `..`, are refused as malformed before any plane. A handoff conflicts with\nthe enrollment variables, `--detach`, `-f` and `--creds`, and needs `--config <persona-file>`. From\nthere it runs the enrollment consumer above without redeeming anything. See\n[Delegated seats](embedding.md#delegated-seats-outside-the-managers-filesystem).\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | resolved mesh | Target space |\n| `--server <url>` | registry entry | Broker URL override |\n| `--creds <path>` | none | Control-caller creds for an off-registry manager. Managed launches only; refused with `--foreground` and from a managed seat's own shell |\n| `--name <n>`, `-n` | persona's `name:` | Presence-name override (does not choose the persona) |\n| `--file <persona-file>`, `-f` | none | Persona file to launch, on the execution host. A value with no path separator that does not end in `.md` is read as `./<value>`. Refused together with a positional persona, and when the file is a Mesh manifest |\n| `--config <persona-or-path>` | none | Reserved for internal launchers that set `COTAL_MANAGED_HANDOFF_FILE`, `COTAL_ENROLLMENT_FILE` or `COTAL_ENROLLMENT_URL`; any other use is refused and names `--file` |\n| `--harness <connector>`, `-H` | persona's `agent:`, else the [`harness` preference](config.md#preferences), else `claude` | Connector type (`claude`, `opencode`, `jcode`, `hermes`, and so on). `--agent` is the legacy spelling; giving both is refused |\n| `--role <r>` | persona's `role:` | Role override |\n| `--model <m>`, `-m` | persona's `model:` | Model override |\n| `--variant <v>` | persona's `variant:` | Model variant override (connector-defined; e.g. OpenCode reasoning tiers) |\n| `--cwd <dir>`, `-C` | the serving manager's workspace root; with `--foreground`, this cwd | Working directory to root the agent at. On a managed launch, a relative path resolves against the manager's workspace root, not your shell, and a directory that does not exist on the serving manager's host is refused before launch. |\n| `--prompt <text>`, `-p` | none | Initial prompt auto-submitted at start |\n| `--opt <k=v>` | none | Connector-specific launch option, repeatable. Wins per key over the persona's `launchOptions:`; a connector with no option surface refuses it |\n| `--resume <id>` | none | Fork an existing session id into the mesh; only connectors that declare resume support accept it (see [the matrix](connectors.md)). The manager records the source session id, and `ps --wide` shows it. On a managed launch with `--on <instance>`, a Claude session held on this host is carried to that instance first ([Resume a session](connect-claude.md#resume-a-session)); carrying one needs `--on` |\n| `--no-events` | event plane on where supported | Opt out of the session's structured event plane (`--events` only restates the default) |\n| `--share-tools <sel>` | none | Share named operator MCP servers with the agent |\n| `--subscribe <a,b>` | persona's | Channel read-set override; `\"\"` clears it |\n| `--allow-subscribe <a,b>` | = subscribe | Read-ACL override; `\"\"` sets it to the read set |\n| `--allow-publish <a,b>` | deny | Post-ACL override; `\"\"` clears it |\n| `--detach`, `-d` | off | Launch through the manager, wait until the agent joins, and return without attaching (reattach with `cotal attach`) |\n| `--foreground` | off | Run the application directly in this process. Its exit, or the terminal's, may end the agent. Refuses `--detach`, `--creds`, `--on`, `--restarts`, and `--json` or `--plain` except with `--dry-run` |\n| `--on <instance>` | class anycast | Managed launches only: pin the launch to one manager instance id (the whole id, as `ps` prints it). Refused with `--foreground` (no manager to pin) and when empty |\n| `--restarts <n>`, `--restart-window <ms>` | none | Managed launches only, and given together: the manager restarts the agent at most `<n>` times within `<ms>`. Each must be a positive integer |\n| `--live-only` | off | `--foreground` only: skip the durable backstop |\n| `--enrollment-file <private-file>` | none | With `--foreground` and `--space`: a private file holding one seat's enrollment URL (see above) |\n| `--dry-run` | off | Preview the launch without allocating an identity, provisioning anything or starting an application |\n| `--expect-owner <u_\u2026>` | none | With `COTAL_MANAGED_HANDOFF_FILE` only, and required there: the owner the handoff must carry |\n| `--expect-lifecycle-uid <uid>` | none | With `COTAL_MANAGED_HANDOFF_FILE` only, and required there: the lifecycle UID the handoff must carry |\n\n`spawn` refuses `--runtime` and `--allow-stale` with \"manifest deployment options belong to cotal\nteam deploy <file>, not spawn\".\n\nEach session uses its connector's **event plane** by default: a stream of structured events\ndescribing what the agent did, rather than the prose it wrote, on a channel of its own. The channel is named after\nthe agent's principal, `events.<owner>.<actor>`, never after its display name, because two live\nagents are allowed to share a display name and would then share a stream. The launch grants publish\nrights on that channel alone, foreground and detached alike. On an open mesh, which issues no\ncredentials, the launch still allocates the agent an id, so the channel names a stable actor.\n`--no-events` is the explicit opt-out unless the selected registration says\n`policy: { events: \"required\" }`. Required policy makes the\nevent arm and grant mandatory, so `--no-events` and connectors without an event plane are refused.\n\nThe launch decision and the grant are separate on purpose. Holding publish rights on a channel is\nnot a request to publish to it, so writing an event channel into an agent file's `allowPublish`\ndoes not override `--no-events`.\n\nThe persona (`--file` or the positional name, else the [`persona` preference](config.md#preferences),\nelse `default`) is loaded from the target mesh's `.cotal/agents/` when it is a bare name. A reference that contains a path separator or\nends in `.md` is loaded from that file. A relative path resolves against the mesh root, except that\na managed handoff, and an enrollment onto a mesh this machine has not registered yet, resolve the\npersona file against the working directory. A missing\npersona is refused with the catalog directory or the file that was checked. A missing `default`\npersona also names the command that writes one: `cotal up` in the root of a space this machine\nstarted, or `cotal persona create default` for any other space. The launch flags\noverride the file. On a user-auth mesh the\neffective name is also the agent's actor token, so it must match the token grammar (no `-`); the\nspawn is refused with that explanation before any request is sent. `--foreground` runs the agent\nattached to your terminal; a bare spawn and `--detach` hand the launch to the running manager. Every\nmode gets the durable backstop on a mesh that runs the delivery daemon; `--live-only` skips it for a\n`--foreground` spawn (messages posted while it is disconnected are then not replayed). A foreground exit retires\nthe agent's creds and broker footprint, like a manager despawn. On a static-auth mesh, a spawn that\nfails after its footprint exists, for example because the creds directory cannot be written,\nretires that footprint the same way before it exits with the error. The creds and the footprint are\nremoved as separate steps, so a creds file that cannot be deleted does not leave the broker durables\nbehind, and the failed steps are reported together. On a user-auth mesh the two arms\ndiffer: a spawn against a mesh this machine provisioned revokes the actor row on exit, while a\nremote spawn (an enrollment or the advertised provisioning endpoint) removes only this machine's\ncredential files; its grant stays until the mesh operator revokes it, and the launch line says\nwhich arm you are on. A spawn through the advertised provisioning endpoint against a record that\npins no exchange URL is refused before the grant is requested, so no credential lands on this\nmachine. With no sign-in the spawn is refused with `AUTH_REQUIRED` (exit 2) before any request is\nsent. Any provisioning failure after the request, an endpoint 401 or 403 included, is reported as\n`OPERATION_UNKNOWN` (exit 3): the endpoint belongs to the deployment, and nothing says what its\nrefusal left behind. A foreground `--share-tools` selection that names a server the config does not declare is\nrefused before anything is provisioned. A managed spawn is an\n**action**: the manager accepts it and allocates the identity, and the CLI waits for a real outcome\n(the agent joins, exits, or about 30 seconds pass) before it reports or attaches (see\n[the control surface](control-surface.md)). See [Connect Claude Code](connect-claude.md) and\n[Agent files](agent-files.md). (`cotal start` was merged into `cotal spawn --detach`.)\nA managed spawn onto a manager from another Cotal release is refused before any request is sent\nwhen the manager's contract does not declare a field this CLI sends. The refusal names the field,\ncalls it version skew, and gives this CLI's version. A field you leave unset is not sent, so it\nnever causes that refusal.\n\nA manager has 50 seat slots, and each seat counts once. A slot is held by a managed seat (a row in\nthat manager's `cotal ps`, including a seat still joining), by a reserved launch the manager accepted\nbut has not started a process for, or by a cooling hold. A seat that ends within 10 seconds of\nstarting leaves its slot cooling until those 10 seconds pass, unless an operator stopped it. Such a\nseat holds only that cooling slot, even while its launch is still reporting the failure. A spawn\nrefused at the limit states that split and whether waiting can free a slot:\n\n```text\nat capacity (50 of 50 slots: 49 managed, 0 reserved, 1 cooling); waiting frees a cooling slot in 7s, or despawn one\n```\n\nA cooling slot frees at the stated time. A launch that has not settled frees its slot only if it\nfails, and a managed seat frees its slot only when it stops. The refusal counts a launch as pending\nonly while it holds a slot, so a launch whose seat already ended is not counted. The roster counts\npresence, which also includes peers no manager owns, so its total is a different number.\n\nRun from a managed seat's own shell on a static or open mesh, a managed `cotal spawn` launches as\nthat seat when it targets the seat's own space. Without `--space` it picks that target the way the\noperator path does, so a recorded mesh that is not running is skipped. The CLI reads the seat's\nlaunch identity (`COTAL_NAME`, `COTAL_ID`, `COTAL_LIFECYCLE_UID`, `COTAL_SPACE`, and on a static\nmesh the seat's own credential), so the manager records the seat as the spawner, the same as for\nthe seat's `cotal_spawn` tool. On a static mesh that credential also proves the seat's space, so a\nlaunch without `COTAL_SPACE` still runs as the seat, and a target space holding no credential for\nthe seat is refused. An open mesh acts as the seat only when `COTAL_SPACE` names its space. The\nseat can then stop the child with `cotal_despawn`, and the manager stops the child when the seat\nexits. On a static mesh a seat whose agent file lacks `capabilities: [spawn]` is refused, because\nits credential holds no spawn subject.\n`--on <instance>` keeps its pin: the seat's own credential has no instance route, so on a static\nmesh the CLI mints a one-shot `manager-caller` view for the seat, pinned to that instance and\ncarrying the spawn subject only when the seat's credential holds it. On an open mesh the call keeps\nthe TLS requirement the mesh records. A managed spawn from a seat's shell never falls back to the\noperator: `--creds` is refused there, and so is a target the seat cannot act on as itself, such as\n`--server` with an unregistered `--space`, a user-auth mesh, or an open mesh that `COTAL_SPACE` does\nnot name.\n\nForeground user-mode launches serialize their local actor material. A duplicate refuses\nwithout replacing the first launch's files. A new local-user foreground launch also refuses\nan existing grant before rotating it. Departure cleanup is conditional on the launch's token\nand lifecycle generation, so an earlier launch cannot revoke a successor's grant. Remote\nprovisioning remains the mesh operator's authority and can reissue material before the\nclient observes an existing same-host consumer.\n\n## models\n\n```bash\ncotal models [--json | --plain] [-H | --harness <connector>] [--refresh] [--on <manager-instance>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--harness <connector>`, `-H` | all registered applications | Application whose catalog to list |\n| `--on <manager-instance>` | native selection | Ask only the named manager instance. An unavailable or unauthorized pin refuses without choosing another. |\n| `--refresh` | off | Ask the connector to refresh its provider cache |\n| `--json` / `-j` | off | One JSON document: `result.items` holds each catalog as returned; the requested manager ID, answering ID and answering epoch are separate |\n| `--plain` | off | The same report without color |\n\nAsks the selected manager for each application's model catalog (model ids plus their variants)\nfor connectors that expose one. OpenCode and Codex query harness/provider surfaces; Jcode reads\nproviders that enable `model_catalog = true` in the operator Jcode `config.toml`. Jcode's listed\neffort tiers render as `<tiers> (declared, not provider-verified)`, and launch can still refuse one.\nA connector without a catalog says so with `supported: false`, and one whose install has no\ncatalog it can list, such as OpenCode 2.x, gives the reason in `unavailable`; neither is a failed\nread. A connector whose catalog could not be read, or that the manager could not load, makes the\nread `partial` (exit 4) with one `MODEL_CATALOG_UNREADABLE` warning naming its cause. The\nscreen points to that warning without repeating it inline. The remedy is to correct the stated\ncause before retrying; a refresh cannot repair missing connector configuration. A manager-level\nrefusal is reported separately. A connector whose harness the manager did not find at boot\nreports the reason boot recorded, as a spawn does, so restart the manager after installing it. Pick\na result with `cotal spawn --model <id> --variant <v>`, where `<id>` is the model id as the catalog\nprinted it. OpenCode and Codex ids are the full\n`provider/model`; Jcode ids are bare (`opus-5`, not `cliproxy/opus-5`), because the provider is\nselected by the operator's Jcode config and a prefixed id is refused at launch with the bare form\nnamed.\n\n## endpoints\n\n```bash\ncotal endpoints [--space <s>] [--server <url>] [--creds <path>]\n```\n\nLists the mesh presence roster: agents, the manager, and any other protocol endpoint, with each\nendpoint's role, kind, status, and current activity. Unlike `ps`, this is a read-only presence view;\nit is not limited to child processes owned by the manager.\n\n## who\n\n```bash\ncotal who [--waiting] [--all] [--json | --plain] [--space <s>] [--server <url>] [--creds <path>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--waiting` | off | Only rows that are waiting or report a condition |\n| `--all` | off | Also list infrastructure endpoints such as the manager |\n| `--json` / `-j` | off | One JSON document: `result.items` holds one row per participant |\n| `--plain` | off | The same report without color or marks |\n\nReads the live presence roster of the selected space. Each row has the participant's name, role, kind, status and condition,\nactivity, attention mode, harness, model and host, as its presence reports them. Infrastructure\nendpoints are hidden unless `--all` asks for them; the report says how many it hid.\n\n`managed` says whether a manager runs the seat. With no manager on the roster it is `false` for\nevery row and no manager is asked. Otherwise every registered manager is asked for its seats, and a\nseat is matched on its lifecycle UID. In a user-auth space the manager answers only for the\ncaller's own seats, so other seats show `managed: null` with a `MANAGED_SCOPE` warning. An\nidentity the broker does not let ask the managers, such as a user whose grant has no `admin`, sees\nevery seat `null` with a `MANAGERS_DENIED` warning. These two warnings say how far the identity's\ngrant reaches; the roster itself was read in full, so they do not make the run partial. A manager\nthat does not answer leaves every unmatched seat `null` with a warning.\n\nA part `who` could not read makes the run `partial` (exit 4): a roster snapshot that did not finish\n(`ROSTER_INCOMPLETE`), a roster that has gone stale (`ROSTER_STALE`), an error on the roster watch\n(`ROSTER_ERROR`), or a manager census it could not read in full (`MANAGERS_UNREAD`, and the warnings\n`ps` names). A credential that may not read the roster is refused with `PERMISSION_DENIED` (exit 2),\nnever shown an empty roster.\n\n`who` watches the roster and the channel registry on every mesh so it can report those states. On a\nstatic-auth or user-auth mesh the credential `who` connects with may not delete its watch consumers,\nso each one stays on the broker for five minutes after `who` exits. A script that runs `who` in a\ntight loop there can reach the broker's limit of 1000 consumers per stream.\n\n## whoami\n\n```bash\ncotal whoami [--json | --plain] [--space <s>] [--server <url>]\n```\n\nReports the identity this CLI acts under in the selected space and what it may read, post and do.\nIt reads the mesh registry, the login cache and, when it is on this machine, the user-auth grant\nledger. It does not refresh the remote space catalog or contact the IdP. A one-shot command has no\nsession, so there is no unread count or connection state to report.\n\n| Mesh | `identity.kind` | What it may do |\n|---|---|---|\n| Open | `open` | Everything; the broker authenticates nothing |\n| Static auth, signer on this machine | `space-signer` | Everything; each command mints its own short-lived credential for its task, so there is no standing principal |\n| Static auth, no signer here | `no-credential` | Nothing without `--creds <file>` |\n| User auth | `user` | The signed-in user's `cli` actor: `read`, `post` and `capabilities` are its ledger row |\n\nA user who is not signed in has no principal. A signed-in user with no ledger row (`grant:\n\"not-granted\"`) may read and post nothing. When the grant ledger is on another machine the grant\ncannot be read here: `read`, `post` and `capabilities` are `null`, the run is `partial` (exit 4) and\nthe `GRANT_NOT_LOCAL` warning says so. A remote registration can hold a cached login without the\nsecret that derives its owner and principal. Those fields stay `null`; the report calls the\nprincipal unknown on this machine, rather than calling the user signed out. The cached expiry is\nadvisory and the command does not verify the login with the IdP. `dms` says whether the identity\nmay read DM history: on an open mesh, or with the space signer's audit credential on an authenticated mesh. The report\nreads local configuration and does not contact the broker to check its authentication requirements.\nAn unregistered `--space` is refused even with `--server`. Server addresses in the identity report\nand connection refusals omit URL user information, query strings and fragments, and escape terminal\ncontrols. Addresses need a host and a supported broker scheme. TCP addresses cannot have a path;\nWebSocket routes and bare host addresses remain supported. Invalid addresses make `whoami` refuse\nrather than print the raw value. Recovery instructions use a `<url>` placeholder rather than a\nsecret-stripped connection address.\n\nBoth reads refuse an unusable local mesh registry with `SCHEMA_INVALID` (exit 1) before\nconnecting. The diagnostic names the file without repeating its stored field values.\n\n`endpoint invoke <target> <command> --cast` uses the native one-way verb. Only an ephemeral\ncommand can be cast; journaled commands and `--wait` combinations refuse before publication.\nThe receipt records the original request, resolved binding and broker flush, not a responder or\nconfirmed execution. A class cast is not retried or repaired if another instance refuses its\nbinding. A broker publish denial remains a refusal. A lost publication outcome remains unknown.\n\n## service-endpoint\n\n```bash\ncotal service-endpoint approve <proposalId> [--caller-ceiling a,b | --request-id <hex32>] [--space <s>] [--json | --plain]\ncotal service-endpoint refuse <proposalId> [--request-id <hex32>] [--space <s>] [--json | --plain]\ncotal service-endpoint proposal <proposalId> | operation <operationId> | name <endpoint> [--space <s>]\ncotal service-endpoint name-bind <endpoint> (--owner <u_\u2026> --claim <ref> | --request-id <hex32>) [--space <s>]\n```\n\nDecides and inspects service endpoint registrations on a user-auth space whose auth service runs\nwith `--service-endpoints`. Every subcommand is one request to the service's\n`/service-endpoint-host` route as the signed-in user: the route verifies your IdP token and derives\nthe granter itself, so no command names one. `approve` and `refuse` are the owner's: the command\nreads the proposal and its name first and decides at those revisions, so a proposal or name that\nmoved in between is a conflict the host reports, never a decision on something else. `proposal`,\n`operation` and `name` read as the owner: another owner's record reads absent. `name-bind` is the\noperator's: it needs the auth service on this machine and runs on its loopback face under the\nexchange capability, with your signed-in subject as the audit id. The host verifies no domain\ncontrol, so `--claim` is your own reference for the namespace you checked. The answer is the host's\nown. A decision or a binding is recorded under its request id at\n`.cotal/auth/space.<key>/service-endpoint-requests/<id>.json` in this root before it is sent, and\nthe command prints that id, or names the exact resend command in the failure when no answer came\nback. A run interrupted before any answer prints nothing: the record's file name is the id.\nRun the same subcommand and subject again with `--request-id <hex32>` to send the recorded request as it was,\nwhich the host replays with the same approval or binding; a fresh read would carry the revisions\nthe first attempt moved, so the replay never rebuilds the request, and it takes no\n`--caller-ceiling`, `--owner` or `--claim`. An id this root never recorded, or one recorded for a\ndifferent command or subject, is refused with nothing sent. A run without `--request-id` is a new\nrequest, and a proposal already being approved answers `approval-in-progress`.\n\n## Handle inspection\n\n```bash\ncotal auth handle inspect <private-file> [--space <space>] [--json|--plain]\n```\n\nThis reads one protected native handle artifact or a bounded array of artifacts once. Without\n`--space`, it shows unverified public scope, holder and declared expiry claims. Signatures,\nissuer authority, parent links, holder currency and revocation remain not checked. No broker\nis contacted or credential issued. Signature bytes and original signed artifacts are never printed.\nThe human report shows at most 50 scope rows and names omissions; JSON retains public metadata.\n\nAn explicit `--space` selects online inspection on a registered static-auth space whose root\nholds matching native operator authority. One ephemeral provisioner reader is minted without\npersisting it. Its broker account and identity must match before leader-served signer and\nissuer-namespaced revocation reads. The inspector checks signatures, issuer scope, presented\nparent digests and containment. Every required sturdy-handle status must be explicitly unrevoked;\nmissing, malformed, unreadable or stale spec/status pairs fail closed. Reads share one bounded\nchain budget. No records are created or repaired, and no alternate signer or broker is tried.\n\nThese non-atomic observations are signature inspection, not a usable presentation context.\nDeclared expiry, presenter identity, holder/process currency and contract-dependent use authority\nremain separate. No holder credential is conferred and no handle is redeemed. Open and user-auth\nspaces refuse this static reader rather than borrowing an operator identity. Reader-close\nuncertainty is reported separately.\n\n## Account signer export\n\n```bash\ncotal auth signer --out <private-file> [--space <space>] [--force] [--json|--plain]\n```\n\nThis exports the selected space's existing data-account signer through the native stripped-signer\nformat. It does not create a caller, rotate a key, start a service or export the operator root.\nA cached login is not signing authority. The selected root must hold validated static trust;\nconflicting current-folder and selected-root account authorities refuse before publication.\n\n`--out` is required. Its parent must exist and pass the protected physical-directory checks.\nManaged `.cotal` and workstation state are not valid explicit export destinations. The file is private, and secret contents\nnever appear on stdout, in the receipt or in diagnostics. Keep this account-wide signer out of\nlogs and support bundles; it is not a least-privilege endpoint credential.\n\nAn existing path refuses by default. `--force` first inspects an owned private regular file and\naccepts only the stripped format for the same space and account. It refuses full trust bundles,\nother accounts, symlinks and concurrent changes. An identical export is a no-op. New files use\nnative create-only private publication; replacement uses the inspected snapshot under a writer\nlock. An unconfirmed publication remains unknown and is not retried. Platforms without the\nprotected file policy refuse rather than substituting a weaker writer.\n\n## Endpoint authoring\n\n```bash\ncotal endpoint init reviews [--dir <directory>] [--json|--plain]\ncotal endpoint check <contract-file> [--json|--plain]\n```\n\n`init` creates `contract.json` and an executable `handler.mjs` template. The default directory is\nthe supplied name; `--dir` chooses another one. Its parent must exist. Files are created\nexclusively, without following a pre-planted symlink or replacing an existing file. A partial\nwrite retains the named created material for review rather than deleting it automatically.\n\nA simple scaffold name such as `reviews` declares `com.example.reviews`. A full reverse-DNS name\nis kept. Choose a namespace you control before requesting serving authority. Scaffolding creates\nno service definition, reference, credential, registration or process. The handler accepts one\nJSON object with `command` and `args` on stdin and writes one JSON result on stdout.\n\nThe version-1 local contract file has `schemaVersion`, `endpoint` and `cluster`. The cluster has\nnative `urn`, `revision`, `attributes`, `commands` and `events` fields. Each command carries its\nnative declaration plus `input` and `output` schema bundles, each `{root, members?}`. The installed\nnative compiler derives their closure digests; authors do not hand-maintain a second digest next\nto an edited schema. `members` maps native digest references to supplied JSON documents. Unknown\nwrapper or command fields are refused. This file is local authoring input, not a new wire kind.\n\n`check` reads at most 1 MiB, validates the native cluster declaration, compiles every input/output\nwith the installed schema profile and reports the derived cluster closure. All schema references\nmust resolve inside their bundle; no URL or adjacent file is fetched. Attribute and event\nsemantics remain opaque to the native parser, so a file declaring them produces a partial result,\nexit 4, rather than a claim that those semantics were checked.\n\nNeither command contacts a broker, loads the handler, starts a service or proves serving\npermission. Authoring takes no space, broker, caller or invocation flags.\n\nServe the edited contract through the independent service controller:\n\n```bash\ncotal service add reviews --preset exec-endpoint --space <s> --root <absolute-root> --contract <absolute-contract.json> --handler <absolute-node-executable> --handler-args '[\"<absolute-handler.mjs>\"]'\ncotal service start reviews --space <s>\ncotal service status reviews --space <s>\ncotal service logs reviews --space <s>\ncotal service stop reviews --space <s>\n```\n\nThis preset supports Linux and an existing registered static-auth root. The executable is an\nabsolute protected regular file, with argv supplied separately. There is no shell, PATH lookup,\nimplicit installation, account switch or open/user-auth credential fallback. For a Node handler,\nselect the Node executable and pass the script path in `--handler-args`. That JSON array has at\nmost 64 fixed nonsecret strings and its serialized form has at most 8192 characters. The handler\nruns in the contract directory with an empty environment. It is trusted local code with the\noperator's OS permissions, not a sandbox; it must not daemonize or leave independent children.\n\nStart recompiles one protected contract snapshot and pins its cluster closure and the executable\nfile identity across the controller handoff. Changes before launch refuse; changes to the\nexecutable before a later call also refuse. Only ordinary untargeted ephemeral commands are\nsupported. Targeted, journal, trait, attribute and event declarations refuse before registration.\nThe host publishes the schema roots, members, closure manifests and cluster to the native\ncontract store, registers one fresh native instance, and uses the native issuance gate and\nbranded serve grant. The handler receives no signer, executor credential or broker environment.\n\nThe native serving boundary validates input before spawning and output before replying. Each\ncall receives one `command`/`args` JSON object on stdin and must return one UTF-8 JSON value on\nstdout with exit code zero. Calls and casts need a native deadline; the handler budget never\nexceeds it or 120 seconds. At most 32 handlers run concurrently. Input is bounded to 64 KiB,\nstdout to 256 KiB and discarded stderr to 64 KiB. Deadline, pipe, process, JSON and schema failures\ndo not replay effects. Handler payloads and stderr are not forwarded to service logs.\n\nInitialization is recorded only after the real serve subscriptions flush and the native ready\nstatus returns. Controller readiness remains a process-bound initialization observation, not an\nongoing endpoint health probe. Logs and private launch records name the full native instance;\nuse that instance for an attributed `endpoint describe` or `endpoint invoke`. Normal renewal\nuses the same issuer, identity and registration, with one immediate same-broker reconnect. An\nunconfirmed renewal withdraws the service rather than starting a replacement.\n\nStop first prevents new handlers, requests termination of its owned handler groups, waits for\nprocess/pipe closure, drains serving and closes the connection. Native deregistration pins the\noriginal registration revision and preserves successor or in-flight governance state. Private\ncreate-only intent, registration and removal records retain the original coordinates. A later\nlaunch of the same local instance refuses an unsettled or malformed attempt instead of silently\nrepeating it. Unconfirmed initialization or cleanup remains unknown; neither process disappearance\nnor an initialization receipt proves native cleanup. The separate `endpoint deregister` route\nretains its broker-gone and revision guards.\n\n## Endpoint control\n\n```bash\ncotal describe <endpoint> [--on <instance>] [--space <s>] [--server <url>] [--creds <path>]\ncotal invoke <endpoint> <command> [--args '<json>'] [--timeout <ms>] [--space <s>] [--server <url>] [--creds <path>]\ncotal invoke <endpoint> <command> --name <agent> [--admin] [--space <s>]\n```\n\nThe generic v0.4 service surface. `describe` resolves a registered endpoint's command set off the\nwire - the reserved `describe` command answers the registered contract digests, the schemas are\nfetched from the space's content-addressed contract store, recompiled, and verified against those\ndigests - and prints each command with its capability class and targeting shape. `--on <instance>`\npins `describe` to one manager instance's rail (the whole id, as `ps` prints it under its\n`manager <id>` headers), so an operator can read what that instance serves in a multi-manager space;\nunpinned, the class queue answers and the attribution line names whichever instance did. `invoke`\ncalls one command by name: `--args` is a JSON object validated against the fetched input schema\n*before*\npublish, and `--timeout` sets the reply deadline in milliseconds (default 10000). `--server` overrides the\nregistry's broker URL and `--creds` names a creds file for an off-registry connection. A targeted command takes `--name <agent>` (resolved to the agent's current principal through\n`inspect`) or `--self`. `--admin` uses the admin instrument credential, whose cross-agent reach rides\nthe operator-only `any` authorization mode. Neither command has compile-time knowledge of any\nendpoint's schemas - this is the same trust chain every built-in control command now uses. Needs an\nauth mesh: the manager registers its service on both static and per-user meshes (a signed-in user\nrides their bearer; each visible or invoked command still requires its existing grant, and cross-agent\nreach needs the `admin` scope). An open mesh has no service registry.\n\nOn a static-auth root the operator's own credential sends requests to the manager endpoint only, so\nthe broker refuses its `invoke` of any other endpoint before the call runs. When the call and the\nendpoint's whole command surface are untargeted synchronous ephemeral commands, the refusal names\n`cotal auth mint <name> --endpoint <endpoint> --space <space> --out <private-file>`, with the space\nthe call resolved and the `--server` it was given, if any, so the caller is issued for the space that\nrefused even when another space is current. Run the same `invoke` again with `--creds <private-file>`. An `invoke --on <instance>` of another endpoint is refused the same\nway and names no remedy: with `--on` the operator's credential still carries manager rows only, and\na paired caller file takes no instance pin.\n\n## Managed seats\n\n```bash\ncotal ps [--json | --plain] [--on <instance>] [--wide] [--slots] [--space <s>]\ncotal stop --name <n> [--on <instance>] [--space <s>]\ncotal attach --name <n> [--on <instance>] [--no-reconnect] [--space <s>]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |\n| `--name <n>` | none | Managed agent to stop / attach (required) |\n| `--on <instance>` | class anycast (`ps`: class scatter) | Pin to one manager instance id (multi-manager space); takes the whole id as `ps` prints it, not a prefix. An empty value (`--on \"\"`, an unset shell variable) is refused, never treated as absent. A roster principal id (`local.\u2026`) is refused with a message naming the instance id `ps` prints |\n| `--wide` (`ps`) | off | Print each seat as a labeled record that adds the operational facts the manager records: the provider the connector reported serving the model, `cwd`, `pid`, spawner, lifecycle uid, the owning manager's instance id and host, and for a `--resume` seat the session it forked (`forked from <id>`, with the source title and transcript SHA-256 once a Hermes or Jcode seat has recorded its fork; a carried Claude session prints `forked from <host>:<id>` with its title, SHA-256 and `carried <time>`, the time its bytes reached the manager). Model and requested variant stay in the identity row rather than printing twice. A fact the manager did not record (for example a runtime with no real process, or a connector that reported no provider) prints nothing, never a placeholder |\n| `--json` / `-j` (`ps`) | off | One JSON document (see below). Mutually exclusive with `--wide` and `--plain` |\n| `--plain` (`ps`) | off | The same report without color or marks |\n| `--slots` (`ps`) | off | List the durable static slot rows this manager owns instead of live seats, through the `slots` command. Mutually exclusive with `--wide`. A row that is not in the live roster still prints, with `LIVE` false; a retired row never prints |\n| `--no-reconnect` (`attach`) | off | End the attach when its session ends, instead of re-establishing it. For scripts that want one run and one exit code |\n\nOn a controller stack started with `--no-manager`, `ps` refuses `TARGET_UNAVAILABLE`: no manager\nruns, so there are no managed agents to list. Bare `cotal up` adds the manager.\n\nA raw `--creds` file is refused by `ps`, `stop`, `attach` and the other control commands, because\nthat route mints no endpoint-caller triple; the project folder, or `--space` against the registry\nentry, is the route that does.\n\nThe human `ps` report is presentation text and is not a stable parsing target. Scripts use `--json`,\none envelope whose `result` is:\n\n| Field | Meaning |\n|---|---|\n| `kind` | `seats`, or `slots` with `--slots` |\n| `items` | Each seat (or slot) row as its manager returned it, unchanged |\n| `managers` | Each manager instance asked: `{instanceId, state}`, where `state` is `answered`, `silent` (registered, no answer within the deadline, with the `liveness` this command established), `refused` (with its `error` and `code`) or `contract-mismatch` |\n\n`target` is `{space, scope: \"all-managers\"}` for the class scatter, and\n`{space, scope: \"manager\", managerInstanceId}` for `--on` or a user-auth mesh. A manager that is not\n`answered` makes the read `partial` (exit 4), with a `MANAGER_SILENT`, `MANAGER_REFUSED` or\n`MANAGER_CONTRACT_MISMATCH` warning naming it.\n\n`--slots --wide` is refused: `--slots` lists durable static slot rows, `--wide` prints live seat facts, and the two answer different questions. Across a multi-manager scatter, `--slots` prints each manager's rows under its own instance header, the same way the plain `ps` scatter does.\n\nA revision-32 manager also supports a bounded native `slots` query through\n`endpoint invoke manager slots --on <instance> --args '{\"names\":[\"worker\"],\"includeRetired\":true}'`.\nIt point-reads up to 100 distinct aliases and can return their retired slot/head and\ncleanup observations. The default `ps --slots` query is unchanged and excludes retired\nrows. Omitted aliases, live-map absence and stop acknowledgements are not retirement\nproof. Match the returned principal and lifecycle UID before interpreting any terminal.\n\nThese are operator clients over the running manager's control plane. The default row includes the\nconnector, model pin, optional requested variant, and runtime as operational descriptors for the\nmanaged row. They do not make a shared display name a unique protocol identity; use `--json` when\nunambiguous owner+actor attribution is required. An omitted variant means no override was requested;\nCotal does not invent an effective provider default it cannot observe. `ps` also prints two state\nfacts per managed agent, because they answer different questions: the process fact from the manager's\nown runtime handle (`running` with its uptime, or `exited` with how long it ran), and the mesh fact\nfrom the roster (`idle` / `working` / `waiting` / `mesh offline`, or `not in roster` when the seat has\nno presence row at all: a seat that has not joined yet, or one that never did). When the seat's\nconnector relays a harness-reported condition, the mesh fact carries its code and how long it has\nheld, so a seat whose turn died on a provider rate limit reads `waiting (rate_limit for 40m)` rather\nthan a bare `waiting`, and `--json` carries the whole `condition` object. When the connector reports\nthe seat's last work event (presence `activeAt`), the mesh fact ends with its age, such as\n`\xB7 active 3s ago`, and `--json` carries `activeAt`. A seat whose turn stopped advancing keeps\nheartbeating, so its presence row stays fresh and this age is what shows the stall. A seat can be\n`running` and `mesh offline` at once: the process is alive and its presence has lapsed. That row says\nhow long, as in `mesh offline for <age>` with an age such as `3.5h`, counted from the seat's last\npresence heartbeat, which `--json` carries as `offlineSince` (epoch ms). The age is read only from\nthe seat's own presence record, matched on its principal and lifecycle uid, so a same-named peer or\nan older lifecycle never dates it. The manager log names each managed seat that is offline on the\nmesh while its slot is held\n(`seat offline on the mesh: <name> - last heartbeat <time>; process <state>`), including one its\nwatch first sees offline after a reconnect, and each one that comes back\n(`seat back on the mesh: <name>`), so a watchdog that only checks process liveness has a line to\nact on. The manager does not reap or re-key such a seat. The mesh fact is only a verdict while the\nmanager's own presence watch is fresh: when that watch has been silent past the liveness window, or\nhas not replayed the bucket yet, every row prints `mesh unknown` with the reason instead (`--json`\ncarries it as `meshView: stale | unpopulated`), because `offline` and `not in roster` would then\ndescribe the manager's watch rather than the seat. The manager rebinds a watch that goes\nsilent under a live connection on its own, so `mesh unknown` normally clears within a liveness window.\nOn a user-auth mesh `ps` also renders each managed agent's last credential-refresh outcome, fail-closed.\n\n**Mode split (chosen up front, never try-scatter-then-degrade):**\n\n- **Static / open mesh.** Bare `ps` is a **class scatter**: it freezes the live manager class from\n the records registry, merges every registered instance's agents grouped and attributed per\n instance, and a non-answering instance is shown as `registered, no answer within the deadline`\n (never silently omitted). A refused list or a missing answer makes the census incomplete: rows\n from other instances remain visible, but `ps` prints an incomplete-census warning on stderr and\n exits 4 (`partial`), including with `--json`. Those rows are not a complete seat count. A contract\n mismatch prints one plain comparison of the requested and served input/output digest pairs and\n advises aligning manager versions. The no-answer label means only that the instance is registered\n and did not answer. It does not say the host is down, because a dead host never deregisters itself and a\n live one can be slow; if it is gone, deregister it.\n `--on <instance>` pins the read to one exact instance id instead. A wrong pin fails loud\n rather than falling through: a well-formed id that no live manager carries is reported as\n `manager instance <id> did not answer` (nothing else is asked), and a credential without that\n instance's rail is reported as refused by the broker, not as an unresponsive manager. A manager\n that answers with a refusal is shown with its own cause; \"no manager reachable\" is said only when\n nothing answered at all. If the scatter's own registry read fails (the freeze or the reconcile),\n `ps` says the manager registry could not be read rather than pronouncing on the managers, which\n may all be up.\n\n**The verdict is scoped to the endpoint rail the request rode.** An issued caller rides the\nversioned `ep.v1` rail, a separate subject space from the legacy `ep` rail, and an endpoint serves\nboth (SPEC 13.15). A manager older than the versioned rail serves `ep` alone, so it can be running,\nregistered and answering while an issued caller's request reaches nobody. Silence on `ep.v1` is\nreported as `no manager answered on the <rail> rail` with `ep.v1` as the rail, and names both causes\nit is consistent with: no manager running, or one older than the rail. The CLI cannot tell them\napart, because the service registry records no package version, so check whether a manager is\nrunning and, if it is, its version. The same scoping applies to `cotal run`'s hosted verbs, which\ndrop the `--local` suggestion there, since `--local` drives the run from the calling process and\nnames the caller as its answerer.\n\n**`stop` and `attach` route by seat locality.** A seat can only be stopped or attached by the\nmanager actually running it, and the class queue does not know which one that is. So on a\nstatic/open mesh both verbs first ask every registered instance which one hosts the named seat, then\naddress that instance directly. This happens by default; you do not need `--on`.\n\n`--on <instance>` remains the override, for when you already know where the seat lives or the\nlookup itself is degraded. On a **user-auth mesh**, the exchange selects one authorized manager\nfor a short-lived `manager-caller` view. `--on` requests a specific instance. Without it, a command\nrun from a managed seat's shell selects the manager that launched the seat (`COTAL_MANAGER_INSTANCE`),\nand any other caller's selection must be unique. Discovery and the command use that instance route.\nThe caller gains no registry read or scatter permission. An absent, ambiguous or unauthorized\nselection refuses before sending the command. With several of your managers live, the refusal lists\ntheir instance ids so you can pass one to `--on`. By default, when every manager you registered has\nexited, the refusal says to start one with [`supervise`](#supervise); the command is not sent to the local manager.\n\nA trusted hosted composition may opt into an [interactive local primary](embedding.md#interactive-manager-primary).\nUnder that policy, unpinned human control selects the genuine local Manager up front, even when your\nown remote Manager is live; an unavailable local Manager refuses without remote substitution. This\naffects all interactive Manager verbs, not only `ps`. Select your remote Manager explicitly for its\nspawn, run or terminal operations. Explicit pins and managed-seat launching bindings keep their rules.\nThere is no user flag for the policy, and stock `up` and recovery defaults do not enable it.\n\nA seat is reported as **not found** only when every reachable instance answered for itself. An\ninstance that stayed silent past the deadline, or that refused the read rather than answering, said\nnothing about which seats it hosts, so the seat may be running on it. That case reports that the\nlocation could not be established, names the instances that did not answer, and states outright\nthat it is not a report that the seat is gone. Read it as unknown and retry with\n`--on <instance>`; a retry loop that treats it as \"already gone\" stops looking for a seat that is\nstill running. A single manager cannot tell \"hosted elsewhere\" from \"does not exist\": it answers\n`not-found` for both, which is why the search asks all of them and why an incomplete search\nconcludes nothing.\n- **User-auth mesh.** `cotal ps` reports what **one** authorized manager knows about your agents\n (an instance-addressed read against its in-memory roster, owner-filtered). It does **not** report\n other manager instances or establish whether they are reachable. Completeness across a\n multi-manager user-auth space is not claimed.\n A manager that does not answer fails the command outright (`TARGET_UNAVAILABLE`, exit 2), rather\n than printing an empty list that could be read as \"no agents\". Your ledger row needs the `admin` scope to\n reach `ps` at all; `spawn` alone is refused by the broker (the ep tier boundary). A broker-refused\n read keeps that refusal and adds the documented scope requirement and `cotal auth grants` remedy.\n The broker does not identify whether the missing permission is for the route, command or target;\n the CLI does not substitute another caller or retry on a different route.\n\n`attach` streams and drives an agent's terminal on the `pty` runtime; detach with the escape key\n(Ctrl-] by default; see the [`detach-key` preference](config.md#preferences)). The key is recognised\nas the legacy control byte and as the kitty keyboard protocol and xterm modifyOtherKeys encodings of\nthe same press, so a terminal with either protocol enabled detaches too. It does so over a one-use, holder-bound\nmesh session ([SPEC](../SPEC.md) \xA713.6): the manager replies with a signed session grant (never a\n`127.0.0.1` URL), the CLI redeems it once over the broker, and the browser console (`cotal console`)\ndrives the same session. `stop` and `attach` need a running manager to talk to. On a static mesh\nthey are cross-agent admin operations. On a user-auth mesh, your own agents (any agent under your\nowner) need only the `spawn` scope; another owner's agent needs `admin` on your ledger row\n([identity & auth](identity-and-auth.md)). Launch detached agents with [`spawn --detach`](#spawn).\n\n**`attach` reconnects when the link dies.** A session lives on a network link, and a laptop that\nsleeps, a VPN that drops or a wifi handover kills it. When that happens `attach` prints\n`[cotal: connection lost, reconnecting]` on stderr and starts asking the manager for a new session:\na fresh grant, a fresh per-session credential, a fresh connection, so every attempt re-runs the same\nauthorization the first attach did. On success it prints `[cotal: reconnected]`, the manager repaints\nthe seat's current screen the way it does for any attach, and you carry on in the same terminal.\nRetries wait 1s, 2s, 5s, 10s, then 30s, for as long as the seat exists. The detach key is read the\nwhole time the loop runs, the waits and the attempts alike, so a reconnect never traps you: press it\nwhile a session is being established and the attach ends there, and a session that lands behind the\npress is handed back to the manager rather than left holding a slot. Everything else you type while\nthere is no session is dropped rather than queued, so keystrokes aimed at a terminal that turned out\nto be frozen, Ctrl-C included, are not delivered to the agent by a reconnect you did not know had\nhappened. That starts before the first session, not at the first reconnect: at a terminal, `attach`\nreads and drops what you type while it is still resolving the mesh, so a key struck at a prompt that\nhas not come up yet does not reach the agent when it does.\nThe terminal is in raw mode for the whole reconnect, including when the link died before the first\nsession finished opening, so the detach key works there too instead of echoing as `^]`.\n\nA **pipe** carries script input. For example, `printf 'ls\\n' | cotal attach --name web` is\nbuffered until the session opens. Buffering continues across reconnects, so\n`tail -f log | cotal attach --name web` does not lose the part of its feed written while the link was\ndown. Only a terminal gets the reader; `--no-reconnect` keeps the old behaviour on both.\n\nIt stops on its own when reconnecting cannot help, and says why: a manager that refuses the attach\nexits non-zero with the manager's own message, and a reconnect that finds the seat no longer there\n(despawned, or its agent exited while the link was down) exits cleanly with `seat <name> is gone`.\nIt stops the same way on a refusal the CLI raises itself, such as a broker that denies the request.\nA local connect refusal that retrying cannot fix, such as a static-auth mesh whose seed is now\nmissing, also exits non-zero with the refusal's own sentence. A broker that is still unreachable,\nor a user-auth service that does not answer, keeps 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.\nShow and edit accept a definition whose `name` is not its catalog name. The seeded `default`\npersona is one: its file is `default.md` and its agents launch as `default_agent`.\n\nEditing opens a private draft using the editor preference (`VISUAL`, then `EDITOR`, then saved\npreferences). A successful editor exit and valid definition are required before saving. A changed\nor removed source refuses with `CONFLICT` (exit 2), leaving the current definition intact. Local\nCLI create, edit and remove share a per-persona lock during comparison and commit, not while an\neditor or confirmation is open. Manager and external writes do not participate in that lock.\n`--json` and `--no-input` refuse editing before an editor opens.\n\nList, show and mutation receipts support one-document `--json` and line-oriented `--plain`.\nShow explicitly returns the full definition and source. List does not return the body or launch\noptions. A malformed entry produces a partial inventory (exit 4); an unreadable catalog refuses\nrather than reporting it empty. Remove asks with cancellation as the default in a terminal.\nPlain confirmation uses numbered choices. Scripts and JSON removal require `--yes/-y`.\n\nThe legacy `personas` parser remains available during migration: `list [-v] [--running]`,\n`show`, `edit`, `new` and `rm --force`. Its `new` requires `--subscribe`, reads `--from -` from\nstdin, and overwrites an existing persona only with `--force`. Its local writes share the same\ndraft validation and commit fence. Only its explicit `--running` overlay connects to the mesh.\n\n## supervise\n\n```bash\ncotal supervise [--execution runtime|none] [--runtime <name>] [--space <s>] [--server <url>] [--spawn <names>]\n```\n\n`supervise` is a deprecated entry kept for existing units, scripts and containers, and help and\ncompletion omit it. It accepts the same flags and prints a warning on every run naming the\n`manager` service preset and the release that removes it; see\n[deprecated daemon entries](UPGRADING.md#deprecated-daemon-entries). The manager that `cotal up`\nstarts runs the same code through a hidden entry and prints no warning.\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space to supervise. On a root that hosts several spaces, name one of them here |\n| `--server <url>` | hosting mesh, or matching registered mesh | Broker URL. A registered mesh supplies it when omitted; a different explicit value is refused before anything is dialed. |\n| `--runtime <name>` | `pty` | Agent runtime (`pty` built in; extension runtimes are explicit-only) |\n| `--execution runtime|none` | `runtime` | NONE is a genuine hosting user-auth control instance with no Runtime, connector inspection, workload or terminal execution |\n| `--console-port <n>` | none | Protocol-console port |\n| `--console-host <host>` | loopback | Bind host for the console endpoint. Loopback keeps it machine-local; `cotal up` passes the address it bound the broker to, which is what lets the browser console reach this manager from another machine. `cotal attach` does not use this face: it redeems a mesh session grant over the broker |\n| `--max-sessions <n>` | 64 | Live-session ceiling. Each console pane and each `cotal attach` is one session, so size for agents \xD7 panes, not agent count. A capacity refusal names this flag. `cotal up --max-sessions` records the same number on the mesh so a later `supervise` started by repair keeps it |\n| `--roster <file>` | none | Declarative roster to boot at startup. See [Roster files](define-a-team.md#roster-files) |\n| `--launch <spec>` | none | Resolved manifest launch spec (from `up -f` / `team boot`) |\n| `--spawn <names>` | none | Comma-separated personas to pre-spawn at startup |\n\n`--ws-port`, `--resume-attempt` and `--resume-commit-token` are passed by `cotal up` when it starts\nthe manager, for the console session client and for resuming preserved seats. They are not for\ndirect use.\n\nThe manager is the agent supervisor and control plane: it answers `spawn --detach`, `stop`, `ps`,\n`attach`, and the `cotal_*` manager tools. `cotal up --detach` starts one for you; run `supervise`\ndirectly to recover a dead manager or drive a custom runtime. Default runtime is `pty`; install an\noptional provider first (`cotal ext add @cotal-ai/orca`, `@cotal-ai/tmux`, `@cotal-ai/cmux`, or `@cotal-ai/herdr`) and\nselect it explicitly. A missing provider or app fails loudly; there is no fallback. See [Deploy](deploy.md).\nBoot inventory decides whether this process takes unpinned `spawn`/`launch` on the class rail:\nif every declared connector is unavailable, those commands stay on this instance rail only\n(`status` reports `classSpawn: false`). `describe` still answers on the class rail, so an\nunpinned spawn can bind-fence against a skip member; re-issue, or pin `--on`. A partial\ninventory keeps the class rail and names `--on` on a harness refusal, because sibling\ninventories are not readable from the serve credential. See [control surface](control-surface.md#instance-routing).\n\nUse `--execution none` only for a fresh control instance in an existing hosting user-auth root.\nIt requires that space's signing trust, provider pin and user-mode mesh record, without remote\nmanager authority. It rejects `--runtime`, startup workload flags and participant registrations\nbefore resolving a Runtime or connector. The mode needs no connector seeding. An existing\nunclassified identity or retained workload refuses admission; a recorded NONE instance cannot\nrestart as runtime. Do not delete identity or capability files to change modes.\n\nNONE retains native registration, status and the selected Manager's empty managed-seat ps.\nIt does not adopt foreground agents, roster peers or HTTP-hosted sessions. Workload operations\nstill need an explicitly selected execution-owning participant Manager and its existing authority.\nStarting a control instance does not enable an auth service's interactive local-primary policy.\nNONE also refuses model inspection and caller-driven persona, purge, transcript, preservation,\nresume, cancellation, turn-yield and self-stop writes. It starts no transcript receiver or sweep.\nIts native registration, lease, renewal, session-ledger and goal maintenance can still write.\n\nLocal runtime startup now records identity and execution mode during admission. A later failed\nauthentication or registration can leave those files classified as runtime. Invalid supervise\narguments refuse before connector seeding, with one error line rather than seeding first.\n\nOn a normal `SIGINT`/`SIGTERM`, the manager stops every seat and requires the selected runtime to\nprove the seat is gone before it releases the manager lease or service registration. A stop that\ncannot prove exit fails loud and keeps manager authority instead of reporting a clean shutdown while\nan orphan still holds broker rails. When the runtime refuses the stop itself, the shutdown fails at\nonce and its error names the refusal as `stop failed: <message>`. After an abrupt manager death, the same logical successor\nterminalizes only its own durable static slots, verify-evicts the predecessor's broker principal,\nrecords that result in the lifecycle's caller-readable audit detail, reaps the predecessor's seat\nprocess through the runtime's custody reference recorded on the slot (the pty runtime verifies the\nprocess start identity in its seat record, so a reused pid is never signalled), and only then\nretires the lifecycle and frees the alias. A seat whose spawn carried `supervise` with restarts left\nis restarted instead of retired: the successor reaps any seat the slot still names, relaunches it\nunder the same name, lifecycle uid, credential, persona and cwd, and counts the restart against the\npolicy. A seat on a runtime that can adopt a live seat (tmux) and still running when the successor\nstarts is adopted in place: the successor checks that the recorded tmux server, window and pane are\nthe ones the slot names and that the pane's process still runs, then manages that seat again under\nits existing credential, without a restart. Both paths read the record the manager writes to\n`.cotal/seat-records/<space>/<name>.json` once a supervised or adoptable seat is ready, rewrites at\nevery restart and credential renewal, and removes when the seat is freed. A spent budget, a missing\nrecord, an expired credential, a persona file or credential that no longer matches the record, or\nan adoptable seat whose pane has exited retires the lifecycle as above. A runtime that custodies its seats reserves that\nreference before it launches one, and the manager records it on the slot's first durable row, so a\nmanager that dies part-way through a spawn also leaves a seat its successor can address. A\nsame-lifecycle restart or a resume records the new seat's reference on the slot the same way, and\nwhen the slot does not take it the restart or resume fails and stops any seat it started, so the\nslot never names a seat that has already exited while its replacement runs. A resumed seat keeps\nits retained credentials, so the resume frees it only once its exit is proved; a seat whose stop\ncannot be proved stays managed, and the resume's error says so, naming a stop the runtime refused\nas `stop failed: <message>`. A spawn\nthat launched its seat and then failed is rolled back by the manager that launched it, and that\nrollback reaps the seat through the same reserved reference before the lifecycle retires. Missing or unverified broker evidence keeps the slot\nterminalizing, and so does a runtime that cannot reap by reference. While the name stays held, a\nsame-name spawn or a resume re-drives that teardown with the same recorded reference, so it reaps\nthe seat again or keeps the name held.\n\nA `meshes add --mode user` entry is a **participant** registration, not hosting authority. A\nparticipant may run `supervise` only when the host advertises the remote manager authority service\nand the signed-in actor has the dedicated `supervise` ledger scope. The CLI obtains the closed,\nloopback-only `manager-service` view; `spawn` and `admin` do not substitute for that scope. The\nhost issues the manager's public-nkey JWT material through its lifecycle-bound prepare \u2192 activate\n\u2192 renew protocol, never by handing the participant a signer or static provisioner credential.\nThe host also performs instance-scoped eviction and guarded gate reconciliation. A remote manager\nrefreshes its short-lived registration executor before clean deregistration, so a long-running\nprocess removes its service row on `SIGINT` or `SIGTERM`. After an unclean stop, the same instance\nverify-evicts its superseded family and advances the process epoch. If an abandoned frozen gate\nholds the manager governance slot, a different supervise-scoped manager asks the host to reconcile\nthat holder after a complete gone verdict, then retries its registration once.\n\nA remote supervise never uses local signing trust: with host-issued authority in hand, the\nmanager mints from that authority alone and consults local records only to refuse a conflict,\nnamely the supervised space's own trust records under the cwd root. A root that hosts another\nstatic space beside the sign-in is a normal configuration and is never read as this space's\ntrust.\n\nThe broker URL in the registry entry decides the transport. A remote broker is often published\nover a `wss://` edge rather than a raw `nats://` port, and `supervise` dials whichever scheme the\nrecord holds, starting with the manager-authority registration it runs before the manager exists.\nThe record also decides whether that registration requires TLS, so a participant never downgrades\nthe credential exchange to a plaintext connection the registry did not describe.\n\nWithout that advertised host service or scope, `supervise` refuses before it starts a manager.\nRun `cotal spawn` without `--detach` to launch a foreground agent, or ask the space host to enable\nthe authority service and grant `supervise` for detached agents. If a running remote manager loses\nrenewal, it reports degraded state and refuses unsafe new starts and restarts; live agents are not\nsilently replaced. Do not run `cotal down` or `cotal up` on a participant machine to repair this\ncondition.\n\n## daemon\n\n```bash\ncotal daemon serve [--daemon-home <dir>] [--json|--plain]\ncotal daemon status [--daemon-home <dir>] [--json|--plain]\ncotal daemon login --idp <auth base URL> [--client-id <id>] [--daemon-home <dir>] [--json|--plain]\ncotal daemon connect-check --space <s> [--daemon-home <dir>] [--json|--plain]\ncotal daemon register --space <s> [--daemon-home <dir>] [--json|--plain]\ncotal daemon service-connect --space <s> [--daemon-home <dir>] [--json|--plain]\ncotal daemon service-disconnect --space <s> [--daemon-home <dir>] [--json|--plain]\ncotal daemon service-status --space <s> [--daemon-home <dir>] [--json|--plain]\n```\n\nThis release serves a local endpoint for status, sign-in, a connection check, a registration request\nand the daemon's own service connection. It does not start a Manager, serve any command on its\nservice, connect to Cloud, or install a system service. It supports Linux only. On other\nplatforms, an unset `XDG_RUNTIME_DIR` produces\n`TARGET_UNAVAILABLE` (`NO_RUNTIME_DIR`); with it set, the platform check refuses `UNSUPPORTED`.\n\n`serve` runs in the foreground and prints `daemon ready at <socket>` once the endpoint is bound\nand checked. That success line is plain text; `--json` shapes its refusals. `status` prints the\nstate, version, protocol, process ID, start time and uptime. `state: \"ready\"` means only that the\nlocal control listener is bound and verified, not that a mesh or Cloud connection exists.\n\nThe endpoint is `$XDG_RUNTIME_DIR/cotal/daemon.sock`. Without `XDG_RUNTIME_DIR` both forms refuse\n`TARGET_UNAVAILABLE` (`NO_RUNTIME_DIR`). Both refuse `PERMISSION_DENIED` for a runtime directory\nthat is a symlink, not a directory, not owned by you, or not mode `0700`. `status` also refuses\n`PERMISSION_DENIED` for an endpoint entry that is not a socket, not yours, or not mode `0600`.\nThe daemon drops connections from other users. The kernel separates users, not processes of one\nuser: any process running as you can use the local controls, including sign-in and cancellation.\n\n`status` with no daemon running refuses `TARGET_UNAVAILABLE` (`NOT_RUNNING`). `serve` refuses\n`CONFLICT` (`ALREADY_RUNNING`) for any existing socket, including one left after a crash, and\n`PERMISSION_DENIED` (`SOCKET_NOT_SOCKET`) for a non-socket entry. It removes neither.\nSIGTERM and SIGINT shut down cleanly and remove only the daemon's own socket. SIGKILL or SIGHUP,\nincluding closing the terminal, leaves a stale socket. To recover, run `cotal daemon status`.\nIf it reports `NOT_RUNNING`, confirm no daemon process is running, then remove the named socket\npath by hand and start again.\n\n`--daemon-home <dir>` runs or reaches a separate daemon whose own state lives in `<dir>`: its\nsign-in session, its per-space identity, register request and service store, all at the paths\nbelow with `<dir>` in place of `<COTAL_HOME>`. Spaces are still read from the shared Cotal home's\nmesh registry, which this daemon never writes. Its socket is\n`$XDG_RUNTIME_DIR/cotal/home-<16 hex>/daemon.sock`, derived from the directory, so it runs beside\nthe default daemon. `<dir>` must already exist as an absolute directory you own with mode `0700`,\nand cannot be the Cotal home. A relative path or the Cotal home refuses `USAGE_INVALID`, a missing\ndirectory `TARGET_NOT_FOUND`, and a symlinked, foreign or loose one `PERMISSION_DENIED`. Nothing is\ncreated or repaired.\n\n`serve` holds the lifetime lock `<dir>/daemon.lock` while it runs. A second `serve` for the same\ndirectory refuses `CONFLICT` (`LOCK_HELD`), naming the live holder. A start that fails for any other\nreason releases the lock again. A killed daemon leaves its socket behind, which still refuses\n`CONFLICT` (`ALREADY_RUNNING`) and is never removed, so recover as above. Every client command with\n`--daemon-home` sends nothing unless the process at that socket is the lock's live owner (same pid\nand process start). Otherwise it refuses `PERMISSION_DENIED` (`PEER_NOT_LOCK_OWNER`).\n\nThis daemon is a different machine identity. Nothing is copied from the default daemon: it needs\nits own `login`, its own `register`, and the space owner's approval of its own instance. Without\n`--daemon-home` every command behaves as before.\n\n`login` signs the running daemon in with device approval. The daemon runs the same sign-in as\n`cotal login` and keeps the session in the machine's Cotal home, where `cotal login` keeps it. The\ncommand prints only the approval link, code and expiry to stderr, then the signed-in account. No\nsession token crosses the endpoint or reaches the terminal. `--idp` takes an HTTPS auth URL, or an\nHTTP loopback IP URL; `--client-id` defaults to `cotal-cli`.\n\nOne sign-in runs at a time: a second `login` refuses `CONFLICT` (`LOGIN_IN_PROGRESS`) without\ncontacting the provider. Ctrl-C, or the command's connection closing, requests cancellation on this\nmachine. If cancellation takes effect before persistence, nothing is saved, even if device approval\nalready happened. An already-completed attempt remains completed; cancellation revokes nothing\nat the provider. A cancelled attempt reports `USAGE_INVALID` (`CANCELLED`). A denied or expired\napproval refuses `PERMISSION_DENIED` (`DENIED` or `EXPIRED`), and an unreachable provider refuses\n`DEPENDENCY_UNAVAILABLE`\n(`UNREACHABLE`); none of these saves a session. Any other provider answer fails `OPERATION_FAILED`\n(`REFUSED`). Such a failure can come after the session was saved, so run `login` again. A failure\nis reported with a fixed sentence; the provider's own error text is never relayed. `status`\nkeeps answering while a sign-in waits.\n\n`connect-check` asks the running daemon whether it could register with a space as its own service.\nThe daemon is not a Manager and never borrows a Manager's identity or authority. The check reads\nthe space's registration on this machine and the daemon's sign-in, then the space's public account\nidentity from its auth service (`GET <exchange>/service-endpoint-authority`, no redirects). It sends\nno registration and holds no credential. A space whose auth service does not host service endpoints\nrefuses `UNSUPPORTED` (`UNSUPPORTED_BY_SPACE`). The daemon does not fall back to anything else.\n\nThe other refusals are `TARGET_NOT_FOUND` (`NOT_REGISTERED`) for a space this machine has not\nregistered, `UNSUPPORTED` (`NOT_USER_AUTH`) for a space without user sign-in, `AUTH_REQUIRED`\n(`NOT_SIGNED_IN`) when the machine is not signed in to the space's provider, and\n`DEPENDENCY_UNAVAILABLE` (`UNREACHABLE` or `REFUSED`) when the auth service cannot be reached or\nanswers with something unusable.\n\nOn success it reports `ready-to-register` with the space's account, the daemon's service name\n`ai.cotal.machine` and the daemon's own instance ID. Nothing has been registered. The space owner must\nstill approve the daemon, and the service name needs the space operator's binding. The first\nsuccess creates the daemon's identity for that space at\n`<COTAL_HOME>/daemon/<space segment>/service-identity.json` (mode `0600`). It holds the instance ID\nand the account, and no credential. Later checks reuse it. If the space later answers with a\ndifferent account, the check refuses `CONFLICT` (`ACCOUNT_MISMATCH`) and changes nothing: confirm\nthe space was not replaced before you remove that file by hand.\n\n`register` asks the running daemon to request its own service registration with a space. It first\nruns `connect-check` and returns any outcome other than `ready-to-register` unchanged, without\ncontacting the space again. Otherwise it sends one registration request for the daemon's instance\nwith this machine's sign-in, through the space's stock service authority. The daemon never sees or\nrelays that sign-in proof. The request ID is saved before the first send, at\n`<COTAL_HOME>/daemon/<space segment>/register-request.json`, and every later `register`, including\nafter a restart, replays the same request unchanged. The stock writer does not guarantee durability\nacross power loss.\n\nThe fixed service name binds to one owner per space. Different owners in a shared space need distinct\noperator-approved endpoint names; this command does not yet select one. The operator must verify or\ndelegate the namespace before binding it.\n\nThe space answers in one of three ways, each reported with exit 0:\n\n- `approval-required`: the space owner must approve this daemon and the space operator must bind its\n service name `ai.cotal.machine`.\n- `pending`: the space is processing an approved registration.\n- `registered`: the registration is recorded. It is not activated or serving, so the machine is\n still not connected.\n\nNo release ships a command or page that lets an owner approve the daemon or an operator bind its\nname. A refusal by the space is `PERMISSION_DENIED` with the space's reason as the native code, such\nas `PROPOSAL_REFUSED`. A reason the daemon does not recognize is reported as `UNRECOGNIZED`, and the\nspace's own text is never shown. A new proposal after a refusal needs a new request, which `register`\ndoes not send. A sign-in the provider rejects (expired or revoked) is `AUTH_REQUIRED`: run\n`cotal daemon login` again. A failure before anything was sent is `DEPENDENCY_UNAVAILABLE`. Only a\nrequest whose outcome is unconfirmed is `OPERATION_UNKNOWN`: run the same command again\nto replay it. A changed registration for the same instance is refused `CONFLICT` and nothing is sent.\n\n`service-connect` connects the running daemon to the space as its own registered service. It replays\nthe saved register request unchanged to read its registration, gets its service credential from the\nspace's auth provider, verifies the bearer signature and witness against the pinned authority's\nissuer keys, checks the serving snapshot against its embedded contract, and connects to the broker\nas that service. It binds the registered\nresponder before reporting ready. It succeeds, exit 0, only after the broker accepts the connection,\nthe responder subscriptions are flushed and the space accepts the daemon as ready. The renewal and ready request IDs are kept in\n`<COTAL_HOME>/daemon/<space segment>/service-store/`, mode `0600`, and reused after a restart.\n\nEvery other answer is a refusal and nothing is connected. Not yet approved is `LIFECYCLE_BLOCKED`\n(`APPROVAL_PENDING`). A refused registration is `PERMISSION_DENIED`, and so is a space that ended the\ndaemon's authority, with its code as the native code, such as `APPROVAL_REVOKED`; connecting again\ndoes not reconnect it. A space or broker that cannot be reached is `DEPENDENCY_UNAVAILABLE`, and\nconnecting again retries the same requests. Without a register request the answer is\n`TARGET_NOT_FOUND`.\n\nWhile the daemon runs, the connection renews itself and reconnects on its own. The access bearer\nlives at most 60 seconds, so the broker drops the connection when each one expires and the daemon\nreconnects with the next. While the broker connection is down, for that reason or any other, the\nanswer is `connecting`, not connected; it is connected again only after the broker is back and the\nspace accepts the daemon as ready again. This is not a persistent service: a restarted daemon starts\nwith no service connection and stays disconnected until you run `service-connect` again, and an\nended connection is remembered only until the daemon stops.\n`service-status` reads the daemon's current answer without contacting the space. `service-disconnect`\ndrains the responder and closes the connection, and stopping the daemon closes all of them.\n\nThe service answers its existing `machine-status` command with `null`. This is a liveness response;\nit contains no vitals, hostname or machine metadata. Its stock Describe answer is explicitly public:\nit identifies the endpoint and derived owner token and includes the protocol and command schemas.\nCallers still need broker-enforced invoke access to the selected instance and command. Connecting the\ndaemon grants no caller access. Owner approval, operator name binding and caller grants remain\nseparate actions. An authority that does not publish its issuer identity cannot support this\nserving connection; the daemon refuses rather than trusting a decoded token or using Manager\ncredentials. The registration contract is unchanged.\n\n## service\n\n```bash\ncotal service [--space <s>] [--json|--plain]\ncotal service show <name>\ncotal service add <name> --preset <preset> <preset-options>\ncotal service add <name> --file <definition.json>\ncotal service start <name> [--instance <id>|--new-instance] [--foreground|--dry-run]\ncotal service stop <name> [--instance <id>] [--with-agents] [--yes|--dry-run]\ncotal service restart <name> [--instance <id>] [--with-agents] [--yes|--dry-run]\ncotal service status <name> [--instance <id>]\ncotal service logs <name> [--instance <id>] [--tail <n>] [--follow]\ncotal service install <name> [--instance <id>] [--dry-run]\ncotal service uninstall <name> [--instance <id>] [--yes|--dry-run]\ncotal service remove <name> [--yes]\n```\n\n`svc` and `services` are aliases. Every form takes `--space`, `--json` and `--plain`.\n\n| Flag | Meaning |\n|---|---|\n| `-s, --space <s>` | The space whose services to use, resolved as in [Selecting a space](#selecting-a-space). A space that is prepared but not started can be named |\n| `--preset <name>` | add: save a declared preset. Its options become flags of `add` |\n| `-f, --file <definition.json>` | add: import a version-1 definition file |\n| `--instance <full-id>` | Select one instance by its full ID, not a prefix |\n| `--new-instance` | start: allocate another instance of a process definition |\n| `--foreground` | start: keep the process in this terminal |\n| `--with-agents` | stop, restart: request the manager's native agent-stop policy |\n| `--dry-run` | start, stop, restart: preview the effect. install, uninstall: inspect the unit |\n| `-y, --yes` | stop, restart, remove, uninstall: confirm in a script |\n| `--tail <1..1000>` | logs: how many records to read (default 100) |\n| `--follow` | logs: keep reading |\n\nA service is a saved definition in one space plus the instances started from it. Bare `service`\nlists the definitions, their instances and the installed presets. Nothing starts a dependency for\nyou: starting the manager does not start its broker. `--json` writes one envelope for every form,\nand `logs --follow --json` writes one record per line.\n\n### Adding a service\n\nThe presets built into the CLI are `auth-service`, `broker`, `delivery`, `exec-endpoint`,\n`feedback-intake` and `manager`. The `linear` and `web` presets come with the `@cotal-ai/linear`\nand `@cotal-ai/web` extensions, which `cotal ext add <package>` installs. Bare `service` lists the\npresets this machine has, and a name it does not have is refused as `DEPENDENCY_UNAVAILABLE`.\n\n`cotal service add <name> --preset <preset> --help` lists a preset's options. A required option is\nmarked `(required)`, and an option that takes a path shows `<dir>` or `<file>`. A missing required\noption is refused with each one named.\n\n`add` checks the options against the preset's schema, and checks that each declared path exists\nwith its required protection. It does not check that a root or credential suits the preset, such\nas a root's auth mode or which credential a file holds. Those are checked when the service starts.\nA refusal names the option or field at fault. Option values are not printed.\n\n`--file` imports a version-1 definition. This one runs a program and treats it as ready once its\nloopback health URL answers 200:\n\n```json\n{\n \"schemaVersion\": 1,\n \"space\": \"main\",\n \"launch\": { \"type\": \"process\", \"executable\": \"/usr/bin/node\", \"args\": [\"/srv/reviews/server.mjs\"], \"cwd\": \"/srv/reviews\" },\n \"readiness\": { \"type\": \"http\", \"url\": \"http://127.0.0.1:8080/health\", \"expectedStatus\": 200, \"timeoutMs\": 10000 },\n \"envRefs\": { \"REVIEWS_TOKEN\": \"REVIEWS_TOKEN\" },\n \"credentialRef\": { \"kind\": \"file\", \"path\": \"/srv/reviews/reviews.creds\" }\n}\n```\n\n| Field | Meaning |\n|---|---|\n| `schemaVersion` | `1` |\n| `space` | The selected space's name. Another name is refused |\n| `launch` | `{type: \"process\", executable, args, cwd}`: an executable on `PATH` or by path, its arguments, and an absolute working directory. Or `{type: \"adapter\", adapter, options}` for an installed adapter, which a preset writes for you |\n| `readiness` | `{type: \"process\", timeoutMs}`: ready once the process runs. `{type: \"http\", url, expectedStatus, timeoutMs}`: ready when a loopback `http://127.0.0.1` or `http://[::1]` URL answers that status. An adapter definition uses `{type: \"adapter\", timeoutMs}`. `timeoutMs` is 1 to 120000 |\n| `envRefs` | Optional. Child variable name to the name of a variable in the environment `start` runs in. `CLAUDECODE` and names starting `COTAL_` or `CLAUDE_CODE_` are refused |\n| `credentialRef` | Optional. `{kind: \"file\", path}`: an absolute, protected credential file. The child receives its path as `COTAL_CREDS`, never its contents |\n\nNo other field is accepted. The file is at most 256 KiB.\n\n### Running a service\n\n`start` records the instance before it launches anything and prints its full ID. A start that fails\nbefore its process exists records the instance as failed. Its refusal names the cause, such as a\nspace that is prepared but not started. `start` can then try again on the same instance, and\n`stop`, `remove` and `cotal down` find nothing to stop.\n\n`stop` and `restart` ask before they act in a terminal and need `--yes` in a script. Enter, Ctrl-C,\nCtrl-D or closed input at that question leaves the service unchanged and exits 0. Without an\ninstalled unit, a `restart` whose process is already gone stops nothing, so it does not ask. A\nstart or stop that finds nothing to do says so and reports `no-op`.\n\n`remove` forgets a definition once every instance is stopped and no unit is installed. Logs stay.\n\n`install` writes an owned systemd user unit for a saved direct-process definition with no\n`envRefs` or `credentialRef`, and enables it without starting it. It is Linux only. `uninstall`\nremoves that unit. A preset is not installed as a unit in this release, the manager included.\n\nThe sections [Local service definitions](#local-service-definitions),\n[Service presets](#service-presets) and\n[Owned process services](#owned-process-services)\ndescribe each preset and the process driver in detail.\n\n## reconcile-gate\n\n```bash\ncotal manager reconcile [--space <s>] [--server <url>] [--endpoint <e>] [--instance <id>] [--yes|--dry-run] [--json|--plain]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space the frozen gate lives in |\n| `--server <url>` | the local mesh | Broker URL |\n| `--endpoint <e>` | `manager` | Endpoint whose gate is frozen |\n| `--instance <id>` | this folder's persisted manager instance | Instance id |\n| `--yes` | ask and default to cancellation | Confirm this selected gate repair outside a terminal |\n| `--dry-run` | off | Resolve the local coordinate without minting or checking native guards |\n| `--json`, `--plain` | terminal report | Finite structured receipt or plain shared report |\n\n**When you need this.** A manager restart killed after deregistration begins but before the new\nincarnation finishes leaves the endpoint's issuance gate *frozen*, held by a\nprocess that no longer exists. The freeze is what stops two incarnations serving at once, which is\ncorrect. The successor manager now completes that dead registration itself on boot, including on\nthe remote user-auth path. A foreign remote manager blocked by this gate also asks the host to repair\nit before one registration retry. Both use the same guard this command uses: they act only when the freeze-holder is affirmatively gone under a complete\nCONNZ sweep (`gone` and `sweepComplete=true`). If that registration's spec write already committed,\nit finishes the same freeze at the committed registration revision. If the spec did not advance, it\nabort-reopens the gate at generation+1 with processEpoch unchanged and continues the normal takeover.\nLive, unknown, unestablishable, and\nwrong-op-kind still refuse; there is no TTL.\n\nUse this command when the automatic path cannot run: the delivery daemon is down, the repair targets a\nnon-manager endpoint, or you want to lift the freeze without starting a manager. It checks that the\nholder really is gone, then finishes the dead operation the same way as the\ninterrupted restart would have: revoke the old credentials, evict their holders with verification,\nand reopen the gate.\n\nThe command revokes the old credentials 16 at a time. It then verifies the holders' eviction in\nshared sweeps of up to 256 holders on the delivery daemon. Each sweep scans the broker a fixed number\nof times and kicks live connections 16 at a time, so holders that are already gone add almost\nnothing and live ones add one broker round trip per 16 connections. The daemon must serve the\n`evictPrincipals` verb; an older daemon refuses it and the gate stays frozen.\n\nEach sweep durably records the holders it verified before the next sweep starts. If a holder is not\nverified gone, the command leaves the gate frozen with those records kept. An interrupted sweep\nrecords nothing, and the sweeps before it stay recorded. A retry still repeats the freeze-holder\nliveness check, then skips only progress bound to the same registration operation, frozen-gate\nrevision, and holder set.\nThe output reports holders completed before this attempt, completed now, and still remaining. A new\nfreeze or changed holder set starts from zero. Cursor cleanup happens only after reopen; a retained\ncursor is harmless because its old gate revision cannot authorize a later freeze. The governance slot\nrelease after the reopen is cleanup in the same way: a slot it fails to release does not fail the\nrepair, because the reopened gate is already past the slot's generation.\n\n**It refuses far more often than it acts, on purpose**, and always says which check stopped it:\n\n| Refusal | What it means | What to do |\n|---|---|---|\n| `holder-alive` | The freeze-holder still has a live connection: a manager *is* running | Stop that process first. Reconciling would evict a live manager's credentials |\n| `holder-unknown` | The connection sweep could not prove the holder absent | Not safe to proceed: an unprovable holder is treated as a live one. Re-run once the broker answers completely |\n| `liveness-unestablishable` | The delivery daemon gave no verdict: it was unreachable, timed out, or refused | Act on the delivery lease line in the refusal (below). Silence is never read as death |\n| `not-frozen` / `no-gate` | The gate is open, or there is no gate at that coordinate | Nothing to repair: check `--endpoint` / `--instance` |\n| `wrong-op-kind` | Frozen under a takeover or retirement, not a registration | Out of scope for this command; it will not reinterpret another operation's intent |\n| `eviction-unverified` | The holder looked gone but eviction could not be verified | The gate is left frozen, unchanged. Investigate the broker before retrying |\n| `raced` | A newer manager moved the gate mid-repair | Re-run `cotal doctor` and look again |\n\nWhen the daemon gives no verdict, the refusal also reads the delivery lease (`lease.0`) and names\nwhat is blocking the rail:\n\n| Lease reading | What to do |\n|---|---|\n| absent | No daemon is running. Start it (`cotal up` runs it) and re-run |\n| unreadable | The daemon cannot be named, so do not assume none is running. Fix the lease read, then re-run |\n| held, not ready | That holder claimed the shard and has not bound its rails. Wait for it, or stop it so its lease lapses |\n| held, ready, no answer | The query may have gone to another daemon still subscribed to the rail, such as a stopped one whose lease lapsed. Re-run before stopping anything. If no run gets an answer, stop any other delivery daemon for the space, then stop or restart the holder |\n| changed hands | The holder took the shard after the query was sent, so it was never asked. Re-run before stopping anything |\n\nThe command reads the lease before it sends the query and again after the query fails. It names a\nholder as the blocker only when the same run of the same daemon held the lease both times, and two\nrows from a daemon too old to record its run never count as the same run. Even then a ready holder\nmay not have been asked: the rail is queue-grouped, so any daemon still subscribed to it can take\nthe query. A row whose times are not valid dates reads as unreadable.\n\nA daemon that answered and refused keeps its own reason, followed by the same lease line. The lease\nline names the holder, whether it is ready, the space account that holds the lease bucket, when that\nholder acquired the shard, and when the row was last written. A ready holder rewrites the row on\nevery renewal and keeps its acquisition time, which only a successful acquisition sets. A row\nwritten by a daemon that predates the acquisition time reports it as unknown. The lease reads never\nchange the outcome: the gate stays frozen and the command exits 2. A manager's boot self-heal uses\nthe same check and reports the same line.\n\n`raced` means the repair's own reopen lost its compare-and-set and wrote nothing. A reopen whose\noutcome is unknown, such as one whose acknowledgement was lost, fails with an `unavailable` error\nthat says it may have committed. Re-run the command: it refuses `not-frozen` if that reopen landed and\nfinishes the repair if it did not.\n\nThere is no `--force`, and no path that discards gate state: the only way this reopens a gate is by\nproving the holder is gone and then completing the operation properly.\n\n**What reopening the gate does for the endpoint's governance slot.** A registration takes the\nendpoint-wide governance slot before it publishes its spec, and holds it until its gate reopens. An\ninstance that died between those two points leaves the slot held with no registration behind it.\nThis command does not write that slot and never has; the registration path is its only writer. What\nthe reopen does is advance the holder's gate past the generation the slot is stamped with, which is\nwhat marks the slot abandoned. The next registration for that endpoint then reclaims it as part of\nits ordinary start. So the repair here is still one command followed by starting the manager, and\nthe slot needs no separate step.\n\n## endpoint deregister\n\n```bash\ncotal endpoint deregister <class> <full-instance-id> [--space <space>] [--server <url>] [--yes|--dry-run] [--json|--plain]\n```\n\nThis removes a departed instance's registration, not a process or an agent. Both the native\nclass and full instance ID are required; there is no default instance, prefix match or saved\nreference. The selected static space must have validated local signing authority. Open and\nuser-auth targets refuse instead of borrowing a static credential path.\n\nThe command resolves one root, account and broker policy, including its TLS requirement.\nA terminal reviews that full target and defaults to Cancel. Scripts need `--yes`.\n`--dry-run` reports the offline target and intended guard without contacting the broker,\nissuing an operation credential or deleting anything. It does not prove apply will be allowed.\n\nApply delegates to the existing native guard. An answering instance refuses. An unanswered\ninstance also refuses unless the broker affirmatively reports that instance rail empty.\nAn unavailable probe or an in-flight registration refuses. After that evidence, the native\nhelper checks governance and deletes status before spec, pinning both revisions. The issuance\ngate is not changed and no process is stopped; the same instance may register on a later start.\n\nThe JSON/plain receipt retains the full coordinate and acknowledged removed revisions.\nA registration that moves during deletion is not success: its spec was not removed, but an\nolder status may already have been removed. That uncertain outcome retains the target and is\nnot retried. Raw probe diagnostics and credential material are not copied into the receipt.\n\nThe old `deregister-instance` spelling is a hidden migration refusal. It starts no operation\nand names the literal replacement; its former default-to-this-folder instance is not retained.\n\n## runtimes\n\n```bash\ncotal runtimes [--json | --plain]\n```\n\nLists every agent runtime the manager can spawn through: the built-in `pty`, the official providers\n(`orca`, `tmux`, `cmux`, `herdr`), and any custom provider installed via `cotal ext add`. Each installed\nprovider is probed so you can see what is actually reachable on this machine before selecting it:\n\n```\nRUNTIME STATE PACKAGE\npty built in\norca installed \xB7 reachable @cotal-ai/orca\ntmux not installed @cotal-ai/tmux\ncmux not installed @cotal-ai/cmux\nherdr not installed @cotal-ai/herdr\nadd one that is not installed with `cotal ext add <package>`\n```\n\n`installed \xB7 reachable` / `unreachable` is the provider's own `available()` probe; `not installed`\nmeans it is a known runtime you can add with `cotal ext add` and the package shown. `--json` writes\none document whose `result.items` are `{name, state, package, reachable, error}`, with `state` one of\n`built-in`, `installed` or `not-installed` and `reachable` null when the provider was not asked. An installed provider\nwhose package fails to load is shown with its error and makes the read `partial` (exit 4). The command\nreads this machine only, so its `target` is null. Selecting an unknown or uninstalled runtime\nvia `up`/`spawn --runtime <name>` fails loud and, for a known one, points at the exact `cotal ext add`\npackage. There is no silent fallback to `pty`.\n\n## seats\n\n```bash\ncotal seats [--drain]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--drain` | off | Retire every seat whose agent has exited. A seat whose agent still runs is kept |\n\nThe pty runtime used to start a detached custodian process for every Linux seat. It now spawns\nin-process, but custodians that an earlier manager started keep running, and one whose agent has\nexited stays resident while a manager still holds its connection. This command lists the custody\nrecords under `COTAL_SEAT_ROOT` (default `~/.cotal/seats`), one line per seat:\n\n| State | Meaning |\n|---|---|\n| `live-child` | The agent process still runs. The seat is never signalled, and a manager can still adopt it |\n| `childless` | The agent has exited, or the record comes from an earlier boot. `--drain` retires the seat |\n| `drained` | `--drain` proved the custodian and the agent gone and removed the record |\n| `refused` | The record cannot be read, carries no start or boot identity, this host publishes no boot identity, or the reap could not prove the processes gone. The record stays on disk |\n\nA drain signals only a custodian whose recorded start identity still matches the live process, so\na reused pid is never touched. No process outlives a reboot, so a record from an earlier boot is\nreported childless and `--drain` removes it without signalling anything. A record with no start or\nboot identity is refused with or without `--drain`, and is never reported as running or exited.\nOn a host that publishes no boot identity (`/proc/sys/kernel/random/boot_id`) every record is\nrefused the same way, because no record can be tied to this boot.\nThat refusal and an unreadable record signal nothing. A refusal from the reap itself can come after the drain already\nsent `SIGKILL` to the custodian. Its detail names the pid or process group the reap could not prove\ngone, so check those processes before you retry. The command exits non-zero when any record is\nrefused. It is Linux-only and throws on other platforms.\n\n## send\n\n```bash\ncotal send -t <agent> \"<text>\" [--space <s>] [--server <url>] [--creds <path>]\ncotal send -c <channel> \"<text>\"\ncotal send -r <role> \"<text>\"\n```\n\nChoose one destination: `--to/-t` for a direct message, `--channel/-c` for a channel,\nor `--role/-r` for the role's native work queue. Choose one body source: a quoted positional\nargument, `--file/-f <path>`, or explicit `--stdin`. There are no implicit stdin reads. Files and\nstdin must contain nonempty UTF-8 text; their content is preserved and never printed in the receipt.\nNo destination, several destinations, several body sources or an empty message is refused.\nThe recipient grammar is checked before connector installation or target preparation. Channel\nand role names use the native subject grammar; a channel publication cannot name a wildcard.\nA direct target resolves by its full identity or an unambiguous name. A name collision refuses;\nno fuzzy match selects an agent for you.\n\nFor a channel post, repeat `--mention <agent>` to add explicit wake hints without changing the\ndestination. Each one resolves like a direct target and is sent as that agent's name, which is what\na receiving session compares. An instance id is refused when its name also resolves to another\nparticipant. 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. A message over the broker's message limit, envelope included, refuses\n`UNSUPPORTED` (exit 2) with its size and the limit, and nothing is sent. A channel post the broker\nrefuses was not stored, so it refuses `PERMISSION_DENIED` (exit 2): the channel does not exist in\nthe space or this identity may not post to it. The remedy lists the channels this identity may post\nto, read from the grant ledger as `cotal whoami` reads it. When the ledger is on another machine it\nsays so and lists the space's registered channels instead. Any other unconfirmed publish outcome is\nunknown 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 `send --to` or `send --role` with `cotal_dm` or from a `chat`\nsession. On a static or open mesh every send has its own principal under the same display name, so\nthe sender's name works only while it names one principal among the senders the recipient has heard\nfrom and the present peers. Once the recipient has heard two sends from the same login and host, a\nreply by that name refuses with `TARGET_AMBIGUOUS` and lists the principals. Answer by the id on\nthe message instead (`from.id` in `chat --stdio`), or with `/reply` in `chat`. The reply is stored\nunder the sender's id in the space's DM history, which an operator's DM view such as the\ndashboard's Direct messages lens shows. The `cotal send` that asked has already exited, so the reply\nnever 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. A DM row shows its recipient's name when a message in the window came from that\nprincipal, and the principal otherwise. `result.items[].to` is the principal.\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, lists them under `Your spaces`, and names `cotal use <name>` to select one. A\nspace registered from elsewhere that has not granted your account refuses with those discovered\nspaces as its remedy, because only that space's operator can grant you. On the root that holds the\nspace's grant ledger, the refusal names the operator's `cotal actor grant` command instead. 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, cotal creates a missing Cotal config directory with mode `0700`, even with a\ngroup-writable umask, whichever command writes there first. The extension writer creates a missing\ninstall prefix with mode `0755` and its `package.json` with 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 a seed that failed or was interrupted, or a lost authority (rebuilds that seed's 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\nA seed that fails names the connector and the reason, such as npm missing from PATH. Until\n`cotal ext seed --repair` finishes it, each command that seeds refuses. The refusal says whether\nthe last seed failed, and why, or was interrupted.\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"
|
|
27286
|
+
"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, or with none selected the space prepared in the current folder, and lists common\ncommands. It points a prepared space to the `cotal up` that starts it, and a machine with no space\nto `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 terminal whose `TERM` is `dumb` gets no color either. A non-empty `NO_COLOR` turns color off on a terminal too. `FORCE_COLOR` turns color on even when output is\npiped or `TERM` is `dumb`, unless it is `0` or `false` in any letter case, 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.\nUnder a locale whose character set is not UTF-8, as `LC_ALL`, then `LC_CTYPE`, then `LANG` decide,\nreports and error lines draw their marks and punctuation as plain does, and keep their color. With\nnone of the three set, the marks are drawn in UTF-8. Windows consoles are always treated as UTF-8.\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. A command run from a folder that no longer exists refuses with `USAGE_INVALID`, exit 1,\nbefore it does anything, and says to `cd` to an existing folder. `help`, `version` and `completion`\nstill work there.\n\nOutput that cannot be written, for example to a full disk, is reported in one line on stderr, and\nthe command stops with exit 3, or with the non-zero status it had already set. It may have stopped\npartway, so check what it did before you retry. A reader that closes the output early, as `| head`\ndoes, ends the command quietly with the status it had already set, or 0.\n\n## Commands\n\n| Area | Command | Purpose |\n|---|---|---|\n| Set up & lifecycle | [`setup`](#setup) | Prepare a space, application and runtimes; opt into startup separately |\n| Set up & lifecycle | [`update`](#update) | Reconcile first-party extensions and check or opt into a coherent CLI upgrade |\n| Set up & lifecycle | [`up`](#up) | Start a local mesh (nats-server + JetStream), or boot a whole manifest with `-f` |\n| Set up & lifecycle | [`down`](#down) | Stop the whole stack, selected registered components, or a manifest deploy |\n| Set up & lifecycle | [`backup`](#backups) | Create an offline full-space or registry-only artifact from a preserved cut |\n| Set up & lifecycle | [`clean`](#clean) | Configurable cleanup: purge history (live), or wipe the local store / identity (stopped) |\n| Set up & lifecycle | [`reset`](#reset) | Preview or reset one stopped local store or space identity |\n| Set up & lifecycle | [`space`](#selecting-a-space) | List the spaces this machine can reach, or show one in full |\n| Set up & lifecycle | [`use`](#selecting-a-space) | Select the space commands target when none is named, or show the selection |\n| Set up & lifecycle | [`meshes`](#mesh-registry) | List, register and drop meshes; `space` replaces its listing |\n| Set up & lifecycle | [`sync`](#mesh-registry) | Refresh the signed-in account's advertised spaces |\n| Set up & lifecycle | [`status`](#mesh-registry) | Read-only diagnostics for setup, processes, and the selected mesh |\n| Agents & personas | [`spawn`](#spawn) | Launch an agent from a persona (foreground, or `--detach` via the manager) |\n| Agents & personas | [`models`](#models) | List connector model catalogs and variants from the manager |\n| Agents & personas | [`ps`](#managed-seats) | List managed agents and their mesh status |\n| Agents & personas | [`stop`](#managed-seats) | Ask the manager to stop a managed agent |\n| Agents & personas | [`attach`](#managed-seats) | Stream and drive a managed agent's terminal (pty runtime) |\n| Agents & personas | [`input`](#input) | Type one line into a managed agent's terminal without attaching |\n| Agents & personas | [`persona`](#persona) | List, show, edit, create, or remove local personas |\n| Agents & personas | [`supervise`](#supervise) | Deprecated and hidden from help: run a manager daemon; use the `manager` service preset |\n| Agents & personas | [`service`](#service) | Save, start, stop and inspect owned local services, from a preset or a definition file |\n| Agents & personas | [`daemon`](#daemon) | Linux: serve the local machine daemon's control endpoint, read its status, sign it in, check and request its own registration with a space |\n| Agents & personas | [`runtimes`](#runtimes) | List the agent runtimes the manager can spawn through and whether each is reachable |\n| Agents & personas | [`seats`](#seats) | List the pty seat custodians an earlier Linux manager left, and drain the ones whose agent has exited |\n| Agents & personas | [`endpoint deregister`](#endpoint-deregister) | Remove the service registration of an instance whose host is gone, when the broker affirms nothing is subscribed on its rail |\n| Agents & personas | [`reconcile-gate`](#reconcile-gate) | Unfreeze an issuance gate left frozen by a crashed restart when the successor cannot boot-heal it (holder gone, complete CONNZ sweep) |\n| Messaging & watching | [`who`](#who) | The live roster of the selected space, and whether a manager runs each seat |\n| Messaging & watching | [`whoami`](#whoami) | The identity this CLI acts under, and what it may read, post and do |\n| Messaging & watching | [`endpoints`](#endpoints) | List every endpoint in the live presence roster, including infrastructure |\n| Messaging & watching | [`describe` / `invoke`](#endpoint-control) | Resolve a v0.4 service's command surface off the wire; invoke one command by name |\n| Messaging & watching | [`send`](#send) | Send one message, then exit: DM a peer, post a channel, or ask a role |\n| Messaging & watching | [`chat`](#chat) | Open a live session under its own identity: terminal view, `--plain` lines, or `--stdio` for programs |\n| Messaging & watching | [`channels`](#channels) | Inspect or set the channel registry |\n| Messaging & watching | [`history`](#history) | Read retained messages, or clear them |\n| Messaging & watching | [`pending`](#pending) | Inspect one DM durable's native counts and bounded candidate IDs |\n| Messaging & watching | [`console`](#console) | Live protocol view for a space (TUI, or `--plain` line stream) |\n| Messaging & watching | [Web dashboard](#web) | Browser dashboard, run as the `web` service preset (installed as the `@cotal-ai/web` extension) |\n| Extensions & misc | [`linear`](#linear) | Serve the official Linear MCP server as an endpoint agents can call (installed as the `@cotal-ai/linear` extension) |\n| Auth & meshes | [`mint`](#mint) | Mint a creds file for a space (static auth mode) |\n| Auth & meshes | [`login`](#login) | Sign in to a per-user-auth mesh's IdP (once per machine) |\n| Auth & meshes | [`logout`](#login) | Revoke the IdP session and clear the cached login |\n| Auth & meshes | [`actor`](#actor) | Manage a user-auth space's actor ledger (grant / revoke / list) |\n| Auth & meshes | [`doctor`](#doctor) | Credential-health diagnosis and repair (`doctor auth`) |\n| Auth & meshes | [`join`](#join) | Join a space as your own presence (interactive) |\n| Manifest | [`topology`](#manifest-deploys) | Validate and view a mesh manifest's access graph (read-only) |\n| Extensions & misc | [`ext`](#ext) | List, install and remove operator CLI extensions |\n| Extensions & misc | [`docs`](#docs) | Read this version's protocol and CLI docs, offline |\n| Extensions & misc | [`config`](#config) | Show and set your preferences: the defaults your commands use |\n| Extensions & misc | [`completion`](#completion) | Report, print or install shell completion |\n| Extensions & misc | [`feedback`](#feedback) | Send feedback to the Cotal developers |\n| Extensions & misc | [`telemetry`](#telemetry) | Show or set the anonymous usage telemetry level (`off`, `minimal`, `regular`) |\n| Extensions & misc | [`deliver`](#server-daemons) | Deprecated and hidden from help: run the delivery daemon; use the `delivery` service preset |\n| Workflow runs | [`run`](#run) | Operate durable workflow runs: start, resume, list, inspect, answer a checkpoint, check an edited program with migrate |\n| Extensions & misc | [`feedback-intake`](#server-daemons) | Deprecated and hidden from help: run a feedback intake server; use the `feedback-intake` service preset |\n| Extensions & misc | [`auth-service`](#server-daemons) | Deprecated and hidden from help: run a user-auth space's auth service by hand; use the `auth-service` service preset |\n\nThe manifest modes of `up`, `spawn`, and `down` (`-f <cotal.yaml>`) plus `topology` are covered\ntogether under [Manifest deploys](#manifest-deploys).\n\n## setup\n\n```bash\ncotal setup [--destination <local|self-hosted>] [-H|--harness <connector>] [--runtime <name>]... [--project <dir>]\n [--space <name>] [--connection-file <bundle.json>] [--configure-only | --start [-d|--detach]]\n [--on <manager-instance>]\n [--demo|--full] [-y|--yes] [--json|--plain]\ncotal setup --skills\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--destination` | asked | `local`: a space this machine starts. `self-hosted`: a space registered here that runs on a server, including a Cloud space `cotal login` discovered. `cloud` is refused before anything is written or contacted, and the refusal names that route: `cotal login`, then `--destination self-hosted --space <name>`. |\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 but cannot be chosen. Its hint says to sign\nin with `cotal login` and choose Self-hosted, because a Cloud space is registered here when you sign in.\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. The refusal prints the\nscripted form below and not the full command help:\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\nAfter an upgrade, `up`, `down` and the `service` verbs still control the stack members and\nfirst-party service instances an earlier release recorded, as long as their adapter's declaration\ndid not change. A third-party adapter's instances stay pinned to the package version that started\nthem.\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: 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 `tls://` address records TLS as required, so without `--tls-cert` and `--tls-key`, or a recorded TLS policy, `up` refuses it `USAGE_INVALID` before anything starts. A controller stack's first `up` records it, and a later `--server` naming another address refuses `CONFLICT`, naming the recorded address and the bare `cotal up` that starts the stack there. A different `--store-dir`, `--max-file-store` or `--host` refuses the same way, naming each recorded value. 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. Both refusals exit 1 before anything is written. 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). Also accepted with `-f` |\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, unreadable or not JSON refuses `USAGE_INVALID`, naming the file, before anything is written |\n| `--restore <dir>` | none | Restore a completed offline backup before exposing the normal listener |\n| `--restore-only registry` | artifact selection | Restore only the registry component |\n| `--accept-missing-source` | off | Explicit disaster consent when the inode-bound preserved source is absent |\n| `--accept-stale-checkpoint` | off | Explicit consent to resume a seat whose checkpoint was captured outside its recorded recency horizon |\n| `--open` | off (auth) | Unauthenticated dev mesh: no JWT, no ACLs |\n| `--user-auth` | off | Per-user auth: people `cotal login`; connects are authorized against the actor ledger |\n| `--idp <url>` | none | With `--user-auth`: the IdP auth base URL to pin on first enable. A later run may omit it or pass any spelling that normalizes to the pinned URL, such as one with a trailing slash; a different IdP is refused. With `-f`, overrides the manifest's `broker.idp` under the same rules: https (or http on a loopback IP literal), with no credentials, query or fragment |\n| `--exchange-public-port <n>` | none | With `--user-auth`: add the public exchange face on this loopback port, for an HTTPS reverse proxy to forward to |\n| `--exchange-public-url <https://\u2026>` | none | With `--exchange-public-port`: advertise the reverse proxy's HTTPS URL in discovery |\n| `--exchange-trusted-proxy` | off | With `--exchange-public-port`: attribute public failure buckets to the last `X-Forwarded-For` hop. Enable only when the listener is reachable solely through a trusted proxy; otherwise the socket address is used |\n| `--advertised-server <url>` | `--server` | With `--exchange-public-port`: the broker address the public discovery bundle tells participants to dial, such as a `wss://` address behind your proxy. Must be a `nats://`, `tls://`, `ws://` or `wss://` URL |\n| `--agent-provisioning-url <https://\u2026>` | none | With `--exchange-public-port`: the deployment's agent-provisioning endpoint the public bundle advertises. A participant's `cotal spawn` on a remote user-auth mesh sends its login bearer there to provision the agent; without one, that spawn needs a one-time enrollment ([Run a mesh](run-a-mesh.md#remote-supervised-seats-by-enrollment)). HTTPS only |\n| `--service-endpoints` | off | With `--user-auth`: the auth service also hosts service endpoints: the public registration door and the authenticated host route that `cotal service-endpoint` drives |\n| `--detach` | on | Run in the background, which is also the no-flag default |\n| `--foreground` | off | Keep native stack lifetime in this terminal. Required when a systemd/launchd unit supervises `up` itself. Cannot combine with `--detach` |\n| `--tls-cert <path>` | none | PEM certificate to serve TLS with. Must be given together with `--tls-key`. Before starting the broker, Cotal checks readability, private-key mode, key/certificate match, the validity window, and host coverage. `nats-server` accepts an expired certificate and leaves the failure to clients, so Cotal performs these checks first. The decision is recorded; a later bare `cotal up` keeps serving TLS |\n| `--tls-key <path>` | none | PEM private key for `--tls-cert`. Refused if group- or other-readable (tighten to `600`) |\n| `--file <cotal.yaml>`, `-f` | none | Launch a whole mesh from a manifest |\n| `--dry-run` | off | With `-f`: print the plan, mutate nothing. A preview from an unprepared folder under the machine home reads that folder's empty policy rather than inheriting the home store; applying there requires `space create` first. |\n| `--runtime <name>` | `pty` (or the manifest's, with `-f`) | Agent runtime for the mesh manager (`pty` built in; others are installed extensions, explicit-only). Resolved + probed before the broker starts; an uninstalled/unreachable runtime fails loud. Without `-f`, that refusal is `DEPENDENCY_UNAVAILABLE` (exit 2) before anything is written, and for a known runtime the remedy names the `cotal ext add <pkg>` to run. With `-f`, overrides the manifest's runtime |\n| `--max-sessions <n>` | 64 | Live-session ceiling for the mesh manager. Each console pane and each `cotal attach` is one session, so size for agents \xD7 panes, not agent count. Recorded on the mesh and reused by every later manager launch, so a repair or resume does not silently drop back to 64. A running manager cannot change it: `cotal down` first, then `cotal up --max-sessions <n>` |\n| `--no-manager` | off | Broker-only boot: start the broker and, in auth mode, the delivery daemon, and no local manager. A refresh under the flag of a mesh whose manager is live refuses rather than keeping or stopping it. For a controller stack, use `cotal service stop stack-manager --space <s> --yes` first. A legacy stack has no command that stops only its manager: `cotal down` stops the whole stack, then `cotal up --no-manager --space <s>` starts it broker-only. Cannot be combined with `--runtime`, `--max-sessions`, or an agent-declaring manifest |\n| `--rotate-sys` | off | Rotate the space's system account and re-mint its two `$SYS` creds. Needs a stopped mesh; refused with `--open` |\n\n`cotal up` boots a local nats-server with JetStream and, in auth mode (the default), JWT auth and\nper-agent ACLs. It runs in the background by default and records the mesh so `cotal spawn` from\nany directory can find it; `--detach` explicitly names the same mode. `--foreground` keeps the\nnative stack in this terminal. A contradictory mode pair refuses before setup or seed work. An\nunqualified `up` from a folder that inherits the machine's home `.cotal` refuses before seeding:\nprepare that folder with `space create <space> --root .`, then run `up --root .`, or move to the\nhome folder if the home stack was intended. When `--root` names another prepared project, run bare\n`cotal down` from that project folder: bare down\nstops the folder's stack, and `down --space` is not a remote replacement for that scope. With no\n`--space`, `up` runs the folder's space: the one its auth account names, else the one its stack or\nrunning mesh records, else the one prepared there, else `main`. A second `up` in the same folder\nruns the same space again. A folder whose stack and registrations name more than one space refuses\nwith `CONFLICT` and asks for `--space`. With no\n`--server`, it auto-selects a free port if the default address is taken, and a later `cotal up` of\nthe same space from the same folder starts from the URL that mesh recorded; an explicit `--server`\nstays fail-loud on collision. When the broker at that address is the named space's own, started\nfrom another folder, `up --space` refuses `CONFLICT`, exit 2, naming that folder. 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. On a controller stack, Ctrl-C or\n`SIGTERM` in that terminal runs the same guarded stop as bare `cotal down` and exits 130 or 143. A\nsignal while the stack is still starting lets the start settle, then runs that stop. When the start\nfails, the stop still runs before the start failure is printed. A hangup, such as a closed terminal,\nruns no stop: the command exits 129 and the stack keeps running until `cotal down` stops it. A\nsystemd unit that runs `up --foreground` should set `KillMode=mixed`, so the stop signal reaches\nonly `cotal up`, which runs the guarded stack stop, and a `TimeoutStopSec` long enough for that stop.\nThe default `control-group` mode signals every member at the same time, and a manager signalled\ndirectly does not 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`, the broker store and the persona catalog\n`.cotal/agents` owner-private (`0700`) too, as do `space create` and `setup`. An existing directory\nkeeps 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.\n\nAn explicit `--store-dir` on `up`, `up --restore` or `backup restore` must name a directory or a\npath to create in one. A regular file there refuses `USAGE_INVALID` (exit 1) and names it. The\nexisting part of the path follows the [`space create`](#selecting-a-space) rule for other local\nwriters, so a group- or world-writable directory in it refuses `PERMISSION_DENIED` before anything\nis written. A non-empty directory is accepted, since it may hold an existing store. A control\ndirectory refuses `USAGE_INVALID` too, before anything is written: a root's `.cotal` or a path in it,\nother than that root's own `.cotal/nats`, and the machine home's `.cotal`. A store there would mix\nwith credentials and records, or with another root's store, and `reset store` could not remove it.\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.\nThe journal records the absolute root it was written in, and the stack runs only from that folder.\nIn a copy of a controller project, or after the folder is moved, commands that read the journal\nrefuse `CONFLICT`, exit 2, and name the recorded folder: a copy is told to use the stack from the\noriginal, and a moved folder to move back there first. `space create` in a copy whose records name\nanother root says so and names that root.\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. A journal whose stack never launched a\nmember pins neither, as when its first `up` was refused because another broker already answers at\nthe address. The next `up` plans it as a first start, with that address or another `--server`, and\n`down` reports that nothing runs there. That refusal names the address and any space this machine\nregistered there. Its next step for a first start is another `--server` or stopping that space. For a\nstack that ran, it is stopping that space and running bare `cotal up`, because the stack keeps its\naddress. 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. Its `down` drops the registration that kept the broker address, so with no\n`--server` or `--host` it starts at the address in the broker configuration its last start rendered\nthere, and in open mode when that start was open and no mode is asked for, and says so in one line.\nAfter `clean all` that configuration is gone. A root deployed with `up -f` then keeps only its fresh\nteam summary, and plain `cotal up` refuses `CONFLICT` and names `cotal up -f <manifest>` when the\nsummary names another address, mode or space. `space create` in such a folder refuses `CONFLICT`\nand 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`. A `down` that exits while it stops a member, because it was killed or its terminal\nclosed, leaves that member `stopping`. When that member is the manager and its request was not yet\npublished, the manager was handed nothing: the next bare `up` or `down` returns the record to what\nits process shows, and the next stop releases the reservation that `down` took once its pid has\nexited. When it is the broker or the delivery daemon, whose stop is a signal to its recorded\nprocess, the refusal names the same `--abandon-stop` command once that process is gone. That\ncommand records the member `failed` and stops the rest of the stack with a `STOP_ABANDONED`\nwarning. Whether the stop caused the exit is not confirmed. 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 stack definition saved for another folder under the\nsame space name refuses `CONFLICT`, naming that folder and every stack definition saved for it;\n`space forget` removes them once that folder's stack is stopped. 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 --space <space>`, or `cotal up` to add the manager after `--no-manager`,\nand `cotal down`. The spawn carries `--space` because `up` keeps an existing selection, so a bare\n`cotal spawn` can target another space. When another space stays selected, the receipt says so and\nnames `cotal use <space>`, as a legacy `up` does.\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. `down` drops that record, so a\nlater start without `--store-dir` reads the store its last broker served from the broker config `up`\nwrote. When that store is not the default `.cotal/nats` and still holds JetStream data while the\ndefault store holds none, such as after `backup restore --store-dir`, the start refuses `CONFLICT`\n(exit 2) and names it. Pass `--store-dir <dir>` to reopen it.\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`.\nThis holds on a controller stack too, both for an `up` right after the crash and for one after a\n`down` that recorded the members as already stopped, because that `down` keeps the credential.\nThe wait never writes the lease. A row that is written again while `up` waits has a live holder, so\nthe start is refused and `up` exits nonzero. A row held by any other process is refused at once,\nas before.\n\nA delivery daemon that `up` launched and that exits before it is ready fails the start. The error\nquotes the failure the daemon logged during this launch and names its log,\n`.cotal/delivery.<spaceKey>.log`. It describes a lease held elsewhere only when the daemon logged\nno failure. A daemon start that fails on a root whose maintenance journal holds a restore or a\npreserved cut never suggests `clean store`, because that store holds the restored or preserved\ndata. Check the root with `cotal status` first.\n\nA full restore recreates the delivery daemon's durables from their checkpoints, at their recorded\nstart sequence. The daemon binds such a restored durable when it otherwise matches its own\nconfiguration, so a restored store starts. A durable that differs in any other setting still\nrefuses.\n\n\n`--user-auth --idp <url>` starts the space's auth service alongside the broker: the NATS\nauth callout plus its capability-gated local exchange, and optionally the closed public exchange\nface configured by the three `--exchange-*` flags above. The service is torn down with `cotal down`,\nand a re-run of `cotal up` heals a dead service on a running broker. `up` waits for the service to\nfinish binding: while the daemon it launched (or found running) stays alive, the wait extends past\nthe base 15s up to 60s; a daemon that exits is refused at once with \"exited before becoming ready\",\nand one alive past 60s is refused as \"alive and still starting\" (wedged), naming the pid record and\nthe service log. `--user-auth` and `--open`\ncontradict each other and are refused loudly; a running broker cannot change auth mode\nwithout a `cotal down` first. See [identity & auth](identity-and-auth.md).\n\n`--rotate-sys` renews the two `$SYS` credentials (`membership-observer`, `connection-evictor`).\nThey carry a 30-day expiry and nothing re-signs them in place, because the trust record never\ncarries the system-account seed, so they are renewed by issuing a **new system account** under the\nsame broker operator and minting fresh creds against it. The root keeps that seed in its own secret\nstore, written at space creation and replaced here, so an auth service started with\n`--service-endpoints` can mint the short-lived observer credentials service caller issuance needs. A plain re-`up` does **not** do this: it reuses the\nexisting trust record, and its `$SYS` creds along with it.\n\nThe rotation is safe to run on a real space, with one operational cost. The data account, the account\nsigning key, every agent credential minted from it, and the JetStream store are all untouched; what\ndies is the retired system account, and with it any out-of-band copy of the old `$SYS` creds, on every\nbroker that loads the rotated config. The cost is that **earlier full backups stop being restorable**\n(see below), so this is not a no-consequence operation. It needs the broker to restart on the rewritten\nconfig, so it runs as part of a boot:\n\n```bash\ncotal down\ncotal up --rotate-sys --detach # agents reconnect; nothing is re-provisioned\ncotal doctor auth # both $SYS creds healthy again, 30 days out\n```\n\nA rotation is a stopped, fresh boot, and anything that is not one refuses it, all for the same reason\n(the on-disk material and the broker it runs on must never end up on different generations):\n\n- a live mesh, because the running broker would keep serving the retired account;\n- an open mesh, whether that comes from `--open` or from `broker.auth: false` in a manifest, which\n has no system account at all;\n- `--restore`, because reinstating a trust root and superseding it in one command leaves no way to\n say which authority the mesh came up on;\n- an unfinished restore or resume attempt on this root, including one `cotal up` would recover on\n its own, because those paths can adopt a live listener and return without booting a broker;\n- a root that hosts more than one space, because the system account lives in the shared broker\n record and a rotation would retire every tenant's, while the root holds one `$SYS` cred pair\n pinned to one data account.\n\nTwo things to know before you run it:\n\n- **The retirement is config-load-bound.** Old `$SYS` creds are refused by any broker that loads the\n rotated config. A stale `nats-server` still running the *previous* config in memory would keep\n honouring them, so stop every broker for this root first. `--rotate-sys` refuses if this root's\n mesh is recorded as running, if anything unidentified is answering at the address it was given, or\n if the root's pid file names a live (or unreadable) process. Those are Cotal's own ownership\n records, not a scan of the process table: a `nats-server` you started by hand against this root's\n `server.conf` on some other port writes none of them and will not be seen. Do not run one.\n- **It invalidates earlier full backups.** A full artifact binds to the trust chain it was taken\n against, and that commitment covers the operator JWT and the system account. Every full backup\n taken before a rotation refuses to restore afterwards, so take a fresh `cotal backup` once the\n rotated mesh is up. `cotal up --restore` names this case when the data account still matches.\n\nThe commit is not atomic (a trust-record write plus two credential writes), so an interrupted\nrotation leaves the record ahead of the creds. That split is detected rather than silent: every\n`cotal up` on an auth mesh, and every `cotal doctor auth`, compares each `$SYS` cred's issuer against\nthe persisted record and names the retired account. `up` warns rather than refusing, because these\ncreds power the membership graph and live eviction, both of which degrade fail-soft; the mesh is not\nworth taking down over them. Re-running the rotation heals it, at the cost of one generation.\n\nWhile those creds are expired the mesh keeps delivering messages, but the\n[membership feed](delivery-daemon.md) and live connection eviction stay down; `cotal doctor auth`\nand the manager's log both name the credential and this repair.\n\n## down\n\nEvery `down` runs to its end when the reader of its output goes away, as `| head` does. It\nkeeps stopping, writes nothing more, and exits with the stop's own status.\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. When the\ndelivery daemon was already gone, its credential stays: that daemon never released its lease, and\nthe next `up` uses the credential to recognise the row and wait it out before it mints a new one.\nThe JSON `cleanup` field then reads `registration-removed-delivery-credential-kept`. Saved\ndefinitions and the journal remain for the next `up`, and the space stays prepared at its root. A\nselection that names it stays, so the next command run without `--space` refuses with\n`TARGET_UNAVAILABLE`, exit 2, and names the root to start it in, rather than reach another space.\nNo 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` stops only the stack of the folder it runs in. In a folder whose root holds no\nstack, when the space named by `--space`, or else the selected space, is a controller stack rooted\nin another folder, `down` refuses with `TARGET_NOT_FOUND`, exit 2, and stops nothing. The refusal\nnames that space and its root, and its next step runs the same `down` from that root.\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. When the stack's last completed\n`down` took `--with-agents`, or the root never started a stack, no agent of it can remain, and\n`--with-agents` reports the same 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 asks for no\nconfirmation, and `--yes` there changes nothing, so `cotal down --yes` stops a stack on any root.\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. The additive ledger refusal names each ledger with the command\nthat finishes it: `cotal down --run <id>` for a readable one, and for an unreadable one the reason it\ncould not be read and the same command once it is repaired. 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.\nThe preserved state records its store by absolute path. If the project folder is moved after\n`down --preserve-state`, `up`, `backup create` and `backup restore` refuse `CONFLICT`, exit 2, name\nthe folder it was moved from, and say to move it back there first. A store missing from its recorded\npath is refused `TARGET_NOT_FOUND` with that path; only `backup restore --accept-missing-source`\nrestores over a lost store.\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>` | the recorded store, else `.cotal/nats` | With `--preserve-state`: the store to cut. The mesh record names the store the broker runs on, such as a `backup restore --store-dir` target, and a different path refuses `CONFLICT` before anything stops |\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`. A bare `down` or\n`down --dry-run` whose `--space` names a space whose stack is recorded in another folder refuses\n`TARGET_NOT_FOUND`, exit 2, naming that folder and the stop or preview to run there. 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. On a root deployed with `up -f`, plain\n `cotal up` then refuses and names `cotal up -f <manifest>` when the deployment ran at another\n address, mode or space.\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). On a\ncontroller root, a member whose process is gone counts as stopped once a completed `down` recorded it\nas already stopped, so `down --yes` after a crash is enough. 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. After an\n`--accept-missing-source` restore there is no old store, so it only retires the journal.\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 none of whose members ever started has\nno store to reset, so both refuse `TARGET_NOT_FOUND` and name `cotal up`. A stack whose last\noperation is not a completed `down` refuses `LIFECYCLE_BLOCKED` and names `cotal down --yes`. Each\nrefusal names one reason and the command that clears it. A space that may still run names\n`cotal down` from its root. A member that exited without a recorded stop, as after a crash, is\nsettled by `cotal down --yes`: it finds the member gone and records it as already stopped, and the\nreset then accepts it. Only a member that no completed `down` recorded that way needs `cotal up` and\nthen `cotal down --yes`. A legacy root has no stack journal, and its `down` drops the registration\na reset selects, so no reset reaches it once stopped. There the step after `cotal down` is\n`cotal clean store --force` from its root for `reset store`, or `cotal clean all --force` for\n`reset space`, with `--store-dir <dir>` when the registration records a custom store.\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 (`--store-dir` defaults to the cut's recorded source store), 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. Any other refusal or failure shows the check's\nown sentence as its native detail, such as a broker still listening at the cut's recorded endpoint.\n\nA `backup create` interrupted by SIGINT, SIGTERM or SIGHUP after it claims the cut exits 130, 143\nor 129 and names its attempt, the partial artifact it left and `cotal backup recover <attempt> --yes`.\nThat claim follows the restore claim's rule below: it holds the cut until its backup process and\nbrokers are proven gone, or for 30 minutes when they cannot be. Until then `up`, another create and\n`backup recover` refuse `LIFECYCLE_BLOCKED` and name the backup process and the deadline, and\n`cotal status` names the attempt and its next step. Once it is stale, `backup recover` removes the\npartial artifact, the clone and the broker files, each matched by inode, and a create to another\ndirectory does the same before it starts, then backs up the cut's recorded store, a custom\n`--store-dir` one included. A create naming the partial artifact refuses `CONFLICT`\nand names that recovery.\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`.\nA directory with no `manifest.json` is refused as an incomplete artifact, the kind an interrupted\n`backup create` leaves, with the fault `manifest-missing`.\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 rolls the attempt back once it is stale: its restore\nprocess and every recorded watchdog and broker are proven dead, and either every broker this root\nstarted is proven gone or the claim's deadline has passed. 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.\nA restore made with `--accept-missing-source` replaced a missing source and retains no old store,\nso its cleanup removes nothing and only retires the journal. A later `down --preserve-state` then\nstarts a new backup cycle.\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.\nA restore's full attempt id has the form `restore-<uuid>`. `backup recover` also takes an\ninterrupted create's `backup-<uuid>`, as described above. `cotal status` shows the attempt a root\nholds under 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. An artifact of another space refuses\n`CONFLICT` (exit 2) and names both spaces; the preserved cut is not changed. A receipt means the\nactive journal was written. Its `seatResume` field says whether retained seats were handed to the\nmanager's resume (`requested`) or dropped by a registry restore (`not-requested`). It does not\nconfirm that any seat joined. `--store-dir` names a store the restore creates, so a path that\nalready exists, or whose parent directory does not, refuses `USAGE_INVALID` (exit 1) before anything\nis staged. 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, and a deadline 30 minutes after the restore\nbegan). While the claim is live, ordinary `up`, a repeated restore and `backup recover` refuse\n`LIFECYCLE_BLOCKED` (exit 2) and name the restore process and the deadline. A command that finds\nthe root's maintenance lock held refuses the same way and names the pid that holds it. The attempt\nis stale once the restore process and every recorded owner are proven dead on this host, and\neither the deadline has passed or every broker the root started is proven gone. A broker's\nwatchdog stops it when the restore process exits, so a restore stopped by Ctrl-C is stale within\nseconds. A retried restore rolls a stale attempt back and continues; an operator can also roll it\nback with `cotal backup recover <full-attempt-id> --yes`. Nothing ever rolls back a live attempt.\nA restore interrupted by SIGINT, SIGTERM or SIGHUP before it commits exits 130, 143 or 129 and\nprints its attempt id and that recovery command. 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`. Without it, the restore refuses `TARGET_NOT_FOUND`\n(exit 2), names the missing path and changes nothing. 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. Before the\ndaemons start, re-entry recreates the memory-only buckets on an adopted restore or resume listener,\nbecause the interrupted `up` may have stopped before it made them. 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. A registry record that cannot be\nread refuses both with `SCHEMA_INVALID`, exit 1, naming its file.\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. `--root` defaults to this project. Outside one it is\nrequired, and leaving it out is a one-line `USAGE_INVALID` refusal whose remedy says what to pass:\nan open space keeps no credentials there, so any folder will do. 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. An open broker answers for any name, so an open space must also\nbe one the broker serves, as its streams show. A name it does not serve is refused with\n`TARGET_NOT_FOUND`, exit 2, naming the spaces it does serve. 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`, `TARGET_NOT_FOUND`, `DEPENDENCY_UNAVAILABLE`,\n`UNSUPPORTED`, `AUTH_REQUIRED` or `PERMISSION_DENIED` (exit 2) for what the checks found. A name that is already\nregistered is a `CONFLICT`, and its remedy offers `--force` only for a registration added by hand.\nA broker that requires auth, with a `--root` that holds another space's trust, is a\n`DEPENDENCY_UNAVAILABLE` that names the space the folder holds.\n`--force` replaces a registration added by hand, never one this machine started or a catalog lists,\nand 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, the persona catalog `.cotal/agents` included, use mode 0700,\neven with a group-writable umask. An unsafe existing path refuses before the\nprepared record is written, without changing its permissions. The refusal names every unsafe path\nin the chain at once, the reason and one remedy such as `chmod g-w <paths>` that lists each path.\nIt starts nothing and leaves the selection as it is. `cotal up` applies the same rule to the root\nit starts in, before it writes anything, so it never starts a stack that `down` would refuse to\nstop from that root. `cotal up` starts a space only under a name of 1 to 200 letters, digits,\nunderscores or hyphens, so any other name refuses with `USAGE_INVALID`, exit 1, before anything is\nwritten. 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. A folder this\naccount cannot write, such as `/`, refuses `PERMISSION_DENIED`, exit 2, with nothing written. `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. A prepared space records its folder by path, and moving the folder leaves nothing in it that\nnames the space. A bare `cotal up` in a folder that names no space, while the selected space is\nprepared in a folder that no longer exists, refuses `TARGET_UNAVAILABLE`, exit 2, rather than start the\ndefault space there. Forget the space with `cotal space forget <name>`, prepare it again in its new\nfolder with `cotal space create <name>`, then run `cotal up`. Until then, `space create` of that name\nsays its recorded folder no longer exists.\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. Forget also\nremoves the per-space preferences saved under that name, so a later space with the same name starts\nwithout them, and the receipt lists each removed key and value so you can set them again. If that\nremoval fails after the record is gone, the run is `partial` and names the `config unset` to finish it.\nForget also removes the stack definitions `cotal up` saved for the space at its root, with their\nstopped instance records, so another folder can start a stack under the same name. The result lists\nthem. While a member was not stopped cleanly, or the root holds preserved state, they stay and the\nresult warns with `STACK_DEFINITIONS_KEPT`. For a member that was not stopped cleanly, the warning\nnames `cotal down --yes` in the root and then `space forget` again.\n`status` in a forgotten space's root then selects no space and probes no broker. When nothing left\nin that root's `.cotal/` names a space, its next step is `cotal space create <name> --root <root>`,\nbecause a bare `cotal up` there would start the default space `main` in that folder. A command run\nthere that finds no broker at the default address says that no space is registered or prepared for\nthe folder and names the same step.\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.\nFor a stopped stack it names `cotal reset store <space> --yes`, which removes the stored streams, and\n`space forget`, which removes only its local record. For a space that never ran it names only\n`space forget`, because no streams were stored.\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\nThe receipt's Next names the route back. On a space this machine started, the broker keeps running\nwithout the streams, and only a broker start creates them. `cotal down --yes` in its root, or\n`cotal down` on a legacy root, then `cotal up` starts it again with empty streams. Running\n`space forget` after that stop drops it instead. Until then a bare `cotal up` there refuses\n`TARGET_NOT_FOUND`, exit 2, saying the broker no longer holds the streams and naming that restart.\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. Inside a project whose root has a\nregistered space other than the selection, the command still targets the selection. Before it acts,\nit warns on stderr and in its JSON `warnings` with `SELECTION_NOT_FOLDER_SPACE`, naming both spaces\nand the `--space` that targets the folder's space.\n\nA prepared space whose project folder was deleted cannot start. A command that targets it, `persona`\nincluded, refuses with `TARGET_UNAVAILABLE`, exit 2, saying the folder no longer exists and naming\n`cotal space forget <space>`. `cotal space create <space> --root <dir>` then prepares it again.\n`use`, `up --space` and `space forget` also say the folder no longer exists.\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\n`cotal space create <space> --root <root>`, which prepares the journal's space there again so that\n`cotal up` starts it. 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`, and names the space and root it concerns under\n`target` when they are known, as for a prepared space that is not started.\n\n`--creds <file>` presents that credential to `--server`. Without `--server`, it goes to the broker\nthe `--space` is registered at, `main` when no space is named, and keeps that registration's TLS\nrequirement. A space not registered here is refused with `TARGET_NOT_FOUND`, exit 2, before\nanything is dialed. The default address is never assumed for a credential.\nThe file is read and checked with the NATS client's own parser before anything is dialed, and the\nrefusal names the file. A missing file is `TARGET_NOT_FOUND`, exit 2. A directory, an empty file and\na file the client cannot parse are `SCHEMA_INVALID`, exit 1, with the cause: no complete JWT block,\nno seed block, an invalid seed, or Windows (CRLF) line endings. A file that other accounts can read\nis still used, with a warning on stderr that gives the `chmod 600` command. `join --creds` reads its\nfile the same way.\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. A fresh summary that names another folder, as one does after its folder was moved or\ncopied, is listed with its recorded path and is not counted as unreadable. 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 or a member's\nprocess is gone, Next offers `cotal up --root <root>` first. 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 and continue. They warn once when an account's catalog starts failing and stay quiet while\nit keeps failing; the next failure after a successful refresh warns again. `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, where the command warns that it targets the selection. `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`, and `null` on an open mesh with no delivery lease), `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\n`deliveryResponder` is `stale` when the ready row in the delivery lease belongs to a daemon that is\ngone. That includes this folder's own daemon once its recorded process has exited without releasing\nthe row, as after a kill. The row expires on its own within 30 seconds.\n\nAn unreachable broker, a stopped process or an unbound responder is a state the report names, and\n`status` exits 0. An open mesh runs no delivery daemon, so when it records no delivery lease its\ndelivery responder row says the responder is not used and suggests no command. 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`. A broker that takes the connection and does not answer, as a\nstopped one does, leaves it unchecked too, and `status` still exits.\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). An optional component that is not installed prints `not-installed`, which exits `0`.\nThat exit replaces the report's own, so a partial report followed by a clean pass exits 0:\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**: `not-installed` when the dashboard extension is not installed and no PID record exists.\n Otherwise, the local PID record, then the `/api/meta` response at the address the dashboard\n recorded 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 broker that takes the connection and does not answer, as a\nstopped one does, refuses the manager and delivery probes, and the pass still exits. 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. When the space's service registry lists no live manager, an\nunpinned operator `models` refuses `TARGET_UNAVAILABLE` (exit 2) at once, as `ps` does, and names\nbare `cotal up` as the next step. 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. The Participant column holds the name\nthat `endpoint describe` takes, and the role has its own column.\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. In a user-auth\nspace, `--all` shows each of your managers as a `manager/manager` row whose id ends in\n`manager_<instanceId>`, the id `--on` takes, with its host and its runtime in the activity, such as\n`supervisor (tmux)`.\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.\nA mesh where nobody is present is a complete roster: `who` says so and exits 0.\n\n`who` watches the roster and the channel registry on every mesh so it can report those states. On a\nstatic-auth or user-auth mesh the credential `who` connects with may not delete its watch consumers,\nso each one stays on the broker for five minutes after `who` exits. A script that runs `who` in a\ntight loop there can reach the broker's limit of 1000 consumers per stream.\n\n## whoami\n\n```bash\ncotal whoami [--json | --plain] [--space <s>] [--server <url>]\n```\n\nReports the identity this CLI acts under in the selected space and what it may read, post and do.\nIt reads the mesh registry, the login cache and, when it is on this machine, the user-auth grant\nledger. It does not refresh the remote space catalog or contact the IdP. A one-shot command has no\nsession, so there is no unread count or connection state to report.\n\n| Mesh | `identity.kind` | What it may do |\n|---|---|---|\n| Open | `open` | Everything; the broker authenticates nothing |\n| Static auth, signer on this machine | `space-signer` | Everything; each command mints its own short-lived credential for its task, so there is no standing principal |\n| Static auth, no signer here | `no-credential` | Nothing without `--creds <file>` |\n| User auth | `user` | The signed-in user's `cli` actor: `read`, `post` and `capabilities` are its ledger row |\n\nA user who is not signed in has no principal. A signed-in user with no ledger row (`grant:\n\"not-granted\"`) may read and post nothing. When the grant ledger is on another machine the grant\ncannot be read here: `read`, `post` and `capabilities` are `null`, the run is `partial` (exit 4) and\nthe `GRANT_NOT_LOCAL` warning says so. A remote registration can hold a cached login without the\nsecret that derives its owner and principal. Those fields stay `null`; the report calls the\nprincipal unknown on this machine, rather than calling the user signed out. The cached expiry is\nadvisory and the command does not verify the login with the IdP. `dms` says whether the identity\nmay read DM history: on an open mesh, or with the space signer's audit credential on an authenticated mesh. The report\nreads local configuration and does not contact the broker to check its authentication requirements.\nAn unregistered `--space` is refused even with `--server`. Server addresses in the identity report\nand connection refusals omit URL user information, query strings and fragments, and escape terminal\ncontrols. Addresses need a host and a supported broker scheme. TCP addresses cannot have a path;\nWebSocket routes and bare host addresses remain supported. Invalid addresses make `whoami` refuse\nrather than print the raw value. Recovery instructions use a `<url>` placeholder rather than a\nsecret-stripped connection address.\n\nBoth reads refuse an unusable local mesh registry with `SCHEMA_INVALID` (exit 1) before\nconnecting. The diagnostic names the file without repeating its stored field values.\n\n`endpoint invoke <target> <command> --cast` uses the native one-way verb. Only an ephemeral\ncommand can be cast; journaled commands and `--wait` combinations refuse before publication.\nThe receipt records the original request, resolved binding and broker flush, not a responder or\nconfirmed execution. A class cast is not retried or repaired if another instance refuses its\nbinding. A broker publish denial remains a refusal. A lost publication outcome remains unknown.\n\n## service-endpoint\n\n```bash\ncotal service-endpoint approve <proposalId> [--caller-ceiling a,b | --request-id <hex32>] [--space <s>] [--json | --plain]\ncotal service-endpoint refuse <proposalId> [--request-id <hex32>] [--space <s>] [--json | --plain]\ncotal service-endpoint proposal <proposalId> | operation <operationId> | name <endpoint> [--space <s>]\ncotal service-endpoint name-bind <endpoint> (--owner <u_\u2026> --claim <ref> | --request-id <hex32>) [--space <s>]\n```\n\nDecides and inspects service endpoint registrations on a user-auth space whose auth service runs\nwith `--service-endpoints`. Every subcommand is one request to the service's\n`/service-endpoint-host` route as the signed-in user: the route verifies your IdP token and derives\nthe granter itself, so no command names one. `approve` and `refuse` are the owner's: the command\nreads the proposal and its name first and decides at those revisions, so a proposal or name that\nmoved in between is a conflict the host reports, never a decision on something else. `proposal`,\n`operation` and `name` read as the owner: another owner's record reads absent. `name-bind` is the\noperator's: it needs the auth service on this machine and runs on its loopback face under the\nexchange capability, with your signed-in subject as the audit id. The host verifies no domain\ncontrol, so `--claim` is your own reference for the namespace you checked. The answer is the host's\nown. A decision or a binding is recorded under its request id at\n`.cotal/auth/space.<key>/service-endpoint-requests/<id>.json` in this root before it is sent, and\nthe command prints that id, or names the exact resend command in the failure when no answer came\nback. A run interrupted before any answer prints nothing: the record's file name is the id.\nRun the same subcommand and subject again with `--request-id <hex32>` to send the recorded request as it was,\nwhich the host replays with the same approval or binding; a fresh read would carry the revisions\nthe first attempt moved, so the replay never rebuilds the request, and it takes no\n`--caller-ceiling`, `--owner` or `--claim`. An id this root never recorded, or one recorded for a\ndifferent command or subject, is refused with nothing sent. A run without `--request-id` is a new\nrequest, and a proposal already being approved answers `approval-in-progress`.\n\n## Handle inspection\n\n```bash\ncotal auth handle inspect <private-file> [--space <space>] [--json|--plain]\n```\n\nThis reads one protected native handle artifact or a bounded array of artifacts once. Without\n`--space`, it shows unverified public scope, holder and declared expiry claims. Signatures,\nissuer authority, parent links, holder currency and revocation remain not checked. No broker\nis contacted or credential issued. Signature bytes and original signed artifacts are never printed.\nThe human report shows at most 50 scope rows and names omissions; JSON retains public metadata.\n\nAn explicit `--space` selects online inspection on a registered static-auth space whose root\nholds matching native operator authority. One ephemeral provisioner reader is minted without\npersisting it. Its broker account and identity must match before leader-served signer and\nissuer-namespaced revocation reads. The inspector checks signatures, issuer scope, presented\nparent digests and containment. Every required sturdy-handle status must be explicitly unrevoked;\nmissing, malformed, unreadable or stale spec/status pairs fail closed. Reads share one bounded\nchain budget. No records are created or repaired, and no alternate signer or broker is tried.\n\nThese non-atomic observations are signature inspection, not a usable presentation context.\nDeclared expiry, presenter identity, holder/process currency and contract-dependent use authority\nremain separate. No holder credential is conferred and no handle is redeemed. Open and user-auth\nspaces refuse this static reader rather than borrowing an operator identity. Reader-close\nuncertainty is reported separately.\n\n## Account signer export\n\n```bash\ncotal auth signer --out <private-file> [--space <space>] [--force] [--json|--plain]\n```\n\nThis exports the selected space's existing data-account signer through the native stripped-signer\nformat. It does not create a caller, rotate a key, start a service or export the operator root.\nA cached login is not signing authority. The selected root must hold validated static trust;\nconflicting current-folder and selected-root account authorities refuse before publication.\n\n`--out` is required. Its parent must exist and pass the protected physical-directory checks.\nManaged `.cotal` and workstation state are not valid explicit export destinations. The file is private, and secret contents\nnever appear on stdout, in the receipt or in diagnostics. Keep this account-wide signer out of\nlogs and support bundles; it is not a least-privilege endpoint credential.\n\nAn existing path refuses by default. `--force` first inspects an owned private regular file and\naccepts only the stripped format for the same space and account. It refuses full trust bundles,\nother accounts, symlinks and concurrent changes. An identical export is a no-op. New files use\nnative create-only private publication; replacement uses the inspected snapshot under a writer\nlock. An unconfirmed publication remains unknown and is not retried. Platforms without the\nprotected file policy refuse rather than substituting a weaker writer.\n\n## Endpoint authoring\n\n```bash\ncotal endpoint init reviews [--dir <directory>] [--json|--plain]\ncotal endpoint check <contract-file> [--json|--plain]\n```\n\n`init` creates `contract.json` and an executable `handler.mjs` template. The default directory is\nthe supplied name; `--dir` chooses another one. Its parent must exist. Files are created\nexclusively, without following a pre-planted symlink or replacing an existing file. A partial\nwrite retains the named created material for review rather than deleting it automatically.\n\nA simple scaffold name such as `reviews` declares `com.example.reviews`. A full reverse-DNS name\nis kept. Choose a namespace you control before requesting serving authority. Scaffolding creates\nno service definition, reference, credential, registration or process. The handler accepts one\nJSON object with `command` and `args` on stdin and writes one JSON result on stdout.\n\nThe version-1 local contract file has `schemaVersion`, `endpoint` and `cluster`. The cluster has\nnative `urn`, `revision`, `attributes`, `commands` and `events` fields. Each command carries its\nnative declaration plus `input` and `output` schema bundles, each `{root, members?}`. The installed\nnative compiler derives their closure digests; authors do not hand-maintain a second digest next\nto an edited schema. `members` maps native digest references to supplied JSON documents. Unknown\nwrapper or command fields are refused. This file is local authoring input, not a new wire kind.\n\n`check` reads at most 1 MiB, validates the native cluster declaration, compiles every input/output\nwith the installed schema profile and reports the derived cluster closure. All schema references\nmust resolve inside their bundle; no URL or adjacent file is fetched. Attribute and event\nsemantics remain opaque to the native parser, so a file declaring them produces a partial result,\nexit 4, rather than a claim that those semantics were checked.\n\nNeither command contacts a broker, loads the handler, starts a service or proves serving\npermission. Authoring takes no space, broker, caller or invocation flags.\n\nServe the edited contract through the independent service controller:\n\n```bash\ncotal service add reviews --preset exec-endpoint --space <s> --root <absolute-root> --contract <absolute-contract.json> --handler <absolute-node-executable> --handler-args '[\"<absolute-handler.mjs>\"]'\ncotal service start reviews --space <s>\ncotal service status reviews --space <s>\ncotal service logs reviews --space <s>\ncotal service stop reviews --space <s>\n```\n\nThis preset supports Linux and an existing registered static-auth root. The executable is an\nabsolute protected regular file, with argv supplied separately. A symlink on the root, contract or\nexecutable path refuses, because a link could be repointed after the check, and the refusal names\nthe physical path to use instead, such as `/usr/bin/python3.14` for `/usr/bin/python3`. 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. A stack manager whose\nprocess died stays registered, so `ps` waits out the manager's answer deadline first, then refuses\nthe same way and names bare `cotal up`, which starts it again.\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\neach one on its own row with its instance id, host, activity and the time its process started, and\nits Next gives your command again with `--on <instanceId>` for each. The code stays\n`PERMISSION_DENIED` and the exit 2. `who --all` lists the same managers at any time. 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,\nor a user-auth service that does not answer, keeps 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: a `--server` that no registration names reads the project in this\nfolder, as launch does, and outside one it refuses with `TARGET_NOT_FOUND` and writes nothing. With\nno space selected and no project here, the refusal says so. Remote `--on` operations refuse as\nunsupported here.\n\nA persona name uses letters, digits, `_` and `-`, up to 128 characters, and a name refusal says\nwhich rule it broke.\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. A file that names another persona is\nrefused with `SCHEMA_INVALID`, naming the file's own name and the `persona create` that keeps it.\nThey 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`. Both refuse control\ncharacters with `USAGE_INVALID`. `--harness` must name a harness this machine knows, as\n`config set harness` requires, or creation refuses with `USAGE_INVALID` and writes nothing.\nCreation refuses an existing definition. Mutation receipts contain names and paths, not bodies.\nShow and edit accept a definition whose `name` is not its catalog name. The seeded `default`\npersona is one: its file is `default.md` and its agents launch as `default_agent`.\n\nEditing opens a private draft using the editor preference (`VISUAL`, then `EDITOR`, then saved\npreferences). The value runs as a shell command, so quote a path that holds spaces. With no editor\nset, or one the shell cannot find or run, editing refuses with `DEPENDENCY_UNAVAILABLE` (exit 2)\nand names the setting to fix. A successful editor exit and valid definition are required before\nsaving. A changed\nor removed source refuses with `CONFLICT` (exit 2), leaving the current definition intact. The\nrefusal says which one happened and keeps your edited draft in its private folder, naming it, so\nthe edit is not lost. 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.\nCreate, edit and remove refuse a catalog folder you cannot write, such as one set to mode 0500,\nwith `PERMISSION_DENIED`, and leave it and its files as they were.\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. Its `list` escapes control characters in the names, fields and\nbodies it prints, as `persona show` does. Its `show` does the same on a terminal, and writes the\nfile byte for byte when its output is piped or redirected. Only its explicit `--running` overlay connects to the mesh.\n\n## supervise\n\n```bash\ncotal supervise [--execution runtime|none] [--runtime <name>] [--space <s>] [--server <url>] [--spawn <names>]\n```\n\n`supervise` is a deprecated entry kept for existing units, scripts and containers, and help and\ncompletion omit it. It accepts the same flags and prints a warning on every run naming the\n`manager` service preset and the release that removes it; see\n[deprecated daemon entries](UPGRADING.md#deprecated-daemon-entries). The manager that `cotal up`\nstarts runs the same code through a hidden entry and prints no warning.\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space to supervise. On a root that hosts several spaces, name one of them here |\n| `--server <url>` | hosting mesh, or matching registered mesh | Broker URL. A registered mesh supplies it when omitted; a different explicit value is refused before anything is dialed. |\n| `--runtime <name>` | `pty` | Agent runtime (`pty` built in; extension runtimes are explicit-only) |\n| `--execution runtime|none` | `runtime` | NONE is a genuine hosting user-auth control instance with no Runtime, connector inspection, workload or terminal execution |\n| `--console-port <n>` | none | Protocol-console port |\n| `--console-host <host>` | loopback | Bind host for the console endpoint. Loopback keeps it machine-local; `cotal up` passes the address it bound the broker to, which is what lets the browser console reach this manager from another machine. `cotal attach` does not use this face: it redeems a mesh session grant over the broker |\n| `--max-sessions <n>` | 64 | Live-session ceiling. Each console pane and each `cotal attach` is one session, so size for agents \xD7 panes, not agent count. A capacity refusal names this flag. `cotal up --max-sessions` records the same number on the mesh so a later `supervise` started by repair keeps it |\n| `--roster <file>` | none | Declarative roster to boot at startup. See [Roster files](define-a-team.md#roster-files) |\n| `--launch <spec>` | none | Resolved manifest launch spec (from `up -f` / `team boot`) |\n| `--spawn <names>` | none | Comma-separated personas to pre-spawn at startup |\n\n`--ws-port`, `--resume-attempt` and `--resume-commit-token` are passed by `cotal up` when it starts\nthe manager, for the console session client and for resuming preserved seats. They are not for\ndirect use.\n\nThe manager is the agent supervisor and control plane: it answers `spawn --detach`, `stop`, `ps`,\n`attach`, and the `cotal_*` manager tools. `cotal up --detach` starts one for you; run `supervise`\ndirectly to recover a dead manager or drive a custom runtime. Default runtime is `pty`; install an\noptional provider first (`cotal ext add @cotal-ai/orca`, `@cotal-ai/tmux`, `@cotal-ai/cmux`, or `@cotal-ai/herdr`) and\nselect it explicitly. A missing provider or app fails loudly; there is no fallback. See [Deploy](deploy.md).\nBoot inventory decides whether this process takes unpinned `spawn`/`launch` on the class rail:\nif every declared connector is unavailable, those commands stay on this instance rail only\n(`status` reports `classSpawn: false`). `describe` still answers on the class rail, so an\nunpinned spawn can bind-fence against a skip member; re-issue, or pin `--on`. A partial\ninventory keeps the class rail and names `--on` on a harness refusal, because sibling\ninventories are not readable from the serve credential. See [control surface](control-surface.md#instance-routing).\n\nUse `--execution none` only for a fresh control instance in an existing hosting user-auth root.\nIt requires that space's signing trust, provider pin and user-mode mesh record, without remote\nmanager authority. It rejects `--runtime`, startup workload flags and participant registrations\nbefore resolving a Runtime or connector. The mode needs no connector seeding. An existing\nunclassified identity or retained workload refuses admission; a recorded NONE instance cannot\nrestart as runtime. Do not delete identity or capability files to change modes.\n\nNONE retains native registration, status and the selected Manager's empty managed-seat ps.\nIt does not adopt foreground agents, roster peers or HTTP-hosted sessions. Workload operations\nstill need an explicitly selected execution-owning participant Manager and its existing authority.\nStarting a control instance does not enable an auth service's interactive local-primary policy.\nNONE also refuses model inspection and caller-driven persona, purge, transcript, preservation,\nresume, cancellation, turn-yield and self-stop writes. It starts no transcript receiver or sweep.\nIts native registration, lease, renewal, session-ledger and goal maintenance can still write.\n\nLocal runtime startup now records identity and execution mode during admission. A later failed\nauthentication or registration can leave those files classified as runtime. Invalid supervise\narguments refuse before connector seeding, with one error line rather than seeding first.\n\nOn a normal `SIGINT`/`SIGTERM`, the manager stops every seat and requires the selected runtime to\nprove the seat is gone before it releases the manager lease or service registration. A stop that\ncannot prove exit fails loud and keeps manager authority instead of reporting a clean shutdown while\nan orphan still holds broker rails. When the runtime refuses the stop itself, the shutdown fails at\nonce and its error names the refusal as `stop failed: <message>`. After an abrupt manager death, the same logical successor\nterminalizes only its own durable static slots, verify-evicts the predecessor's broker principal,\nrecords that result in the lifecycle's caller-readable audit detail, reaps the predecessor's seat\nprocess through the runtime's custody reference recorded on the slot (the pty runtime verifies the\nprocess start identity in its seat record, so a reused pid is never signalled), and only then\nretires the lifecycle and frees the alias. A seat whose spawn carried `supervise` with restarts left\nis restarted instead of retired: the successor reaps any seat the slot still names, relaunches it\nunder the same name, lifecycle uid, credential, persona and cwd, and counts the restart against the\npolicy. A seat on a runtime that can adopt a live seat (tmux) and still running when the successor\nstarts is adopted in place: the successor checks that the recorded tmux server, window and pane are\nthe ones the slot names and that the pane's process still runs, then manages that seat again under\nits existing credential, without a restart. Both paths read the record the manager writes to\n`.cotal/seat-records/<space>/<name>.json` once a supervised or adoptable seat is ready, rewrites at\nevery restart and credential renewal, and removes when the seat is freed. A spent budget, a missing\nrecord, an expired credential, a persona file or credential that no longer matches the record, or\nan adoptable seat whose pane has exited retires the lifecycle as above. A runtime that custodies its seats reserves that\nreference before it launches one, and the manager records it on the slot's first durable row, so a\nmanager that dies part-way through a spawn also leaves a seat its successor can address. A\nsame-lifecycle restart or a resume records the new seat's reference on the slot the same way, and\nwhen the slot does not take it the restart or resume fails and stops any seat it started, so the\nslot never names a seat that has already exited while its replacement runs. A resumed seat keeps\nits retained credentials, so the resume frees it only once its exit is proved; a seat whose stop\ncannot be proved stays managed, and the resume's error says so, naming a stop the runtime refused\nas `stop failed: <message>`. A spawn\nthat launched its seat and then failed is rolled back by the manager that launched it, and that\nrollback reaps the seat through the same reserved reference before the lifecycle retires. Missing or unverified broker evidence keeps the slot\nterminalizing, and so does a runtime that cannot reap by reference. While the name stays held, a\nsame-name spawn or a resume re-drives that teardown with the same recorded reference, so it reaps\nthe seat again or keeps the name held.\n\nA `meshes add --mode user` entry is a **participant** registration, not hosting authority. A\nparticipant may run `supervise` only when the host advertises the remote manager authority service\nand the signed-in actor has the dedicated `supervise` ledger scope. The CLI obtains the closed,\nloopback-only `manager-service` view; `spawn` and `admin` do not substitute for that scope. The\nhost issues the manager's public-nkey JWT material through its lifecycle-bound prepare \u2192 activate\n\u2192 renew protocol, never by handing the participant a signer or static provisioner credential.\nThe host also performs instance-scoped eviction and guarded gate reconciliation. A remote manager\nrefreshes its short-lived registration executor before clean deregistration, so a long-running\nprocess removes its service row on `SIGINT` or `SIGTERM`. After an unclean stop, the same instance\nverify-evicts its superseded family and advances the process epoch. If an abandoned frozen gate\nholds the manager governance slot, a different supervise-scoped manager asks the host to reconcile\nthat holder after a complete gone verdict, then retries its registration once.\n\nA remote supervise never uses local signing trust: with host-issued authority in hand, the\nmanager mints from that authority alone and consults local records only to refuse a conflict,\nnamely the supervised space's own trust records under the cwd root. A root that hosts another\nstatic space beside the sign-in is a normal configuration and is never read as this space's\ntrust.\n\nThe broker URL in the registry entry decides the transport. A remote broker is often published\nover a `wss://` edge rather than a raw `nats://` port, and `supervise` dials whichever scheme the\nrecord holds, starting with the manager-authority registration it runs before the manager exists.\nThe record also decides whether that registration requires TLS, so a participant never downgrades\nthe credential exchange to a plaintext connection the registry did not describe.\n\nWithout that advertised host service or scope, `supervise` refuses before it starts a manager.\nRun `cotal spawn` without `--detach` to launch a foreground agent, or ask the space host to enable\nthe authority service and grant `supervise` for detached agents. If a running remote manager loses\nrenewal, it reports degraded state and refuses unsafe new starts and restarts; live agents are not\nsilently replaced. Do not run `cotal down` or `cotal up` on a participant machine to repair this\ncondition.\n\n## daemon\n\n```bash\ncotal daemon serve [--daemon-home <dir>] [--json|--plain]\ncotal daemon status [--daemon-home <dir>] [--json|--plain]\ncotal daemon login --idp <auth base URL> [--client-id <id>] [--daemon-home <dir>] [--json|--plain]\ncotal daemon connect-check --space <s> [--daemon-home <dir>] [--json|--plain]\ncotal daemon register --space <s> [--daemon-home <dir>] [--json|--plain]\ncotal daemon service-connect --space <s> [--daemon-home <dir>] [--json|--plain]\ncotal daemon service-disconnect --space <s> [--daemon-home <dir>] [--json|--plain]\ncotal daemon service-status --space <s> [--daemon-home <dir>] [--json|--plain]\n```\n\nThis release serves a local endpoint for status, sign-in, a connection check, a registration request\nand the daemon's own service connection. It does not start a Manager, serve any command on its\nservice, connect to Cloud, or install a system service. It supports Linux only. On other\nplatforms, an unset `XDG_RUNTIME_DIR` produces\n`TARGET_UNAVAILABLE` (`NO_RUNTIME_DIR`); with it set, the platform check refuses `UNSUPPORTED`.\n\n`serve` runs in the foreground and prints `daemon ready at <socket>` once the endpoint is bound\nand checked. That success line is plain text; `--json` shapes its refusals. `status` prints the\nstate, version, protocol, process ID, start time and uptime. `state: \"ready\"` means only that the\nlocal control listener is bound and verified, not that a mesh or Cloud connection exists.\n\nThe endpoint is `$XDG_RUNTIME_DIR/cotal/daemon.sock`. Without `XDG_RUNTIME_DIR` both forms refuse\n`TARGET_UNAVAILABLE` (`NO_RUNTIME_DIR`). Both refuse `PERMISSION_DENIED` for a runtime directory\nthat is a symlink, not a directory, not owned by you, or not mode `0700`. `status` also refuses\n`PERMISSION_DENIED` for an endpoint entry that is not a socket, not yours, or not mode `0600`.\nThe daemon drops connections from other users. The kernel separates users, not processes of one\nuser: any process running as you can use the local controls, including sign-in and cancellation.\n\n`status` with no daemon running refuses `TARGET_UNAVAILABLE` (`NOT_RUNNING`). `serve` refuses\n`CONFLICT` (`ALREADY_RUNNING`) for any existing socket, including one left after a crash, and\n`PERMISSION_DENIED` (`SOCKET_NOT_SOCKET`) for a non-socket entry. It removes neither.\nSIGTERM and SIGINT shut down cleanly and remove only the daemon's own socket. SIGKILL or SIGHUP,\nincluding closing the terminal, leaves a stale socket. To recover, run `cotal daemon status`.\nIf it reports `NOT_RUNNING`, confirm no daemon process is running, then remove the named socket\npath by hand and start again.\n\n`--daemon-home <dir>` runs or reaches a separate daemon whose own state lives in `<dir>`: its\nsign-in session, its per-space identity, register request and service store, all at the paths\nbelow with `<dir>` in place of `<COTAL_HOME>`. Spaces are still read from the shared Cotal home's\nmesh registry, which this daemon never writes. Its socket is\n`$XDG_RUNTIME_DIR/cotal/home-<16 hex>/daemon.sock`, derived from the directory, so it runs beside\nthe default daemon. `<dir>` must already exist as an absolute directory you own with mode `0700`,\nand cannot be the Cotal home. A relative path or the Cotal home refuses `USAGE_INVALID`, a missing\ndirectory `TARGET_NOT_FOUND`, and a symlinked, foreign or loose one `PERMISSION_DENIED`. Nothing is\ncreated or repaired.\n\n`serve` holds the lifetime lock `<dir>/daemon.lock` while it runs. A second `serve` for the same\ndirectory refuses `CONFLICT` (`LOCK_HELD`), naming the live holder. A start that fails for any other\nreason releases the lock again. A killed daemon leaves its socket behind, which still refuses\n`CONFLICT` (`ALREADY_RUNNING`) and is never removed, so recover as above. Every client command with\n`--daemon-home` sends nothing unless the process at that socket is the lock's live owner (same pid\nand process start). Otherwise it refuses `PERMISSION_DENIED` (`PEER_NOT_LOCK_OWNER`).\n\nThis daemon is a different machine identity. Nothing is copied from the default daemon: it needs\nits own `login`, its own `register`, and the space owner's approval of its own instance. Without\n`--daemon-home` every command behaves as before.\n\n`login` signs the running daemon in with device approval. The daemon runs the same sign-in as\n`cotal login` and keeps the session in the machine's Cotal home, where `cotal login` keeps it. The\ncommand prints only the approval link, code and expiry to stderr, then the signed-in account. No\nsession token crosses the endpoint or reaches the terminal. `--idp` takes an HTTPS auth URL, or an\nHTTP loopback IP URL; `--client-id` defaults to `cotal-cli`.\n\nOne sign-in runs at a time: a second `login` refuses `CONFLICT` (`LOGIN_IN_PROGRESS`) without\ncontacting the provider. Ctrl-C, or the command's connection closing, requests cancellation on this\nmachine. If cancellation takes effect before persistence, nothing is saved, even if device approval\nalready happened. An already-completed attempt remains completed; cancellation revokes nothing\nat the provider. A cancelled attempt reports `USAGE_INVALID` (`CANCELLED`). A denied or expired\napproval refuses `PERMISSION_DENIED` (`DENIED` or `EXPIRED`), and an unreachable provider refuses\n`DEPENDENCY_UNAVAILABLE`\n(`UNREACHABLE`); none of these saves a session. Any other provider answer fails `OPERATION_FAILED`\n(`REFUSED`). Such a failure can come after the session was saved, so run `login` again. A failure\nis reported with a fixed sentence; the provider's own error text is never relayed. `status`\nkeeps answering while a sign-in waits.\n\n`connect-check` asks the running daemon whether it could register with a space as its own service.\nThe daemon is not a Manager and never borrows a Manager's identity or authority. The check reads\nthe space's registration on this machine and the daemon's sign-in, then the space's public account\nidentity from its auth service (`GET <exchange>/service-endpoint-authority`, no redirects). It sends\nno registration and holds no credential. A space whose auth service does not host service endpoints\nrefuses `UNSUPPORTED` (`UNSUPPORTED_BY_SPACE`). The daemon does not fall back to anything else.\n\nThe other refusals are `TARGET_NOT_FOUND` (`NOT_REGISTERED`) for a space this machine has not\nregistered, `UNSUPPORTED` (`NOT_USER_AUTH`) for a space without user sign-in, `AUTH_REQUIRED`\n(`NOT_SIGNED_IN`) when the machine is not signed in to the space's provider, and\n`DEPENDENCY_UNAVAILABLE` (`UNREACHABLE` or `REFUSED`) when the auth service cannot be reached or\nanswers with something unusable.\n\nOn success it reports `ready-to-register` with the space's account, the daemon's service name\n`ai.cotal.machine` and the daemon's own instance ID. Nothing has been registered. The space owner must\nstill approve the daemon, and the service name needs the space operator's binding. The first\nsuccess creates the daemon's identity for that space at\n`<COTAL_HOME>/daemon/<space segment>/service-identity.json` (mode `0600`). It holds the instance ID\nand the account, and no credential. Later checks reuse it. If the space later answers with a\ndifferent account, the check refuses `CONFLICT` (`ACCOUNT_MISMATCH`) and changes nothing: confirm\nthe space was not replaced before you remove that file by hand.\n\n`register` asks the running daemon to request its own service registration with a space. It first\nruns `connect-check` and returns any outcome other than `ready-to-register` unchanged, without\ncontacting the space again. Otherwise it sends one registration request for the daemon's instance\nwith this machine's sign-in, through the space's stock service authority. The daemon never sees or\nrelays that sign-in proof. The request ID is saved before the first send, at\n`<COTAL_HOME>/daemon/<space segment>/register-request.json`, and every later `register`, including\nafter a restart, replays the same request unchanged. The stock writer does not guarantee durability\nacross power loss.\n\nThe fixed service name binds to one owner per space. Different owners in a shared space need distinct\noperator-approved endpoint names; this command does not yet select one. The operator must verify or\ndelegate the namespace before binding it.\n\nThe space answers in one of three ways, each reported with exit 0:\n\n- `approval-required`: the space owner must approve this daemon and the space operator must bind its\n service name `ai.cotal.machine`.\n- `pending`: the space is processing an approved registration.\n- `registered`: the registration is recorded. It is not activated or serving, so the machine is\n still not connected.\n\nNo release ships a command or page that lets an owner approve the daemon or an operator bind its\nname. A refusal by the space is `PERMISSION_DENIED` with the space's reason as the native code, such\nas `PROPOSAL_REFUSED`. A reason the daemon does not recognize is reported as `UNRECOGNIZED`, and the\nspace's own text is never shown. A new proposal after a refusal needs a new request, which `register`\ndoes not send. A sign-in the provider rejects (expired or revoked) is `AUTH_REQUIRED`: run\n`cotal daemon login` again. A failure before anything was sent is `DEPENDENCY_UNAVAILABLE`. Only a\nrequest whose outcome is unconfirmed is `OPERATION_UNKNOWN`: run the same command again\nto replay it. A changed registration for the same instance is refused `CONFLICT` and nothing is sent.\n\n`service-connect` connects the running daemon to the space as its own registered service. It replays\nthe saved register request unchanged to read its registration, gets its service credential from the\nspace's auth provider, verifies the bearer signature and witness against the pinned authority's\nissuer keys, checks the serving snapshot against its embedded contract, and connects to the broker\nas that service. It binds the registered\nresponder before reporting ready. It succeeds, exit 0, only after the broker accepts the connection,\nthe responder subscriptions are flushed and the space accepts the daemon as ready. The renewal and ready request IDs are kept in\n`<COTAL_HOME>/daemon/<space segment>/service-store/`, mode `0600`, and reused after a restart.\n\nEvery other answer is a refusal and nothing is connected. Not yet approved is `LIFECYCLE_BLOCKED`\n(`APPROVAL_PENDING`). A refused registration is `PERMISSION_DENIED`, and so is a space that ended the\ndaemon's authority, with its code as the native code, such as `APPROVAL_REVOKED`; connecting again\ndoes not reconnect it. A space or broker that cannot be reached is `DEPENDENCY_UNAVAILABLE`, and\nconnecting again retries the same requests. Without a register request the answer is\n`TARGET_NOT_FOUND`.\n\nWhile the daemon runs, the connection renews itself and reconnects on its own. The access bearer\nlives at most 60 seconds, so the broker drops the connection when each one expires and the daemon\nreconnects with the next. While the broker connection is down, for that reason or any other, the\nanswer is `connecting`, not connected; it is connected again only after the broker is back and the\nspace accepts the daemon as ready again. This is not a persistent service: a restarted daemon starts\nwith no service connection and stays disconnected until you run `service-connect` again, and an\nended connection is remembered only until the daemon stops.\n`service-status` reads the daemon's current answer without contacting the space. `service-disconnect`\ndrains the responder and closes the connection, and stopping the daemon closes all of them.\n\nThe service answers its existing `machine-status` command with `null`. This is a liveness response;\nit contains no vitals, hostname or machine metadata. Its stock Describe answer is explicitly public:\nit identifies the endpoint and derived owner token and includes the protocol and command schemas.\nCallers still need broker-enforced invoke access to the selected instance and command. Connecting the\ndaemon grants no caller access. Owner approval, operator name binding and caller grants remain\nseparate actions. An authority that does not publish its issuer identity cannot support this\nserving connection; the daemon refuses rather than trusting a decoded token or using Manager\ncredentials. The registration contract is unchanged.\n\n## service\n\n```bash\ncotal service [--space <s>] [--json|--plain]\ncotal service show <name>\ncotal service add <name> --preset <preset> <preset-options>\ncotal service add <name> --file <definition.json>\ncotal service start <name> [--instance <id>|--new-instance] [--foreground|--dry-run]\ncotal service stop <name> [--instance <id>] [--with-agents] [--yes|--dry-run]\ncotal service restart <name> [--instance <id>] [--with-agents] [--yes|--dry-run]\ncotal service status <name> [--instance <id>]\ncotal service logs <name> [--instance <id>] [--tail <n>] [--follow]\ncotal service install <name> [--instance <id>] [--dry-run]\ncotal service uninstall <name> [--instance <id>] [--yes|--dry-run]\ncotal service remove <name> [--yes]\n```\n\n`svc` and `services` are aliases. Every form takes `--space`, `--json` and `--plain`.\n\n| Flag | Meaning |\n|---|---|\n| `-s, --space <s>` | The space whose services to use, resolved as in [Selecting a space](#selecting-a-space). A space that is prepared but not started can be named |\n| `--preset <name>` | add: save a declared preset. Its options become flags of `add` |\n| `-f, --file <definition.json>` | add: import a version-1 definition file |\n| `--instance <full-id>` | Select one instance by its full ID, not a prefix |\n| `--new-instance` | start: allocate another instance of a process definition |\n| `--foreground` | start: keep the process in this terminal |\n| `--with-agents` | stop, restart: request the manager's native agent-stop policy |\n| `--dry-run` | start, stop, restart: preview the effect. install, uninstall: inspect the unit |\n| `-y, --yes` | stop, restart, remove, uninstall: confirm in a script |\n| `--tail <1..1000>` | logs: how many records to read (default 100) |\n| `--follow` | logs: keep reading |\n\nA service is a saved definition in one space plus the instances started from it. Bare `service`\nlists the definitions, their instances and the installed presets. Nothing starts a dependency for\nyou: starting the manager does not start its broker. `--json` writes one envelope for every form,\nand `logs --follow --json` writes one record per line.\n\n### Adding a service\n\nThe presets built into the CLI are `auth-service`, `broker`, `delivery`, `exec-endpoint`,\n`feedback-intake` and `manager`. The `linear` and `web` presets come with the `@cotal-ai/linear`\nand `@cotal-ai/web` extensions, which `cotal ext add <package>` installs. Bare `service` lists the\npresets this machine has, and a name it does not have is refused as `DEPENDENCY_UNAVAILABLE`.\n\n`cotal service add <name> --preset <preset> --help` lists a preset's options. A required option is\nmarked `(required)`, and an option that takes a path shows `<dir>` or `<file>`. A missing required\noption is refused with each one named.\n\n`add` checks the options against the preset's schema, and checks that each declared path exists\nwith its required protection. It does not check that a root or credential suits the preset, such\nas a root's auth mode or which credential a file holds. Those are checked when the service starts.\nA refusal names the option or field at fault. Option values are not printed.\n\n`--file` imports a version-1 definition. This one runs a program and treats it as ready once its\nloopback health URL answers 200:\n\n```json\n{\n \"schemaVersion\": 1,\n \"space\": \"main\",\n \"launch\": { \"type\": \"process\", \"executable\": \"/usr/bin/node\", \"args\": [\"/srv/reviews/server.mjs\"], \"cwd\": \"/srv/reviews\" },\n \"readiness\": { \"type\": \"http\", \"url\": \"http://127.0.0.1:8080/health\", \"expectedStatus\": 200, \"timeoutMs\": 10000 },\n \"envRefs\": { \"REVIEWS_TOKEN\": \"REVIEWS_TOKEN\" },\n \"credentialRef\": { \"kind\": \"file\", \"path\": \"/srv/reviews/reviews.creds\" }\n}\n```\n\n| Field | Meaning |\n|---|---|\n| `schemaVersion` | `1` |\n| `space` | The selected space's name. Another name is refused |\n| `launch` | `{type: \"process\", executable, args, cwd}`: an executable on `PATH` or by path, its arguments, and an absolute working directory. Or `{type: \"adapter\", adapter, options}` for an installed adapter, which a preset writes for you |\n| `readiness` | `{type: \"process\", timeoutMs}`: ready once the process runs. `{type: \"http\", url, expectedStatus, timeoutMs}`: ready when a loopback `http://127.0.0.1` or `http://[::1]` URL answers that status. An adapter definition uses `{type: \"adapter\", timeoutMs}`. `timeoutMs` is 1 to 120000 |\n| `envRefs` | Optional. Child variable name to the name of a variable in the environment `start` runs in. `CLAUDECODE` and names starting `COTAL_` or `CLAUDE_CODE_` are refused |\n| `credentialRef` | Optional. `{kind: \"file\", path}`: an absolute, protected credential file. The child receives its path as `COTAL_CREDS`, never its contents |\n\nNo other field is accepted. The file is at most 256 KiB.\n\n### Running a service\n\n`start` records the instance before it launches anything and prints its full ID. A start that fails\nbefore its process exists records the instance as failed. Its refusal names the cause, such as a\nspace that is prepared but not started. `start` can then try again on the same instance, and\n`stop`, `remove` and `cotal down` find nothing to stop.\n\n`stop` and `restart` ask before they act in a terminal and need `--yes` in a script. Enter, Ctrl-C,\nCtrl-D or closed input at that question leaves the service unchanged and exits 0. Without an\ninstalled unit, a `restart` whose process is already gone stops nothing, so it does not ask. A\nstart or stop that finds nothing to do says so and reports `no-op`.\n\n`remove` forgets a definition once every instance is stopped and no unit is installed. Logs stay.\n\n`install` writes an owned systemd user unit for a saved direct-process definition with no\n`envRefs` or `credentialRef`, and enables it without starting it. It is Linux only. `uninstall`\nremoves that unit. A preset is not installed as a unit in this release, the manager included.\n\nThe sections [Local service definitions](#local-service-definitions),\n[Service presets](#service-presets) and\n[Owned process services](#owned-process-services)\ndescribe each preset and the process driver in detail.\n\n## reconcile-gate\n\n```bash\ncotal manager reconcile [--space <s>] [--server <url>] [--endpoint <e>] [--instance <id>] [--yes|--dry-run] [--json|--plain]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--space <s>` | this folder's auth space | Space the frozen gate lives in |\n| `--server <url>` | the local mesh | Broker URL |\n| `--endpoint <e>` | `manager` | Endpoint whose gate is frozen |\n| `--instance <id>` | this folder's persisted manager instance | Instance id |\n| `--yes` | ask and default to cancellation | Confirm this selected gate repair outside a terminal |\n| `--dry-run` | off | Resolve the local coordinate without minting or checking native guards |\n| `--json`, `--plain` | terminal report | Finite structured receipt or plain shared report |\n\n**When you need this.** A manager restart killed after deregistration begins but before the new\nincarnation finishes leaves the endpoint's issuance gate *frozen*, held by a\nprocess that no longer exists. The freeze is what stops two incarnations serving at once, which is\ncorrect. The successor manager now completes that dead registration itself on boot, including on\nthe remote user-auth path. A foreign remote manager blocked by this gate also asks the host to repair\nit before one registration retry. Both use the same guard this command uses: they act only when the freeze-holder is affirmatively gone under a complete\nCONNZ sweep (`gone` and `sweepComplete=true`). If that registration's spec write already committed,\nit finishes the same freeze at the committed registration revision. If the spec did not advance, it\nabort-reopens the gate at generation+1 with processEpoch unchanged and continues the normal takeover.\nLive, unknown, unestablishable, and\nwrong-op-kind still refuse; there is no TTL.\n\nUse this command when the automatic path cannot run: the delivery daemon is down, the repair targets a\nnon-manager endpoint, or you want to lift the freeze without starting a manager. It checks that the\nholder really is gone, then finishes the dead operation the same way as the\ninterrupted restart would have: revoke the old credentials, evict their holders with verification,\nand reopen the gate.\n\nThe command revokes the old credentials 16 at a time. It then verifies the holders' eviction in\nshared sweeps of up to 256 holders on the delivery daemon. Each sweep scans the broker a fixed number\nof times and kicks live connections 16 at a time, so holders that are already gone add almost\nnothing and live ones add one broker round trip per 16 connections. The daemon must serve the\n`evictPrincipals` verb; an older daemon refuses it and the gate stays frozen.\n\nEach sweep durably records the holders it verified before the next sweep starts. If a holder is not\nverified gone, the command leaves the gate frozen with those records kept. An interrupted sweep\nrecords nothing, and the sweeps before it stay recorded. A retry still repeats the freeze-holder\nliveness check, then skips only progress bound to the same registration operation, frozen-gate\nrevision, and holder set.\nThe output reports holders completed before this attempt, completed now, and still remaining. A new\nfreeze or changed holder set starts from zero. Cursor cleanup happens only after reopen; a retained\ncursor is harmless because its old gate revision cannot authorize a later freeze. The governance slot\nrelease after the reopen is cleanup in the same way: a slot it fails to release does not fail the\nrepair, because the reopened gate is already past the slot's generation.\n\n**It refuses far more often than it acts, on purpose**, and always says which check stopped it:\n\n| Refusal | What it means | What to do |\n|---|---|---|\n| `holder-alive` | The freeze-holder still has a live connection: a manager *is* running | Stop that process first. Reconciling would evict a live manager's credentials |\n| `holder-unknown` | The connection sweep could not prove the holder absent | Not safe to proceed: an unprovable holder is treated as a live one. Re-run once the broker answers completely |\n| `liveness-unestablishable` | The delivery daemon gave no verdict: it was unreachable, timed out, or refused | Act on the delivery lease line in the refusal (below). Silence is never read as death |\n| `not-frozen` / `no-gate` | The gate is open, or there is no gate at that coordinate | Nothing to repair: check `--endpoint` / `--instance` |\n| `wrong-op-kind` | Frozen under a takeover or retirement, not a registration | Out of scope for this command; it will not reinterpret another operation's intent |\n| `eviction-unverified` | The holder looked gone but eviction could not be verified | The gate is left frozen, unchanged. Investigate the broker before retrying |\n| `raced` | A newer manager moved the gate mid-repair | Re-run `cotal doctor` and look again |\n\nWhen the daemon gives no verdict, the refusal also reads the delivery lease (`lease.0`) and names\nwhat is blocking the rail:\n\n| Lease reading | What to do |\n|---|---|\n| absent | No daemon is running. Start it (`cotal up` runs it) and re-run |\n| unreadable | The daemon cannot be named, so do not assume none is running. Fix the lease read, then re-run |\n| held, not ready | That holder claimed the shard and has not bound its rails. Wait for it, or stop it so its lease lapses |\n| held, ready, no answer | The query may have gone to another daemon still subscribed to the rail, such as a stopped one whose lease lapsed. Re-run before stopping anything. If no run gets an answer, stop any other delivery daemon for the space, then stop or restart the holder |\n| changed hands | The holder took the shard after the query was sent, so it was never asked. Re-run before stopping anything |\n\nThe command reads the lease before it sends the query and again after the query fails. It names a\nholder as the blocker only when the same run of the same daemon held the lease both times, and two\nrows from a daemon too old to record its run never count as the same run. Even then a ready holder\nmay not have been asked: the rail is queue-grouped, so any daemon still subscribed to it can take\nthe query. A row whose times are not valid dates reads as unreadable.\n\nA daemon that answered and refused keeps its own reason, followed by the same lease line. The lease\nline names the holder, whether it is ready, the space account that holds the lease bucket, when that\nholder acquired the shard, and when the row was last written. A ready holder rewrites the row on\nevery renewal and keeps its acquisition time, which only a successful acquisition sets. A row\nwritten by a daemon that predates the acquisition time reports it as unknown. The lease reads never\nchange the outcome: the gate stays frozen and the command exits 2. A manager's boot self-heal uses\nthe same check and reports the same line.\n\n`raced` means the repair's own reopen lost its compare-and-set and wrote nothing. A reopen whose\noutcome is unknown, such as one whose acknowledgement was lost, fails with an `unavailable` error\nthat says it may have committed. Re-run the command: it refuses `not-frozen` if that reopen landed and\nfinishes the repair if it did not.\n\nThere is no `--force`, and no path that discards gate state: the only way this reopens a gate is by\nproving the holder is gone and then completing the operation properly.\n\n**What reopening the gate does for the endpoint's governance slot.** A registration takes the\nendpoint-wide governance slot before it publishes its spec, and holds it until its gate reopens. An\ninstance that died between those two points leaves the slot held with no registration behind it.\nThis command does not write that slot and never has; the registration path is its only writer. What\nthe reopen does is advance the holder's gate past the generation the slot is stamped with, which is\nwhat marks the slot abandoned. The next registration for that endpoint then reclaims it as part of\nits ordinary start. So the repair here is still one command followed by starting the manager, and\nthe slot needs no separate step.\n\n## endpoint deregister\n\n```bash\ncotal endpoint deregister <class> <full-instance-id> [--space <space>] [--server <url>] [--yes|--dry-run] [--json|--plain]\n```\n\nThis removes a departed instance's registration, not a process or an agent. Both the native\nclass and full instance ID are required; there is no default instance, prefix match or saved\nreference. The selected static space must have validated local signing authority. Open and\nuser-auth targets refuse instead of borrowing a static credential path.\n\nThe command resolves one root, account and broker policy, including its TLS requirement.\nA terminal reviews that full target and defaults to Cancel. Scripts need `--yes`.\n`--dry-run` reports the offline target and intended guard without contacting the broker,\nissuing an operation credential or deleting anything. It does not prove apply will be allowed.\n\nApply delegates to the existing native guard. An answering instance refuses. An unanswered\ninstance also refuses unless the broker affirmatively reports that instance rail empty.\nAn unavailable probe or an in-flight registration refuses. After that evidence, the native\nhelper checks governance and deletes status before spec, pinning both revisions. The issuance\ngate is not changed and no process is stopped; the same instance may register on a later start.\n\nThe JSON/plain receipt retains the full coordinate and acknowledged removed revisions.\nA registration that moves during deletion is not success: its spec was not removed, but an\nolder status may already have been removed. That uncertain outcome retains the target and is\nnot retried. Raw probe diagnostics and credential material are not copied into the receipt.\n\nThe old `deregister-instance` spelling is a hidden migration refusal. It starts no operation\nand names the literal replacement; its former default-to-this-folder instance is not retained.\n\n## runtimes\n\n```bash\ncotal runtimes [--json | --plain]\n```\n\nLists every agent runtime the manager can spawn through: the built-in `pty`, the official providers\n(`orca`, `tmux`, `cmux`, `herdr`), and any custom provider installed via `cotal ext add`. Each installed\nprovider is probed so you can see what is actually reachable on this machine before selecting it:\n\n```\nRUNTIME STATE PACKAGE\npty built in\norca installed \xB7 reachable @cotal-ai/orca\ntmux not installed @cotal-ai/tmux\ncmux not installed @cotal-ai/cmux\nherdr not installed @cotal-ai/herdr\nadd one that is not installed with `cotal ext add <package>`\n```\n\n`installed \xB7 reachable` / `unreachable` is the provider's own `available()` probe; `not installed`\nmeans it is a known runtime you can add with `cotal ext add` and the package shown. `--json` writes\none document whose `result.items` are `{name, state, package, reachable, error}`, with `state` one of\n`built-in`, `installed` or `not-installed` and `reachable` null when the provider was not asked. An installed provider\nwhose package fails to load is shown with its error and makes the read `partial` (exit 4). The command\nreads this machine only, so its `target` is null. Selecting an unknown or uninstalled runtime\nvia `up`/`spawn --runtime <name>` fails loud and, for a known one, points at the exact `cotal ext add`\npackage. There is no silent fallback to `pty`.\n\n## seats\n\n```bash\ncotal seats [--drain]\n```\n\n| Flag | Default | Meaning |\n|---|---|---|\n| `--drain` | off | Retire every seat whose agent has exited. A seat whose agent still runs is kept |\n\nThe pty runtime used to start a detached custodian process for every Linux seat. It now spawns\nin-process, but custodians that an earlier manager started keep running, and one whose agent has\nexited stays resident while a manager still holds its connection. This command lists the custody\nrecords under `COTAL_SEAT_ROOT` (default `~/.cotal/seats`), one line per seat:\n\n| State | Meaning |\n|---|---|\n| `live-child` | The agent process still runs. The seat is never signalled, and a manager can still adopt it |\n| `childless` | The agent has exited, or the record comes from an earlier boot. `--drain` retires the seat |\n| `drained` | `--drain` proved the custodian and the agent gone and removed the record |\n| `refused` | The record cannot be read, carries no start or boot identity, this host publishes no boot identity, or the reap could not prove the processes gone. The record stays on disk |\n\nA drain signals only a custodian whose recorded start identity still matches the live process, so\na reused pid is never touched. No process outlives a reboot, so a record from an earlier boot is\nreported childless and `--drain` removes it without signalling anything. A record with no start or\nboot identity is refused with or without `--drain`, and is never reported as running or exited.\nOn a host that publishes no boot identity (`/proc/sys/kernel/random/boot_id`) every record is\nrefused the same way, because no record can be tied to this boot.\nThat refusal and an unreadable record signal nothing. A refusal from the reap itself can come after the drain already\nsent `SIGKILL` to the custodian. Its detail names the pid or process group the reap could not prove\ngone, so check those processes before you retry. The command exits non-zero when any record is\nrefused. It is Linux-only and throws on other platforms.\n\n## send\n\n```bash\ncotal send -t <agent> \"<text>\" [--space <s>] [--server <url>] [--creds <path>]\ncotal send -c <channel> \"<text>\"\ncotal send -r <role> \"<text>\"\n```\n\nChoose one destination: `--to/-t` for a direct message, `--channel/-c` for a channel,\nor `--role/-r` for the role's native work queue. Choose one body source: a quoted positional\nargument, `--file/-f <path>`, or explicit `--stdin`; `--file -` reads stdin as `--stdin` does. There\nare no implicit stdin reads. Files and stdin must contain nonempty UTF-8 text; their content is\npreserved and never printed in the receipt. A file that cannot be read is refused with its path,\n`TARGET_NOT_FOUND` when it does not exist.\nNo destination, several destinations, several body sources or an empty message is refused. A lone\n`-` is refused too, because stdin is read only through `--stdin` or `--file -`.\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\nwith `TARGET_AMBIGUOUS` and lists the full identities that carry the name, and a target that is not\npresent refuses with `TARGET_NOT_FOUND` and points at `cotal who --all`. No fuzzy match selects an\nagent for you.\n\nFor a channel post, repeat `--mention <agent>` to add explicit wake hints without changing the\ndestination. Each one resolves like a direct target and is sent as that agent's name, which is what\na receiving session compares. An instance id is refused when its name also resolves to another\nparticipant, and a shared name refuses with the full identities to send to with `--to` instead.\nDirect 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. A message over the broker's message limit, envelope included, refuses\n`UNSUPPORTED` (exit 2) with its size and the limit, and nothing is sent. A channel post the broker\nrefuses was not stored, so it refuses `PERMISSION_DENIED` (exit 2): the channel does not exist in\nthe space or this identity may not post to it. The remedy lists the channels this identity may post\nto, read from the grant ledger as `cotal whoami` reads it. When the ledger is on another machine it\nsays so and lists the space's registered channels instead. Any other unconfirmed publish outcome is\nunknown 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. A `--creds` credential\nthat may not read the roster and the channel registry that way, such as one from `cotal auth mint`,\nrefuses with `PERMISSION_DENIED` (exit 2) before anything is sent, and the remedy says to run\nwithout `--creds`.\n\nA recipient can still answer a `send --to` or `send --role` with `cotal_dm` or from a `chat`\nsession. On a static or open mesh every send has its own principal under the same display name, so\nthe sender's name works only while it names one principal among the senders the recipient has heard\nfrom and the present peers. Once the recipient has heard two sends from the same login and host, a\nreply by that name refuses with `TARGET_AMBIGUOUS` and lists the principals. Answer by the id on\nthe message instead (`from.id` in `chat --stdio`), or with `/reply` in `chat`. The reply is stored\nunder the sender's id in the space's DM history, which an operator's DM view such as the\ndashboard's Direct messages lens shows. The `cotal send` that asked has already exited, so the reply\nnever 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 static or user session shows its end time in the banner, and says when it ends a minute before.\nAfter a session ends, `--plain` answers every line with how to leave, and `/quit` or the end of\ninput leaves.\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. `/leave` does not change that list, so `/join` returns the\nsession to a channel it left. 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,\nand it writes its punctuation in ASCII as every plain report does.\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\nIn `--plain`, a line you type becomes the draft, and it is sent only when you answer `y` to the\nquestion that follows. `:to` lists the recipients by number, and `:to <target>` sets one directly:\n`#<channel>`, `@<name>`, `@<owner>.<actor>` or `role:<role>`. A target in another form, such as a\nname without `@`, is refused with that list of forms, and the numbered list opens. In the list, an\nempty line keeps the current recipient, and `/quit` leaves. `:multi` starts a message of several lines, which a line\nholding only `.` ends. A line starting with `/` runs a slash command, and `//` sends a line that\nstarts with `/`. `/help` lists the slash commands, in `--plain` and in the terminal view.\n`/history` reads the current channel's retained messages. On an open mesh, which runs no delivery\ndaemon, it reads them the way `cotal history` does.\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. A session that cannot reconnect because its inbox is gone from the broker, as when the\nspace restarted and retired it while the connection was down, ends by itself with that reason and\nexits 5 when its close is confirmed. 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 that follows the first request's result. Requests run one at a time in arrival order, and a\nrefusal waits its turn too, so every `result` and `error` record comes back in the order its line\narrived. `message`, `presence` and `gap` records can arrive between results, so match each result\nby its id. A missing field, a field the type does not take or a malformed value refuses with\n`USAGE_INVALID` before the session 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. A\nsession that could not reconnect gives that reason instead, and its `close` carries it as `lost`.\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. The delivery service\nwrites that feed, and an open mesh runs none, so `members` refuses an open mesh with `UNSUPPORTED`.\n\nRemoval keeps retained history unless `--purge` is present. The confirmation names the channel,\nspace and history scope. Scripts pass `--yes`. Removing a channel that has no saved entry is a\n`no-op`, and its Metadata row says `no saved entry`; `--purge` still deletes that channel's\nretained messages. In `--json`, `result.removed` says whether a saved entry was removed. Neither\nremoval nor replay settings revoke grants or stop participants; future posts can create retained\nhistory 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. A DM row shows its recipient's name when a message in the window came from that\nprincipal, and the principal otherwise. `result.items[].to` is the principal.\nA line break or other control character in a message's text shows as its `\\uXXXX` escape in the\ntext and `--plain` reports, so a message cannot clear the screen or start a row of its own.\n`--json` carries the text as it was sent, in `result.items[].text`.\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`. The\nline stream is also what a pipe gets. On `--plain`, and under a locale that is not UTF-8, it writes\nno status marks and its punctuation in ASCII. 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\nMessage bodies, names and other text from peers are untrusted. Both views show a control character\nin them as its `\\uXXXX` escape, so a message cannot clear the screen, retitle the terminal or plant\na hyperlink. The TUI keeps a body's line breaks, and `--plain` prints each message on one line.\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, and an\n`--expires-at` that is not in the future refuses before anything is written. 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, lists them under `Your spaces`, and names `cotal use <name>` to select one. A\nspace registered from elsewhere that has not granted your account refuses with those discovered\nspaces as its remedy, because only that space's operator can grant you. On the root that holds the\nspace's grant ledger, the refusal names the operator's `cotal actor grant` command instead. 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 file that other accounts can open is a finding that names its mode, with `chmod 600`\non the file as its repair. Each file is also parsed the way `--creds` checks one, so a file the NATS\nclient cannot parse is a finding even when its claims are legible, such as one with Windows (CRLF)\nline endings.\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` supplies the where and the auth in one value. See\n[Spaces](spaces.md) and [Identity and auth](identity-and-auth.md).\n\nA `--token` or `--creds` goes to `--server`, or else to the broker the space is registered at,\nkeeping that registration's TLS requirement. A space not registered here is refused before\nanything is dialed. The default address is never assumed for a credential.\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. When the manifest's space is already\nrunning from the same folder, it exits 1 and names `cotal team deploy <file>`, which adds all-new\nchannels and agents, and `cotal down` before another `up -f`. On a root that holds a stack preserved by\n`down --preserve-state`, it refuses `LIFECYCLE_BLOCKED` (exit 2) and starts nothing; bare\n`cotal up` resumes that stack. `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, cotal creates a missing Cotal config directory with mode `0700`, even with a\ngroup-writable umask, whichever command writes there first. The extension writer creates a missing\ninstall prefix with mode `0755` and its `package.json` with 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. While it uninstalls, `remove` reserves each process record the package\ndeclares under every mesh root, and a record folder it creates there is mode `0700`, even with a\ngroup-writable umask.\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 a seed that failed or was interrupted, or a lost authority (rebuilds that seed's 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\nA seed that fails names the connector and the reason, such as npm missing from PATH. Until\n`cotal ext seed --repair` finishes it, each command that seeds refuses. The refusal says whether\nthe last seed failed, and why, or was interrupted.\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. A variable whose value `config set`\nwould refuse still wins, and `config`, `config get` and `status` name it in a `PREFERENCE_INVALID`\nwarning with the reason. 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`. A preferences folder you cannot write is refused with `PERMISSION_DENIED`\n(exit 2), naming the folder, before anything is changed. So is a linked `preferences.json`, which\n`config set` never replaces (see [Preferences](config.md#preferences)).\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 before it creates anything. One refusal names every refused startup\nfile, stub, record and directory on the way to them with its reason, and a group- or other-writable\none with its mode and one `chmod go-w <paths>` that fixes them all. A startup file that is a\nsymlink, as a dotfile manager such as stow links it, is never written through: `install` refuses\nit, exit 2, names it and gives the line to add to the file it points to yourself. Bare `completion`\nwarns that it is a symlink and does not read it. A startup file its owner made read-only is never\nreplaced. `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. Under `DO_NOT_TRACK`, `COTAL_TELEMETRY_DISABLED` or `COTAL_TELEMETRY=off` the file\nis neither read nor created, so a level argument saves nothing and is a `no-op` with an\n`ENVIRONMENT_OPT_OUT` warning. `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 the run ID the command named or the manager returned, only when it matches the native\nrun-token shape. Keep the pin on later status, answer and resume calls.\n\nA hosted verb the manager refuses reports the outcome and code as other commands do, and with\n`--json` as one refusal envelope on stdout. A refusal the manager makes before acting, such as a\ndenied credential, a step in the wrong state or a program that does not validate, is refused, exit\n2. A run or step it holds no record of is `TARGET_NOT_FOUND`, with `cotal run ps` or\n`cotal run show <runId>` as the next step. Any other refusal of `start`, `resume`, `answer` or\n`amend` may follow a recorded change, so it is unknown, exit 3, and is not retried.\n\nAn unpinned hosted verb run by the operator reads the space's service registry first. When it\nlists no live manager, as after `cotal down stack-manager`, the verb refuses `TARGET_UNAVAILABLE`\n(exit 2) at once, as `ps` does, and names bare `cotal up` as the next step, instead of waiting out\nthe manager's answer deadline.\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. A program\nfile that cannot be read is refused with `TARGET_NOT_FOUND`, exit 2, by every verb that takes one.\n`resume` asks the manager to take an existing run back and continue it from its step journal; the\nsource is the 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\nunder `Needs an answer`, and only then does Next offer `answer`. A pause counts as open while its\nlast journal row is pending, so a settled or expired one is not listed there.\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.\nA step whose last journal row recorded an error prints its outcome and the recorded message under\n`Step errors`, a caught failure included, with each control character shown as its `\\uXXXX`\nescape. A failure raised by program code outside any step is recorded in no journal row, so the\nview shows only the `failed` state, and a hosted run's cause is in the manager's log.\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, the recorded `errorMessage` of a failed or refused step,\nand `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 goes 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`.\nWithout `--server` it dials the broker its `--space` is registered at, and refuses an unregistered\nspace before dialing.\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. When the connection\ncomes back, after a broker restart too, the feed resumes and reports the transition as a gap.\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, each step's recorded error 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. A program that does not validate is refused by all three with every problem listed: its\ncode, position, cause and fix, as a hosted `start` lists them. `--json` carries them under\n`result.problems`. A warning leaves the program valid. All three print it on stderr with its code,\nposition, cause and fix, and `validate --json` lists it under `result.warnings`. One is L3021, a `fanOut` over a literal list whose items carry no `id` and\nthat passes no `key`, which the run refuses when it reaches it. 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 scripted\ncheckpoint result needs a `status` of `resolved` or `expired` and may carry `value`, `by` and\n`artifact`; any other shape, a scripted `at` included, is refused `SCHEMA_INVALID`, because the\nsimulator stamps `at` from virtual time. A simulation does not 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. A summary records the absolute folder\n`up -f` ran in. After that folder is moved or copied, the summary still reads but names the old\npath. It settles nothing in the new folder and blocks no `up` or `down` there, and `team show <id>`\nrefuses it with CONFLICT, naming the folder it was recorded at.\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. A manifest that requests no agents leaves no join unchecked, so its receipt is `ok`. 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. When a `cotal down` from another terminal completes,\nit prints `Foreground stack released` and exits 0, as a foreground `up` does, although its start\nreceipt was partial. 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. Every `team remove --dry-run` is `partial`, exit 4: it\nreads the manager's slots and the card revisions in order, not atomically, and the membership feed\nis display-only, so the preview does not establish what the apply will find. The apply reads the\nslots and revisions again before it acts.\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.\nA manifest it cannot resolve is refused with `SCHEMA_INVALID` (exit 1). The refusal names each\nproblem by file, line and column where the parser has them, the key path, and the reason, and\n`--json` lists every problem under `target.issues`. A problem in a referenced persona\nnames that file and the parser's reason without quoting its contents. `team boot` and\n`team deploy` refuse a manifest they cannot resolve the same way, before they contact a broker.\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, and with `--json` its `error`\nrecord names the space and the reason. 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. A line-oriented\nroster, channels, waiting or topology view prints its report again each time that report changes.\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. It reclaims an existing reservation only when\nthe pid it names has exited and no request was published for this launch, and otherwise refuses and\nnames that pid.\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\nBoth read the endpoint's contract first, and so does `auth mint --endpoint`. When no instance\nanswers that read within 10 seconds, the command refuses with `TARGET_UNAVAILABLE`: nothing is\nserving the endpoint, or nothing could reach this caller. Its Next says to start the service that\nserves the endpoint. Nothing was invoked or issued.\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. One refusal names the option's entry and every unsafe directory above it, with one\n`chmod` that fixes them all. `npm i -g` under umask 0002, an ordinary Ubuntu user's default, leaves the bundled\n`nats-server` and its directories group-writable, so where the private-group proof is unavailable,\n`up` refuses the broker's `binary` path and names that `chmod g-w`.\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"
|
|
27287
27287
|
},
|
|
27288
27288
|
{
|
|
27289
27289
|
"slug": "config",
|
|
27290
27290
|
"title": "Configuration",
|
|
27291
27291
|
"kind": "Reference: describes the TypeScript reference implementation (the `cotal` CLI and connectors), not the wire contract.",
|
|
27292
27292
|
"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",
|
|
27293
|
-
"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 per-space secrets: its retained system-account signing seed (`system-signing-seed.json`, written at space creation and replaced by `--rotate-sys`) and, when that space enables per-user auth, its user-auth state (IdP pin, issuer keys, owner secret, callout account, client sentinel credentials). 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'
|
|
27293
|
+
"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. `cotal space forget` removes the values saved for\nthe space it forgets.\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` in any letter case, and a non-empty `NO_COLOR` turns it off; both beat a saved value.\n`auto` colors a terminal unless its `TERM` is `dumb`. `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.\n`persona` must be a persona name, made of letters, digits, `_` and `-`, or a path to a persona file\nwith no control characters; whether that persona exists is checked when a launch uses it. 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. A `preferences.json` that is a symbolic link stops them the\nsame way with `PERMISSION_DENIED` (exit 2), naming the file it points to: `editor` runs as a shell\ncommand, so the file is neither read through the link nor replaced by `cotal config set`.\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 error, followed by the stack of each error that caused it | 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: the mesh registry (`meshes/`, `current-mesh`, onboard marker) and records such as prepared spaces and service records. 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 per-space secrets: its retained system-account signing seed (`system-signing-seed.json`, written at space creation and replaced by `--rotate-sys`) and, when that space enables per-user auth, its user-auth state (IdP pin, issuer keys, owner secret, callout account, client sentinel credentials). 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**: the registry, current pointer and onboard marker, and\nrecords such as prepared spaces and service records. It is not a full workstation sandbox. Broker launch policy, the JetStream\nstore, pidfiles, and auth live under the **project** `.cotal/` found by walking up from the cwd\n([Project: `.cotal/`](#project-files) above, 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\nA controller stack\'s member records live in this tree, so commands run under another `COTAL_HOME`\ncannot see a stack started under this one. While that stack\'s manager or delivery daemon runs,\n`status` there says the stack runs under another Cotal home and names the home in use, and `up`\nrefuses `TARGET_UNAVAILABLE` with nothing started. Both name the step: run with the `COTAL_HOME` the\nstack was started under.\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| `prepared/space.<key>.json` | A space `cotal space create` prepared and no `up` has started yet |\n| `services/<key>/` | Saved service definitions and instance records, including a controller stack\'s `stack-*` members |\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'
|
|
27294
27294
|
},
|
|
27295
27295
|
{
|
|
27296
27296
|
"slug": "connect-claude",
|
|
27297
27297
|
"title": "Connect Claude",
|
|
27298
27298
|
"kind": "Guide (informative)",
|
|
27299
27299
|
"summary": "The Claude Code connector turns a real claude session into a Cotal mesh peer.",
|
|
27300
|
-
"body": "# Connect Claude\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\nThe Claude Code connector turns a real `claude` session into a Cotal mesh peer. A bundled\nplugin inside the session joins NATS, maps lifecycle hooks to presence, and exposes the\nmesh tools. Nothing wraps Claude; it is an ordinary session that happens to be on the\nmesh.\n\nThe shared mesh runtime (agent, `cotal_*` tools, hook relay) lives in\n[`@cotal-ai/connector-core`](../extensions/connector-core); this connector is the thin\nClaude-specific adapter over it. Its lifecycle hook imports the relay from the\n`@cotal-ai/connector-core/relay` subpath, so each hook process loads the relay and its environment\nreaders and none of the NATS client, zod or yaml. Siblings: [OpenCode](connect-opencode.md) (beta),\n[Hermes](connect-hermes.md) (alpha), [pi](connect-pi.md) (alpha); the\n[Connectors](connectors.md) matrix compares them feature-by-feature.\n\n## Set up\n\n> **Team and Enterprise accounts:** [Enable Claude Code channels](#enable-channels-for-your-organization)\n> before running unattended agents. Channels are blocked by default, so incoming messages cannot\n> wake an idle session until an organization Owner enables them.\n\n```bash\ncotal setup --configure-only # install the plugin and seed one agent without startup\ncotal up # brings up the mesh + delivery daemon + a detached manager\n```\n\n`cotal setup` installs the cotal plugin for your user account, so every Claude Code session on\nthis machine gets the `cotal_*` tools, in any project. That covers the terminal, the IDE extensions\nand the Desktop app's local Code sessions. Setup can use Desktop's bundled Code executable when no\nterminal `claude` command is installed. Desktop-only users should open a local Code session once so\nDesktop downloads that executable. Setup checks it through the connector's own readiness probe;\nmanaged `spawn` still requires the terminal `claude` command. With a local mesh already running, run setup once, reopen Claude\nand ask it to join your local mesh. No MCP configuration edits or special launch flags are\nneeded for this native plugin path. It also shares your own MCP servers with spawned sessions on its first run (see\n[Sharing your MCP servers](#sharing-your-mcp-servers)), and seeds one `default` persona; `cotal up` brings up the local stack so\n`cotal spawn --detach` / `cotal_spawn` work right away. Re-running either is idempotent.\nA repeat `cotal setup` and `cotal update` refresh the copy under `~/.cotal/claude-plugin` and update\nexisting Claude plugin registrations, including older cached installations. After an npm upgrade,\nthe first regular Cotal command also refreshes these copies before accepting the new generation. Setup,\nskills installation and refresh share a machine-local lock through the final plugin check, so simultaneous\ncommands cannot overwrite a generation while another command verifies it. Disabled installations\nstay disabled during an update. Claude Code can apply an update run in a project folder to an\nenclosing project that has its own installation, so the folder's local or project installation stays\nold. That installation does not stop the refresh or later commands: Cotal prints a warning naming the\nfolder and the `claude plugin uninstall` command to run there to remove it. A user-scope installation\nthat does not update is still an error. `cotal status` checks the resolved payload's manifest version and\nfile contents. A folder-backed plugin can retain older version metadata in Claude's installation\nrecords; that metadata does not override the files Claude loads. Running sessions keep the\ncode they loaded; reload plugins or reopen Claude to load the update.\nThe install mechanics and the invariants behind them are in\n[setup internals](setup-internals.md).\n\n`cotal setup` also installs Cotal's authored Agent Skills (`SKILL.md`, the agentskills.io format) for\ncoordinating agent teams (today `team-topology`), from one canonical source, on two channels:\n\n- **Claude Code** gets a second, skills-only plugin, `cotal-skills`, from the same `cotal-mesh`\n marketplace, at **user scope** (machine-wide). The Claude connector declares and implements this\n setup provider, including the marketplace assets and native plugin commands; the base CLI only passes\n the vendor-neutral Agent Skills directory. The plugin carries no code and no core dependency,\n and uninstalls on its own with `claude plugin uninstall cotal-skills --scope user`. Its plugin version\n is stamped from the running CLI release. `cotal update` refreshes an existing skills plugin without\n enabling it or installing a removed one; `cotal setup --skills` also installs missing skills. `cotal setup` installs it on first run and on repeat\n runs, so upgraders are not left behind. The same provider reports the plugin and skills plugin rows\n in `cotal status`, which point a stale or missing skills plugin at `cotal setup --skills`.\n- **Every other harness** (Codex, Cursor, OpenCode, Gemini CLI, Windsurf/Devin) reads the cross-vendor\n `~/.agents/skills/` directory convention, which has no remote index, so `cotal setup` **reconciles** it\n (and `cotal setup --skills` does only that):\n it installs/updates each Cotal skill, backs up a copy you have edited to `SKILL.md.bak` before\n replacing it, and removes a Cotal skill that is no longer shipped. Only skills Cotal owns are touched;\n your own or third-party skills there are left alone. `cotal status` reports whether the drop is current,\n stale, missing, or has a retired skill to reconcile, and names `cotal setup --skills` as the remedy. This is the working cross-vendor path.\n\nCotal also generates an [Agent Skills discovery index](https://cotal.ai/.well-known/agent-skills/index.json)\non cotal.ai, but that RFC is still a draft with no harness consuming it yet, so it is a forward bet,\nnot a channel to rely on today.\n\n## Enable channels for your organization\n\nFor claude.ai Team and Enterprise organizations, channels are blocked by default. An organization\nOwner must enable them in [Organization settings > Claude Code > Channels](https://claude.ai/admin-settings/claude-code),\nor set this organization-managed setting:\n\n```json\n{\n \"channelsEnabled\": true\n}\n```\n\nUsers cannot override this setting through local or project settings. Cotal's\n`--dangerously-load-development-channels` launch flag cannot override it either. Enable channels\nbefore launching the sessions that need them.\n\nWhen channels are blocked, Cotal's MCP tools and hook-based inbox delivery still work when the agent\ntakes a turn. An incoming message cannot wake an idle Claude Code session. A successful spawn or a\nconnected MCP server does not prove idle wake is available; check Claude's startup notice before\nleaving agents unattended.\n\nPersonal Pro and Max accounts without an organization do not need this admin opt-in. Sessions still\nneed to opt into their channel, which Cotal's managed launch does for its own server. See\n[Anthropic's channel controls](https://code.claude.com/docs/en/channels#enterprise-controls) for\norganization policy and other authentication modes.\n\n## Spawn a session\n\n```bash\ncotal spawn # foreground: your default agent, in this terminal\ncotal spawn dave --detach # supervised: the manager runs it in a PTY\n```\n\nA spawn resolves a persona from `.cotal/agents/<name>.md` ([agent files](agent-files.md));\n`--model`, `--variant`, `--cwd`, `--prompt`, ACL overrides, and `--share-tools` apply to\nboth forms ([run a mesh](run-a-mesh.md) has the full resolution rules). The session joins\nwith identity from its environment and auto-registers presence by the time it is\ninteractive.\n\nInside the session, the agent orients with one read-only tool, `cotal_orientation`: its\nidentity, the channels it reads and may post to, its capabilities, the tools available,\nwho's present, and unread counts. The full tool surface is the\n[MCP tool catalog](mcp-tools.md). In auth mode the team-supervision tools\n(`cotal_spawn` / `cotal_persona` / `cotal_personas`) are injected **only** for personas declaring\n`capabilities: [spawn]` (the same grant that opens the privileged control subject), so an\nagent's toolset matches its declared capabilities. `cotal_run` is gated separately by\n`run`; use `capabilities: [spawn, run]` for both. Fresh setup defaults include both.\nSee [workflow tool setup](workflows.md#from-an-agent-session) for a first run and missing-tool checks.\nClearing retained history is\noperator-only ([run a mesh](run-a-mesh.md)), never an agent tool.\n\n## How it binds\n\nClaude Code exposes four integration surfaces, and three of them collapse into a single\ndual-purpose MCP server:\n\n| Surface | Mechanism |\n|---|---|\n| Outbound, ambient | `http` lifecycle hooks \u2192 POST to the connector (presence, activity) |\n| Outbound, deliberate | MCP tools `cotal_send` / `cotal_dm` / `cotal_anycast` (+ `cotal_feedback`) |\n| Inbound, pull | MCP tool `cotal_inbox` (same server) |\n| Inbound, push | Channel nudge + hook drain (below) |\n\nThe manager launches the *real* `claude` (no wrapper):\n\n```\nclaude --mcp-config '{\"mcpServers\":{\"cotal\":{\u2026}}}' \\\n --dangerously-load-development-channels server:cotal \\\n --permission-mode bypassPermissions\n# env: COTAL_SPACE, COTAL_NAME, COTAL_ROLE, COTAL_CHANNEL=1, plus claude's documented auth vars\n```\n\n- **Tool permissions.** A managed seat has no human at its terminal, so it launches with\n `--permission-mode bypassPermissions`: a prompt under the default mode would stop its first mesh\n tool call until someone answered it, and nobody can. To get prompts back, pass\n `--opt permission-mode=default` (or another mode); the connector adds the default only when the\n launch options carry neither `permission-mode` nor `dangerously-skip-permissions`. Claude refuses\n the bypass mode as root and under a policy that disables it; such a host must pass a mode.\n\n- **Model auth.** Locally, `claude` still reads macOS Keychain / `~/.claude`. In a container or\n CI there is no Keychain, so the connector forwards the documented credential set:\n `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`), `ANTHROPIC_API_KEY` /\n `ANTHROPIC_AUTH_TOKEN`, and the cloud-provider flags plus their credential vars. Host-session\n markers (`CLAUDE_CODE_CHILD_SESSION`, `CLAUDECODE`) stay out so a nested seat still saves a\n transcript. See [Deploy](deploy.md).\n- **Persona privacy.** The persona body is written to a private file and Claude receives only\n `--append-system-prompt-file <path>`. The body never appears in the spawned process argv. The\n carrier is a 0600 file inside a 0700 directory on POSIX, with equivalent owner-only ACL hardening\n on Windows. That is OS-user isolation: any process running as your user can read it while it\n exists. The manager or the foreground `cotal spawn` removes it, and the shared-server MCP config\n file, once it has proved the `claude` process gone. If the launcher is killed first, a watcher\n started beside `claude` removes them when `claude` exits.\n- **MCP servers.** A spawned agent loads Claude's native MCP sources alongside Cotal and declared\n sharing overrides. Explicit `--share-tools` selections and carried private homes keep strict\n isolation. Claude still applies its own server approvals, disabled settings and managed policy\n (see below).\n- **Lean MCP server.** Every session runs its own cotal MCP server, so Node starts it with\n `--optimize-for-size --max-semi-space-size=1` to shrink its memory footprint. Managed launches and\n the plugin's `.mcp.json` (personal plugin sessions) both pass these flags. `--lite-mode` and\n `--jitless` would save more, but they remove `WebAssembly`, which Node's built-in `fetch` needs,\n so WebAssembly stays enabled.\n- **Light hook.** Claude Code starts the plugin's hook as a new process for every tool call, prompt\n and stop. The hook imports `@cotal-ai/core` only through small subpaths such as\n `@cotal-ai/core/peer-label`, never the package root, which would bundle YAML and NATS into it.\n `pnpm smoke:claude-hook-weight` holds that.\n- **Installed plugin.** The plugin is installed once, at user scope (`claude plugin install\n cotal@cotal-mesh --scope user`), because its hooks bind only to an *installed* plugin. Earlier\n releases installed it per project, at local scope; the next `cotal setup` removes those installs and\n keeps the user-scope one. If Claude Code cannot change a project's settings, setup still removes\n the others, then fails naming that project and the `claude plugin uninstall` command to run there. A session Cotal launches loads the package's own copy with `--plugin-dir`,\n which replaces the installed one for that session, so it still runs one cotal server.\n The repo's `.claude-plugin/marketplace.json` lists the committed plugin tree under\n `claude-plugin/`, which each release regenerates with the built bundles, the skills and the\n release version, so an install from the repo or from a pinned commit runs without a build\n ([Release](release.md)). `cotal setup` (npx, no clone) materializes the same marketplace under\n `~/.cotal/claude-plugin/` from the installed CLI (each plugin dir is rebuilt from scratch and\n atomically replaced, never merged, so no stale file rides in). The\n `cotal-skills` plugin installs from that same marketplace at user scope (`claude plugin install\n cotal-skills@cotal-mesh --scope user`); its manifest and install behavior ship inside the Claude connector, and\n its version tracks the CLI release so updates land.\n- **Identity-gated.** A launched session joins under the identity in `COTAL_NAME`, `COTAL_LINK`\n or `COTAL_AGENT_FILE`. A plain `claude` with none of them joins only when its conversation calls\n `cotal_connect`, so your own sessions do not appear as stray peers. Until then its MCP\n server builds no mesh agent and opens no broker connection. See\n [Sessions Cotal did not launch](#sessions-cotal-did-not-launch).\n- **Hands-free.** The dev-channels flag prints a one-time confirm prompt. The runtime waits for the\n dialog title in normalized terminal text and presses Enter once when it appears, so startup speed\n does not affect a supervised launch. Claude skips that prompt when channels are off for the\n session, for example under a Team or Enterprise organization whose managed settings do not set\n `channelsEnabled: true`, and opens its input screen instead. The runtime then ends its watch\n without a key once the input screen's footer shows `(shift+tab to cycle)` or `? for shortcuts` on\n two screen reads in a row. That seat gets mesh messages through its hooks only, so a message does\n not wake it while it is idle. The PTY runtimes read the child's screen, and the tmux, cmux, Orca\n and Herdr runtimes read the pane's screen. If neither the prompt nor the input screen appears\n within 15 seconds, the seat ends with a bounded error naming the unmatched prompt instead of\n hanging silently or answering another dialog. The PTY runtime writes that error to the seat's output and\n stops the child as a graceful stop does, so a child that ignores SIGTERM is killed 3 seconds\n later. The other runtimes write the error to the manager's log.\n- **Trusted directory.** An authorized supervised spawn accepts the selected workspace through\n Claude's native trust dialog, which Claude draws in one of two layouts. When a numbered list\n shows Yes first and selected, the runtime confirms it with Enter. When No comes first and is\n selected, the runtime moves to Yes and confirms only after seeing Yes selected. It never confirms\n while No is selected, sends each response once, and stops watching after the\n development-channel confirmation or on the input screen. A changed or unsupported dialog receives no guessed input\n and still fails within the startup bound. Claude saves trust with its own repository and linked\n worktree rules; Cotal does not rewrite the manager's configuration. Accepting trust enables the\n workspace's hooks and settings, which can select a tool permission mode when no launch flag\n overrides it. Cotal adds no tool-permission bypass flag. Review the checkout before authorizing\n a spawn there. A foreground `cotal spawn` keeps the trust dialog in your terminal.\n\nInbound mesh messages arrive in context as\n`<channel source=\"cotal\" from=\"bob\" kind=\"dm\" \u2026>\u2026</channel>`: each meta key a tag\nattribute the agent can read for routing.\n\nThe consuming connector holds a crash-safe claim for its broker target, space, principal and\nlifecycle on this OS user account. A second patched session on the same host refuses before\npulling that inbox, even when its launch files and control socket differ. Reconnect keeps the\nclaim. Hooks and non-consuming helper connections do not claim it. The control listener binds\nbefore the broker connection and never removes an existing socket path.\n\nThis is cooperative same-host containment. Other hosts, broker URL aliases and older clients\nare not fenced. Preserve the existing session before choosing a distinct actor for a new\nlaunch. An uncertain shutdown keeps custody until the consuming process exits.\n\n## Sessions Cotal did not launch\n\nA Claude Code session you start yourself, where the `cotal` plugin is installed, gets the tools the\nhosted connector offers: `cotal_connect`, `cotal_wait`, `cotal_disconnect` and the routed mesh tools.\nNothing joins until the conversation calls `cotal_connect` with a name. The plugin's server then runs\n`cotal app-identity provision` with the CLI that `cotal setup` recorded beside the plugin\n(`dist/cli.json`), so it does not need `cotal` on `PATH`. Each conversation gets an identity of its\nown on the mesh a bare `cotal spawn` from the session's directory would pick, once a person approves\nit. A static-auth or open mesh has no page where a person approves a connection, so the plugin\nopens no identity there. A plugin that `cotal setup` did not install has no record, and\n`cotal_connect` names that remedy.\n\nPersonal native Code sessions can join a signed-in mesh whose authority offers connection approval.\nThe plugin binds the real native session and transcript before requesting an identity. Each\nconversation gets a separate actor. A new identity is issued only after a person approves it in the\nauthority's signed-in web page. A model's tool arguments, a client flag or a native tool approval\nnever approve it. The request first comes back pending with the page's address and its expiry, and\nnothing is issued yet. Connecting again sends the same request and secret: while it is pending it\nanswers the same page, and once approved it issues the identity. A denial or an expiry drops the\nrequest, and the next connection asks again. No login proof reaches the conversation or its tools.\nA remote mesh must advertise conversation identity v1 and connection approval v1 on its pinned HTTPS\ndiscovery endpoint. An absent capability, a different origin or a changed registration is refused\nbefore authenticated issuance, for a request without a selection too. There is no immediate-grant\nfallback. A signed-in mesh whose authority this machine holds has no approval page, so it issues no\nnew identity either.\n\nEvery identity sends and receives direct messages and publishes its own activity on its own event\nchannel. These come with the identity and are not optional, so the approval page shows them before\nthe person decides. Native activity sharing includes the conversation's user messages, assistant\nreplies, tool inputs and tool results after activation. It is not only presence or status. Reasoning\nis not shared by default, and earlier or pre-approval history is not shared. The space's activity\nread permissions determine who can read it. Decline the connection if you do not want this sharing.\nA request without a selection proposes nothing beyond these rights: no role,\ncapabilities or ordinary channel reads.\n\n`cotal_connect` in a native Code session also takes a selection: `channels` to read\n(`access: \"read\"`) or read and post to (`\"read-post\"`), a `role`, and `capabilities` (`spawn`,\n`run`). The selection is a proposal. The person may approve it, change it within their own current\naccess, or approve nothing beyond the identity's own permissions. The issued grant must match what\nthe person approved item for item, or the connection is refused. When the authority refuses an item\noutside your own grant before asking anyone, nothing is issued and the reply names every refused\nitem. There is no fallback to the event-only identity. Its own event channel stays the first post\nentry. A remote mesh must also advertise `conversationIdentitySelection` version 1, or a selection\nis refused before any request.\nEndpoint calls are not selectable through this connection approval contract, which accepts only\nan empty endpoint selection. These conversations do not offer `cotal_describe` or `cotal_invoke`.\n\nA connection is offered the tools its grant can use: `cotal_spawn`, `cotal_persona`,\n`cotal_personas` and `cotal_despawn` with `spawn`, `cotal_run` with `run`, and `cotal_reconnect`.\nIts manager calls exchange the conversation's own secret for a manager-call view, so they carry only\nits grant and name the manager instance when more than one could answer. A conversation is not a\nmanaged seat. `cotal_despawn` without a name refuses rather than stopping your app, and\n`cotal_disconnect` retires only that connection. No run turn is addressed to it, because a workflow\naddresses turns to agents it spawned, so `cotal_yield` is not offered. Persona text and model pins\ndo not change the conversation's own instructions or model.\n\nThe plugin's ordinary command hooks supply the session and transcript binding. Its native module\nuses that binding without intercepting `classic.*` events, which Claude's built-in security guard\nwithholds from user plugins. The guard stays enabled. If an organization disables command hooks or\nnative modules, its administrator must permit them before this connection path can work. Command\nhooks still report lifecycle status and activity; the native module owns incoming message delivery.\n\nA signed-in identity keeps its request and secret in private local recovery state. A lost create\nresponse retries the same request without rotating its secret or allocating another identity.\nRetiring a request that is still pending drops it, and no identity is issued for it. An identity\nissued before connection approval keeps working, and its recovery stays exact. Another plugin server reuses that identity only after the process that held it no longer exists.\n`cotal_disconnect`, session end and a signal request retirement of that lifecycle only. Other\nconversations stay connected. When the mesh reports retirement pending or does not confirm it, the\nconversation stays disconnected and keeps its saved record. Every tool then reports the same\nunfinished cleanup with the mesh's retirement phase. Calling `cotal_disconnect` again retries it at\nonce. `cotal_connect` retries it too, and connects with a new identity once the mesh confirms\nretirement. A restarted plugin server recovers the same way. After each connection, the plugin\nretires the same mesh's saved identities whose plugin server process no longer exists, such as one\nkilled before it could retire them. This runs in the background, so the connection does not wait\nfor it. A server process that still exists keeps its identity, and a retirement that does not finish\nstays saved for a later run. A mesh requiring events refuses a connection without a native\ntranscript source.\n\nThe gateway's MCP annotations distinguish read-only queries from updates. Orientation, connection\nstatus, the roster, the docs and channel information are read-only. Sending messages, connecting and joining\nchannels are additive. Inbox reads and waits consume messages; status, channel mode, leaving and\ndisconnecting can replace or remove state. These hints do not grant access or override Claude's\npermission settings or an organization's approval policy.\n\nA message for a connected conversation starts a turn through the plugin's\n[native hooks module](https://code.claude.com/docs/en/plugins/mods/overview). This works in supported\nClaude Code hosts, including local sessions in the Desktop app's Code tab. The module runs a local\nwake reader as a child of the same host that runs the MCP server. Each server uses its own socket\nin a private directory. Short messages arrive in full with their sender, message ID and reply\ncontext. Claude can answer these without calling `cotal_inbox`. The module uses Claude's\nplugin-sender attribution, never the person's own identity. While Claude is busy, delivery happens\nat a main-conversation tool boundary before the next request. A request already being sent has a\nfixed message snapshot. A later arrival waits for another tool boundary or starts its own attributed\nturn when the current turn ends; storing a note alone does not count as delivery to that request.\n\nEach inline batch carries at most eight whole messages. Each rendered message must fit within\n1,024 Unicode code points, and the encoded socket frame must fit within 4,096 characters. Larger\nmessages remain queued. A notification-only reader or an oversized message uses\n`Cotal: a new message for connection_id <id>. Read it with cotal_inbox.` and a bounded preview.\nA preview is incomplete; Claude reads its contents through the inbox. An inline batch also names\nhow many other messages remain pending.\n\nThe transcript shows a compact Cotal indicator. Expand it to inspect the delivery or preview.\nThis changes only the display. Presentation observes only prompt rows; in-turn appends use their\nreturned row ID, without observing protected notes or skill output.\n\nSocket writes and previews do not acknowledge messages. A complete inline batch is committed only\nafter the native host confirms admission to the bound conversation. An idle prompt also needs its\nstored prompt row; a busy append needs its returned row ID and complete stored text. An offer waits\nfor a busy conversation's next safe boundary while its reader remains connected. Admission after\nclaim has its own deadline. This confirms host handoff, not that\nClaude understood or completed the work. Delivery remains at least once after an unknown result.\nMessage IDs distinguish retries from new requests.\n\nA wake follows the rule `cotal_wait` follows and does not compete with an active wait. Confirmed\ninline delivery rearms the next wake without an inbox call. A replacement reader reoffers unread\nwork. An offer from a closed connection cannot suppress its replacement connection's messages.\nClaude's Stop and organization controls still apply. Refused delivery stays pending without\na retry loop; genuine person-origin input resumes it with a fresh offer. `cotal_connect` reports\nwhether the incoming reader is attached; attachment alone does not prove native admission.\nWhen native modules are unavailable or blocked, the conversation can listen with `cotal_wait`.\n\nClaude Desktop chat does not run a plugin's local server. On macOS and Windows, where Claude Desktop\nis installed, `cotal setup` adds a `cotal` server to Desktop's own config,\n`claude_desktop_config.json` (Settings > Developer > Edit Config), and keeps every other entry there.\nIt runs the same server with `--desktop`, by the absolute paths of the node that runs the CLI and of\nthe plugin copy under `~/.cotal/claude-plugin`. Quit and reopen Claude Desktop to load it. A repeat\n`cotal setup` and `cotal update` keep the entry current, `cotal status` reports it, and a config\nfile that is not valid JSON is refused and left as it was. Setup can use Desktop's bundled Code\nexecutable when there is no standalone `claude` CLI. Its installed MCP and command hooks use the\nrecorded Node executable, so the app need not find Node on `PATH`. On Linux, setup writes nothing\nfor Desktop Chat.\n\nDesktop Chat supplies no native Code transcript binding, so this local server cannot join a\nsigned-in space, such as one on Cotal Cloud, and refuses before identity issuance. Desktop Chat joins\na signed-in space only through the hosted connector that the space's discovery document advertises\nas `cloudCapabilities.hostedConnector`, version 1 with an HTTPS URL. Add that URL in Claude under\nSettings > Connectors > Add custom connector. Setup for a signed-in space writes no Desktop entry and\nprints the URL instead. `cotal status` prints it on the Claude Desktop row when the selected space is\nsigned in, and the refusal names it. When the space advertises none, all three say so. The Desktop\nserver's instructions tell the model to use the hosted connector for a signed-in space. The hosted\nconnector is a separate connection path; it does not supply autonomous chat wake.\n\nDesktop's working directory is not your project, so its mesh is the `cotal use` mesh, else the only\none running. Desktop runs no monitor, so a Desktop conversation hears messages when it calls a tool,\nand keeps calling `cotal_wait` when you ask it to keep listening. A chat with no turn running cannot\nbe woken by any server: Claude's chat surfaces start a turn only from the person.\n\n## How messages reach the session\n\nDurable deliveries land in the connector's inbox from JetStream consumers\n([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding)); live channel traffic can instead arrive\nthrough an at-most-once core subscription. A durable message sent while the agent is busy\nor offline waits on the stream. Two things move a message from inbox to model; one\ndelivers, the other only wakes:\n\n- **Hook drain (delivery).** `SessionStart` / `UserPromptSubmit` hooks read automatic inbox items and\n inject them as `additionalContext`. This is the single authoritative path: deterministic and works\n on any Claude Code build. Quiet ambient is excluded and stays buffered for `cotal_inbox`.\n A message is **acked only once the hook reply carrying it has cleared both legs of its journey**:\n the connector's control socket to the hook process (which gives up after 2s), and the hook\n process's own stdout to Claude Code (which it force-exits 1s after starting to write). The relay\n sends a receipt back down the control socket from that stdout write's callback, and only on a\n clean write (a runtime whose pipe has gone away fails it), and the connector treats that receipt,\n not its own socket write, as delivery. So a large injection killed mid-flush, or one written to a\n broken pipe, leaves the message un-acked and JetStream redelivers it. What this does *not* prove is\n that Claude Code read or applied the reply: a payload small enough to fit the pipe buffer is\n reported written the moment the kernel takes it. That residual is why the path errs toward\n at-least-once rather than treating a confirmed write as a confirmed read. Acking when\n the reply was merely *formatted* meant a lost reply was a lost message: it was already marked\n handled, so its own redelivery was silently acked on arrival.\n A hook whose handler throws still returns an empty reply so the session is never blocked, and\n that reply carries nothing, so it commits nothing: the batch it had started to surface stays\n un-acked and goes out on a later frame. The seat also drops any `turn-pending` row that breaks\n the manager contract, such as one with no integer deadline, and says so once in its log. A reply\n with no `turns` array changes nothing: the seat keeps the turns it already holds.\n This errs toward **at-least-once**: if a reply lands but its confirmation does not, the batch is\n surfaced again and flagged as a possible repeat. A duplicate injection is noise; a buried DM stops\n the peer answering at all.\n- **Channel nudge (wake).** An arriving message fires a `notifications/claude/channel`\n event that wakes an *idle* session into a turn, so the drain runs *now* instead of at\n the next prompt. The nudge never acks anything. A nudge that the host rejects is retried with a\n bounded backoff while anything is still pending. For an idle session it is the only wake source,\n so dropping it means silence until someone types. When the channel becomes active, the connector\n first re-fires a focus mention remembered during startup, otherwise one buffered wake. A rejected\n push keeps its bounded retry, and JetStream redelivery remains the durable backstop for unacked\n inbox items. Neither a redelivery nor that retry repeats a nudge already pushed for that message,\n whether the message had its own nudge or was counted in a batch one, so a session held in a long\n tool call gets one nudge per message. Once a hook frame carries the message, or the push that\n announced it fails, its next redelivery nudges again, so a reply that never reached Claude Code\n still recovers. If the channel cannot run at all, delivery still waits for the next hook. Live-only\n traffic has no durable retry.\n\n**Two priority tiers.** A *directed* message (DM, anycast, or a channel message that\n`@mentions` us) always nudges. *Ambient* channel chatter does not nudge mid-turn; it\naccumulates, and the `Stop` \u2192 idle transition fires one batch nudge so the backlog drains\ntogether.\n\n**Constraints (accepted).** Channels are a Claude Code research preview (\u2265 v2.1.80;\npermission relay \u2265 v2.1.81): Anthropic auth only, admin-enabled on Team/Enterprise, and a\ncustom channel needs the `--dangerously-load-development-channels` launch flag. The hook\ndrain does not depend on any of that; the channel only adds \"wake me when idle.\"\n\nThe same channel also relays **tool-permission requests** onto the mesh, so a peer (a\nhuman at the CLI, a policy node) can approve or deny an agent's pending tool call through\nCotal rather than a per-terminal prompt.\n\n### Attention\n\nAn agent picks how aggressively peer traffic reaches it with\n`cotal_status({ attention })` (three modes, orthogonal to presence):\n\n| arrival | open (default) | dnd | focus |\n|---|---|---|---|\n| directed (dm / anycast) | wake + inject | wake + inject | wake + inject |\n| channel `@mention` | wake + inject | wake + inject | ack-drop; wake to *pull*; not injected |\n| ambient channel chatter | wake when idle; hold while working | never wakes; injects next turn | ack-drop; recall via `cotal_inbox` |\n\nPer-channel overrides refine this: **quiet** (delivered, never wakes; `@mention` still\nwakes) and **muted** (dropped on receive, mentions included; DMs/anycast unaffected), set\nwith `cotal_channel_mode` or as agent-file defaults (`quiet:` / `muted:`,\n[agent files](agent-files.md)). A per-channel override is the final word for that channel.\nQuiet ambient is pull-only: it never hitchhikes on a human prompt, DM, mention, or other\nconnector-driven turn. `cotal_inbox` explicitly surfaces and clears it. A quiet-channel\n`@mention` remains automatic and injects normally.\n\nA pull is bounded too, and clears only what it hands over. One `cotal_inbox` call carries at most a\nreceivable window (direct messages and role requests first, then channel traffic, replayed history\nlast); whatever does not fit stays buffered, is named in the reply, and comes back on the next call.\nA message too large for one whole response is delivered in parts: once no smaller mail is waiting,\neach call carries its next part, and it is cleared only after its last part goes out, because clearing\nwhat was not handed over is the loss this bound exists to stop.\nThat matters most on the path where it is easiest to lose mail: reconnecting brings a channel-history\nreplay with it, so the largest payload and the least expendable message arrive in the same read.\n\nThe local inbox is bounded. On pathological overflow it evicts pull-only items first, then other\nchannel traffic, and a direct message or role request only when the whole buffer is directed mail.\nIt never evicts a message the connector is handing to the session or a turn is still carrying.\nAn evicted channel item is acknowledged. An evicted direct message or role request never is, and\nthe broker redelivers it after the ack wait until a redelivery finds room. A direct message stays\npending on the session's DM durable, where `cotal deliver pending <name>` counts it. A role request\nstays on its role's shared queue, which that command does not read. A full inbox therefore delays\ndirected mail until the session drains it.\nIf the bounded live/durable classification guard also fills, the connector fails closed:\notherwise-normal ambient becomes pull-only until restart. Muted hard-drop and normal focus recall\nstill take precedence. Focus also keeps a bounded exclusion list so mode toggles cannot recall\nquiet/muted traffic; if that safety bound fills, recall skips the affected channel and reports it\nas incomplete rather than risk resurfacing excluded content. Recall cannot tell one message with an\nempty id from an identical one with another disposition, so in focus such a message is held in the\nlocal inbox as pull-only instead of being dropped, and a mention of it still wakes the agent. When\nthe session settles an id-less copy, it reads the chat stream's last sequence. Identical copies\narrive in stream order, and those reads can answer out of order, so the first read that can see the\ncopies binds them in arrival order, latest first, each to the latest unbound stream copy at or below\nthe lowest sequence read for it or any later identical copy.\nWhile the connection stays up, every stream copy at or below that sequence reached the session\nfirst, so a later identical copy sent during a reconnect gap is above it and stays unbound. A copy\nthat arrives while a read runs may not be in that read, so it binds nothing there and recall reads\nthe channel again, up to three times; if it still could be a stream copy in the last read, recall\nleaves that stream copy in the stream and reports the channel as incomplete. A settled copy a\ncomplete read cannot bind is behind the focus start or out of retention, and is forgotten. Recall\nhands back into the inbox only the stream copies nothing is bound to, such as one sent during a\nreconnect gap or one the inbox evicted on overflow, and `cotal_inbox` hands each over once. Overflow\nfrees only the evicted copy, held or quiet, and a copy no read has bound yet keeps its place in\narrival order, so an identical muted copy stays out of recall. When\nthe inbox is full, recall leaves them in the stream for a later call and reports the channel as\nincomplete. A history read that fails, or a channel with replay off, settles nothing and is reported\nas incomplete, and recall calls run one at a time. If the sequence read for a settled copy fails,\nor answers only after the connection dropped, recall skips that channel for the rest of the focus\nperiod and reports it as incomplete. Recall cannot tell a late copy of a message it handed back from\na new identical message, so every copy takes its own disposition: a new identical quiet mention is\nstill delivered automatically, and a late copy can surface a second time.\nA recalled message that already went out in part is read to its last part, even if an exclusion\nlands after its first part. One session reads its inbox one call at a time: a `cotal_inbox` call\nthat overlaps another waits for it to finish, so neither decides from a view the other has already\nmoved past.\nIf the separate hard-drop disposition guard fills, channel traffic is dropped for the rest of the\nsession rather than risk a late copy bypassing an earlier muted/focus decision; DMs and anycast are\nunaffected.\n\nAttention is **advisory UX, not a boundary**: any peer can wake a dnd/focus agent by\nnaming it, and `muted` means \"I opted out of receiving\", not \"the channel is blocked\";\nthe broker still authorizes and delivers. Focus's real effect is shrinking the\nuntrusted-ambient injection surface (only subject-authenticated dm/anycast auto-inject).\nIt resets to **open** on `SessionStart`, so a restarted agent never stays silently deaf.\nYour attention is mirrored into presence so peers can see it.\n\nWhatever does reach a turn is framed so a peer cannot write the frame. A line that begins at column\nzero is written by the connector; one message is one line plus indented continuations, with the\nsender inside a single bracket pair. A message body, a sender name and role, and a service or\nchannel label are all peer-controlled, so each passes through the same neutralization the\n`cotal_inbox` reply uses: no line break a splitter may honour and no bracket survives into a\nrendered attribution. This matters more for an injected block than for a reply, because the agent\ndid not ask for it and so never had the chance to distrust it.\n\n## Presence mapping\n\nThe connector wires a small subset of Claude Code hooks to presence states; presence is\ncoarse, and \"what it is doing\" rides on activity updates. Presence is **advisory**: a presence\npublish that fails (the endpoint mid-reconnect, say) is swallowed and never prevents the same hook\nfrom delivering messages or flushing held ones.\nA `SessionStart` during an open turn, including compaction, preserves the current `working` or\n`waiting` status until `Stop`, `StopFailure`, or `SessionEnd` closes the turn.\n\n| Hook | \u2192 state |\n|---|---|\n| `SessionStart` | `idle` only when no turn is open (join; surfaces the inbox; captures the live model into `meta.model` when no pin) |\n| `UserPromptSubmit` | `working` (turn starts; surfaces the inbox) |\n| `PreToolUse` | no change; records *what* is about to run, so a permission wait can name it |\n| `PostToolBatch` | `working` when the main agent's batch resolves after a prompt left the seat `waiting`, which clears its condition so the turn's `Stop` yields; a subagent's batch (it carries `agent_id`) or one relayed after the turn's terminal changes nothing |\n| `Notification` (`permission_prompt` / `agent_needs_input`) | `waiting` with condition `approval` / `input` (activity leads with the pending tool, e.g. `Bash: git push \u2026`) |\n| `Stop` / `StopFailure` | `idle` (turn done / died on an API error; flushes anything held while busy). `StopFailure` also relays Claude Code's native error value as `condition.source` and maps it to the closed condition vocabulary. On the [event plane](#event-plane) it closes the run with `RUN_ERROR`. |\n| `SessionEnd` | `offline` (graceful leave) |\n\nThe connector also leaves gracefully when its stdin closes. An MCP client closes it to end the\nsession, and a killed `claude` closes it with no `SessionEnd`, so a dead session drops off the\nroster instead of staying on it as a live peer.\n\n`StopFailure` maps `rate_limit` and `overloaded` directly; auth and credential failures to\n`auth`; account and billing failures to `billing`; `invalid_request` to `request`;\n`model_not_found` to `model`; `server_error` to `server`; `max_output_tokens` to `context`; and\n`unknown` to `failed`. The native value remains in `condition.source`.\n\nHooks are relayed over the connector's **authenticated** local control endpoint (per-user\nsocket + per-launch token, constant-time checked), so a local process that finds the path\nstill can't drive presence or stop the agent. Claude Code's own\n[hooks reference](https://code.claude.com/docs/en/hooks) lists every hook event. The hooks the\nplugin registers, with their matchers, are listed in the\n[adapter README](../extensions/connector-claude-code/README.md#claude-code-hooks).\n\n## Event plane\n\nA connected native session publishes a structured account of its activity: user messages,\nassistant replies, run boundaries and tool calls with their inputs and results. Reasoning is off\nby default. Programs can read these events directly. The launcher sets `COTAL_EVENTS` by default; pass `--no-events` to opt out on an unrestricted\nspace. A user-auth registration with `policy: { events: \"required\" }` carries `eventsRequired` in the\nprivate launch material, so the connector arms even without `COTAL_EVENTS`; `--no-events` is refused.\nA hand-driven user-mode session may carry the same decision as `COTAL_EVENTS_REQUIRED=1`. Its own\npublish grant must cover `events.<owner>.<actor>` or the connector refuses before joining. A personal\nnative Code connection to a signed-in mesh uses the same recorder and captures its transcript\nboundary after approved issuance, before joining. Earlier history and time spent waiting for\napproval are not published.\nIts native lifecycle drives presence and recording for that session only; a subagent or another\nconversation cannot end its main run. The event channel keeps the space's normal read permissions.\nA personal connection makes manager calls only with its own selected grant.\n\nIf the event plane stops, the seat keeps running and its mesh connection, on every connector and\non every space. The connector log records `AG-UI emitter stopped` with the reason and the delay\nbefore it re-establishes the plane: at once after a failure, then every second while the plane\nstays down, a fixed wait that neither grows nor is capped, so the plane is back within a second\nof the fault clearing. The re-establish\nis a fresh start: it reopens the write-ahead log, settles any frame it left pending, builds the\nmapper from the run the log has open, and reads on from the stored cursor, which is the same\nrecovery a connector restart runs. A failed publish or a failed log write is never retried on the\nemitter it failed on. A turn that ends while the plane is down keeps its terminal, and so does a\nturn whose terminal's own log write failed: once the re-establish has read on, the run the log had\nopen is closed with that terminal, a failure as a failure, rather than left open for the next\nprompt to close as a finish. That holds when the next prompt was already written before the plane\ncame back: the failure is published after the failed turn's own records and before the next run\nopens, in a frame of the failed run, since a run error names no run and the frame is its only\nattribution. Every turn that ends while the plane is down keeps its own terminal. A terminal belongs\nto the last turn the transcript had begun when its hook fired: the connector reports a turn's\nterminal once that turn's records are written and before the next turn's are, so the position the\ntranscript has reached at the hook names the turn, including a turn the connector opens no run for,\nsuch as a task notification, which takes its terminal and publishes nothing. A terminal repeated\nfor a turn that has one replaces nothing and is kept for no later turn. A line the connector cannot\nread, written past that position while the plane was down, does not hold the terminal: the\nre-establish reports the gap and places the terminal after the records before it. A record no read\nanswers, such as an OpenCode part still filling when its turn died, holds it no more: the terminal\nis placed after the records before it, and the turn end reads the session once rather than without end. The OpenCode source reads every record at or before the end it reported at a turn end as settled, so a part left filling by a failed turn does not stop the turns after it from publishing. A terminal reported while the first start is still waiting on the broker reaches the emitter before its first read, so a prompt written in that window does not close the failed turn as a finish. The same placement runs with\nthe plane up: a terminal reported while the flush before it is still reading, with the next prompt\nwritten before that read, closes its own turn in the place of the finish that prompt synthesizes. The seat used to exit on a space that requires events; for Claude Code the\nconnector is the MCP server, so that exit showed as the server disconnecting, and Claude Code\nrestarted it on the next tool call.\n\nA transcript line that is not a record does not stop the plane. Claude writes the transcript, and a\nwrite cut short by a full disk leaves part of one record glued to the next on a single line, which\nnever becomes readable. The connector steps over that line, keeps the records after it, and writes\n`skipped an unreadable transcript record at offset <offset> (<bytes> bytes)` with the parser's\nreason to its stderr.\n\nA new session includes its first run even when Claude writes a positional startup prompt before the\nconnector receives `SessionStart`. That from-zero read is keyed only to Claude's explicit\n`source: \"startup\"`; resumed, forked, cleared, and compacted sessions adopt at the transcript boundary\ncaptured at that adopt, before the mesh link connects, so nothing Claude appends while the connector\nis still starting up lands behind the cursor and is silently dropped. Crash recovery follows the\ncursor already stored in the event write-ahead log, regardless of the new process's startup label.\n\nClaude starts each hook in its own process, so a prompt or stop relay can reach Cotal before the\n`SessionStart` relay. The connector holds those event flushes and the terminal until `SessionStart`\nsupplies the source, then enqueues adopt, flush, and close in that order.\n\n`SessionStart` can also run before the connector process has bound its local control socket. The hook\nthe `SessionStart` relay retries only transient pre-connect listener errors, with capped backoff\ninside its existing two-second budget. Later hooks and permanent local faults still fail open\nimmediately. Once a socket has connected, a broken exchange is not retried: the connector may\nalready have handled the frame, so replaying it could apply one lifecycle event twice.\nThat retained `SessionStart` can itself arrive before Claude creates the transcript path. A genuinely\nnew startup waits up to five seconds for that file with capped backoff, and the same deadline bounds\none stalled file read; expiry fails loud instead of silently losing the first run. A forked session\ngets the same wait, because Claude copies the parent transcript into the fork's own file after the\nhook, and then adopts at the end of that copy. Resumed, cleared and compacted starts and recovered\ncursors still require their existing source at once.\n\nTool arguments (`TOOL_CALL_ARGS`) and tool results (`TOOL_CALL_RESULT`) are not republished\nonto this channel. The durable emitter drops those events before they are written to the\nwrite-ahead log, because this channel's read ACL is not the ACL the tool ran under. Content is\nmandatory on both kinds, so the event is suppressed rather than emptied or replaced with a\nplaceholder. Tool start and end still go out. A restart that finds a pending pre-fix frame\nstill carrying those kinds HALTS rather than republishing it.\n\nThe channel is **`events.<owner>.<actor>`**, named after the session's principal. What the actor\nhalf is depends on the mesh, and the difference matters when you go looking for it: on a static mesh\nit is a key the manager allocated, never the display name, so two live agents sharing a display name\ndo not share a stream; on a user-auth mesh it is the agent's own name, because that is what the\nledger row is keyed on. Spelled out again with both halves below. The launch grants publish rights\non that channel alone. A spawn\nthat asks for a *different* agent's event channel is refused at the door rather than granted, since\nthat channel is that session's event stream. The same rule runs on restart: a manager\nresume document that names another agent's event channel is refused rather than adopted, because the\nmanaged row is re-armed from that document and the credential is re-minted from the row.\n\nThe rule reads a **concrete** channel, two principal tokens and nothing else. A pattern such as\n`events.<owner>.>` is not an event channel to it and passes untouched, governed by ordinary ACL\nauthority: on a user mesh the delegation envelope, on a static mesh the spawning credential itself.\nThat is deliberate, because the pattern is the form an operator writes on purpose for an observer,\nand it is worth knowing rather than assuming the fence is total.\n\nTo let something else read a plane, grant it out of band. The refusal prints the command for the\nmesh it is running on, spelled out in full, and only that one.\n\nOn a **user-auth** mesh:\n\n```bash\ncotal actor grant <reader> --owner <owner> --scope '' --allow-subscribe 'events.<owner>.<actor>' --allow-publish ''\n```\n\nEvery field, deliberately. `actor grant` is an upsert of the whole row, so it refuses a grant that\nleaves off any of the three ACL flags. Only `--full` turns an omitted flag into the wide default\n(`>` read, `>` post, `spawn,role:default` scope), which is the opposite of what a scoped watcher is for.\n\nOn a **static** mesh there is no actor ledger for `actor grant` to write to, and the refusal says\nso; mint the reader instead:\n\n```bash\ncotal auth mint watcher --profile agent --allow-subscribe 'events.<owner>.<actor>' --provision\n```\n\nThe **agent** profile, not the observer one. `mint` reads `--allow-subscribe` only for that\nprofile, and refuses it anywhere else: `--profile observer --allow-subscribe <channel>` exits\nnon-zero and writes no creds file, because the observer profile carries a fixed read set over the\nwhole chat plane, which is the opposite of what a scoped watcher is for. The agent profile also prints the lifecycle uid the\nreader needs, since an authed consuming endpoint refuses to start without one.\n\nOn an **open** mesh there is nothing to grant: the mesh has no credentials and no ACLs, so any peer\nthat lists the channel reads it, and the refusal says so instead of naming a command. The\nown-channel rule still applies there, because a spawn is not the place to hand out a read on\nanother agent's tool inputs and outputs.\n\nTwo things a reader has to do that are not obvious, both on `CotalEndpoint`. It must pass the event\nchannel in `channels`: an endpoint reads the channels it lists, so one constructed without\nthe event channel joins nothing and the frames never arrive. And it reads history with `readHistory(channel)`, the delivery daemon's mediated read, not\n`channelHistory(channel)`: a scoped credential is denied the ad-hoc consumer the direct read\ncreates, by design. `cotal console` and the web console already do both.\n\nThe `<owner>.<actor>` pair is the session's principal. On a user-auth mesh the actor half is the\nagent's own name, so the channel is `events.<your-owner>.<agent-name>`. On a\nstatic mesh the owner half is the literal `local` and the actor is a key the manager allocated, so\nthe channel is `events.local.<key>`; the spawn reply carries that key as `id`. Note\nthat `cotal console` and the web console keep event channels out of their channel lists on purpose,\nsince a plane is a machine feed rather than a conversation; they draw the frames when you open the\nchannel by name.\n\nThe rule governs the manager's doors, which are the ones a caller other than you can reach. A\nforeground `cotal spawn` on your own machine mints from your own signing material, so it can still\ngrant any channel you name: that is the out-of-band grant, not a way around the rule.\n\n**Failed turns publish run errors.** Claude Code decides for itself\nwhether a turn finished or died and fires one of two hooks accordingly, so the connector relays that\ndecision rather than making one of its own: a turn that ended on an API error ends its run with\n`RUN_ERROR` carrying the fixed message `run failed` and no code. Neither the detail Claude Code\nreported nor its error kind is published there: both are upstream values that can echo your prompt or\ntool output, and the events channel has a different read ACL. The error kind still reaches presence\nas the agent's condition (`rate_limit`, `auth`, `billing` and the rest). A turn that ended normally still\nends with a run-finished event carrying no outcome, which says the turn ended and does not claim it\nsucceeded.\n\nEvents are written to a per-session write-ahead log before they are published, so a hook that fires\nafter a restart resumes at the cursor it left rather than replaying or skipping, and a run that was\nopen when the session stopped is closed rather than left dangling.\n\nA delayed initial connection or a broker reconnect pauses event publication without closing MCP.\nThe recorder waits for both the mesh binding and its transport; unpublished records stay in the\ntranscript until the connection recovers. Stopping the connector cancels that wait. A recording error,\nincluding a conflicting transcript or damaged event state, stops the publishing plane and not the\nsession: it is reported with the delay before the next re-establish, on every space, and the seat keeps\nits mesh connection. A second transcript path offered to a bound recorder is refused the same way, and\nthe first path stays bound.\n\nOne channel carries **every session of one agent**, because it is named after the principal and not\nafter the session. Alongside the per-session logs the connector keeps one small record per principal,\nholding the last sequence the broker assigned on that channel, so a new session continues the stream\nits predecessor left instead of starting again from nothing. Both live under the events state root\n(`COTAL_WORKSPACE_ROOT`), and neither is something you edit by hand.\n\nA **missing** record is not a fault: the connector rebuilds it from the session logs beside it,\nwhich is how an agent that was already running before this record existed keeps its stream. That\nrebuild stops if any one of those session logs is damaged. Unreadable, not valid JSON, and written\nfor a different principal all count, and so does a session directory or a log that is a link rather\nthan the real file the connector wrote, or a log that has more than one name. A tip taken from the\nrest would be too low, and it would stop publication later with nothing left to point at the cause.\nThe connector names the file instead, and the only way past it is the directory removal described\nbelow, under the same condition. A record that **disagrees with the broker** is a fault, and the\nconnector stops publishing and says why rather than guessing. A record that **moved while a session\nwas writing to it** is refused the same way: it means something else wrote the principal's record,\nand the connector reports which value it held and which the file holds rather than writing over the\nlater one. There is no command to clear it. The state is the principal's directory under the events\nroot, and clearing it by hand means removing that directory whole: the sequence, the cursor and the\nper-session logs only mean anything together, so removing part of it leaves a state the next start\nrefuses. Removing it is only half a remedy, and the half that comes first is the channel. The\ndirectory is where the agent's memory of the tip lives, not the tip itself, so on a channel that\nstill holds frames the next session opens expecting an empty one and stops on the same\ndisagreement, with the logs a tip could have been rebuilt from now gone. Purge the channel first,\nthen remove the directory.\n\nReading it: `cotal console` and the web console draw event frames directly. A frame carries no text\npart by design, so a surface that renders a message as flat text shows a marker instead of prose.\n\n**On a per-user-auth mesh, the default event plane needs the spawner's grant to cover the channel.** The event\nchannel is added to the child's publish set, and delegation only narrows: an agent may hand down\na subset of what it holds and no more. So a peer-initiated spawn is refused unless the\nspawning identity's own grant already covers the child's event channel. The refusal prints the\nexact `cotal actor grant` command that widens it. An operator launch, whose chain reaches an\nadmin-scoped or roster row, is unaffected. Passing `events: false` is the explicit opt-out.\n\nArming the event plane through a typed spawn request (`manager.spawn` with `events`, including\nthe CLI's `cotal spawn --detach --events`) is allowed on a user mesh when the child runs under\nthe caller's owner. This also satisfies a registration policy that requires the plane. A spawn\nunder another owner still needs the caller's admin tier. Without it, an explicit request or a\nrequired plane is refused before provisioning. A silent non-owner caller in a space without that\npolicy has the plane disarmed with a notice in a successful reply. Later provisioning can still\nrefuse the spawn. The child's own-channel rule and the ledger's delegation envelope still apply.\n\n## Resume a session\n\n`--resume <session-id>` pulls an existing Claude session, its context and transcript,\ninto the mesh. It **forks**: Claude mints a *new* session id from that transcript\n(`--resume <id> --fork-session`), so the meshed agent gets its own session and the\noriginal is untouched.\n\n- `cotal spawn --resume <id>` (foreground) is the primary surface: the transcript is on\n *your* machine, and errors are Claude's own stderr, inline.\n- `--detach --resume <id> --on <instance>` carries a session held on *your* machine to that\n manager instance, which may run on another host. The CLI finds the transcript under your\n Claude config (`~/.claude`, or `$CLAUDE_CONFIG_DIR`), sends it through a JetStream Object\n Store bucket only that instance reads, under a writer credential pinned to that one transcript,\n and prints `carried session <id> to <instance>:\n sha256:<hex>, <sent> of <size> bytes sent in <chunks> chunks`. Without `--agent`, the CLI first asks that\n instance which harness the persona resolves to, so a persona with `agent: claude` is carried even\n when your `COTAL_DEFAULT_AGENT` names another connector. A re-run of the same bytes\n sends nothing, and an interrupted carry continues where it stopped. The seat forks it in a\n private Claude home under the manager's `.cotal/seat-homes/`, which no other seat's Claude\n lists or finds, and which is removed when the seat stops. When Claude starts the fork, the seat\n records the SHA-256 of the transcript it read from its own project; the manager stops a seat\n whose record names other bytes than the carried ones, or that records none within the join\n timeout after it joins, an uncertain launch included, and otherwise shows that record as the\n seat's provenance. `cotal attach` to such a seat names its source after the seat\n name, as `(resumed from <host>:<id>)`. A remote manager receives a carry when its host issues it a\n transfer reader. On a user-auth mesh the CLI exchanges the operator's login for a one-object\n `transfer-writer` view, which needs scope `admin`.\n- A session name in place of an id is refused, listing each session on this host that carries\n that name with its id, SHA-256 and modification time. An id this host does not hold resolves\n against the **manager host's** `~/.claude`, as before.\n- A seat-private home holds no login. The manager host needs `CLAUDE_CODE_OAUTH_TOKEN` (from\n `claude setup-token`), `ANTHROPIC_AUTH_TOKEN`, or a cloud provider selection in its\n environment; `ANTHROPIC_API_KEY` alone is refused. The private home trusts the authorized\n launch directory without changing the manager's home. Claude must be 2.1.234 or later.\n- The manager waits for a real outcome: `\u2713 started` means the agent *joined the mesh*,\n `\u2717 exited on launch` carries Claude's last output and the seat's exit diagnostic, such as a\n startup confirmation that never appeared, and an uncertain launch (~30 s) is reported without\n tearing the agent down.\n- Resume is an **operator surface only**, deliberately not exposed on MCP `cotal_spawn`\n (a mesh peer naming host-local transcripts would widen `spawn` into transcript\n disclosure). Only the Claude connector supports it today; OpenCode and Hermes fail loud.\n- Needs a `claude` new enough for `--resume \u2026 --fork-session` (verified on 2.1.197).\n\n## Sharing your MCP servers\n\nA spawned session loads the MCP servers your normal Claude Code configuration enables on the\nlaunch host: user, local, project, plugin and connected-account sources. Claude handles discovery,\nprecedence, disabled servers, project approval and managed policy. Cotal adds its mesh server\nand any overrides declared in `connectors.claude.mcpServers` ([format](config.md)). It does not\ncopy native server definitions or credentials into the launch file or onto another machine.\nThe selected `CLAUDE_CONFIG_DIR` remains in effect for an ordinary local spawn.\n\nFirst-run setup still seeds eligible user-scope declarations into the Cotal config and preserves\nan existing list. That list is not the source of truth for native discovery. A same-named declared\noverride takes precedence; changing the native definition does not rewrite an override you kept.\nAn empty Cotal sharing map leaves native discovery enabled.\n\nUse `--share-tools tavily,figma` to load only those declared servers plus Cotal, or\n`--share-tools none` for Cotal alone. These explicit selections use `--strict-mcp-config`.\nCarried sessions also stay strict and keep their seat-private home; operator profiles and\nconnected-account credentials are not copied into them. Native managed policy still applies.\n\nThe child environment stays allowlisted. References in declared shared servers and the selected\nuser-owned MCP configuration forward their named variables. Repository and plugin files cannot\nadd ambient secret names merely by referencing them; supply such names through an explicit\nshared declaration or the existing `spawn.env` policy. Native files retain their own configured\nvalues. Documented native MCP switches, including the connected-account opt-out and tool-search\nchoice, keep their explicit values. Cotal does not forward the entire parent environment or force\ntool definitions to load upfront.\n\nSharing a credential grants it to the session, so enable only servers the session may use.\nEach enabled server may start a process per seat; an explicit sharing selection can reduce that\ncost. Native discovery does not change Cotal's mesh identity, hooks or channel wake path.\n\n### One Cotal path per session\n\nA spawned session is on its mesh through the local Cotal server. A connected-account source can\nalso give it a hosted Cotal connector, which would join the same session a second time under\nanother name. The plugin's `PreToolUse` hook refuses every `cotal_*` tool of any server other than\nthe local one in a session Cotal launched, with the reason in the refusal. It decides in the hook\nprocess, so the refusal holds when the control socket is down. A Claude Code session that Cotal did\nnot launch keeps the hosted connector. Where the plugin is installed, its local server's\ninstructions tell the model to use the local path unless you ask for the hosted one.\n\nIn such a session the plugin also keeps each hosted connection with the session that opened it. A\n`PostToolUse` hook on the hosted `cotal_connect` records the returned `connection_id` under the\nplugin's data directory (`CLAUDE_PLUGIN_DATA`), and `cotal_disconnect` releases it. A fork made with\n`--fork-session` inherits that id in its transcript but has a new session id, so its calls with the\nid are refused and it is told to call `cotal_connect`. A resumed session and its subagents keep the\nsession id and pass. An id the plugin never saw opened goes to the hosted connector, which checks\nthe account.\nThe guard runs only where the `cotal` plugin is installed. A Claude Code session with only the\nhosted connector has no hook, so nothing records its connections and its forks are not refused.\n\nA hosted connector cannot start a turn, so a hosted conversation hears a message only when it calls a\ntool. `cotal_wait` holds until a message that would wake the agent arrives and returns it, or returns\nwith none after the service's bound. The conversation calls it again to keep listening. Every other\nhosted tool adds a line to its result when messages are waiting. A conversation with no turn running\nis not woken.\n\n## Host sessions in one process\n\nA host serving many managed seats can run their MCP servers in one process with the subpath\n`@cotal-ai/connector-claude-code/mcp`, which loads with both `import` and `require`:\n\n```ts\nimport { serveClaudeSession } from \"@cotal-ai/connector-claude-code/mcp\";\n\nconst session = await serveClaudeSession({ env, input, output });\nawait session.close();\n```\n\nLoading the module only exports it; it serves its own stdio only when run, as `node dist/mcp.cjs`.\nThe subpath ships CommonJS declarations so TypeScript hosts can use either module format,\nincluding CommonJS consumers with Node16 or Node18 module settings.\nClaude Code pushes `claude/channel` only to a stdio server it launched, so each `claude` still\nlaunches a stdio command, such as a small relay that pipes its stdio to the host.\n\n- **Managed only.** `env` is a complete launch environment, copied when the call starts. It must\n carry a mesh identity and the full control pair, or the call rejects before the session joins the\n mesh. The presence environment label, bearer commands, carried-fork records and feedback contact\n lookup use that copy. Feedback's Git fallback reads the session's global config, including its\n includes, without reading the host repository config. Set `COTAL_FEEDBACK_EMAIL` or pass the\n feedback tool's `email` argument to choose another contact. Include the `PATH`, `HOME` and other OS\n variables subprocesses need; the host's environment is not merged into them. Subprocesses still\n use the host process's working directory. A session's card does not: its `meta.cwd` is the `cwd`\n its own `SessionStart` hook reports on startup, resume or fork, absent until then, and unchanged by\n clear or compact. It is advisory display data that every reader of the space's roster can see. A plain `claude` with no identity is served by `mcp.cjs` run directly. Sharing a process\n does not isolate untrusted tenants or their files.\n- **Streams.** `input` carries JSON-RPC from the client and `output` carries this session's replies\n and `claude/channel` pushes. The caller owns both streams. Input that has emitted `end` or is\n destroyed rejects startup. Buffered unread input remains eligible. When an active session's\n `input` emits `end` or `close`, the session closes, including after a relay connection resets.\n Teardown may pause `input` and removes the transport's error handler. A host that reuses the\n stream must resume it as needed and keep its own stream error handlers.\n- **`fatalBind`.** Off by default: a control socket that cannot be bound rejects the call and the\n host keeps its other sessions. Only a process that is the session turns it on, as `mcp.cjs` does.\n- **Teardown.** `close()` closes the control socket, the wake policy, the AG-UI emitter, the mesh\n endpoint and the MCP server, and is idempotent. It removes its input listeners without closing\n the caller's streams. The returned promise rejects if teardown fails. A startup failure closes\n what was started before the call rejects. A manager shutdown frame or input EOF or closure closes the\n session and then calls `onShutdown(code)` once. A teardown failure is logged and reported with\n code 1, without becoming an unhandled rejection in the host. Close waits for a pending broker\n dial to settle. A dial that fails after shutdown starts is not a teardown failure. The session\n releases local inbox custody only after its connections are proved closed, so the same lifecycle\n can reopen after a successful close. An active duplicate or an unproved close retains custody.\n\n## Feedback\n\n`cotal_feedback` works out of the box: without a key it posts to the public intake at\n`https://cotal.ai/v1/feedback` (needs a contact email: `COTAL_FEEDBACK_EMAIL`, then\n`git config user.email`, else the agent asks). Set `COTAL_FEEDBACK_KEY=fbk_<key>` in a\nbeta tester's environment to route to the keyed intake (`Authorization: Bearer`, identity\nderived from the key); `COTAL_FEEDBACK_URL` overrides either endpoint. The CLI can send\ntoo: `cotal feedback \"<summary>\" [--type bug]`. Each submission carries\n`origin: human | agent`, whether the tester asked, or the agent auto-reported a major\nissue.\n"
|
|
27300
|
+
"body": "# Connect Claude\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\nThe Claude Code connector turns a real `claude` session into a Cotal mesh peer. A bundled\nplugin inside the session joins NATS, maps lifecycle hooks to presence, and exposes the\nmesh tools. Nothing wraps Claude; it is an ordinary session that happens to be on the\nmesh.\n\nThe shared mesh runtime (agent, `cotal_*` tools, hook relay) lives in\n[`@cotal-ai/connector-core`](../extensions/connector-core); this connector is the thin\nClaude-specific adapter over it. Its lifecycle hook imports the relay from the\n`@cotal-ai/connector-core/relay` subpath, so each hook process loads the relay and its environment\nreaders and none of the NATS client, zod or yaml. Siblings: [OpenCode](connect-opencode.md) (beta),\n[Hermes](connect-hermes.md) (alpha), [pi](connect-pi.md) (alpha); the\n[Connectors](connectors.md) matrix compares them feature-by-feature.\n\n## Set up\n\n> **Team and Enterprise accounts:** [Enable Claude Code channels](#enable-channels-for-your-organization)\n> before running unattended agents. Channels are blocked by default, so incoming messages cannot\n> wake an idle session until an organization Owner enables them.\n\n```bash\ncotal setup --configure-only # install the plugin and seed one agent without startup\ncotal up # brings up the mesh + delivery daemon + a detached manager\n```\n\n`cotal setup` installs the cotal plugin for your user account, so every Claude Code session on\nthis machine gets the `cotal_*` tools, in any project. That covers the terminal, the IDE extensions\nand the Desktop app's local Code sessions. Setup can use Desktop's bundled Code executable when no\nterminal `claude` command is installed. Desktop-only users should open a local Code session once so\nDesktop downloads that executable. Setup checks it through the connector's own readiness probe;\nmanaged `spawn` still requires the terminal `claude` command. With a local mesh already running, run setup once, reopen Claude\nand ask it to join your local mesh. No MCP configuration edits or special launch flags are\nneeded for this native plugin path. It also shares your own MCP servers with spawned sessions on its first run (see\n[Sharing your MCP servers](#sharing-your-mcp-servers)), and seeds one `default` persona; `cotal up` brings up the local stack so\n`cotal spawn --detach` / `cotal_spawn` work right away. Re-running either is idempotent.\nA repeat `cotal setup` and `cotal update` refresh the copy under `~/.cotal/claude-plugin` and update\nexisting Claude plugin registrations, including older cached installations. After an npm upgrade,\nthe first regular Cotal command also refreshes these copies before accepting the new generation. Setup,\nskills installation and refresh share a machine-local lock through the final plugin check, so simultaneous\ncommands cannot overwrite a generation while another command verifies it. Disabled installations\nstay disabled during an update. Claude Code can apply an update run in a project folder to an\nenclosing project that has its own installation, so the folder's local or project installation stays\nold. That installation does not stop the refresh or later commands: Cotal prints a warning naming the\nfolder and the `claude plugin uninstall` command to run there to remove it. A user-scope installation\nthat does not update is still an error. `cotal status` checks the resolved payload's manifest version and\nfile contents. A folder-backed plugin can retain older version metadata in Claude's installation\nrecords; that metadata does not override the files Claude loads. Running sessions keep the\ncode they loaded; reload plugins or reopen Claude to load the update.\nThe install mechanics and the invariants behind them are in\n[setup internals](setup-internals.md).\n\n`cotal setup` also installs Cotal's authored Agent Skills (`SKILL.md`, the agentskills.io format) for\ncoordinating agent teams (today `team-topology`), from one canonical source, on two channels:\n\n- **Claude Code** gets a second, skills-only plugin, `cotal-skills`, from the same `cotal-mesh`\n marketplace, at **user scope** (machine-wide). The Claude connector declares and implements this\n setup provider, including the marketplace assets and native plugin commands; the base CLI only passes\n the vendor-neutral Agent Skills directory. The plugin carries no code and no core dependency,\n and uninstalls on its own with `claude plugin uninstall cotal-skills --scope user`. Its plugin version\n is stamped from the running CLI release. `cotal update` refreshes an existing skills plugin without\n enabling it or installing a removed one; `cotal setup --skills` also installs missing skills. `cotal setup` installs it on first run and on repeat\n runs, so upgraders are not left behind. The same provider reports the plugin and skills plugin rows\n in `cotal status`, which point a stale or missing skills plugin at `cotal setup --skills`.\n- **Every other harness** (Codex, Cursor, OpenCode, Gemini CLI, Windsurf/Devin) reads the cross-vendor\n `~/.agents/skills/` directory convention, which has no remote index, so `cotal setup` **reconciles** it\n (and `cotal setup --skills` does only that):\n it installs/updates each Cotal skill, backs up a copy you have edited to `SKILL.md.bak` before\n replacing it, and removes a Cotal skill that is no longer shipped. Only skills Cotal owns are touched;\n your own or third-party skills there are left alone. `cotal status` reports whether the drop is current,\n stale, missing, or has a retired skill to reconcile, and names `cotal setup --skills` as the remedy. This is the working cross-vendor path.\n\nCotal also generates an [Agent Skills discovery index](https://cotal.ai/.well-known/agent-skills/index.json)\non cotal.ai, but that RFC is still a draft with no harness consuming it yet, so it is a forward bet,\nnot a channel to rely on today.\n\n## Enable channels for your organization\n\nFor claude.ai Team and Enterprise organizations, channels are blocked by default. An organization\nOwner must enable them in [Organization settings > Claude Code > Channels](https://claude.ai/admin-settings/claude-code),\nor set this organization-managed setting:\n\n```json\n{\n \"channelsEnabled\": true\n}\n```\n\nUsers cannot override this setting through local or project settings. Cotal's\n`--dangerously-load-development-channels` launch flag cannot override it either. Enable channels\nbefore launching the sessions that need them.\n\nWhen channels are blocked, Cotal's MCP tools and hook-based inbox delivery still work when the agent\ntakes a turn. An incoming message cannot wake an idle Claude Code session. A successful spawn or a\nconnected MCP server does not prove idle wake is available; check Claude's startup notice before\nleaving agents unattended.\n\nPersonal Pro and Max accounts without an organization do not need this admin opt-in. Sessions still\nneed to opt into their channel, which Cotal's managed launch does for its own server. See\n[Anthropic's channel controls](https://code.claude.com/docs/en/channels#enterprise-controls) for\norganization policy and other authentication modes.\n\n## Spawn a session\n\n```bash\ncotal spawn # foreground: your default agent, in this terminal\ncotal spawn dave --detach # supervised: the manager runs it in a PTY\n```\n\nA spawn resolves a persona from `.cotal/agents/<name>.md` ([agent files](agent-files.md));\n`--model`, `--variant`, `--cwd`, `--prompt`, ACL overrides, and `--share-tools` apply to\nboth forms ([run a mesh](run-a-mesh.md) has the full resolution rules). The session joins\nwith identity from its environment and auto-registers presence by the time it is\ninteractive.\n\nInside the session, the agent orients with one read-only tool, `cotal_orientation`: its\nidentity, the channels it reads and may post to, its capabilities, the tools available,\nwho's present, and unread counts. The full tool surface is the\n[MCP tool catalog](mcp-tools.md). In auth mode the team-supervision tools\n(`cotal_spawn` / `cotal_persona` / `cotal_personas`) are injected **only** for personas declaring\n`capabilities: [spawn]` (the same grant that opens the privileged control subject), so an\nagent's toolset matches its declared capabilities. `cotal_run` is gated separately by\n`run`; use `capabilities: [spawn, run]` for both. Fresh setup defaults include both.\nSee [workflow tool setup](workflows.md#from-an-agent-session) for a first run and missing-tool checks.\nClearing retained history is\noperator-only ([run a mesh](run-a-mesh.md)), never an agent tool.\n\n## How it binds\n\nClaude Code exposes four integration surfaces, and three of them collapse into a single\ndual-purpose MCP server:\n\n| Surface | Mechanism |\n|---|---|\n| Outbound, ambient | `http` lifecycle hooks \u2192 POST to the connector (presence, activity) |\n| Outbound, deliberate | MCP tools `cotal_send` / `cotal_dm` / `cotal_anycast` (+ `cotal_feedback`) |\n| Inbound, pull | MCP tool `cotal_inbox` (same server) |\n| Inbound, push | Channel nudge + hook drain (below) |\n\nThe manager launches the *real* `claude` (no wrapper):\n\n```\nclaude --mcp-config '{\"mcpServers\":{\"cotal\":{\u2026}}}' \\\n --dangerously-load-development-channels server:cotal \\\n --permission-mode bypassPermissions\n# env: COTAL_SPACE, COTAL_NAME, COTAL_ROLE, COTAL_CHANNEL=1, plus claude's documented auth vars\n```\n\n- **Tool permissions.** A managed seat has no human at its terminal, so it launches with\n `--permission-mode bypassPermissions`: a prompt under the default mode would stop its first mesh\n tool call until someone answered it, and nobody can. To get prompts back, pass\n `--opt permission-mode=default` (or another mode); the connector adds the default only when the\n launch options carry neither `permission-mode` nor `dangerously-skip-permissions`. Claude refuses\n the bypass mode as root and under a policy that disables it; such a host must pass a mode.\n\n- **Model auth.** Locally, `claude` still reads macOS Keychain / `~/.claude`. In a container or\n CI there is no Keychain, so the connector forwards the documented credential set:\n `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`), `ANTHROPIC_API_KEY` /\n `ANTHROPIC_AUTH_TOKEN`, and the cloud-provider flags plus their credential vars. Host-session\n markers (`CLAUDE_CODE_CHILD_SESSION`, `CLAUDECODE`) stay out so a nested seat still saves a\n transcript. See [Deploy](deploy.md).\n- **Persona privacy.** The persona body is written to a private file and Claude receives only\n `--append-system-prompt-file <path>`. The body never appears in the spawned process argv. The\n carrier is a 0600 file inside a 0700 directory on POSIX, with equivalent owner-only ACL hardening\n on Windows. That is OS-user isolation: any process running as your user can read it while it\n exists. The manager or the foreground `cotal spawn` removes it, and the shared-server MCP config\n file, once it has proved the `claude` process gone. If the launcher is killed first, a watcher\n started beside `claude` removes them when `claude` exits.\n- **MCP servers.** A spawned agent loads Claude's native MCP sources alongside Cotal and declared\n sharing overrides. Explicit `--share-tools` selections and carried private homes keep strict\n isolation. Claude still applies its own server approvals, disabled settings and managed policy\n (see below).\n- **Lean MCP server.** Every session runs its own cotal MCP server, so Node starts it with\n `--optimize-for-size --max-semi-space-size=1` to shrink its memory footprint. Managed launches and\n the plugin's `.mcp.json` (personal plugin sessions) both pass these flags. `--lite-mode` and\n `--jitless` would save more, but they remove `WebAssembly`, which Node's built-in `fetch` needs,\n so WebAssembly stays enabled.\n- **Light hook.** Claude Code starts the plugin's hook as a new process for every tool call, prompt\n and stop. The hook imports `@cotal-ai/core` only through small subpaths such as\n `@cotal-ai/core/peer-label`, never the package root, which would bundle YAML and NATS into it.\n `pnpm smoke:claude-hook-weight` holds that.\n- **Installed plugin.** The plugin is installed once, at user scope (`claude plugin install\n cotal@cotal-mesh --scope user`), because its hooks bind only to an *installed* plugin. Earlier\n releases installed it per project, at local scope; the next `cotal setup` removes those installs and\n keeps the user-scope one. If Claude Code cannot change a project's settings, setup still removes\n the others, then fails naming that project and the `claude plugin uninstall` command to run there. A session Cotal launches loads the package's own copy with `--plugin-dir`,\n which replaces the installed one for that session, so it still runs one cotal server.\n The repo's `.claude-plugin/marketplace.json` lists the committed plugin tree under\n `claude-plugin/`, which each release regenerates with the built bundles, the skills and the\n release version, so an install from the repo or from a pinned commit runs without a build\n ([Release](release.md)). `cotal setup` (npx, no clone) materializes the same marketplace under\n `~/.cotal/claude-plugin/` from the installed CLI (each plugin dir is rebuilt from scratch and\n atomically replaced, never merged, so no stale file rides in). The\n `cotal-skills` plugin installs from that same marketplace at user scope (`claude plugin install\n cotal-skills@cotal-mesh --scope user`); its manifest and install behavior ship inside the Claude connector, and\n its version tracks the CLI release so updates land.\n- **Identity-gated.** A launched session joins under the identity in `COTAL_NAME`, `COTAL_LINK`\n or `COTAL_AGENT_FILE`. A plain `claude` with none of them joins only when its conversation calls\n `cotal_connect`, so your own sessions do not appear as stray peers. Until then its MCP\n server builds no mesh agent and opens no broker connection. See\n [Sessions Cotal did not launch](#sessions-cotal-did-not-launch).\n- **Hands-free.** The dev-channels flag prints a one-time confirm prompt. The runtime waits for the\n dialog title in normalized terminal text and presses Enter once when it appears, so startup speed\n does not affect a supervised launch. Claude skips that prompt when channels are off for the\n session, for example under a Team or Enterprise organization whose managed settings do not set\n `channelsEnabled: true`, and opens its input screen instead. The runtime then ends its watch\n without a key once the input screen's footer shows `(shift+tab to cycle)` or `? for shortcuts` on\n two screen reads in a row. That seat gets mesh messages through its hooks only, so a message does\n not wake it while it is idle. The PTY runtimes read the child's screen, and the tmux, cmux, Orca\n and Herdr runtimes read the pane's screen. If neither the prompt nor the input screen appears\n within 15 seconds, the seat ends with a bounded error naming the unmatched prompt instead of\n hanging silently or answering another dialog. The PTY runtime writes that error to the seat's output and\n stops the child as a graceful stop does, so a child that ignores SIGTERM is killed 3 seconds\n later. The other runtimes write the error to the manager's log.\n- **Trusted directory.** An authorized supervised spawn accepts the selected workspace through\n Claude's native trust dialog, which Claude draws in one of two layouts. When a numbered list\n shows Yes first and selected, the runtime confirms it with Enter. When No comes first and is\n selected, the runtime moves to Yes and confirms only after seeing Yes selected. It never confirms\n while No is selected, sends each response once, and stops watching after the\n development-channel confirmation or on the input screen. A changed or unsupported dialog receives no guessed input\n and still fails within the startup bound. Claude saves trust with its own repository and linked\n worktree rules; Cotal does not rewrite the manager's configuration. Accepting trust enables the\n workspace's hooks and settings, which can select a tool permission mode when no launch flag\n overrides it. Cotal adds no tool-permission bypass flag. Review the checkout before authorizing\n a spawn there. A foreground `cotal spawn` keeps the trust dialog in your terminal.\n\nInbound mesh messages arrive in context as\n`<channel source=\"cotal\" from=\"bob\" kind=\"dm\" \u2026>\u2026</channel>`: each meta key a tag\nattribute the agent can read for routing.\n\nThe consuming connector holds a crash-safe claim for its broker target, space, principal and\nlifecycle on this OS user account. A second patched session on the same host refuses before\npulling that inbox, even when its launch files and control socket differ. Reconnect keeps the\nclaim. Hooks and non-consuming helper connections do not claim it. The control listener binds\nbefore the broker connection and never removes an existing socket path.\n\nThis is cooperative same-host containment. Other hosts, broker URL aliases and older clients\nare not fenced. Preserve the existing session before choosing a distinct actor for a new\nlaunch. An uncertain shutdown keeps custody until the consuming process exits.\n\n## Sessions Cotal did not launch\n\nA Claude Code session you start yourself, where the `cotal` plugin is installed, gets the tools the\nhosted connector offers: `cotal_connect`, `cotal_wait`, `cotal_disconnect` and the routed mesh tools.\nNothing joins until the conversation calls `cotal_connect` with a name. The plugin's server then runs\n`cotal app-identity provision` with the CLI that `cotal setup` recorded beside the plugin\n(`dist/cli.json`), so it does not need `cotal` on `PATH`. Each conversation gets an identity of its\nown on the mesh a bare `cotal spawn` from the session's directory would pick, once a person approves\nit. A static-auth or open mesh has no page where a person approves a connection, so the plugin\nopens no identity there. A plugin that `cotal setup` did not install has no record, and\n`cotal_connect` names that remedy.\n\nPersonal native Code sessions can join a signed-in mesh whose authority offers connection approval.\nThe plugin binds the real native session and transcript before requesting an identity. Each\nconversation gets a separate actor. A new identity is issued only after a person approves it in the\nauthority's signed-in web page. A model's tool arguments, a client flag or a native tool approval\nnever approve it. The request first comes back pending with the page's address and its expiry, and\nnothing is issued yet. Connecting again sends the same request and secret: while it is pending it\nanswers the same page, and once approved it issues the identity. A denial or an expiry drops the\nrequest, and the next connection asks again. No login proof reaches the conversation or its tools.\nA remote mesh must advertise conversation identity v1 and connection approval v1 on its pinned HTTPS\ndiscovery endpoint. An absent capability, a different origin or a changed registration is refused\nbefore authenticated issuance, for a request without a selection too. There is no immediate-grant\nfallback. A signed-in mesh whose authority this machine holds has no approval page, so it issues no\nnew identity either.\n\nEvery identity sends and receives direct messages and publishes its own activity on its own event\nchannel. These come with the identity and are not optional, so the approval page shows them before\nthe person decides. Native activity sharing includes the conversation's user messages, assistant\nreplies, tool inputs and tool results after activation. It is not only presence or status. Reasoning\nis not shared by default, and earlier or pre-approval history is not shared. The space's activity\nread permissions determine who can read it. Decline the connection if you do not want this sharing.\nA request without a selection proposes nothing beyond these rights: no role,\ncapabilities or ordinary channel reads.\n\n`cotal_connect` in a native Code session also takes a selection: `channels` to read\n(`access: \"read\"`) or read and post to (`\"read-post\"`), a `role`, and `capabilities` (`spawn`,\n`run`). The selection is a proposal. The person may approve it, change it within their own current\naccess, or approve nothing beyond the identity's own permissions. The issued grant must match what\nthe person approved item for item, or the connection is refused. When the authority refuses an item\noutside your own grant before asking anyone, nothing is issued and the reply names every refused\nitem. There is no fallback to the event-only identity. Its own event channel stays the first post\nentry. A remote mesh must also advertise `conversationIdentitySelection` version 1, or a selection\nis refused before any request.\nEndpoint calls are not selectable through this connection approval contract, which accepts only\nan empty endpoint selection. These conversations do not offer `cotal_describe` or `cotal_invoke`.\n\nA connection is offered the tools its grant can use: `cotal_spawn`, `cotal_persona`,\n`cotal_personas` and `cotal_despawn` with `spawn`, `cotal_run` with `run`, and `cotal_reconnect`.\nIts manager calls exchange the conversation's own secret for a manager-call view, so they carry only\nits grant and name the manager instance when more than one could answer. A conversation is not a\nmanaged seat. `cotal_despawn` without a name refuses rather than stopping your app, and\n`cotal_disconnect` retires only that connection. No run turn is addressed to it, because a workflow\naddresses turns to agents it spawned, so `cotal_yield` is not offered. Persona text and model pins\ndo not change the conversation's own instructions or model.\n\nThe plugin's ordinary command hooks supply the session and transcript binding. Its native module\nuses that binding without intercepting `classic.*` events, which Claude's built-in security guard\nwithholds from user plugins. The guard stays enabled. If an organization disables command hooks or\nnative modules, its administrator must permit them before this connection path can work. Command\nhooks still report lifecycle status and activity; the native module owns incoming message delivery.\n\nA signed-in identity keeps its request and secret in private local recovery state. A lost create\nresponse retries the same request without rotating its secret or allocating another identity.\nRetiring a request that is still pending drops it, and no identity is issued for it. An identity\nissued before connection approval keeps working, and its recovery stays exact. Another plugin server reuses that identity only after the process that held it no longer exists.\n`cotal_disconnect`, session end and a signal request retirement of that lifecycle only. Other\nconversations stay connected. When the mesh reports retirement pending or does not confirm it, the\nconversation stays disconnected and keeps its saved record. Every tool then reports the same\nunfinished cleanup with the mesh's retirement phase. Calling `cotal_disconnect` again retries it at\nonce. `cotal_connect` retries it too, and connects with a new identity once the mesh confirms\nretirement. A restarted plugin server recovers the same way. After each connection, the plugin\nretires the same mesh's saved identities whose plugin server process no longer exists, such as one\nkilled before it could retire them. This runs in the background, so the connection does not wait\nfor it. A server process that still exists keeps its identity, and a retirement that does not finish\nstays saved for a later run. A mesh requiring events refuses a connection without a native\ntranscript source.\n\nThe gateway's MCP annotations distinguish read-only queries from updates. Orientation, connection\nstatus, the roster, the docs and channel information are read-only. Sending messages, connecting and joining\nchannels are additive. Inbox reads and waits consume messages; status, channel mode, leaving and\ndisconnecting can replace or remove state. These hints do not grant access or override Claude's\npermission settings or an organization's approval policy.\n\nA message for a connected conversation starts a turn through the plugin's\n[native hooks module](https://code.claude.com/docs/en/plugins/mods/overview). This works in supported\nClaude Code hosts, including local sessions in the Desktop app's Code tab. The module runs a local\nwake reader as a child of the same host that runs the MCP server. Each server uses its own socket\nin a private directory. Short messages arrive in full with their sender, message ID and reply\ncontext. Claude can answer these without calling `cotal_inbox`. The module uses Claude's\nplugin-sender attribution, never the person's own identity. While Claude is busy, delivery happens\nat a main-conversation tool boundary before the next request. A request already being sent has a\nfixed message snapshot. A later arrival waits for another tool boundary or starts its own attributed\nturn when the current turn ends; storing a note alone does not count as delivery to that request.\n\nEach inline batch carries at most eight whole messages. Each rendered message must fit within\n1,024 Unicode code points, and the encoded socket frame must fit within 4,096 characters. Larger\nmessages remain queued. A notification-only reader or an oversized message uses\n`Cotal: a new message for connection_id <id>. Read it with cotal_inbox.` and a bounded preview.\nA preview is incomplete; Claude reads its contents through the inbox. An inline batch also names\nhow many other messages remain pending.\n\nThe transcript shows a compact Cotal indicator. Expand it to inspect the delivery or preview.\nThis changes only the display. Presentation observes only prompt rows; in-turn appends use their\nreturned row ID, without observing protected notes or skill output.\n\nSocket writes and previews do not acknowledge messages. A complete inline batch is committed only\nafter the native host confirms admission to the bound conversation. An idle prompt also needs its\nstored prompt row; a busy append needs its returned row ID and complete stored text. An offer waits\nfor a busy conversation's next safe boundary while its reader remains connected. Admission after\nclaim has its own deadline. This confirms host handoff, not that\nClaude understood or completed the work. Delivery remains at least once after an unknown result.\nMessage IDs distinguish retries from new requests.\n\nA wake follows the rule `cotal_wait` follows and does not compete with an active wait. Confirmed\ninline delivery rearms the next wake without an inbox call. A replacement reader reoffers unread\nwork. An offer from a closed connection cannot suppress its replacement connection's messages.\nClaude's Stop and organization controls still apply. Refused delivery stays pending without\na retry loop; genuine person-origin input resumes it with a fresh offer. `cotal_connect` reports\nwhether the incoming reader is attached; attachment alone does not prove native admission.\nWhen native modules are unavailable or blocked, the conversation can listen with `cotal_wait`.\n\nClaude Desktop chat does not run a plugin's local server. On macOS and Windows, where Claude Desktop\nis installed, `cotal setup` adds a `cotal` server to Desktop's own config,\n`claude_desktop_config.json` (Settings > Developer > Edit Config), and keeps every other entry there.\nIt runs the same server with `--desktop`, by the absolute paths of the node that runs the CLI and of\nthe plugin copy under `~/.cotal/claude-plugin`. Quit and reopen Claude Desktop to load it. A repeat\n`cotal setup` and `cotal update` keep the entry current, `cotal status` reports it, and a config\nfile that is not valid JSON is refused and left as it was. Desktop reads that file when it starts\nand writes the copy it holds back while you use it, so an entry added while it runs is lost. On\nmacOS, setup and update therefore leave the file alone while Claude Desktop is running, finish the\nrest of their work, and say so. Quit Claude Desktop, then run `cotal setup` again from a terminal\noutside it; a terminal in its Code tab keeps Desktop running. On Windows they still write while\nDesktop runs, because the loss was not reproduced there. Setup can use Desktop's bundled Code\nexecutable when there is no standalone `claude` CLI. Its installed MCP and command hooks use the\nrecorded Node executable, so the app need not find Node on `PATH`. On Linux, setup writes nothing\nfor Desktop Chat.\n\nDesktop Chat supplies no native Code transcript binding, so this local server cannot join a\nsigned-in space, such as one on Cotal Cloud, and refuses before identity issuance. Desktop Chat joins\na signed-in space only through the hosted connector that the space's discovery document advertises\nas `cloudCapabilities.hostedConnector`, version 1 with an HTTPS URL. Add that URL in Claude under\nSettings > Connectors > Add custom connector. Setup for a signed-in space writes no Desktop entry and\nprints the URL instead. `cotal status` prints it on the Claude Desktop row when the selected space is\nsigned in, and the refusal names it. When the space advertises none, all three say so. The Desktop\nserver's instructions tell the model to use the hosted connector for a signed-in space. The hosted\nconnector is a separate connection path; it does not supply autonomous chat wake.\n\nDesktop's working directory is not your project, so its mesh is the `cotal use` mesh, else the only\none running. Desktop runs no monitor, so a Desktop conversation hears messages when it calls a tool,\nand keeps calling `cotal_wait` when you ask it to keep listening. A chat with no turn running cannot\nbe woken by any server: Claude's chat surfaces start a turn only from the person.\n\n## How messages reach the session\n\nDurable deliveries land in the connector's inbox from JetStream consumers\n([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding)); live channel traffic can instead arrive\nthrough an at-most-once core subscription. A durable message sent while the agent is busy\nor offline waits on the stream. Two things move a message from inbox to model; one\ndelivers, the other only wakes:\n\n- **Hook drain (delivery).** `SessionStart` / `UserPromptSubmit` hooks read automatic inbox items and\n inject them as `additionalContext`. This is the single authoritative path: deterministic and works\n on any Claude Code build. Quiet ambient is excluded and stays buffered for `cotal_inbox`.\n A message is **acked only once the hook reply carrying it has cleared both legs of its journey**:\n the connector's control socket to the hook process (which gives up after 2s), and the hook\n process's own stdout to Claude Code (which it force-exits 1s after starting to write). The relay\n sends a receipt back down the control socket from that stdout write's callback, and only on a\n clean write (a runtime whose pipe has gone away fails it), and the connector treats that receipt,\n not its own socket write, as delivery. So a large injection killed mid-flush, or one written to a\n broken pipe, leaves the message un-acked and JetStream redelivers it. What this does *not* prove is\n that Claude Code read or applied the reply: a payload small enough to fit the pipe buffer is\n reported written the moment the kernel takes it. That residual is why the path errs toward\n at-least-once rather than treating a confirmed write as a confirmed read. Acking when\n the reply was merely *formatted* meant a lost reply was a lost message: it was already marked\n handled, so its own redelivery was silently acked on arrival.\n A hook whose handler throws still returns an empty reply so the session is never blocked, and\n that reply carries nothing, so it commits nothing: the batch it had started to surface stays\n un-acked and goes out on a later frame. The seat also drops any `turn-pending` row that breaks\n the manager contract, such as one with no integer deadline, and says so once in its log. A reply\n with no `turns` array changes nothing: the seat keeps the turns it already holds.\n This errs toward **at-least-once**: if a reply lands but its confirmation does not, the batch is\n surfaced again and flagged as a possible repeat. A duplicate injection is noise; a buried DM stops\n the peer answering at all.\n- **Channel nudge (wake).** An arriving message fires a `notifications/claude/channel`\n event that wakes an *idle* session into a turn, so the drain runs *now* instead of at\n the next prompt. The nudge never acks anything. A nudge that the host rejects is retried with a\n bounded backoff while anything is still pending. For an idle session it is the only wake source,\n so dropping it means silence until someone types. When the channel becomes active, the connector\n first re-fires a focus mention remembered during startup, otherwise one buffered wake. A rejected\n push keeps its bounded retry, and JetStream redelivery remains the durable backstop for unacked\n inbox items. Neither a redelivery nor that retry repeats a nudge already pushed for that message,\n whether the message had its own nudge or was counted in a batch one, so a session held in a long\n tool call gets one nudge per message. Once a hook frame carries the message, or the push that\n announced it fails, its next redelivery nudges again, so a reply that never reached Claude Code\n still recovers. If the channel cannot run at all, delivery still waits for the next hook. Live-only\n traffic has no durable retry.\n\n**Two priority tiers.** A *directed* message (DM, anycast, or a channel message that\n`@mentions` us) always nudges. *Ambient* channel chatter does not nudge mid-turn; it\naccumulates, and the `Stop` \u2192 idle transition fires one batch nudge so the backlog drains\ntogether.\n\n**Constraints (accepted).** Channels are a Claude Code research preview (\u2265 v2.1.80;\npermission relay \u2265 v2.1.81): Anthropic auth only, admin-enabled on Team/Enterprise, and a\ncustom channel needs the `--dangerously-load-development-channels` launch flag. The hook\ndrain does not depend on any of that; the channel only adds \"wake me when idle.\"\n\nThe same channel also relays **tool-permission requests** onto the mesh, so a peer (a\nhuman at the CLI, a policy node) can approve or deny an agent's pending tool call through\nCotal rather than a per-terminal prompt.\n\n### Attention\n\nAn agent picks how aggressively peer traffic reaches it with\n`cotal_status({ attention })` (three modes, orthogonal to presence):\n\n| arrival | open (default) | dnd | focus |\n|---|---|---|---|\n| directed (dm / anycast) | wake + inject | wake + inject | wake + inject |\n| channel `@mention` | wake + inject | wake + inject | ack-drop; wake to *pull*; not injected |\n| ambient channel chatter | wake when idle; hold while working | never wakes; injects next turn | ack-drop; recall via `cotal_inbox` |\n\nPer-channel overrides refine this: **quiet** (delivered, never wakes; `@mention` still\nwakes) and **muted** (dropped on receive, mentions included; DMs/anycast unaffected), set\nwith `cotal_channel_mode` or as agent-file defaults (`quiet:` / `muted:`,\n[agent files](agent-files.md)). A per-channel override is the final word for that channel.\nQuiet ambient is pull-only: it never hitchhikes on a human prompt, DM, mention, or other\nconnector-driven turn. `cotal_inbox` explicitly surfaces and clears it. A quiet-channel\n`@mention` remains automatic and injects normally.\n\nA pull is bounded too, and clears only what it hands over. One `cotal_inbox` call carries at most a\nreceivable window (direct messages and role requests first, then channel traffic, replayed history\nlast); whatever does not fit stays buffered, is named in the reply, and comes back on the next call.\nA message too large for one whole response is delivered in parts: once no smaller mail is waiting,\neach call carries its next part, and it is cleared only after its last part goes out, because clearing\nwhat was not handed over is the loss this bound exists to stop.\nThat matters most on the path where it is easiest to lose mail: reconnecting brings a channel-history\nreplay with it, so the largest payload and the least expendable message arrive in the same read.\n\nThe local inbox is bounded. On pathological overflow it evicts pull-only items first, then other\nchannel traffic, and a direct message or role request only when the whole buffer is directed mail.\nIt never evicts a message the connector is handing to the session or a turn is still carrying.\nAn evicted channel item is acknowledged. An evicted direct message or role request never is, and\nthe broker redelivers it after the ack wait until a redelivery finds room. A direct message stays\npending on the session's DM durable, where `cotal deliver pending <name>` counts it. A role request\nstays on its role's shared queue, which that command does not read. A full inbox therefore delays\ndirected mail until the session drains it.\nIf the bounded live/durable classification guard also fills, the connector fails closed:\notherwise-normal ambient becomes pull-only until restart. Muted hard-drop and normal focus recall\nstill take precedence. Focus also keeps a bounded exclusion list so mode toggles cannot recall\nquiet/muted traffic; if that safety bound fills, recall skips the affected channel and reports it\nas incomplete rather than risk resurfacing excluded content. Recall cannot tell one message with an\nempty id from an identical one with another disposition, so in focus such a message is held in the\nlocal inbox as pull-only instead of being dropped, and a mention of it still wakes the agent. When\nthe session settles an id-less copy, it reads the chat stream's last sequence. Identical copies\narrive in stream order, and those reads can answer out of order, so the first read that can see the\ncopies binds them in arrival order, latest first, each to the latest unbound stream copy at or below\nthe lowest sequence read for it or any later identical copy.\nWhile the connection stays up, every stream copy at or below that sequence reached the session\nfirst, so a later identical copy sent during a reconnect gap is above it and stays unbound. A copy\nthat arrives while a read runs may not be in that read, so it binds nothing there and recall reads\nthe channel again, up to three times; if it still could be a stream copy in the last read, recall\nleaves that stream copy in the stream and reports the channel as incomplete. A settled copy a\ncomplete read cannot bind is behind the focus start or out of retention, and is forgotten. Recall\nhands back into the inbox only the stream copies nothing is bound to, such as one sent during a\nreconnect gap or one the inbox evicted on overflow, and `cotal_inbox` hands each over once. Overflow\nfrees only the evicted copy, held or quiet, and a copy no read has bound yet keeps its place in\narrival order, so an identical muted copy stays out of recall. When\nthe inbox is full, recall leaves them in the stream for a later call and reports the channel as\nincomplete. A history read that fails, or a channel with replay off, settles nothing and is reported\nas incomplete, and recall calls run one at a time. If the sequence read for a settled copy fails,\nor answers only after the connection dropped, recall skips that channel for the rest of the focus\nperiod and reports it as incomplete. Recall cannot tell a late copy of a message it handed back from\na new identical message, so every copy takes its own disposition: a new identical quiet mention is\nstill delivered automatically, and a late copy can surface a second time.\nA recalled message that already went out in part is read to its last part, even if an exclusion\nlands after its first part. One session reads its inbox one call at a time: a `cotal_inbox` call\nthat overlaps another waits for it to finish, so neither decides from a view the other has already\nmoved past.\nIf the separate hard-drop disposition guard fills, channel traffic is dropped for the rest of the\nsession rather than risk a late copy bypassing an earlier muted/focus decision; DMs and anycast are\nunaffected.\n\nAttention is **advisory UX, not a boundary**: any peer can wake a dnd/focus agent by\nnaming it, and `muted` means \"I opted out of receiving\", not \"the channel is blocked\";\nthe broker still authorizes and delivers. Focus's real effect is shrinking the\nuntrusted-ambient injection surface (only subject-authenticated dm/anycast auto-inject).\nIt resets to **open** on `SessionStart`, so a restarted agent never stays silently deaf.\nYour attention is mirrored into presence so peers can see it.\n\nWhatever does reach a turn is framed so a peer cannot write the frame. A line that begins at column\nzero is written by the connector; one message is one line plus indented continuations, with the\nsender inside a single bracket pair. A message body, a sender name and role, and a service or\nchannel label are all peer-controlled, so each passes through the same neutralization the\n`cotal_inbox` reply uses: no line break a splitter may honour and no bracket survives into a\nrendered attribution. This matters more for an injected block than for a reply, because the agent\ndid not ask for it and so never had the chance to distrust it.\n\n## Presence mapping\n\nThe connector wires a small subset of Claude Code hooks to presence states; presence is\ncoarse, and \"what it is doing\" rides on activity updates. Presence is **advisory**: a presence\npublish that fails (the endpoint mid-reconnect, say) is swallowed and never prevents the same hook\nfrom delivering messages or flushing held ones.\nA `SessionStart` during an open turn, including compaction, preserves the current `working` or\n`waiting` status until `Stop`, `StopFailure`, or `SessionEnd` closes the turn.\n\n| Hook | \u2192 state |\n|---|---|\n| `SessionStart` | `idle` only when no turn is open (join; surfaces the inbox; captures the live model into `meta.model` when no pin) |\n| `UserPromptSubmit` | `working` (turn starts; surfaces the inbox) |\n| `PreToolUse` | no change; records *what* is about to run, so a permission wait can name it |\n| `PostToolBatch` | `working` when the main agent's batch resolves after a prompt left the seat `waiting`, which clears its condition so the turn's `Stop` yields; a subagent's batch (it carries `agent_id`) or one relayed after the turn's terminal changes nothing |\n| `Notification` (`permission_prompt` / `agent_needs_input`) | `waiting` with condition `approval` / `input` (activity leads with the pending tool, e.g. `Bash: git push \u2026`) |\n| `Stop` / `StopFailure` | `idle` (turn done / died on an API error; flushes anything held while busy). `StopFailure` also relays Claude Code's native error value as `condition.source` and maps it to the closed condition vocabulary. On the [event plane](#event-plane) it closes the run with `RUN_ERROR`. |\n| `SessionEnd` | `offline` (graceful leave) |\n\nThe connector also leaves gracefully when its stdin closes. An MCP client closes it to end the\nsession, and a killed `claude` closes it with no `SessionEnd`, so a dead session drops off the\nroster instead of staying on it as a live peer.\n\n`StopFailure` maps `rate_limit` and `overloaded` directly; auth and credential failures to\n`auth`; account and billing failures to `billing`; `invalid_request` to `request`;\n`model_not_found` to `model`; `server_error` to `server`; `max_output_tokens` to `context`; and\n`unknown` to `failed`. The native value remains in `condition.source`.\n\nHooks are relayed over the connector's **authenticated** local control endpoint (per-user\nsocket + per-launch token, constant-time checked), so a local process that finds the path\nstill can't drive presence or stop the agent. Claude Code's own\n[hooks reference](https://code.claude.com/docs/en/hooks) lists every hook event. The hooks the\nplugin registers, with their matchers, are listed in the\n[adapter README](../extensions/connector-claude-code/README.md#claude-code-hooks).\n\n## Event plane\n\nA connected native session publishes a structured account of its activity: user messages,\nassistant replies, run boundaries and tool calls with their inputs and results. Reasoning is off\nby default. Programs can read these events directly. The launcher sets `COTAL_EVENTS` by default; pass `--no-events` to opt out on an unrestricted\nspace. A user-auth registration with `policy: { events: \"required\" }` carries `eventsRequired` in the\nprivate launch material, so the connector arms even without `COTAL_EVENTS`; `--no-events` is refused.\nA hand-driven user-mode session may carry the same decision as `COTAL_EVENTS_REQUIRED=1`. Its own\npublish grant must cover `events.<owner>.<actor>` or the connector refuses before joining. A personal\nnative Code connection to a signed-in mesh uses the same recorder and captures its transcript\nboundary after approved issuance, before joining. Earlier history and time spent waiting for\napproval are not published.\nIts native lifecycle drives presence and recording for that session only; a subagent or another\nconversation cannot end its main run. The event channel keeps the space's normal read permissions.\nA personal connection makes manager calls only with its own selected grant.\n\nIf the event plane stops, the seat keeps running and its mesh connection, on every connector and\non every space. The connector log records `AG-UI emitter stopped` with the reason and the delay\nbefore it re-establishes the plane: at once after a failure, then every second while the plane\nstays down, a fixed wait that neither grows nor is capped, so the plane is back within a second\nof the fault clearing. The re-establish\nis a fresh start: it reopens the write-ahead log, settles any frame it left pending, builds the\nmapper from the run the log has open, and reads on from the stored cursor, which is the same\nrecovery a connector restart runs. A failed publish or a failed log write is never retried on the\nemitter it failed on. A turn that ends while the plane is down keeps its terminal, and so does a\nturn whose terminal's own log write failed: once the re-establish has read on, the run the log had\nopen is closed with that terminal, a failure as a failure, rather than left open for the next\nprompt to close as a finish. That holds when the next prompt was already written before the plane\ncame back: the failure is published after the failed turn's own records and before the next run\nopens, in a frame of the failed run, since a run error names no run and the frame is its only\nattribution. Every turn that ends while the plane is down keeps its own terminal. A terminal belongs\nto the last turn the transcript had begun when its hook fired: the connector reports a turn's\nterminal once that turn's records are written and before the next turn's are, so the position the\ntranscript has reached at the hook names the turn, including a turn the connector opens no run for,\nsuch as a task notification, which takes its terminal and publishes nothing. A terminal repeated\nfor a turn that has one replaces nothing and is kept for no later turn. A line the connector cannot\nread, written past that position while the plane was down, does not hold the terminal: the\nre-establish reports the gap and places the terminal after the records before it. A record no read\nanswers, such as an OpenCode part still filling when its turn died, holds it no more: the terminal\nis placed after the records before it, and the turn end reads the session once rather than without end. The OpenCode source reads every record at or before the end it reported at a turn end as settled, so a part left filling by a failed turn does not stop the turns after it from publishing. A terminal reported while the first start is still waiting on the broker reaches the emitter before its first read, so a prompt written in that window does not close the failed turn as a finish. The same placement runs with\nthe plane up: a terminal reported while the flush before it is still reading, with the next prompt\nwritten before that read, closes its own turn in the place of the finish that prompt synthesizes. The seat used to exit on a space that requires events; for Claude Code the\nconnector is the MCP server, so that exit showed as the server disconnecting, and Claude Code\nrestarted it on the next tool call.\n\nA transcript line that is not a record does not stop the plane. Claude writes the transcript, and a\nwrite cut short by a full disk leaves part of one record glued to the next on a single line, which\nnever becomes readable. The connector steps over that line, keeps the records after it, and writes\n`skipped an unreadable transcript record at offset <offset> (<bytes> bytes)` with the parser's\nreason to its stderr.\n\nA new session includes its first run even when Claude writes a positional startup prompt before the\nconnector receives `SessionStart`. That from-zero read is keyed only to Claude's explicit\n`source: \"startup\"`; resumed, forked, cleared, and compacted sessions adopt at the transcript boundary\ncaptured at that adopt, before the mesh link connects, so nothing Claude appends while the connector\nis still starting up lands behind the cursor and is silently dropped. Crash recovery follows the\ncursor already stored in the event write-ahead log, regardless of the new process's startup label.\n\nClaude starts each hook in its own process, so a prompt or stop relay can reach Cotal before the\n`SessionStart` relay. The connector holds those event flushes and the terminal until `SessionStart`\nsupplies the source, then enqueues adopt, flush, and close in that order.\n\n`SessionStart` can also run before the connector process has bound its local control socket. The hook\nthe `SessionStart` relay retries only transient pre-connect listener errors, with capped backoff\ninside its existing two-second budget. Later hooks and permanent local faults still fail open\nimmediately. Once a socket has connected, a broken exchange is not retried: the connector may\nalready have handled the frame, so replaying it could apply one lifecycle event twice.\nThat retained `SessionStart` can itself arrive before Claude creates the transcript path. A genuinely\nnew startup waits up to five seconds for that file with capped backoff, and the same deadline bounds\none stalled file read; expiry fails loud instead of silently losing the first run. A forked session\ngets the same wait, because Claude copies the parent transcript into the fork's own file after the\nhook, and then adopts at the end of that copy. Resumed, cleared and compacted starts and recovered\ncursors still require their existing source at once.\n\nTool arguments (`TOOL_CALL_ARGS`) and tool results (`TOOL_CALL_RESULT`) are not republished\nonto this channel. The durable emitter drops those events before they are written to the\nwrite-ahead log, because this channel's read ACL is not the ACL the tool ran under. Content is\nmandatory on both kinds, so the event is suppressed rather than emptied or replaced with a\nplaceholder. Tool start and end still go out. A restart that finds a pending pre-fix frame\nstill carrying those kinds HALTS rather than republishing it.\n\nThe channel is **`events.<owner>.<actor>`**, named after the session's principal. What the actor\nhalf is depends on the mesh, and the difference matters when you go looking for it: on a static mesh\nit is a key the manager allocated, never the display name, so two live agents sharing a display name\ndo not share a stream; on a user-auth mesh it is the agent's own name, because that is what the\nledger row is keyed on. Spelled out again with both halves below. The launch grants publish rights\non that channel alone. A spawn\nthat asks for a *different* agent's event channel is refused at the door rather than granted, since\nthat channel is that session's event stream. The same rule runs on restart: a manager\nresume document that names another agent's event channel is refused rather than adopted, because the\nmanaged row is re-armed from that document and the credential is re-minted from the row.\n\nThe rule reads a **concrete** channel, two principal tokens and nothing else. A pattern such as\n`events.<owner>.>` is not an event channel to it and passes untouched, governed by ordinary ACL\nauthority: on a user mesh the delegation envelope, on a static mesh the spawning credential itself.\nThat is deliberate, because the pattern is the form an operator writes on purpose for an observer,\nand it is worth knowing rather than assuming the fence is total.\n\nTo let something else read a plane, grant it out of band. The refusal prints the command for the\nmesh it is running on, spelled out in full, and only that one.\n\nOn a **user-auth** mesh:\n\n```bash\ncotal actor grant <reader> --owner <owner> --scope '' --allow-subscribe 'events.<owner>.<actor>' --allow-publish ''\n```\n\nEvery field, deliberately. `actor grant` is an upsert of the whole row, so it refuses a grant that\nleaves off any of the three ACL flags. Only `--full` turns an omitted flag into the wide default\n(`>` read, `>` post, `spawn,role:default` scope), which is the opposite of what a scoped watcher is for.\n\nOn a **static** mesh there is no actor ledger for `actor grant` to write to, and the refusal says\nso; mint the reader instead:\n\n```bash\ncotal auth mint watcher --profile agent --allow-subscribe 'events.<owner>.<actor>' --provision\n```\n\nThe **agent** profile, not the observer one. `mint` reads `--allow-subscribe` only for that\nprofile, and refuses it anywhere else: `--profile observer --allow-subscribe <channel>` exits\nnon-zero and writes no creds file, because the observer profile carries a fixed read set over the\nwhole chat plane, which is the opposite of what a scoped watcher is for. The agent profile also prints the lifecycle uid the\nreader needs, since an authed consuming endpoint refuses to start without one.\n\nOn an **open** mesh there is nothing to grant: the mesh has no credentials and no ACLs, so any peer\nthat lists the channel reads it, and the refusal says so instead of naming a command. The\nown-channel rule still applies there, because a spawn is not the place to hand out a read on\nanother agent's tool inputs and outputs.\n\nTwo things a reader has to do that are not obvious, both on `CotalEndpoint`. It must pass the event\nchannel in `channels`: an endpoint reads the channels it lists, so one constructed without\nthe event channel joins nothing and the frames never arrive. And it reads history with `readHistory(channel)`, the delivery daemon's mediated read, not\n`channelHistory(channel)`: a scoped credential is denied the ad-hoc consumer the direct read\ncreates, by design. `cotal console` and the web console already do both.\n\nThe `<owner>.<actor>` pair is the session's principal. On a user-auth mesh the actor half is the\nagent's own name, so the channel is `events.<your-owner>.<agent-name>`. On a\nstatic mesh the owner half is the literal `local` and the actor is a key the manager allocated, so\nthe channel is `events.local.<key>`; the spawn reply carries that key as `id`. Note\nthat `cotal console` and the web console keep event channels out of their channel lists on purpose,\nsince a plane is a machine feed rather than a conversation; they draw the frames when you open the\nchannel by name.\n\nThe rule governs the manager's doors, which are the ones a caller other than you can reach. A\nforeground `cotal spawn` on your own machine mints from your own signing material, so it can still\ngrant any channel you name: that is the out-of-band grant, not a way around the rule.\n\n**Failed turns publish run errors.** Claude Code decides for itself\nwhether a turn finished or died and fires one of two hooks accordingly, so the connector relays that\ndecision rather than making one of its own: a turn that ended on an API error ends its run with\n`RUN_ERROR` carrying the fixed message `run failed` and no code. Neither the detail Claude Code\nreported nor its error kind is published there: both are upstream values that can echo your prompt or\ntool output, and the events channel has a different read ACL. The error kind still reaches presence\nas the agent's condition (`rate_limit`, `auth`, `billing` and the rest). A turn that ended normally still\nends with a run-finished event carrying no outcome, which says the turn ended and does not claim it\nsucceeded.\n\nEvents are written to a per-session write-ahead log before they are published, so a hook that fires\nafter a restart resumes at the cursor it left rather than replaying or skipping, and a run that was\nopen when the session stopped is closed rather than left dangling.\n\nA delayed initial connection or a broker reconnect pauses event publication without closing MCP.\nThe recorder waits for both the mesh binding and its transport; unpublished records stay in the\ntranscript until the connection recovers. Stopping the connector cancels that wait. A recording error,\nincluding a conflicting transcript or damaged event state, stops the publishing plane and not the\nsession: it is reported with the delay before the next re-establish, on every space, and the seat keeps\nits mesh connection. A second transcript path offered to a bound recorder is refused the same way, and\nthe first path stays bound.\n\nOne channel carries **every session of one agent**, because it is named after the principal and not\nafter the session. Alongside the per-session logs the connector keeps one small record per principal,\nholding the last sequence the broker assigned on that channel, so a new session continues the stream\nits predecessor left instead of starting again from nothing. Both live under the events state root\n(`COTAL_WORKSPACE_ROOT`), and neither is something you edit by hand.\n\nA **missing** record is not a fault: the connector rebuilds it from the session logs beside it,\nwhich is how an agent that was already running before this record existed keeps its stream. That\nrebuild stops if any one of those session logs is damaged. Unreadable, not valid JSON, and written\nfor a different principal all count, and so does a session directory or a log that is a link rather\nthan the real file the connector wrote, or a log that has more than one name. A tip taken from the\nrest would be too low, and it would stop publication later with nothing left to point at the cause.\nThe connector names the file instead, and the only way past it is the directory removal described\nbelow, under the same condition. A record that **disagrees with the broker** is a fault, and the\nconnector stops publishing and says why rather than guessing. A record that **moved while a session\nwas writing to it** is refused the same way: it means something else wrote the principal's record,\nand the connector reports which value it held and which the file holds rather than writing over the\nlater one. There is no command to clear it. The state is the principal's directory under the events\nroot, and clearing it by hand means removing that directory whole: the sequence, the cursor and the\nper-session logs only mean anything together, so removing part of it leaves a state the next start\nrefuses. Removing it is only half a remedy, and the half that comes first is the channel. The\ndirectory is where the agent's memory of the tip lives, not the tip itself, so on a channel that\nstill holds frames the next session opens expecting an empty one and stops on the same\ndisagreement, with the logs a tip could have been rebuilt from now gone. Purge the channel first,\nthen remove the directory.\n\nReading it: `cotal console` and the web console draw event frames directly. A frame carries no text\npart by design, so a surface that renders a message as flat text shows a marker instead of prose.\n\n**On a per-user-auth mesh, the default event plane needs the spawner's grant to cover the channel.** The event\nchannel is added to the child's publish set, and delegation only narrows: an agent may hand down\na subset of what it holds and no more. So a peer-initiated spawn is refused unless the\nspawning identity's own grant already covers the child's event channel. The refusal prints the\nexact `cotal actor grant` command that widens it. An operator launch, whose chain reaches an\nadmin-scoped or roster row, is unaffected. Passing `events: false` is the explicit opt-out.\n\nArming the event plane through a typed spawn request (`manager.spawn` with `events`, including\nthe CLI's `cotal spawn --detach --events`) is allowed on a user mesh when the child runs under\nthe caller's owner. This also satisfies a registration policy that requires the plane. A spawn\nunder another owner still needs the caller's admin tier. Without it, an explicit request or a\nrequired plane is refused before provisioning. A silent non-owner caller in a space without that\npolicy has the plane disarmed with a notice in a successful reply. Later provisioning can still\nrefuse the spawn. The child's own-channel rule and the ledger's delegation envelope still apply.\n\n## Resume a session\n\n`--resume <session-id>` pulls an existing Claude session, its context and transcript,\ninto the mesh. It **forks**: Claude mints a *new* session id from that transcript\n(`--resume <id> --fork-session`), so the meshed agent gets its own session and the\noriginal is untouched.\n\n- `cotal spawn --resume <id>` (foreground) is the primary surface: the transcript is on\n *your* machine, and errors are Claude's own stderr, inline.\n- `--detach --resume <id> --on <instance>` carries a session held on *your* machine to that\n manager instance, which may run on another host. The CLI finds the transcript under your\n Claude config (`~/.claude`, or `$CLAUDE_CONFIG_DIR`), sends it through a JetStream Object\n Store bucket only that instance reads, under a writer credential pinned to that one transcript,\n and prints `carried session <id> to <instance>:\n sha256:<hex>, <sent> of <size> bytes sent in <chunks> chunks`. Without `--agent`, the CLI first asks that\n instance which harness the persona resolves to, so a persona with `agent: claude` is carried even\n when your `COTAL_DEFAULT_AGENT` names another connector. A re-run of the same bytes\n sends nothing, and an interrupted carry continues where it stopped. The seat forks it in a\n private Claude home under the manager's `.cotal/seat-homes/`, which no other seat's Claude\n lists or finds, and which is removed when the seat stops. When Claude starts the fork, the seat\n records the SHA-256 of the transcript it read from its own project; the manager stops a seat\n whose record names other bytes than the carried ones, or that records none within the join\n timeout after it joins, an uncertain launch included, and otherwise shows that record as the\n seat's provenance. `cotal attach` to such a seat names its source after the seat\n name, as `(resumed from <host>:<id>)`. A remote manager receives a carry when its host issues it a\n transfer reader. On a user-auth mesh the CLI exchanges the operator's login for a one-object\n `transfer-writer` view, which needs scope `admin`.\n- A session name in place of an id is refused, listing each session on this host that carries\n that name with its id, SHA-256 and modification time. An id this host does not hold resolves\n against the **manager host's** `~/.claude`, as before.\n- A seat-private home holds no login. The manager host needs `CLAUDE_CODE_OAUTH_TOKEN` (from\n `claude setup-token`), `ANTHROPIC_AUTH_TOKEN`, or a cloud provider selection in its\n environment; `ANTHROPIC_API_KEY` alone is refused. The private home trusts the authorized\n launch directory without changing the manager's home. Claude must be 2.1.234 or later.\n- The manager waits for a real outcome: `\u2713 started` means the agent *joined the mesh*,\n `\u2717 exited on launch` carries Claude's last output and the seat's exit diagnostic, such as a\n startup confirmation that never appeared, and an uncertain launch (~30 s) is reported without\n tearing the agent down.\n- Resume is an **operator surface only**, deliberately not exposed on MCP `cotal_spawn`\n (a mesh peer naming host-local transcripts would widen `spawn` into transcript\n disclosure). Only the Claude connector supports it today; OpenCode and Hermes fail loud.\n- Needs a `claude` new enough for `--resume \u2026 --fork-session` (verified on 2.1.197).\n\n## Sharing your MCP servers\n\nA spawned session loads the MCP servers your normal Claude Code configuration enables on the\nlaunch host: user, local, project, plugin and connected-account sources. Claude handles discovery,\nprecedence, disabled servers, project approval and managed policy. Cotal adds its mesh server\nand any overrides declared in `connectors.claude.mcpServers` ([format](config.md)). It does not\ncopy native server definitions or credentials into the launch file or onto another machine.\nThe selected `CLAUDE_CONFIG_DIR` remains in effect for an ordinary local spawn.\n\nFirst-run setup still seeds eligible user-scope declarations into the Cotal config and preserves\nan existing list. That list is not the source of truth for native discovery. A same-named declared\noverride takes precedence; changing the native definition does not rewrite an override you kept.\nAn empty Cotal sharing map leaves native discovery enabled.\n\nUse `--share-tools tavily,figma` to load only those declared servers plus Cotal, or\n`--share-tools none` for Cotal alone. These explicit selections use `--strict-mcp-config`.\nCarried sessions also stay strict and keep their seat-private home; operator profiles and\nconnected-account credentials are not copied into them. Native managed policy still applies.\n\nThe child environment stays allowlisted. References in declared shared servers and the selected\nuser-owned MCP configuration forward their named variables. Repository and plugin files cannot\nadd ambient secret names merely by referencing them; supply such names through an explicit\nshared declaration or the existing `spawn.env` policy. Native files retain their own configured\nvalues. Documented native MCP switches, including the connected-account opt-out and tool-search\nchoice, keep their explicit values. Cotal does not forward the entire parent environment or force\ntool definitions to load upfront.\n\nSharing a credential grants it to the session, so enable only servers the session may use.\nEach enabled server may start a process per seat; an explicit sharing selection can reduce that\ncost. Native discovery does not change Cotal's mesh identity, hooks or channel wake path.\n\n### One Cotal path per session\n\nA spawned session is on its mesh through the local Cotal server. A connected-account source can\nalso give it a hosted Cotal connector, which would join the same session a second time under\nanother name. The plugin's `PreToolUse` hook refuses every `cotal_*` tool of any server other than\nthe local one in a session Cotal launched, with the reason in the refusal. It decides in the hook\nprocess, so the refusal holds when the control socket is down. A Claude Code session that Cotal did\nnot launch keeps the hosted connector. Where the plugin is installed, its local server's\ninstructions tell the model to use the local path unless you ask for the hosted one.\n\nIn such a session the plugin also keeps each hosted connection with the session that opened it. A\n`PostToolUse` hook on the hosted `cotal_connect` records the returned `connection_id` under the\nplugin's data directory (`CLAUDE_PLUGIN_DATA`), and `cotal_disconnect` releases it. A fork made with\n`--fork-session` inherits that id in its transcript but has a new session id, so its calls with the\nid are refused and it is told to call `cotal_connect`. A resumed session and its subagents keep the\nsession id and pass. An id the plugin never saw opened goes to the hosted connector, which checks\nthe account.\nThe guard runs only where the `cotal` plugin is installed. A Claude Code session with only the\nhosted connector has no hook, so nothing records its connections and its forks are not refused.\n\nA hosted connector cannot start a turn, so a hosted conversation hears a message only when it calls a\ntool. `cotal_wait` holds until a message that would wake the agent arrives and returns it, or returns\nwith none after the service's bound. The conversation calls it again to keep listening. Every other\nhosted tool adds a line to its result when messages are waiting. A conversation with no turn running\nis not woken.\n\n## Host sessions in one process\n\nA host serving many managed seats can run their MCP servers in one process with the subpath\n`@cotal-ai/connector-claude-code/mcp`, which loads with both `import` and `require`:\n\n```ts\nimport { serveClaudeSession } from \"@cotal-ai/connector-claude-code/mcp\";\n\nconst session = await serveClaudeSession({ env, input, output });\nawait session.close();\n```\n\nLoading the module only exports it; it serves its own stdio only when run, as `node dist/mcp.cjs`.\nThe subpath ships CommonJS declarations so TypeScript hosts can use either module format,\nincluding CommonJS consumers with Node16 or Node18 module settings.\nClaude Code pushes `claude/channel` only to a stdio server it launched, so each `claude` still\nlaunches a stdio command, such as a small relay that pipes its stdio to the host.\n\n- **Managed only.** `env` is a complete launch environment, copied when the call starts. It must\n carry a mesh identity and the full control pair, or the call rejects before the session joins the\n mesh. The presence environment label, bearer commands, carried-fork records and feedback contact\n lookup use that copy. Feedback's Git fallback reads the session's global config, including its\n includes, without reading the host repository config. Set `COTAL_FEEDBACK_EMAIL` or pass the\n feedback tool's `email` argument to choose another contact. Include the `PATH`, `HOME` and other OS\n variables subprocesses need; the host's environment is not merged into them. Subprocesses still\n use the host process's working directory. A session's card does not: its `meta.cwd` is the `cwd`\n its own `SessionStart` hook reports on startup, resume or fork, absent until then, and unchanged by\n clear or compact. It is advisory display data that every reader of the space's roster can see. A plain `claude` with no identity is served by `mcp.cjs` run directly. Sharing a process\n does not isolate untrusted tenants or their files.\n- **Streams.** `input` carries JSON-RPC from the client and `output` carries this session's replies\n and `claude/channel` pushes. The caller owns both streams. Input that has emitted `end` or is\n destroyed rejects startup. Buffered unread input remains eligible. When an active session's\n `input` emits `end` or `close`, the session closes, including after a relay connection resets.\n Teardown may pause `input` and removes the transport's error handler. A host that reuses the\n stream must resume it as needed and keep its own stream error handlers.\n- **`fatalBind`.** Off by default: a control socket that cannot be bound rejects the call and the\n host keeps its other sessions. Only a process that is the session turns it on, as `mcp.cjs` does.\n- **Teardown.** `close()` closes the control socket, the wake policy, the AG-UI emitter, the mesh\n endpoint and the MCP server, and is idempotent. It removes its input listeners without closing\n the caller's streams. The returned promise rejects if teardown fails. A startup failure closes\n what was started before the call rejects. A manager shutdown frame or input EOF or closure closes the\n session and then calls `onShutdown(code)` once. A teardown failure is logged and reported with\n code 1, without becoming an unhandled rejection in the host. Close waits for a pending broker\n dial to settle. A dial that fails after shutdown starts is not a teardown failure. The session\n releases local inbox custody only after its connections are proved closed, so the same lifecycle\n can reopen after a successful close. An active duplicate or an unproved close retains custody.\n\n## Feedback\n\n`cotal_feedback` works out of the box: without a key it posts to the public intake at\n`https://cotal.ai/v1/feedback` (needs a contact email: `COTAL_FEEDBACK_EMAIL`, then\n`git config user.email`, else the agent asks). Set `COTAL_FEEDBACK_KEY=fbk_<key>` in a\nbeta tester's environment to route to the keyed intake (`Authorization: Bearer`, identity\nderived from the key); `COTAL_FEEDBACK_URL` overrides either endpoint. The CLI can send\ntoo: `cotal feedback \"<summary>\" [--type bug]`. Each submission carries\n`origin: human | agent`, whether the tester asked, or the agent auto-reported a major\nissue.\n"
|
|
27301
27301
|
},
|
|
27302
27302
|
{
|
|
27303
27303
|
"slug": "connect-codex",
|
|
@@ -27395,14 +27395,14 @@ function loadDocsBundle() {
|
|
|
27395
27395
|
"title": "The cotal-lang card",
|
|
27396
27396
|
"kind": "Reference (informative)",
|
|
27397
27397
|
"summary": "One page to write a correct workflow program.",
|
|
27398
|
-
"body": '# The cotal-lang card\n\n> **Reference** (informative) \xB7 **For:** people writing a cotal-lang program \xB7 **Normative:** [spec/cotal-lang.md](../spec/cotal-lang.md)\n\nOne page to write a correct workflow program. The normative reference is\n[spec/cotal-lang.md](../spec/cotal-lang.md); this card compresses the parts programs get wrong\nfirst. A finished program is started with `cotal run start --file <program>` from a terminal or\nwith the `cotal_run` tool from a session ([workflows](workflows.md)); the manager validates it\nand answers with every problem before anything runs. A program is one module of restricted JavaScript: no imports, no `class`, no `Promise`, no\nhost globals. Every effect is journalled under a step key, so a run can stop on any host and\nresume on another with the recorded steps returning instantly.\n\n## Effects\n\n| Primitive | Call | Returns |\n|---|---|---|\n| `spawn` | `await spawn(persona, { name?, cwd?, placement?, worktree?, join?, role?, permits?, supervise?, onFork?, events? })` | agent handle |\n| `turn` | `await turn(agent, { name, deadline? })` | `{ status: "done" \\| "blocked" \\| "handoff", to?, note?, at }` |\n| `ask` | `await ask(agent, { name, schema, deadline?, attempts? })` | the record the agent published |\n| `checkpoint` | `await checkpoint(name, prompt, { schema?, timeout?, onExpiry?, to? })` | see below |\n| `sleep` | `await sleep("10m", { name? })` | `null` |\n| `wait` | `await wait(event, { name?, timeout? })` | the event value, `null` on timeout |\n| `waitUntil` | `await waitUntil(probe, { name, every, deadline, terminal? })` | the terminal observation |\n| `notify` | `await notify(agents, fact, { name? })` | `null` |\n| `monitor` | `await monitor(agent, { name? })` | `null` |\n\n`parallel`, `race`, `fanOut` and `conclave` are the four concurrency scopes (below).\n`waitUntil` calls `probe` every `every` until `terminal(observation)` holds (by default, until the\nobservation is not `null`); an elapsed `deadline` throws L4023, which the program can catch.\n`await once(fn, { name })` runs `fn` so that the `ask` inside it is dispatched at most once: a\nresume that finds it begun and never settled opens a hold for a person to answer instead. Step names\nare kebab-case; where the reference says a name is required, it must be a string literal. Option\nbags are closed: an unknown key is refused (L3011) with the full signature in the answer.\nDurations are a whole number and one unit: `"30s"`, `"10m"`, `"4h"`, `"2d"`.\n`permits` meter `turns` and `wallClock` on this host; `supervise` is `{ restarts, window? }`\n(default window `10m`) and restarts the process in place until that budget is spent.\n`events: false` starts a seat without an event plane, which a connector that publishes none\n(Hermes) needs.\n\n## Results you branch on\n\nA `turn` yields the agent\'s status. An `ask` yields the record the agent published. `schema` is\nopaque to the language: it is hashed and handed to the handler unchanged. Its handler-side\ncontract on `ask` is the shorthand, a record mapping each required top-level field of the reply\nto one of `"string"`, `"number"`, `"boolean"`, `"array"`, `"record"`, `"null"`. A handler\nenforcing it refuses a schema it cannot read with L4022 rather than skipping the check, counts\neach non-conforming reply against `attempts`, and reports L4006 when they are exhausted. The\nreference simulator enforces this. A `checkpoint`\'s `schema` stays uninterpreted in this\nrevision. A `checkpoint` is a durable pause raced against a durable timer:\n\n- resolved: `{ status: "resolved", value?, by?, at, artifact? }`\n- expired with `onExpiry: "proceed"`, or after an `"escalate"` hop expires too: `{ status: "expired", at }`\n- expired with the default `onExpiry: "fail"`: throws L4007\n\n```js\nconst gate = await checkpoint("ship-gate", "Ship 1.4.0 to npm?", { timeout: "4h", onExpiry: "proceed" })\nif (gate.status === "resolved") {\n log(gate.value, gate.by)\n} else {\n log("expired, holding")\n}\n```\n\n## The await rule (L2013)\n\nA call that starts an effect must be awaited where it stands, returned, or passed as a branch\nthunk to a scope. Anything else starts work nothing waits for, and the validator refuses it.\n\n```js\n// refused: L2013\nconst timer = sleep("10m")\n```\n\nValid forms: `await sleep("10m")`, `return sleep("10m")` inside a function, or\n`race({ timeout: () => sleep("10m"), reply: () => turnSomeone() })` as branch thunks. The same\nrule covers user functions declared `async`.\n\n## Concurrency\n\n`parallel` and `race` take branches unevaluated, as a record of thunks. The record keys are the\nbranch keys and survive reordering; array branches are keyed by index, which shifts when you\ninsert one (warning L3023). `fanOut(items, fn, { name, key? })` runs `fn(item, index)` per item;\nthe branch key is `key(item)`, else the item\'s string `id`, else the fan-out
|
|
27398
|
+
"body": '# The cotal-lang card\n\n> **Reference** (informative) \xB7 **For:** people writing a cotal-lang program \xB7 **Normative:** [spec/cotal-lang.md](../spec/cotal-lang.md)\n\nOne page to write a correct workflow program. The normative reference is\n[spec/cotal-lang.md](../spec/cotal-lang.md); this card compresses the parts programs get wrong\nfirst. A finished program is started with `cotal run start --file <program>` from a terminal or\nwith the `cotal_run` tool from a session ([workflows](workflows.md)); the manager validates it\nand answers with every problem before anything runs. A program is one module of restricted JavaScript: no imports, no `class`, no `Promise`, no\nhost globals. Every effect is journalled under a step key, so a run can stop on any host and\nresume on another with the recorded steps returning instantly.\n\n## Effects\n\n| Primitive | Call | Returns |\n|---|---|---|\n| `spawn` | `await spawn(persona, { name?, cwd?, placement?, worktree?, join?, role?, permits?, supervise?, onFork?, events? })` | agent handle |\n| `turn` | `await turn(agent, { name, deadline? })` | `{ status: "done" \\| "blocked" \\| "handoff", to?, note?, at }` |\n| `ask` | `await ask(agent, { name, schema, deadline?, attempts? })` | the record the agent published |\n| `checkpoint` | `await checkpoint(name, prompt, { schema?, timeout?, onExpiry?, to? })` | see below |\n| `sleep` | `await sleep("10m", { name? })` | `null` |\n| `wait` | `await wait(event, { name?, timeout? })` | the event value, `null` on timeout |\n| `waitUntil` | `await waitUntil(probe, { name, every, deadline, terminal? })` | the terminal observation |\n| `notify` | `await notify(agents, fact, { name? })` | `null` |\n| `monitor` | `await monitor(agent, { name? })` | `null` |\n\n`parallel`, `race`, `fanOut` and `conclave` are the four concurrency scopes (below).\n`waitUntil` calls `probe` every `every` until `terminal(observation)` holds (by default, until the\nobservation is not `null`); an elapsed `deadline` throws L4023, which the program can catch.\n`await once(fn, { name })` runs `fn` so that the `ask` inside it is dispatched at most once: a\nresume that finds it begun and never settled opens a hold for a person to answer instead. Step names\nare kebab-case; where the reference says a name is required, it must be a string literal. Option\nbags are closed: an unknown key is refused (L3011) with the full signature in the answer.\nDurations are a whole number and one unit: `"30s"`, `"10m"`, `"4h"`, `"2d"`.\n`permits` meter `turns` and `wallClock` on this host; `supervise` is `{ restarts, window? }`\n(default window `10m`) and restarts the process in place until that budget is spent.\n`events: false` starts a seat without an event plane, which a connector that publishes none\n(Hermes) needs.\n\n## Results you branch on\n\nA `turn` yields the agent\'s status. An `ask` yields the record the agent published. `schema` is\nopaque to the language: it is hashed and handed to the handler unchanged. Its handler-side\ncontract on `ask` is the shorthand, a record mapping each required top-level field of the reply\nto one of `"string"`, `"number"`, `"boolean"`, `"array"`, `"record"`, `"null"`. A handler\nenforcing it refuses a schema it cannot read with L4022 rather than skipping the check, counts\neach non-conforming reply against `attempts`, and reports L4006 when they are exhausted. The\nreference simulator enforces this. A `checkpoint`\'s `schema` stays uninterpreted in this\nrevision. A `checkpoint` is a durable pause raced against a durable timer:\n\n- resolved: `{ status: "resolved", value?, by?, at, artifact? }`\n- expired with `onExpiry: "proceed"`, or after an `"escalate"` hop expires too: `{ status: "expired", at }`\n- expired with the default `onExpiry: "fail"`: throws L4007\n\n```js\nconst gate = await checkpoint("ship-gate", "Ship 1.4.0 to npm?", { timeout: "4h", onExpiry: "proceed" })\nif (gate.status === "resolved") {\n log(gate.value, gate.by)\n} else {\n log("expired, holding")\n}\n```\n\n## The await rule (L2013)\n\nA call that starts an effect must be awaited where it stands, returned, or passed as a branch\nthunk to a scope. Anything else starts work nothing waits for, and the validator refuses it.\n\n```js\n// refused: L2013\nconst timer = sleep("10m")\n```\n\nValid forms: `await sleep("10m")`, `return sleep("10m")` inside a function, or\n`race({ timeout: () => sleep("10m"), reply: () => turnSomeone() })` as branch thunks. The same\nrule covers user functions declared `async`.\n\n## Concurrency\n\n`parallel` and `race` take branches unevaluated, as a record of thunks. The record keys are the\nbranch keys and survive reordering; array branches are keyed by index, which shifts when you\ninsert one (warning L3023). `fanOut(items, fn, { name, key? })` runs `fn(item, index)` per item;\nthe branch key is `key(item)`, else the item\'s string `id`, else the run refuses the fan-out when\nit reaches it (L3021). `cotal run validate` warns with the same code on a literal list it can see.\n\n```js\nasync function review(pr) {\n const seat = await spawn("reviewer", { worktree: pr.id })\n return await turn(seat, { name: "review", deadline: "30m" })\n}\nconst prs = [{ id: "pr-11" }, { id: "pr-12" }]\nconst results = await fanOut(prs, (pr) => review(pr), { name: "review-all", key: (pr) => pr.id })\nlog(results)\n```\n\nA branch may not write to anything born outside it. Return values from branches and read the\nscope\'s result instead.\n\n```js\n// refused: L2032\nlet seen = 0\nawait parallel({\n a: async () => { seen = 1 },\n b: async () => { seen = 2 },\n})\n```\n\n## Values across effects\n\nA value that crosses an effect boundary is frozen on the way back: writing to it is L2031, so\ncopy it into a fresh record first. Effect arguments must have a canonical form: `undefined` or a\nnon-finite number inside one is L3041, a function is L3042. `json.stringify` is the canonical\nform (sorted keys, no spaces), and it refuses what has no canonical form (L4016) rather than\ndropping it.\n\nA value nested deep enough to exhaust the host\'s stack (a `json.stringify` of an array nested\nthousands deep, or deep recursion) fails the run. A `catch` never sees it and a `finally` does not\nrun past it, because the depth at which it happens depends on the host and not on the program.\n\n## Top refusals\n\n| Code | What it refuses | Write instead |\n|---|---|---|\n| L2013 | an effect call nothing awaits | `await` it, `return` it, or pass a thunk branch |\n| L2032 | a branch writing outside itself | return from the branch, read the scope\'s result |\n| L2031 | writing a value that crossed an effect | copy into a fresh record, then write |\n| L2012 | a host global by name | the replacement in the message, e.g. `json.stringify` |\n| L2011 | `Promise` | the four scopes |\n| L1025 | `==`, `!=` | `===`, `!==` |\n| L1001 | `class` | records and functions |\n| L4018 | a record, array or function where a primitive is needed; a non-number under `++`/`--` | convert explicitly |\n| L3013 | a computed step name where a literal is required | a string literal |\n| L3011 | an unknown option key | the signature in the refusal |\n\nEvery code has a row in the reference\'s Appendix A, and the message a refusal prints is that\nrow\'s title, so search the reference for it verbatim.\n'
|
|
27399
27399
|
},
|
|
27400
27400
|
{
|
|
27401
27401
|
"slug": "manifest",
|
|
27402
27402
|
"title": "Mesh manifest (`cotal.yaml`)",
|
|
27403
27403
|
"kind": "Reference: every field of the mesh manifest.",
|
|
27404
27404
|
"summary": "A manifest (cotal.yaml, kind: Mesh) describes a whole team (its channels, its agents, and who may read and post where) in one file.",
|
|
27405
|
-
"body": "# Mesh manifest (`cotal.yaml`)\n\n> **Reference**: every field of the mesh manifest. \xB7 **For:** operators \xB7 **Walkthrough:** [Define a team](define-a-team.md) \xB7 **ACL semantics:** [SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization)\n\nA manifest (`cotal.yaml`, `kind: Mesh`) describes a whole team (its channels, its agents,\nand who may read and post where) in one file. It is **channel-centric**: you list the\nchannels, and under each one name the agents that may read and post; Cotal inverts that\ninto one least-privilege credential per agent. The manifest is a convenience over the CLI;\nit adds no wire concepts. Today it is **single-space** (one `space:` per file).\n\nThe lifecycle (`cotal topology view -f` / `up -f` / `team deploy` / `team remove` / `down`), ownership,\nand teardown behavior are in the guide: [Define a team](define-a-team.md).\n\n## Top level\n\n| Key | Required | Meaning |\n|---|---|---|\n| `apiVersion` | yes | Must be `cotal/v1`. |\n| `kind` | yes | Must be `Mesh`. |\n| `space` | yes | The space name (one per file; `spaces:` is not supported in v1). A space's auth is bound to one root; to run a non-default space in a checkout that already ran `cotal up` (which sets up `main`), use a fresh directory. |\n| `broker` | no | `servers` (comma-separated broker URLs: this sets the address/port; default `nats://127.0.0.1:4222`; **no embedded creds**), `host` (bind interface only, no scheme; does *not* set the port), `auth` (the auth mode: unset/`true`/`\"static\"` = per-agent JWT creds, the default; `false` = an open dev mesh; `\"user\"` = per-user auth, where people `cotal login` and every connect is authorized against the actor ledger; pair with `idp`), `idp` (with `auth: \"user\"`: the IdP auth base URL to pin on first enable; https, or http on a loopback IP literal such as `127.0.0.1`; **no embedded creds**, query or fragment). The port comes from `servers`/`--server`, never `host`/`--host`. |\n| `runtime` | no | Registered manager runtime name. `pty` is built in; optional providers such as `tmux`, `cmux`, `orca`, and `herdr` are installed with `cotal ext add`. |\n| `agent` | no | Default harness (`claude` / `opencode` / `hermes`) for agents that don't set their own. There is **no silent default**; an agent needs this or its own `agent:`. |\n| `personaPermissions` | no | `reject` (default): the manifest is the whole truth. `include`: a persona's own channel grants are inherited for channels the manifest doesn't declare. |\n| `defaults` | no | Channel defaults applied unless a channel overrides: `replay`, `replayWindow`, `deliveryClass` (`live` / `durable`). Semantics in [SPEC \xA77](../SPEC.md#7-channels). |\n| `agents` | no | name \u2192 persona (a channels-first manifest can seed rooms now and add agents later). |\n| `channels` | yes | name \u2192 channel (below). |\n\nUnknown keys are rejected (no silent ignore), and every error is reported with its file and line.\n\n## Agent forms\n\n```yaml\nagents:\n planner: ./agents/planner.md # 1) bare path: reuse a persona file as-is\n builder: # 2) a persona file + overrides (manifest wins)\n persona: ./agents/builder.md\n model: sonnet\n cwd: repos/backend # relative to the manager workspace\n role: implementer\n instructions: Prefer the smallest change that works.\n lead: # 3) inline (no file): needs model, variant or instructions\n model: opus\n role: lead\n capabilities: [spawn] # may spawn helpers\n instructions: Coordinate the team.\n prompt: Introduce yourself in #general and assign the first task.\n```\n\nPer-agent keys: `persona`, `agent` (harness override), `cwd`, `continuity`, `model`, `variant`,\n`launchOptions` (connector-specific options, merged per key over the persona's `launchOptions:`),\n`role`, `description`, `instructions`, `prompt`, `events` (`false` is `--no-events`), `capabilities` (`spawn`,\n[what it grants](identity-and-auth.md); on a per-user-auth mesh also `role:<r>`, so the\nagent may delegate that role when spawning; `admin` is never accepted from a manifest),\n`personaPermissions` (override the top-level policy). Model strings and variants pass to\nthe harness as-is: for Claude use the short form (`opus`, `sonnet`) or the full id; for\nOpenCode use `provider/model` plus an optional variant (`cotal models --agent opencode`\nlists both). Persona file format: [agent files](agent-files.md).\n\n`cwd` is the agent's working directory on the **manager host**. A relative path resolves\nagainst the manager workspace, matching `cotal spawn --cwd`; an absolute path is used\nas supplied. Omitting it keeps the manager workspace as the default. It is never resolved\nagainst the manifest or persona directory on the deploying machine. Changing `cwd` marks\nan already-deployed agent stale and requires a restart. Empty paths and NUL bytes are\nrejected. This field controls the directory only; `continuity` restores a harness session.\n\n`continuity` says whether an agent keeps its harness session across launches. `none`, the\ndefault, starts a new session every time. `exact` reopens the session the manager last\nbound to this agent name, so after `cotal down` and `cotal up -f` the agent comes back\nwith its previous context. The manager owns the session id: on the first launch it\nrecords the session the connector proves over its authenticated control endpoint in\n`<manager workspace>/.cotal/continuity/<name>.json`. A connector that offers no such proof\nfails the launch, and nothing is recorded. The record follows the agent: crash recovery\nupdates it when it rebinds a session, and a stop or a preservation cut records the\nsession the agent last ran, including one it switched to itself. Every later launch, and\na preserved resume of the session that cut retained, reopens the session under the same\nproof and fails if the connector reports a different one. A resume that fails this proof\nstops the seat it started and frees it once its exit is proved; a seat whose stop cannot be\nproved stays managed, and the resume's error says so. A reopen also fails when the\nharness no longer has that session, for example after its transcript was deleted or when\nthe agent never answered and so nothing was stored; the manager never starts an empty\nsession under the old id, and a failed launch names the file to remove to start a new\none. The manifest never names a session id, and the imperative `cotal spawn --resume`\nfork stays separate. A reopened session does not get the kickoff\n`prompt` again. The manager refuses to reopen a recorded session whose space, connector\nor resolved `cwd` differs from the declaration, and names the file to remove to start a\nnew session. A connector that cannot reopen an existing session refuses the manifest at\npreflight (today only `pi` can). Switching `continuity` marks an already-deployed agent\nstale.\n\n`instructions` and `prompt` differ in kind: `instructions` become the session's **system\nprompt** (who the agent is), while `prompt` is a **kickoff message** auto-submitted once\nthe session is up (what to do right now). This is the declarative form of `cotal spawn --prompt`.\nIt is submitted on first boot and again on a stale-restart (it is part of the launch form,\nso changing it marks a running agent `stale` like any other launch field); a manager\nreclaiming a still-live session does not re-submit it. A connector that cannot deliver a\nkickoff prompt refuses the manifest at preflight (today: hermes), the same way an\nunsupported `variant:` is refused.\n\n## Channel grants\n\nA channel carries its registry card (`description`, `instructions`, `replay`, \u2026;\n[SPEC \xA77](../SPEC.md#7-channels)) plus three lists of agent names, the same verbs Cotal\nuses everywhere ([channels & permissions](channels-and-permissions.md)):\n\n| Verb | ACL | Meaning |\n|---|---|---|\n| `subscribe` | none | Auto-listen at boot. A subscriber is implicitly allowed to read. |\n| `allowSubscribe` | **read** | May read the channel. Omitted \u21D2 defaults to `subscribe`. Must be a superset of `subscribe`. |\n| `allowPublish` | **post** | May post. **Default-deny**: an empty or omitted list means nobody posts. |\n\nA read-only channel (no agent posts, e.g. an operator writes the record by hand with\n`cotal send`, which is a CLI action outside agent ACLs):\n\n```yaml\nchannels:\n decisions:\n description: The durable record of what we decided.\n subscribe: [lead]\n allowPublish: [] # read-only for agents\n```\n\nEvery name under a channel must be declared in `agents:`. Channel names must be concrete\n(no wildcards in v1).\n\n## How access is resolved\n\nYou declare membership per channel; Cotal inverts it into each agent's minted creds:\n\n- **Read** comes from `allowSubscribe` (or `subscribe` when `allowSubscribe` is omitted).\n- **Post** comes from `allowPublish`, and is default-deny: an agent you don't list cannot\n post, even to a channel it reads.\n- `subscribe` only sets what an agent *auto-listens to* at boot; it never widens read.\n\nWith `personaPermissions: reject` (the default) the manifest is the complete picture; a\npersona file's own channel grants are ignored, so the file you read is what each\nagent can do. Set `include` (top level or per agent) to *also* inherit a persona's own\ngrants for channels the manifest doesn't mention. `cotal topology view -f` always prints\nthe resolved graph, inherited scopes included.\n\n---\n\nFor implementers: the channel-centric \u2192 per-agent inversion lives in\n[`resolve.ts`](../implementations/cli/src/lib/manifest/resolve.ts); the `team deploy`\nclassification in [`spawn-plan.ts`](../implementations/cli/src/lib/manifest/spawn-plan.ts) and\n[`team-deploy-inspection.ts`](../implementations/cli/src/lib/team-deploy-inspection.ts); the\nteardown of an older `spawn -f` ledger in\n[`down-manifest.ts`](../implementations/cli/src/commands/down-manifest.ts).\n"
|
|
27405
|
+
"body": "# Mesh manifest (`cotal.yaml`)\n\n> **Reference**: every field of the mesh manifest. \xB7 **For:** operators \xB7 **Walkthrough:** [Define a team](define-a-team.md) \xB7 **ACL semantics:** [SPEC \xA79](../SPEC.md#9-nats--jetstream-security-and-authorization)\n\nA manifest (`cotal.yaml`, `kind: Mesh`) describes a whole team (its channels, its agents,\nand who may read and post where) in one file. It is **channel-centric**: you list the\nchannels, and under each one name the agents that may read and post; Cotal inverts that\ninto one least-privilege credential per agent. The manifest is a convenience over the CLI;\nit adds no wire concepts. Today it is **single-space** (one `space:` per file).\n\nThe lifecycle (`cotal topology view -f` / `up -f` / `team deploy` / `team remove` / `down`), ownership,\nand teardown behavior are in the guide: [Define a team](define-a-team.md).\n\n## Top level\n\n| Key | Required | Meaning |\n|---|---|---|\n| `apiVersion` | yes | Must be `cotal/v1`. |\n| `kind` | yes | Must be `Mesh`. |\n| `space` | yes | The space name (one per file; `spaces:` is not supported in v1). A space's auth is bound to one root; to run a non-default space in a checkout that already ran `cotal up` (which sets up `main`), use a fresh directory. |\n| `broker` | no | `servers` (comma-separated broker URLs: this sets the address/port; default `nats://127.0.0.1:4222`; **no embedded creds**; a `tls://` URL needs `--tls-cert` and `--tls-key` on `up -f`, or the start is refused), `host` (bind interface only, no scheme; does *not* set the port), `auth` (the auth mode: unset/`true`/`\"static\"` = per-agent JWT creds, the default; `false` = an open dev mesh; `\"user\"` = per-user auth, where people `cotal login` and every connect is authorized against the actor ledger; pair with `idp`), `idp` (with `auth: \"user\"`: the IdP auth base URL to pin on first enable; https, or http on a loopback IP literal such as `127.0.0.1`; **no embedded creds**, query or fragment). The port comes from `servers`/`--server`, never `host`/`--host`. |\n| `runtime` | no | Registered manager runtime name. `pty` is built in; optional providers such as `tmux`, `cmux`, `orca`, and `herdr` are installed with `cotal ext add`. |\n| `agent` | no | Default harness (`claude` / `opencode` / `hermes`) for agents that don't set their own. There is **no silent default**; an agent needs this or its own `agent:`. |\n| `personaPermissions` | no | `reject` (default): the manifest is the whole truth. `include`: a persona's own channel grants are inherited for channels the manifest doesn't declare. |\n| `defaults` | no | Channel defaults applied unless a channel overrides: `replay`, `replayWindow`, `deliveryClass` (`live` / `durable`). Semantics in [SPEC \xA77](../SPEC.md#7-channels). |\n| `agents` | no | name \u2192 persona (a channels-first manifest can seed rooms now and add agents later). |\n| `channels` | yes | name \u2192 channel (below). |\n\nUnknown keys are rejected (no silent ignore), and every error is reported with its file and line.\n\n## Agent forms\n\n```yaml\nagents:\n planner: ./agents/planner.md # 1) bare path: reuse a persona file as-is\n builder: # 2) a persona file + overrides (manifest wins)\n persona: ./agents/builder.md\n model: sonnet\n cwd: repos/backend # relative to the manager workspace\n role: implementer\n instructions: Prefer the smallest change that works.\n lead: # 3) inline (no file): needs model, variant or instructions\n model: opus\n role: lead\n capabilities: [spawn] # may spawn helpers\n instructions: Coordinate the team.\n prompt: Introduce yourself in #general and assign the first task.\n```\n\nPer-agent keys: `persona`, `agent` (harness override), `cwd`, `continuity`, `model`, `variant`,\n`launchOptions` (connector-specific options, merged per key over the persona's `launchOptions:`),\n`role`, `description`, `instructions`, `prompt`, `events` (`false` is `--no-events`), `capabilities` (`spawn`,\n[what it grants](identity-and-auth.md); on a per-user-auth mesh also `role:<r>`, so the\nagent may delegate that role when spawning; `admin` is never accepted from a manifest),\n`personaPermissions` (override the top-level policy). Model strings and variants pass to\nthe harness as-is: for Claude use the short form (`opus`, `sonnet`) or the full id; for\nOpenCode use `provider/model` plus an optional variant (`cotal models --agent opencode`\nlists both). Persona file format: [agent files](agent-files.md).\n\n`cwd` is the agent's working directory on the **manager host**. A relative path resolves\nagainst the manager workspace, matching `cotal spawn --cwd`; an absolute path is used\nas supplied. Omitting it keeps the manager workspace as the default. It is never resolved\nagainst the manifest or persona directory on the deploying machine. Changing `cwd` marks\nan already-deployed agent stale and requires a restart. Empty paths and NUL bytes are\nrejected. This field controls the directory only; `continuity` restores a harness session.\n\n`continuity` says whether an agent keeps its harness session across launches. `none`, the\ndefault, starts a new session every time. `exact` reopens the session the manager last\nbound to this agent name, so after `cotal down` and `cotal up -f` the agent comes back\nwith its previous context. The manager owns the session id: on the first launch it\nrecords the session the connector proves over its authenticated control endpoint in\n`<manager workspace>/.cotal/continuity/<name>.json`. A connector that offers no such proof\nfails the launch, and nothing is recorded. The record follows the agent: crash recovery\nupdates it when it rebinds a session, and a stop or a preservation cut records the\nsession the agent last ran, including one it switched to itself. Every later launch, and\na preserved resume of the session that cut retained, reopens the session under the same\nproof and fails if the connector reports a different one. A resume that fails this proof\nstops the seat it started and frees it once its exit is proved; a seat whose stop cannot be\nproved stays managed, and the resume's error says so. A reopen also fails when the\nharness no longer has that session, for example after its transcript was deleted or when\nthe agent never answered and so nothing was stored; the manager never starts an empty\nsession under the old id, and a failed launch names the file to remove to start a new\none. The manifest never names a session id, and the imperative `cotal spawn --resume`\nfork stays separate. A reopened session does not get the kickoff\n`prompt` again. The manager refuses to reopen a recorded session whose space, connector\nor resolved `cwd` differs from the declaration, and names the file to remove to start a\nnew session. A connector that cannot reopen an existing session refuses the manifest at\npreflight (today only `pi` can). Switching `continuity` marks an already-deployed agent\nstale.\n\n`instructions` and `prompt` differ in kind: `instructions` become the session's **system\nprompt** (who the agent is), while `prompt` is a **kickoff message** auto-submitted once\nthe session is up (what to do right now). This is the declarative form of `cotal spawn --prompt`.\nIt is submitted on first boot and again on a stale-restart (it is part of the launch form,\nso changing it marks a running agent `stale` like any other launch field); a manager\nreclaiming a still-live session does not re-submit it. A connector that cannot deliver a\nkickoff prompt refuses the manifest at preflight (today: hermes), the same way an\nunsupported `variant:` is refused.\n\n## Channel grants\n\nA channel carries its registry card (`description`, `instructions`, `replay`, \u2026;\n[SPEC \xA77](../SPEC.md#7-channels)) plus three lists of agent names, the same verbs Cotal\nuses everywhere ([channels & permissions](channels-and-permissions.md)):\n\n| Verb | ACL | Meaning |\n|---|---|---|\n| `subscribe` | none | Auto-listen at boot. A subscriber is implicitly allowed to read. |\n| `allowSubscribe` | **read** | May read the channel. Omitted \u21D2 defaults to `subscribe`. Must be a superset of `subscribe`. |\n| `allowPublish` | **post** | May post. **Default-deny**: an empty or omitted list means nobody posts. |\n\nA read-only channel (no agent posts, e.g. an operator writes the record by hand with\n`cotal send`, which is a CLI action outside agent ACLs):\n\n```yaml\nchannels:\n decisions:\n description: The durable record of what we decided.\n subscribe: [lead]\n allowPublish: [] # read-only for agents\n```\n\nEvery name under a channel must be declared in `agents:`. Channel names must be concrete\n(no wildcards in v1).\n\n## How access is resolved\n\nYou declare membership per channel; Cotal inverts it into each agent's minted creds:\n\n- **Read** comes from `allowSubscribe` (or `subscribe` when `allowSubscribe` is omitted).\n- **Post** comes from `allowPublish`, and is default-deny: an agent you don't list cannot\n post, even to a channel it reads.\n- `subscribe` only sets what an agent *auto-listens to* at boot; it never widens read.\n\nWith `personaPermissions: reject` (the default) the manifest is the complete picture; a\npersona file's own channel grants are ignored, so the file you read is what each\nagent can do. Set `include` (top level or per agent) to *also* inherit a persona's own\ngrants for channels the manifest doesn't mention. `cotal topology view -f` always prints\nthe resolved graph, inherited scopes included.\n\n---\n\nFor implementers: the channel-centric \u2192 per-agent inversion lives in\n[`resolve.ts`](../implementations/cli/src/lib/manifest/resolve.ts); the `team deploy`\nclassification in [`spawn-plan.ts`](../implementations/cli/src/lib/manifest/spawn-plan.ts) and\n[`team-deploy-inspection.ts`](../implementations/cli/src/lib/team-deploy-inspection.ts); the\nteardown of an older `spawn -f` ledger in\n[`down-manifest.ts`](../implementations/cli/src/commands/down-manifest.ts).\n"
|
|
27406
27406
|
},
|
|
27407
27407
|
{
|
|
27408
27408
|
"slug": "mesh-view",
|
|
@@ -27416,7 +27416,7 @@ function loadDocsBundle() {
|
|
|
27416
27416
|
"title": "Message flow",
|
|
27417
27417
|
"kind": "Concept (informative)",
|
|
27418
27418
|
"summary": "How peers see each other and how messages reach them: the presence directory, the three delivery modes, and the two delivery guarantees.",
|
|
27419
|
-
"body": '# Message flow\n\n> **Concept** (informative) \xB7 **For:** everyone \xB7 **Normative:** [SPEC \xA74](../SPEC.md#4-delivery-modes), [\xA76](../SPEC.md#6-presence-and-discovery), [\xA76.1](../SPEC.md#61-plane-liveness), [\xA77](../SPEC.md#7-channels), [\xA78](../SPEC.md#8-nats--jetstream-binding)\n\nHow peers see each other and how messages reach them: the presence directory, the three\ndelivery modes, and the two delivery guarantees. This page explains; the linked spec\nsections define.\n\n## Presence\n\nPresence is a per-space directory keyed by instance id: each peer\'s identity card\n(`AgentCard`: name, role, kind, tags, what it can do) plus its live state:\n\n- `idle`: free\n- `waiting`: blocked on input, approval, or a peer\n- `working`: the seat (or its surviving connector) claims it is busy. That is a\n process-alive claim, not a progress claim. A frozen seat can stay `working` while\n its heartbeat stays fresh. Surfaces that have no outside observation of last\n assistant-message age render `progress unknown` rather than treating heartbeat\n age as work age. A stale observation overlays `stalled Xm` on the still-fresh\n presence. The observation classifier is workstation/operator data, not part of\n the wire protocol; human wording stays in each CLI, connector, or web renderer.\n Compact maps and DM pickers use the presence glyph only and make no progress\n claim. See issue #876.\n- `offline`: gone (gracefully, or its heartbeat lapsed)\n\nA peer refreshes its own entry on a heartbeat; observers also derive `offline` from stale\ntimestamps, so a crashed agent cannot linger as "working". That derivation is gated on the\nobserver actually hearing the bucket: if the whole watch has been silent past the liveness\nwindow, the honest output is that the *view* is stale, not that every peer died in one TTL\n(real rosters do not do that). Offline peers stay in the roster for observability. An\n`activity` string rides along ("what I\'m doing right now"), and a peer\'s **attention**\npreference is mirrored here too (below). Each instance writes *only its own* key; presence\nis where discovery lives (our equivalent of `.well-known`), not a place to describe others.\nThe optional `condition` beside status relays a harness-reported cause such as `rate_limit`,\n`approval`, or `input`; missing means the harness reported none. A condition is cleared when a\nnormal next turn starts. The optional `activeAt` is the epoch ms of the last work event the harness\nreported, such as a token or a tool call; missing means the connector reports none. `ts` is only the\nheartbeat, so a seat whose turn stopped advancing keeps a fresh `ts` and an old `activeAt`. The\nconnector records it as events arrive and the next heartbeat carries it. `cotal ps`,\n`cotal endpoints`, `cotal console`, `cotal join` and `cotal_roster` print a condition with its age\nand the age of `activeAt`, such as `waiting (rate_limit for 40m) \xB7 active 40m ago`. The optional\n`statusSince` is the epoch ms when the instance entered its current status and activity. A change to\neither moves it, while a heartbeat or a repeated report does not, so an activity that outlived what\nit described reads as old. `cotal endpoints`, `cotal console`, `cotal join` and `cotal_roster` print\nits age, such as `idle \xB7 unchanged for 40m`. An offline record carries none,\nbecause an observer that derives `offline` from a stale heartbeat does not know when the peer left.\nThe optional `activitySince` is when the current activity was set. A status change does not move it,\nso an activity left behind while hooks flip the status every turn still shows its age after the\nactivity on the same surfaces, such as `(set 9h ago)`. A stamp that is not a finite number shows no\nage. The optional `runtime` describes the OS process holding the mesh connection. Its opaque `id`\nis minted once per process and lifecycle scope and is never reused across scopes. Its `startedAt`\nis the process\'s own `performance.timeOrigin`, shared by all endpoints in that process, and its\nclosed `source` is `"endpoint-process"`. Status, activity, heartbeats and in-process reconnects keep\nthat observation unchanged within the scope. A lifecycle registered later reports the process\'s\nage, not the lifecycle\'s age. It does not date a separately launched parent or model harness.\nAn offline record retains the observation without claiming a running duration. Older publishers\nomit it, which means unknown. A runtime-only change notifies presence observers.\nA peer writes its own condition code and activity, so every surface above prints each on one\nline, shows a control character or line separator in it as a `\\uXXXX` escape, and cuts it at 200\ncharacters.\nThe optional `environment` is an opaque provider reference. Core publishes\nit and never interprets it. Readers reject a row whose `card.id` does not match its KV key, whose\n`card.name` or `status` is missing or has the wrong type, whose `ts` is not a finite number, whose\n`card.role`, `activity` or `attention` is present and not a string, or whose `condition` is present and\nis not an object with a string `code`, such as `null`, a bare string or an object with no `code`. They\nreport that rejection through the recoverable warning path. A kept row whose `ts` is missing or text\nsuch as `"nope"` would never read as stale, so it would stay live after its key expired. A kept object such as\n`{"toString":null,"valueOf":null}` in a printed field would throw when converted and fail every\nreader\'s roster.\nDetails: [SPEC \xA76](../SPEC.md#6-presence-and-discovery). The dashboard surfaces a stale view\non the same header mark it uses for a refused poll ([watch a mesh](watch-a-mesh.md)).\n\n`CotalEndpoint.presenceView()` reports whether its local roster can support an absence verdict.\n`current` is usable, `unpopulated` means the current watch has not completed its initial snapshot,\nand `stale` means the watch has been silent past its liveness window. Both unsafe states carry\n`fresh: false`, so an older consumer degrades instead of treating a partial reconnect refill as a\ncomplete roster. `waitForPresenceSnapshot()` returns `snapshot` or `timeout`; a timeout is a bounded\ngive-up, not proof that the snapshot completed.\n\nA stale view under a live connection is not left to stand. The endpoint rebinds its presence watch\nfrom the bucket\'s current state once per liveness window and reports the rebind as a `warning`\nnaming the silent interval; a held link\'s rebind fails or stays silent and the view stays stale. A\nrebind that is still awaiting the broker when the endpoint stops or rebuilds its connection installs\nnothing. A rebind that lands on a bucket with no keys is current knowledge for an observer that\ndoes not register (nobody is present), and a wipe for one that does (its own key is missing too):\nthe latter re-publishes itself and lets the delivery of that record make the view current.\n`cotal ps` prints `mesh unknown` with the reason, never a liveness word, for a row whose\nmanager reports a view that is not `current` ([cli.md](cli.md)).\n\nPresence publishing is also monitored separately from the watch. One refused heartbeat remains a\nrecoverable warning. If consecutive writes keep failing for a full presence TTL, the endpoint raises\n`PresenceWriteStuckError` with code `presence-write-stuck` and marks the failure record as stuck.\n`cotal_orientation` and `cotal_roster` then say the view is not live and call the roster\nlast-known until a write succeeds. A successful write resets the consecutive count and clears the\ncondition. The condition is local diagnosis, not a new wire field.\n\nThe same two tools also render the view\'s own trust state: under an `unpopulated` view they say\nthe presence watch has not completed its initial snapshot, so the roster may be partial and a\nmissing name is not an absence verdict, and under a `stale` view they name the silent-since instant\nand call the roster last-known. A DM to a name, or a send that @mentions a name, that the observer\ncannot verify is refused with that condition rather than sent, instead of being reported as an\nunknown peer. Both tools and both refusals print each condition in the same words.\n\n## Plane liveness\n\nPresence tells you which peers are around. It does not tell you whether the manager or the\ndelivery daemon is up, and the lease buckets that do know are not readable by agents. So when a\njoin or a send fails, a peer used to have no way to separate a credential problem from a dead\nmanager or an unbound delivery daemon.\n\nAny credentialed peer can now ask. It sends an empty request on\n`cotal.<space>.live.<plane>.<owner>.<actor>`, where `<plane>` is `manager` or `delivery` and\n`<owner>.<actor>` is its own principal, and names its reply subject under that request as\n`<request>.reply.<nonce>`. In code this is `CotalEndpoint.probeLiveness(plane)`. Any other plane\nname is refused before anything is sent.\n\nThe reply is a `LivenessAnswer`:\n\n```json\n{ "plane": "delivery", "responder": "bound", "instance": "3f1c0b52-..." }\n```\n\n`responder` is one of `bound`, `unbound`, `stale` or `unknown`. `instance` is an opaque token\nthe responder mints each time it binds. Two answers with different tokens came from two\nresponders, which is how a caller probing more than once can spot two manager instances that\ndisagree. The token identifies nothing else: it is not the instance id, the principal, a pid, a\nhost or a path. The reply carries nothing beyond these three fields, so the lease row\'s holder\nand workspace path never leave the responder.\n\nHow the caller reads the outcome:\n\n| What happened | `responder` |\n| --- | --- |\n| a well-formed reply | whatever the reply says |\n| the broker answered "no responders" | `unbound` |\n| timeout, permission refusal, transport failure | `unknown` |\n| a reply that does not parse | `unknown` |\n\n`unknown` means the probe did not find out. It is never a health report. Each responder grades\nonly itself. The manager says `bound` while its service endpoint is serving, and `unbound` while\nthat connection is down, both while the client reconnects it and while the manager re-dials it\nafter a close. The delivery daemon\nreads its own shard lease: no ready row is `unbound`, a ready row held by another instance is\n`stale`, its own ready row is `bound`. A responder that cannot read its own state answers\n`unknown`.\n\nResponders serve `live.<plane>.*.*` in the queue group `live.<plane>`, so one probe gets one\nanswer from one instance. Only the plane\'s own credential holds that filter, and its reply grant\nstops at the `.reply.` leaf, so a responder cannot forge a probe and an agent cannot answer one.\nA request whose reply target is outside the caller\'s own `.reply.` subtree is dropped with a\nwarning, and the responder keeps serving. When an endpoint replaces its broker connection it\nbinds its responders again on the new one, under a new token, so a reconnect does not leave the\nplane looking unbound. The normative rules are in\n[SPEC \xA76.1](../SPEC.md#61-plane-liveness).\n\n## Three delivery modes\n\nEvery delivery message is addressed one of three ways\n([SPEC \xA74](../SPEC.md#4-delivery-modes)):\n\n| Mode | Addressed by | Reaches |\n|---|---|---|\n| **multicast** | `channel` | every subscriber of the channel |\n| **unicast** | `to` (instance id) | one specific peer\'s inbox |\n| **anycast** | `toService` (role) | *any one* holder of the role: "whoever is a reviewer" |\n\n\n\n\n\n\n\nChannels are dotted and hierarchical (`team.backend`); publishing is always concrete,\nsubscriptions may wildcard a subtree (`team.>`). Anycast is queued work: a task with no\nworker online *waits*; multiple online instances of a role load-balance; the task is\nremoved once acked.\n\n**Mentions.** A multicast message may carry `mentions: [name\u2026]`, a *priority hint*, not\na routing target. The message still reaches the whole channel, but a mentioned peer is\nwoken immediately while everyone else picks it up when next idle. Names (not instance\nids) ride the wire, so the match survives reconnects.\n\n**Deriving the mode.** A receiver derives how a message was addressed (channel / dm /\nanycast) from the *delivering subject*, never from payload fields: the payload is\nadvisory and forgeable, while the subject is broker-policed\n([SPEC \xA74](../SPEC.md#4-delivery-modes), [identity & auth](identity-and-auth.md)).\n\n**How the block is framed.** Delivered messages arrive as one block: a header, the items,\nand a tail. The tail names the *order of operations* - do what was asked with your own\ntools, verify the result, then reply - and says not to report an action that was not\nperformed, while still naming the reply verbs. This matters because a peer message is\nfrequently a work order and the tail lands where the model decides its next\naction. A tail that lists only reply tools reads as "this is a chat turn, answer it", and\nfor a weak model an answer that sounds finished is cheaper than the work: a live seat told\nto write a file and confirm sent the confirmation seconds later, with no file tool called\nand no file on disk, twice. A footer cannot make a model honest, so this narrows the\nfailure rather than closing it; what it does guarantee is that the connector is not\nsteering toward it.\n\n## Durable transport\n\nPlain pub/sub is at-most-once: a message reaches only whoever is subscribed *at that\ninstant*. Agents are constantly `working` or `offline`; a DM sent mid-turn would simply\nvanish. So delivery rides **JetStream streams**: the broker stores each message and every\nreader keeps its own bookmark, catching up at its own pace with nothing missed and no\ninterruption required. One mechanism covers three needs at once: live delivery, the\ninbound buffer, and late-join history. DMs and anycast are always at-least-once this way\n([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding)).\n\nA send result proves only that the broker accepted and stored the message at a sequence\n(`stored seq N`), not that any recipient read it, and no send claims `delivered`. The `cotal_dm`\ntool reports that sequence together with the recipient\'s roster status at the moment of send\n(`idle`, `working`, or `offline`). When that status is `offline`, `cotal_dm`, `cotal send -t`,\n`cotal chat`, `cotal join` and the console also say the agent is offline and that the message is\ndelivered when it reconnects. The DM durable belongs to the recipient\'s lifecycle, so the same\nsession receives the message when it comes back, and a new agent spawned under that name does not.\nA `cotal_dm` reply to a\nsender that has no roster row, such as a one-shot `cotal send`, reads\n`recipient had no roster row at send` instead: the DM stream keeps it under the sender\'s id, and\nit may never reach an inbox. The stored sequence\nis a fact about the stream; the status-at-send words are a fact about the roster a moment\nbefore publish; retention (how long the durable holds it, whether a same-name respawn\ninherits it) is a third, separate fact, covered below and inspectable with\n[`cotal deliver pending`](cli.md#deliver).\n\nA recipient\'s connector acknowledges a DM only once it has handed it to the session. When the\nsession is busy and its bounded local inbox fills with directed mail, the oldest DM is evicted\nfrom that buffer but left unacknowledged: it stays pending on the recipient\'s durable and the\nbroker redelivers it after the durable\'s ack wait, so it lands once the session drains\n([Connect Claude](connect-claude.md#how-messages-reach-the-session)).\n\nA connector that acks only when a turn ends keeps the messages that turn carries from being\nredelivered while it runs, by restarting their ack wait with JetStream\'s in-progress ack\n([Connect OpenCode](connect-opencode.md#how-it-binds)). A redelivered copy says so: the broker\ncounts each delivery, and a connector marks a message the broker delivered more than once as\nredelivered in the frame it injects. The count comes from the broker, so a sender cannot forge\nor hide it.\n\nDelivery stays at-least-once, so a role request can still run twice: when its holder crashes or\nfails a turn after acting, or when renewal lapsed and the broker handed the request to another\nholder of the role. A stored sequence proves the broker accepted the request, not that one worker\ncompleted it alone. Work with side effects that must not repeat needs its own ownership or an\nidempotency key.\n\n### Reading only your own DMs\n\nA reader whose credential grants nothing but its own DM durable can still receive DMs. Construct\n`CotalEndpoint` with `dmOnly: { handler }`. The endpoint binds the existing\n`dm_<owner>-<actor>-<uid>` durable and reads nothing else: no presence, channel registry, channel\nsubscription, delivery durable, history or JetStream account lookup. On the wire it sends only\nCONSUMER.INFO and MSG.NEXT for that durable and acks under its `$JS.ACK` subtree.\n\nIt never creates the durable. A missing durable fails start with `dm-durable-missing`, and a\nrefused bind fails with `dm-bind-refused`. No other credential is tried. Each DM passes the same\nsender check as every other durable read before the handler sees it, and the handler runs one DM at\na time. A handler acks explicitly; a rejection leaves the copy for redelivery. `stop()` resolves\nafter the running handler settles and the connection closes. When either cannot be proven, it\nthrows and the caller keeps custody. After `stop()` begins, or after a reconnect replaces the\nconnection, a copy the client already received never reaches the handler; it stays unacked and the\nbroker redelivers it after the durable\'s ack wait. An ack, nak or in-progress signal from a handler\nwhose connection has closed throws instead of being lost. The mode registers no presence, so the\nreader is not on the roster. Credential renewal and reconnects use the endpoint\'s normal paths.\nOn a user-auth mesh, the `dm-reader` exchange view mints this credential\n([identity-and-auth.md](identity-and-auth.md)).\n\n## Channel delivery\n\nChannel delivery has two wire-observable classes, fixed per channel\n([SPEC \xA74](../SPEC.md#4-delivery-modes), [\xA77](../SPEC.md#7-channels)):\n\n- **`live`**: native broker subscription, at-most-once. You receive what is published\n while you are subscribed; a busy or offline moment is a gap. Join = subscribe, leave =\n unsubscribe: self-serve, bounded by your read ACL, no privileged mediation.\n- **`durable`**. `live` plus a per-member **durable backstop**: the message is also\n retained for each member and delivered on its next connection or turn, pending until\n acked. At-least-once for current members, within the channel\'s retention window. The\n machinery behind the backstop is the [delivery daemon](delivery-daemon.md).\n\nA message delivered both ways is one logical delivery; receivers dedupe by `id`, and\nreceiver deduplication MUST NOT use the empty string as a key: distinct messages that carry\n`id: ""` are not coalesced by the receiver. Duplicate surfacing is disclosed only where the\npath is already at-least-once (live is at-most-once). The publisher obligation to supply a\nunique string id (SPEC \xA75) is unchanged; an absent or non-string id is a malformed envelope. The\nspace default class is set at creation from the deployment profile (local/self-hosted \u21D2\n`durable`); a channel can override it.\n\n**Replay on join.** A channel\'s registry config (`replay`, `replayWindow`) says whether a\nfresh joiner gets recent history backfilled, marked as historical so an agent doesn\'t\nmistake a resolved old thread for live traffic. Historical channel ambient is delivered\npull-only: it never drives automatic turns or wakes the session, and is read on demand\nthrough `cotal_inbox`. A historical @mention or DM stays automatic. Mail addressed to\nyou is never noise. Replay off is **noise control, not\nconfidentiality**: history stays readable within the read ACL\n([channels & permissions](channels-and-permissions.md)).\n\n## Attention\n\nOrthogonal to all of the above, each agent chooses how much traffic *wakes* it: a global\nmode (`open` / `dnd` / `focus`) plus per-channel overrides (`quiet` / `muted`). This is\n**connector UX**: the broker still authorizes and delivers; attention\nonly shapes when the receiving agent\'s session is interrupted. It is mirrored into\npresence as advisory observability ("locally muted #deploys; DM to reach"), never read\nback into delivery. Semantics and tables:\n[Connect Claude](connect-claude.md#attention); the concrete\nknobs: [`cotal_status` / `cotal_channel_mode`](mcp-tools.md).\n\n## Related\n\n- [Spaces & channels](spaces.md): the isolation boundary vs the topic axis.\n- [Delivery daemon](delivery-daemon.md): the durable backstop\'s three pieces.\n- [Identity & auth](identity-and-auth.md): who may publish and read where.\n- [Watch a mesh](watch-a-mesh.md): seeing presence and traffic live.\n'
|
|
27419
|
+
"body": '# Message flow\n\n> **Concept** (informative) \xB7 **For:** everyone \xB7 **Normative:** [SPEC \xA74](../SPEC.md#4-delivery-modes), [\xA76](../SPEC.md#6-presence-and-discovery), [\xA76.1](../SPEC.md#61-plane-liveness), [\xA77](../SPEC.md#7-channels), [\xA78](../SPEC.md#8-nats--jetstream-binding)\n\nHow peers see each other and how messages reach them: the presence directory, the three\ndelivery modes, and the two delivery guarantees. This page explains; the linked spec\nsections define.\n\n## Presence\n\nPresence is a per-space directory keyed by instance id: each peer\'s identity card\n(`AgentCard`: name, role, kind, tags, what it can do) plus its live state:\n\n- `idle`: free\n- `waiting`: blocked on input, approval, or a peer\n- `working`: the seat (or its surviving connector) claims it is busy. That is a\n process-alive claim, not a progress claim. A frozen seat can stay `working` while\n its heartbeat stays fresh. Surfaces that have no outside observation of last\n assistant-message age render `progress unknown` rather than treating heartbeat\n age as work age. A stale observation overlays `stalled Xm` on the still-fresh\n presence. The observation classifier is workstation/operator data, not part of\n the wire protocol; human wording stays in each CLI, connector, or web renderer.\n Compact maps and DM pickers use the presence glyph only and make no progress\n claim. See issue #876.\n- `offline`: gone (gracefully, or its heartbeat lapsed)\n\nA peer refreshes its own entry on a heartbeat; observers also derive `offline` from stale\ntimestamps, so a crashed agent cannot linger as "working". That derivation is gated on the\nobserver actually hearing the bucket: if the whole watch has been silent past the liveness\nwindow, the honest output is that the *view* is stale, not that every peer died in one TTL\n(real rosters do not do that). Offline peers stay in the roster for observability. An\n`activity` string rides along ("what I\'m doing right now"), and a peer\'s **attention**\npreference is mirrored here too (below). Each instance writes *only its own* key; presence\nis where discovery lives (our equivalent of `.well-known`), not a place to describe others.\nThe optional `condition` beside status relays a harness-reported cause such as `rate_limit`,\n`approval`, or `input`; missing means the harness reported none. A condition is cleared when a\nnormal next turn starts. The optional `activeAt` is the epoch ms of the last work event the harness\nreported, such as a token or a tool call; missing means the connector reports none. `ts` is only the\nheartbeat, so a seat whose turn stopped advancing keeps a fresh `ts` and an old `activeAt`. The\nconnector records it as events arrive and the next heartbeat carries it. `cotal ps`,\n`cotal endpoints`, `cotal console`, `cotal join` and `cotal_roster` print a condition with its age\nand the age of `activeAt`, such as `waiting (rate_limit for 40m) \xB7 active 40m ago`. The optional\n`statusSince` is the epoch ms when the instance entered its current status and activity. A change to\neither moves it, while a heartbeat or a repeated report does not, so an activity that outlived what\nit described reads as old. `cotal endpoints`, `cotal console`, `cotal join` and `cotal_roster` print\nits age, such as `idle \xB7 unchanged for 40m`. An offline record carries none,\nbecause an observer that derives `offline` from a stale heartbeat does not know when the peer left.\nThe optional `activitySince` is when the current activity was set. A status change does not move it,\nso an activity left behind while hooks flip the status every turn still shows its age after the\nactivity on the same surfaces, such as `(set 9h ago)`. A stamp that is not a finite number shows no\nage. The optional `runtime` describes the OS process holding the mesh connection. Its opaque `id`\nis minted once per process and lifecycle scope and is never reused across scopes. Its `startedAt`\nis the process\'s own `performance.timeOrigin`, shared by all endpoints in that process, and its\nclosed `source` is `"endpoint-process"`. Status, activity, heartbeats and in-process reconnects keep\nthat observation unchanged within the scope. A lifecycle registered later reports the process\'s\nage, not the lifecycle\'s age. It does not date a separately launched parent or model harness.\nAn offline record retains the observation without claiming a running duration. Older publishers\nomit it, which means unknown. A runtime-only change notifies presence observers.\nA peer writes its own condition code and activity, so every surface above prints each on one\nline, shows a control character or line separator in it as a `\\uXXXX` escape, and cuts it at 200\ncharacters.\nThe optional `environment` is an opaque provider reference. Core publishes\nit and never interprets it. Readers reject a row whose `card.id` does not match its KV key, whose\n`card.name` or `status` is missing or has the wrong type, whose `ts` is not a finite number, whose\n`card.role`, `activity` or `attention` is present and not a string, or whose `condition` is present and\nis not an object with a string `code`, such as `null`, a bare string or an object with no `code`. They\nreport that rejection through the recoverable warning path. A kept row whose `ts` is missing or text\nsuch as `"nope"` would never read as stale, so it would stay live after its key expired. A kept object such as\n`{"toString":null,"valueOf":null}` in a printed field would throw when converted and fail every\nreader\'s roster.\nDetails: [SPEC \xA76](../SPEC.md#6-presence-and-discovery). The dashboard surfaces a stale view\non the same header mark it uses for a refused poll ([watch a mesh](watch-a-mesh.md)).\n\n`CotalEndpoint.presenceView()` reports whether its local roster can support an absence verdict.\n`current` is usable, `unpopulated` means the current watch has not completed its initial snapshot,\nand `stale` means the watch has been silent past its liveness window. Both unsafe states carry\n`fresh: false`, so an older consumer degrades instead of treating a partial reconnect refill as a\ncomplete roster. `waitForPresenceSnapshot()` returns `snapshot` or `timeout`; a timeout is a bounded\ngive-up, not proof that the snapshot completed. An observer that does not register completes its\nsnapshot at once on a bucket with no keys, and its view is `current` with nobody present.\n\nA stale view under a live connection is not left to stand. The endpoint rebinds its presence watch\nfrom the bucket\'s current state once per liveness window and reports the rebind as a `warning`\nnaming the silent interval; a held link\'s rebind fails or stays silent and the view stays stale. A\nrebind that is still awaiting the broker when the endpoint stops or rebuilds its connection installs\nnothing. A rebind that lands on a bucket with no keys is current knowledge for an observer that\ndoes not register (nobody is present), and a wipe for one that does (its own key is missing too):\nthe latter re-publishes itself and lets the delivery of that record make the view current.\n`cotal ps` prints `mesh unknown` with the reason, never a liveness word, for a row whose\nmanager reports a view that is not `current` ([cli.md](cli.md)).\n\nPresence publishing is also monitored separately from the watch. One refused heartbeat remains a\nrecoverable warning. If consecutive writes keep failing for a full presence TTL, the endpoint raises\n`PresenceWriteStuckError` with code `presence-write-stuck` and marks the failure record as stuck.\n`cotal_orientation` and `cotal_roster` then say the view is not live and call the roster\nlast-known until a write succeeds. A successful write resets the consecutive count and clears the\ncondition. The condition is local diagnosis, not a new wire field.\n\nThe same two tools also render the view\'s own trust state: under an `unpopulated` view they say\nthe presence watch has not completed its initial snapshot, so the roster may be partial and a\nmissing name is not an absence verdict, and under a `stale` view they name the silent-since instant\nand call the roster last-known. A DM to a name, or a send that @mentions a name, that the observer\ncannot verify is refused with that condition rather than sent, instead of being reported as an\nunknown peer. Both tools and both refusals print each condition in the same words.\n\n## Plane liveness\n\nPresence tells you which peers are around. It does not tell you whether the manager or the\ndelivery daemon is up, and the lease buckets that do know are not readable by agents. So when a\njoin or a send fails, a peer used to have no way to separate a credential problem from a dead\nmanager or an unbound delivery daemon.\n\nAny credentialed peer can now ask. It sends an empty request on\n`cotal.<space>.live.<plane>.<owner>.<actor>`, where `<plane>` is `manager` or `delivery` and\n`<owner>.<actor>` is its own principal, and names its reply subject under that request as\n`<request>.reply.<nonce>`. In code this is `CotalEndpoint.probeLiveness(plane)`. Any other plane\nname is refused before anything is sent.\n\nThe reply is a `LivenessAnswer`:\n\n```json\n{ "plane": "delivery", "responder": "bound", "instance": "3f1c0b52-..." }\n```\n\n`responder` is one of `bound`, `unbound`, `stale` or `unknown`. `instance` is an opaque token\nthe responder mints each time it binds. Two answers with different tokens came from two\nresponders, which is how a caller probing more than once can spot two manager instances that\ndisagree. The token identifies nothing else: it is not the instance id, the principal, a pid, a\nhost or a path. The reply carries nothing beyond these three fields, so the lease row\'s holder\nand workspace path never leave the responder.\n\nHow the caller reads the outcome:\n\n| What happened | `responder` |\n| --- | --- |\n| a well-formed reply | whatever the reply says |\n| the broker answered "no responders" | `unbound` |\n| timeout, permission refusal, transport failure | `unknown` |\n| a reply that does not parse | `unknown` |\n\n`unknown` means the probe did not find out. It is never a health report. Each responder grades\nonly itself. The manager says `bound` while its service endpoint is serving, and `unbound` while\nthat connection is down, both while the client reconnects it and while the manager re-dials it\nafter a close. The delivery daemon\nreads its own shard lease: no ready row is `unbound`, a ready row held by another instance is\n`stale`, its own ready row is `bound`. A responder that cannot read its own state answers\n`unknown`.\n\nResponders serve `live.<plane>.*.*` in the queue group `live.<plane>`, so one probe gets one\nanswer from one instance. Only the plane\'s own credential holds that filter, and its reply grant\nstops at the `.reply.` leaf, so a responder cannot forge a probe and an agent cannot answer one.\nA request whose reply target is outside the caller\'s own `.reply.` subtree is dropped with a\nwarning, and the responder keeps serving. When an endpoint replaces its broker connection it\nbinds its responders again on the new one, under a new token, so a reconnect does not leave the\nplane looking unbound. The normative rules are in\n[SPEC \xA76.1](../SPEC.md#61-plane-liveness).\n\n## Three delivery modes\n\nEvery delivery message is addressed one of three ways\n([SPEC \xA74](../SPEC.md#4-delivery-modes)):\n\n| Mode | Addressed by | Reaches |\n|---|---|---|\n| **multicast** | `channel` | every subscriber of the channel |\n| **unicast** | `to` (instance id) | one specific peer\'s inbox |\n| **anycast** | `toService` (role) | *any one* holder of the role: "whoever is a reviewer" |\n\n\n\n\n\n\n\nChannels are dotted and hierarchical (`team.backend`); publishing is always concrete,\nsubscriptions may wildcard a subtree (`team.>`). Anycast is queued work: a task with no\nworker online *waits*; multiple online instances of a role load-balance; the task is\nremoved once acked.\n\n**Mentions.** A multicast message may carry `mentions: [name\u2026]`, a *priority hint*, not\na routing target. The message still reaches the whole channel, but a mentioned peer is\nwoken immediately while everyone else picks it up when next idle. Names (not instance\nids) ride the wire, so the match survives reconnects.\n\n**Deriving the mode.** A receiver derives how a message was addressed (channel / dm /\nanycast) from the *delivering subject*, never from payload fields: the payload is\nadvisory and forgeable, while the subject is broker-policed\n([SPEC \xA74](../SPEC.md#4-delivery-modes), [identity & auth](identity-and-auth.md)).\n\n**How the block is framed.** Delivered messages arrive as one block: a header, the items,\nand a tail. The tail names the *order of operations* - do what was asked with your own\ntools, verify the result, then reply - and says not to report an action that was not\nperformed, while still naming the reply verbs. This matters because a peer message is\nfrequently a work order and the tail lands where the model decides its next\naction. A tail that lists only reply tools reads as "this is a chat turn, answer it", and\nfor a weak model an answer that sounds finished is cheaper than the work: a live seat told\nto write a file and confirm sent the confirmation seconds later, with no file tool called\nand no file on disk, twice. A footer cannot make a model honest, so this narrows the\nfailure rather than closing it; what it does guarantee is that the connector is not\nsteering toward it.\n\n## Durable transport\n\nPlain pub/sub is at-most-once: a message reaches only whoever is subscribed *at that\ninstant*. Agents are constantly `working` or `offline`; a DM sent mid-turn would simply\nvanish. So delivery rides **JetStream streams**: the broker stores each message and every\nreader keeps its own bookmark, catching up at its own pace with nothing missed and no\ninterruption required. One mechanism covers three needs at once: live delivery, the\ninbound buffer, and late-join history. DMs and anycast are always at-least-once this way\n([SPEC \xA78](../SPEC.md#8-nats--jetstream-binding)).\n\nA send result proves only that the broker accepted and stored the message at a sequence\n(`stored seq N`), not that any recipient read it, and no send claims `delivered`. The `cotal_dm`\ntool reports that sequence together with the recipient\'s roster status at the moment of send\n(`idle`, `working`, or `offline`). When that status is `offline`, `cotal_dm`, `cotal send -t`,\n`cotal chat`, `cotal join` and the console also say the agent is offline and that the message is\ndelivered when it reconnects. The DM durable belongs to the recipient\'s lifecycle, so the same\nsession receives the message when it comes back, and a new agent spawned under that name does not.\nA `cotal_dm` reply to a\nsender that has no roster row, such as a one-shot `cotal send`, reads\n`recipient had no roster row at send` instead: the DM stream keeps it under the sender\'s id, and\nit may never reach an inbox. The stored sequence\nis a fact about the stream; the status-at-send words are a fact about the roster a moment\nbefore publish; retention (how long the durable holds it, whether a same-name respawn\ninherits it) is a third, separate fact, covered below and inspectable with\n[`cotal deliver pending`](cli.md#deliver).\n\nA recipient\'s connector acknowledges a DM only once it has handed it to the session. When the\nsession is busy and its bounded local inbox fills with directed mail, the oldest DM is evicted\nfrom that buffer but left unacknowledged: it stays pending on the recipient\'s durable and the\nbroker redelivers it after the durable\'s ack wait, so it lands once the session drains\n([Connect Claude](connect-claude.md#how-messages-reach-the-session)).\n\nA connector that acks only when a turn ends keeps the messages that turn carries from being\nredelivered while it runs, by restarting their ack wait with JetStream\'s in-progress ack\n([Connect OpenCode](connect-opencode.md#how-it-binds)). A redelivered copy says so: the broker\ncounts each delivery, and a connector marks a message the broker delivered more than once as\nredelivered in the frame it injects. The count comes from the broker, so a sender cannot forge\nor hide it.\n\nDelivery stays at-least-once, so a role request can still run twice: when its holder crashes or\nfails a turn after acting, or when renewal lapsed and the broker handed the request to another\nholder of the role. A stored sequence proves the broker accepted the request, not that one worker\ncompleted it alone. Work with side effects that must not repeat needs its own ownership or an\nidempotency key.\n\n### Reading only your own DMs\n\nA reader whose credential grants nothing but its own DM durable can still receive DMs. Construct\n`CotalEndpoint` with `dmOnly: { handler }`. The endpoint binds the existing\n`dm_<owner>-<actor>-<uid>` durable and reads nothing else: no presence, channel registry, channel\nsubscription, delivery durable, history or JetStream account lookup. On the wire it sends only\nCONSUMER.INFO and MSG.NEXT for that durable and acks under its `$JS.ACK` subtree.\n\nIt never creates the durable. A missing durable fails start with `dm-durable-missing`, and a\nrefused bind fails with `dm-bind-refused`. No other credential is tried. Each DM passes the same\nsender check as every other durable read before the handler sees it, and the handler runs one DM at\na time. A handler acks explicitly; a rejection leaves the copy for redelivery. `stop()` resolves\nafter the running handler settles and the connection closes. When either cannot be proven, it\nthrows and the caller keeps custody. After `stop()` begins, or after a reconnect replaces the\nconnection, a copy the client already received never reaches the handler; it stays unacked and the\nbroker redelivers it after the durable\'s ack wait. An ack, nak or in-progress signal from a handler\nwhose connection has closed throws instead of being lost. The mode registers no presence, so the\nreader is not on the roster. Credential renewal and reconnects use the endpoint\'s normal paths.\nOn a user-auth mesh, the `dm-reader` exchange view mints this credential\n([identity-and-auth.md](identity-and-auth.md)).\n\n## Channel delivery\n\nChannel delivery has two wire-observable classes, fixed per channel\n([SPEC \xA74](../SPEC.md#4-delivery-modes), [\xA77](../SPEC.md#7-channels)):\n\n- **`live`**: native broker subscription, at-most-once. You receive what is published\n while you are subscribed; a busy or offline moment is a gap. Join = subscribe, leave =\n unsubscribe: self-serve, bounded by your read ACL, no privileged mediation.\n- **`durable`**. `live` plus a per-member **durable backstop**: the message is also\n retained for each member and delivered on its next connection or turn, pending until\n acked. At-least-once for current members, within the channel\'s retention window. The\n machinery behind the backstop is the [delivery daemon](delivery-daemon.md).\n\nA message delivered both ways is one logical delivery; receivers dedupe by `id`, and\nreceiver deduplication MUST NOT use the empty string as a key: distinct messages that carry\n`id: ""` are not coalesced by the receiver. Duplicate surfacing is disclosed only where the\npath is already at-least-once (live is at-most-once). The publisher obligation to supply a\nunique string id (SPEC \xA75) is unchanged; an absent or non-string id is a malformed envelope. The\nspace default class is set at creation from the deployment profile (local/self-hosted \u21D2\n`durable`); a channel can override it.\n\n**Replay on join.** A channel\'s registry config (`replay`, `replayWindow`) says whether a\nfresh joiner gets recent history backfilled, marked as historical so an agent doesn\'t\nmistake a resolved old thread for live traffic. Historical channel ambient is delivered\npull-only: it never drives automatic turns or wakes the session, and is read on demand\nthrough `cotal_inbox`. A historical @mention or DM stays automatic. Mail addressed to\nyou is never noise. Replay off is **noise control, not\nconfidentiality**: history stays readable within the read ACL\n([channels & permissions](channels-and-permissions.md)).\n\n## Attention\n\nOrthogonal to all of the above, each agent chooses how much traffic *wakes* it: a global\nmode (`open` / `dnd` / `focus`) plus per-channel overrides (`quiet` / `muted`). This is\n**connector UX**: the broker still authorizes and delivers; attention\nonly shapes when the receiving agent\'s session is interrupted. It is mirrored into\npresence as advisory observability ("locally muted #deploys; DM to reach"), never read\nback into delivery. Semantics and tables:\n[Connect Claude](connect-claude.md#attention); the concrete\nknobs: [`cotal_status` / `cotal_channel_mode`](mcp-tools.md).\n\n## Related\n\n- [Spaces & channels](spaces.md): the isolation boundary vs the topic axis.\n- [Delivery daemon](delivery-daemon.md): the durable backstop\'s three pieces.\n- [Identity & auth](identity-and-auth.md): who may publish and read where.\n- [Watch a mesh](watch-a-mesh.md): seeing presence and traffic live.\n'
|
|
27420
27420
|
},
|
|
27421
27421
|
{
|
|
27422
27422
|
"slug": "release",
|
|
@@ -27437,7 +27437,7 @@ function loadDocsBundle() {
|
|
|
27437
27437
|
"title": "Run a mesh",
|
|
27438
27438
|
"kind": "Guide (informative)",
|
|
27439
27439
|
"summary": "Day-to-day operation of a local mesh: what cotal up actually runs, how spawning resolves personas, harnesses, and models, how to reach a mesh from any directory, and the operator-only maintenance v\u2026",
|
|
27440
|
-
"body": "# Run a mesh\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\nDay-to-day operation of a local mesh: what `cotal up` actually runs, how spawning\nresolves personas, harnesses, and models, how to reach a mesh from any directory, and the\noperator-only maintenance verbs. Every command's full flag set is in the\n[CLI reference](cli.md).\n\n## The stack\n\n`cotal up` brings up the whole local stack and bare `cotal down` stops it. Managed\nagents stay running as unmanaged OS processes; pass `--with-agents` to take them\nwith the stack. Seats of the built-in pty runtime run inside the manager process, so\nthey stop with the manager either way. On POSIX, a foreground broker has its own session and\nprocess group, so terminal Ctrl-C reaches the CLI owner before the broker. The owner stops the\nmanager through the same stop as bare down and prints the same report. When that stop is refused,\nfor example because the manager cannot prove it can spare, the stack stays running, including\nthe manager, broker, delivery and any auth service. End it with `cotal down --with-agents`.\nA current manager records what its stop does with its seats before\nbare down signals it. A pre-pin legacy manager instead receives a reduced-guarantee\nwarning and is signalled according to the documented upgrade contract. Its running binary\nmay still carry the older destructive SIGTERM handler, so the CLI does not claim its\npre-signal agent inventory was spared; those agents may have been reaped.\n\n- **Broker**: a local `nats-server` (logs to `.cotal/nats.log`).\n- **Delivery daemon**: the durable backstop, auth mode only\n ([what it does](delivery-daemon.md)).\n- **Manager**: a detached supervisor answering the control plane, so\n `cotal spawn --detach` and the `cotal_spawn` tool work right after `up`.\n\nCotal creates the presence bucket in memory storage. Its records are liveness that every endpoint\nrewrites each heartbeat, so nothing is lost when a broker restart empties it, and nats-server's file\nstore write latch cannot reach it. A broker stop removes the memory stream itself, so every `cotal up`,\nincluding the resume after `cotal down --preserve-state`, creates it again before any daemon starts.\nJetStream fixes a stream's storage class when it is created, so a presence bucket created file-backed\nby an older cotal stays file-backed until that stream is recreated.\n\nA file-backed presence bucket can remain open and watchable while refusing every write. A bound\nendpoint reports this as `presence-write-stuck` after one full presence TTL of consecutive failures.\nThe roster is last-known while that condition is active. Restarting the broker clears nats-server's\nin-memory store latch and preserves the JetStream root. Current credentials split the required stream\nauthority: the `cotal up` provisioner can create the presence stream but cannot delete it, while the\nteardown credential can delete it but cannot recreate it. Cotal therefore reports the condition but\ndoes not attempt an unsafe partial delete-and-recreate. Stop and restart the broker to recover.\nA broker below nats-server 2.14.5 carries the latch (nats-server fixed it in 2.14.5). When `cotal up` starts or finds such a broker and the space's presence bucket is file-backed, it says so. A memory-backed bucket gets no warning. A broker below the SPEC \xA713.12 floor of 2.12 is refused at connect with the floor sentence.\n\nThree modes:\n\n- **Default (static auth).** JWT-authed, on by default: sender authenticity and per-agent\n ACLs, enforced by the broker ([how](identity-and-auth.md)).\n- **`--user-auth --idp <url>`.** Per-user auth: people `cotal login` once, the operator\n grants their agents on the actor ledger, and every connect is authorized live against\n that grant. Starts the space's auth service alongside the broker\n ([how](identity-and-auth.md)).\n- **`--open`.** An unauthenticated, live-only dev mesh (no auth, no delivery daemon). For\n quick local experiments.\n\nThe broker and local services bind **loopback** by default. `--host 0.0.0.0` widens the broker\nbind independently of the auth mode, so \"network-reachable\" never silently means\n\"unauthenticated\". With no explicit `--server`, `cotal up` auto-selects a free local port when\nthe default address is already held by another project; an explicit `--server` fails loud on\ncollision.\n\n`--host` is a boot flag, not a live rebind. A fresh `cotal up` writes the generated\n`.cotal/auth/server.conf` (project-local, not `~/.cotal`) with that bind and starts nats against\nit. If anything is already answering at the mesh URL, `up` refreshes the recorded mesh and\nleaves the running nats listener alone, so passing `--host 0.0.0.0` on a live or orphaned\nbroker does not change who can connect. To change the bind: `cotal down`, then `cotal up --host\n<addr>` against a stopped broker so the generated file is rewritten. Do not edit `server.conf`\nby hand; the next real boot overwrites it.\n\nOn a stopped shared broker, `up` renders every persisted space account and every enabled\nspace's auth-callout account into the resolver preload, regardless of which space starts\nthe broker. A missing callout account for an enabled space stops the boot rather than\nstarting with a reduced resolver. An already-running broker is refreshed without rewriting\nits config.\n\nA broker-only host is a first-class `up` mode. `cotal up --no-manager` boots the broker and, in\nauth mode, the delivery daemon, and no local manager, so the broker host never has a manager to\nstop and never leaves a manager slot stale. A refresh under the flag of a mesh whose manager is\nlive refuses rather than 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. Without the flag,\nauth-mode `up` still starts nats, the delivery daemon, and a\nlocal manager. A space may run more than one manager, addressed by instance id\n([control surface](control-surface.md#instance-routing)); putting no manager on the broker host\nis a topology choice, not a singleton invariant. A manager whose boot inventory has no\navailable connector does not take unpinned `spawn`/`launch` on the class rail, so a sibling\nthat can launch the harness can. `describe` still rides the class rail, so an unpinned spawn\ncan bind-fence when that skip member answered describe; re-issue, or pin `--on`. Pin one\ninstance with `--on` when a partial inventory still answers with a harness refusal. The\nsupported split is:\n\n```bash\n# broker host (project root that owns the generated conf, pidfiles, and logs)\ncotal up --detach --host 0.0.0.0 --space main --no-manager\n# no local manager starts: the summary lists nats-server + delivery daemon, and there is no\n# `.cotal/manager.<spaceKey>.log` to wait for on this host\n\n# manager host (registered remote mesh, same space)\ncotal space add main --server nats://broker.example:4222 --root ~/meshes/main\ncotal supervise --space main --server nats://broker.example:4222\n```\n\nWait for `\u2713 manager up` in `.cotal/manager.<spaceKey>.log` on the manager host before spawning\nagents. On a broker host started without `--no-manager`, `cotal up --detach` prints `\u2713 running in\nthe background:` with `manager` listed once the manager pidfile is live; stop that local manager\nonly after the `\u2713 manager up` line. A host started WITH `--no-manager` never runs one, so neither\nthe wait nor the stop applies there. That detach stdout is not a safe teardown boundary: it is\npidfile liveness, not `\u2713 manager up`. `\u2713 manager up` is supervise's post-start line after\n`await mgr.start()`. Stopping the manager after only the detach line can still default-terminate\nthe child during registration after it has taken the governance slot. Stopping before that\npost-start log line can leave the endpoint governance slot held until the holder's gate\nreopens past the stamp (the successor's boot heal, or\n[`cotal reconcile-gate`](cli.md#reconcile-gate) when that boot cannot run). See\n[Gate recovery](#gate-recovery).\n\nStandalone `cotal deliver --creds` is not a repair for that split. Production renewal needs\nthe manager and the daemon to address one credential store. The manager renews its own service\ncredential inside that credential's own window and re-dials its service connection with the\nrenewed credential; if the connection closes and cannot be restored within about forty seconds\nit releases its lease and exits so a restart can serve, while a broker that is briefly gone is\nwaited out. Separate host filesystems still\nleave manager root A writing and the daemon reloading root B; that composition is refused\nwhile the daemon stays up. Before every remint the manager challenges the delivery daemon's\nstore identity, and the answer must come from the process holding the delivery lease: the\nreply names the answering endpoint and the manager reads the lease row itself under its own\ncredential, so a non-holder answering on the queue-grouped admin rail is refused instead of\ncounting as the daemon's store. A rail that reports no responder is also settled from the\nlease row, so a live holder on record makes that outcome a refusal rather than an absent\ndaemon. Keep delivery on the broker host under `up`, and share one store\nonly when you are composing a hosted pair ([embedding](embedding.md#supervisor-signing-authority)).\nOn the `--no-manager` split above, the manager host's manager stays off the daemon-credential\nrenewal lease once its store check finds the daemon on another store. A filesystem store is named\nby its root and by a random id in `.cotal/store.id`, which the copied `.cotal/auth` does not carry,\nso this holds when both hosts use the same root path. `cotal doctor auth --fix` on\nthe broker host then renews the daemon credentials once they pass their renewal point.\n\n### Split host bind\n\nA remote manager cannot reach a loopback broker. After changing `--host`, confirm the\ngenerated `host:` in `.cotal/auth/server.conf` and that nats is listening on that address\nbefore registering the mesh on the manager host. Detached child logs stay under the **project**\n`.cotal/` that `up` ran in (see [When something looks absent](#when-something-looks-absent));\nthey are not `~/.cotal` unless that directory is the mesh root.\n\nA user-auth mesh can expose only its credential exchange through an operator-owned HTTPS reverse\nproxy while leaving the existing local exchange untouched:\n\n```bash\ncotal up --user-auth --idp https://idp.example/api/auth \\\n --exchange-public-port 7443 \\\n --exchange-public-url https://auth.example\n```\n\nThe public listener itself still binds `127.0.0.1:7443`; configure the proxy to terminate TLS and\nforward to it. It serves only `/health`, `/jwks`, `/exchange`, `/manager-service-authority`,\n`/manager-instances`, and `/.well-known/cotal-mesh` with the documented methods. It needs no local file capability: the\nsigned IdP JWT or managed-agent actor token is the proof, while the original loopback listener\nremains capability-gated. Add\n`--exchange-trusted-proxy` only when that listener is reachable exclusively through your trusted\nproxy; it keys failure throttling by the last `X-Forwarded-For` hop instead of the socket address.\nThe well-known bundle includes IdP pins and a deny-all sentinel credential, so fetch it only from\nthe configured HTTPS origin. It tells participants to dial `--server` unless `--advertised-server`\nnames the address they can reach, such as a `wss://` address behind the proxy. Add\n`--agent-provisioning-url <https://\u2026>` to advertise an endpoint where a participant's `cotal spawn`\nprovisions its agent; without one, a participant on another machine needs a one-time enrollment,\ndescribed next. To change these listener flags, stop and restart the mesh; a refresh\nof an already-running service does not replace its bind or proxy policy. See\n[Identity & auth](identity-and-auth.md#per-user-authentication) for the trust boundary.\n\n### Remote supervised seats by enrollment\n\nA remote seat does not need to run `cotal login` when the mesh owner pre-mints a single-use\nenrollment for it. Mount the enrollment URL as a private file, place the seat persona on the remote\nmachine, and launch the foreground seat:\n\n```bash\nCOTAL_ENROLLMENT_FILE=/run/secrets/cotal-enrollment \\\n cotal spawn --config ./worker.md --space main\n```\n\nThe URL is redeemed once with an unauthenticated GET. Redirects, off-machine plain HTTP, retries,\nand login fallback are refused. If the seat has no mesh record yet, the enrollment response's stock\nuser-bundle fields register it before the launch. The returned actor token then uses the same remote\nauth-service exchange as a login-provisioned agent. The enrollment URL and file path do not enter the\npreflight or harness environment. A failed or reused enrollment leaves no actor material on disk; ask the owner\nfor a fresh enrollment. When the foreground seat exits, this machine's credential files are removed\nand the mesh-side grant stays until the mesh operator revokes it; the launch line says so. The exact\nserver contract is in\n[Enrollment redeem](identity-and-auth.md#enrollment-redeem).\n\n`cotal status` prints the detailed setup, process, registry, and live mesh status. Its Machine\nsection names the running CLI's source checkout, installed package root, or npx package root beside\nthe version. It has one row per installed connector, which reports whether the executables that\nconnector declares in `requires` are on PATH. Status, setup and the manager's preflight resolve them\nthe same way: an entry written as a path is checked as given, and a directory never counts as the\nexecutable. A connector whose setup provider reports health adds its\nown rows above those. The Claude Code connector reports its plugin and its skills plugin, and a stale\nskills row names the installed and CLI versions it compared.\n\nBefore reporting ready, the manager resolves every installed connector's declared harness\nbinaries against its own environment. A missing binary does not stop unrelated manager work: boot\ncontinues, but prints a named `connector <name> unavailable` line and records that reason in the\nmanager's `status` response. Available connector rows record the absolute paths boot resolved.\nA spawned seat and a seat resumed after `cotal down --preserve-state` both launch from those paths,\nand both are refused with the recorded reason when their connector's row is unavailable.\n`cotal models` takes the same rule and reports that reason in place of the catalog, so it agrees\nwith a launch about a harness installed or removed after boot. The manager looks again only when it\nrestarts. A connector registered after boot has no row, so spawn, resume and `cotal models` check\nits binaries on PATH when they run.\n\nOn an authenticated manager start, unfinished static lifecycle rows reconcile while the control\nendpoint is already serving. The manager `status` response reports\nthe `staticReconciliation` state, the last sweep counts, and each failed alias with its durable\nphase and literal disposition. `cotal status --components` reports the state and per-alias failure\ndetails. A sweep that stops before it plans any alias, for example on a slot row carrying a DEL or\nPURGE marker, reports the state `failed` with the reason in the last sweep's `error`, and\n`cotal status --components` prints it as `static sweep error=<reason>`. The next manager start\ncollects the rows again. A failed exact terminal is retried in the same process after 1, 5,\nand 30 seconds. Each attempt re-reads the durable slot and re-enters the same deterministic terminal\noperation; the delays only schedule work and never release the lifecycle fence. The terminal's\ncleanup removes the lifecycle's credential file and its broker durables and read-ACL row as separate\nsteps. A file that cannot be removed does not leave the broker footprint behind, and its failure\nkeeps the alias held for the next attempt.\n\nOn shutdown, the manager fences new reconciliation work and waits for an exact terminal that already\nstarted. The current serial sweep stops before its next alias, and startup cannot publish the manager\nservice after `stop()` completes.\n\nThe four-attempt budget is per manager process. An exhausted row stays held and reports\n`retry-exhausted` with the remedy to restart the manager. The next process derives a fresh budget\nfrom the still-authoritative durable row. A `recovered` row remains visible until the next static\nreconciliation sweep, then clears. This component reports reconciliation outcomes. It does not say\nwhether footprint cleanup completed independently of the terminal result; that separate durable\nprojection remains tracked by #1274.\n\n`cotal service install <name>` writes an owned systemd user unit for a saved direct-process\nservice definition ([CLI reference](cli.md#service)). It is Linux only, and a preset such as the\nmanager is not installed as a unit in this release. The units below remain the process models for\nthe stack's components, and they are still **examples of process models**: copy them only after\nyou decide which processes the unit should own.\n\n### Supervising the detached stack\n\n`cotal up` runs in the background by default; `cotal up --detach` names the same mode. It is a\nlauncher: it starts the broker, delivery daemon, and manager, reports what started, then exits. Do not wrap it in a systemd service with `Type=oneshot` and\n`RemainAfterExit=yes` and treat `systemctl is-active` as stack health. That unit becomes `active\n(exited)` when the launcher exits successfully and stays active even if every detached process dies.\nWhen background `up` can identify itself as the unit's main process under the current native\ninvocation, it prints a warning naming the unit and the `--foreground` remedy. The advisory does\nnot change startup or rewrite a unit. Inherited terminal/scope metadata is not enough to claim\nthat `up` is the unit's main process.\n\nFor a single-host stack, use `cotal up --foreground` so systemd tracks a long-running\nprocess and restarts the stack if that process fails:\n\n```ini\n[Service]\nType=simple\nWorkingDirectory=/srv/cotal-mesh\nExecStart=/usr/bin/cotal up --foreground --space main --host 0.0.0.0\nRestart=on-failure\nRestartSec=5s\n```\n\nAn active unit then proves the foreground launcher and broker are still running, but it still does\nnot prove that every child component serves. Pair it with the component check below. Also remember\nthat `cotal up` starts a local manager as well as the broker and delivery daemon; run\n`cotal up --no-manager` (add the flag to the unit's `ExecStart` too) on a host intended to be\nbroker-only, so the unit and the host agree.\n\nSeats spawned by the built-in `pty` runtime run with `oom_score_adj` 500, so under memory\npressure the kernel prefers a seat over the broker, manager and delivery daemon, which are left as\nthey were started; the extension runtimes do not own the seat's process and get no preference.\n\nThat `Type=simple` shape puts nats in the unit's cgroup with the foreground `up` process. A\n`Restart=always` (or `on-failure`) of **this** unit therefore restarts nats as well, so remote\nmanagers drop for the time it takes the broker to come back. Wrapping `cotal up --detach` in\n`Type=oneshot` with `RemainAfterExit=yes` does not move nats out of that cgroup. Detached\nspawn starts a new process group, not a new systemd cgroup, and the default\n`KillMode=control-group` still signals every process left in the service cgroup on stop or\nrestart, including the nats PID. Escaping that cgroup needs an explicit unit setting such as\n`KillMode=process`, or a separate nats unit; this CLI does not ship that escape. The\n`Type=oneshot` unit below is a `cotal status --components` liveness check, not a\n`--detach` launcher. Neither trade is universal from\n`Type=simple` alone; it follows from which processes the unit actually owns. `cotal service\ninstall` writes a Linux unit only for a saved direct-process service definition, so the manager\npreset, the broker and its siblings get no unit from it. Pick the example that matches the\nownership you want, and treat `systemctl is-active` as unit health, not mesh health.\n\nA broker that crashes under that foreground `up` keeps its mesh record and exits non-zero, so the\nunit's restart takes the repair path against the recorded store rather than starting a second one.\n\nA manager or delivery daemon that crashes keeps its lease until the bucket TTL removes it (10s for\nthe manager, 30s for delivery). A repair `up` inside that window waits out the lease when it can show\nthe holder is gone, so one run restores both processes. A lease that is renewed during the wait has\na live holder, and the repair refuses it. See [`up`](cli.md#up).\n\nIf the deployment deliberately uses `cotal up --detach` as a boot action, monitor observed state\ninstead of the launcher's exit:\n\n```ini\n[Unit]\nDescription=Check Cotal component liveness\n\n[Service]\nType=oneshot\nWorkingDirectory=/srv/cotal-mesh\nExecStart=/usr/bin/cotal status --components --space main\n```\n\nRun that check from a systemd timer or another monitor and alert on a nonzero exit. The command\ndistinguishes `absent`, `not-serving`, and `refused` components and never treats a sibling's health as\nproof. Its delivery-process check is local to the broker host, so run it there. On a split topology,\nalso probe the broker URL from the manager host and monitor the manager's own service there. A remote\nmanager cannot observe the broker host's delivery PID, and an `active` unit on either host says\nnothing about the other host.\n\nA legacy stack has no command that stops one of its parts without tearing down the mesh. To stop\nparts on their own, use a controller stack: a space prepared by this release with `cotal setup` or\n`cotal space create` starts as one, and `cotal service stop <name> --space <s> --yes` stops one of\nits services. An existing legacy root keeps its own launcher, and nothing moves it in place. A named\n`cotal down <name>` stops a saved service definition, such as a controller stack's `stack-manager`\nor a `web` service, and never one legacy component. Bare `cotal down` stops the whole stack, on a\nlegacy root after first stopping the space's running services through the service controller, and\nleaves managed agents running as unmanaged OS processes, except pty seats, which stop with the\nmanager. `cotal down --with-agents` is the previous reap. If a pinned manager has no\nspare-capability record, stop its managed agents explicitly before running that whole-stack\ncommand. A current manager always publishes the record, so it is absent only for an older manager,\nwhich may not understand the reap request.\n\n## Remote supervised agents\n\nOn a remote user-auth mesh, foreground `cotal spawn` remains the default participant path. A\nparticipant can run detached agents only after the host advertises and operates the remote manager\nauthority service, and the participant's actor-ledger row includes `supervise`. This is not implied\nby `spawn` or `admin`.\n\nThe participant's loopback/operator exchange obtains one closed `manager-service` view for its\nordinary derived owner, a fixed server-selected manager actor, and one opaque manager instance.\nThe host, not the participant, issues the public-nkey JWT material via the replay-safe,\nlifecycle-bound prepare \u2192 activate \u2192 renew exchange, plus a one-shot target-pinned retirement\nrequest for a host-managed terminal. It never exports the space signer, a static\nprovisioner credential, or generic storage authority. Remote registration publishes its service\nstatus at the registered revision and current process epoch, so manager-caller selection can find it.\nEvery manager-authority request carries the participant's fresh login proof and refuses a redirect,\nso a 3xx answer from the pinned endpoint fails the request rather than sending the proof on.\nAn answer whose body is not JSON also fails the request, naming the endpoint and the HTTP status.\n\nStock participant supervision asks its host to enroll a detached agent and to prepare its terminal\nretirement, over the same manager-authority transport. The stock auth service answers both when it\nruns with a public exchange face: it grants the agent under the participant's owner at a lifecycle\nUID it picks, bounded by the participant actor's own grant, provisions that UID's durables, and on\nretirement releases them and revokes the grant before the manager's terminal rail. It refuses a\nsecond enrollment of a name whose grant still stands until that agent's retirement is prepared. A\nhost platform that keeps these writers in its own storage intercepts both requests on its own route\ninstead. Copying host secrets or actor-ledger files to a participant is not supported. Foreground\nspawning and operator-local hosted managers use their existing paths.\n\nThe remote manager that `cotal supervise` starts can host workflow runs through its host: the host\nadmits each run and signs only the run's own driver, mediator and operator credentials. A logged-in\nuser's `cotal run start` against it is admitted: the auth callout issues the user's manager\nconnection, and the host binds each run to the owner who registered the manager. The run spawns\nagents that user owns, enrolled by the host like any detached spawn, with the reach the user's own\nrow grants when the spawn runs. A spawn may be placed on that manager and on no other instance. The\nhost's own manager refuses user-auth runs by name.\n[User-auth run start](design/user-auth-run-start.md)\nrecords the path.\n\nThe registry entry decides the broker URL `supervise` dials, so a mesh published over `wss://` is\ndialed as a websocket. The manager-authority registration it runs first also takes its TLS\nrequirement from that entry, so the prepare credential is not exchanged over a plaintext\nconnection the record did not describe. `cotal meshes add` records both.\n\nWhen the authority service, login, or renewal is unavailable, the remote manager degrades\nfail-closed: it refuses new agents, restarts, and credential replacement rather than pretending\nlocal authority exists. Existing agents remain live only while their independent credentials are\nvalid. A hosted composition must revoke the managed grant and finish its resumable release before it\nrequests terminal retirement. Deleting DM or delivery consumers is not retirement and must not reset\na resumable lifecycle's frontier or pending state. The alias remains held until the terminal barrier\nconfirms. Restore service and renew successfully before asking it to recover an agent. See\n[Identity & auth](identity-and-auth.md#remote-manager-authority) and the [CLI\nreference](cli.md#supervise).\n\n## Spawning agents\n\n```bash\ncotal spawn # foreground: your default agent, in this terminal\ncotal spawn reviewer --detach # supervised: the manager runs it in a PTY\ncotal attach --name reviewer # watch/type into a detached agent (Ctrl-] detaches)\ncotal ps # what the manager is running\ncotal stop --name reviewer # stop one\n```\n\nHow a spawn resolves:\n\n- **Persona.** A bare `cotal spawn` uses `.cotal/agents/default.md`; a positional name\n picks `.cotal/agents/<name>.md`; `--config` takes an explicit ref or path. Set\n `COTAL_DEFAULT_PERSONA=<name-or-path>` to change the fallback. Fields and format:\n [agent files](agent-files.md).\n- **Harness.** Resolution order is an explicit `--agent` or `cotal_spawn` `agent` argument,\n then the persona file's `agent:` pin, then the invoking caller's `COTAL_DEFAULT_AGENT`,\n then the manager's `COTAL_DEFAULT_AGENT`, then the product default (Claude). Compared in\n [Connectors](connectors.md); per-connector guides:\n [Claude](connect-claude.md) \xB7 [OpenCode](connect-opencode.md) \xB7\n [Hermes](connect-hermes.md) \xB7 [pi](connect-pi.md).\n- **Model.** `--model` overrides the persona file's `model:` (Claude: `opus` / `sonnet` or\n a full id; OpenCode: `provider/model`). Connectors that expose a catalog report it via\n `cotal models --agent opencode`: model ids plus available variants; pick one with\n `--model provider/model --variant high`.\n- **Tools.** A spawned Claude Code agent gets the cotal tools plus the MCP servers the cotal\n config shares, which first-run `cotal setup` fills with your own; narrow them per spawn with\n `--share-tools` ([config](config.md)).\n- **Launch options.** `--opt key=value` (repeatable) passes a native harness flag straight\n through; a persona or manifest `launchOptions:` mapping does the same declaratively (a\n `--opt` wins per key). It is a **raw passthrough**, with no allow/deny list: Claude renders\n each as `--key value` (a bare `--key` for an empty value), OpenCode merges them into its\n agent config, and Hermes has no option surface so it fails loud. The trust boundary is the\n `spawn` capability itself, not the flag set, so granting `spawn` is host-launch authority\n ([security](security.md)). A key must be a plain flag name; malformed or prototype-polluting\n keys are refused.\n\nDetach from an attached PTY with **Ctrl-]** (the agent keeps running); rebind it with\n`COTAL_DETACH_KEY=ctrl-<char>` when it clashes with a keybinding inside the agent's TUI.\n\n**Runtimes.** The manager spawns into a **pty** by default. It spawns the PTY in-process on\nevery platform, so replacing the manager worker closes its seats and the pty runtime gives no hot\nupdate. Any manager stop, bare `cotal down` included, stops and deprovisions those seats. A stopping\nmanager refuses new spawns and first waits for the ones it already accepted, so their seats stop too. On Linux\nit can still adopt and reap seats that an earlier manager launched under a detached per-seat\ncustodian, so those seats drain under the new manager; it starts no new custodian. A custodian whose agent has exited exits a few seconds later on its own. `cotal seats`\nlists the custodians left on the machine, and `cotal seats --drain` retires the ones whose agent\nhas exited while keeping every seat whose agent still runs ([cli.md](cli.md#seats)). When a pty\nagent exits on its own, in-process or under a custodian, the manager logs a `seat reaped:` line\nwith the exit code and, for a signalled child, the signal number. The line ends with the last line\nthe child printed that starts with a connector's `[cotal-<name>]` or `[cotal-<name>/<part>]`\nprefix, cut to 240 characters, when it printed one. A custodian keeps the same record beside the\nseat's custody record, so a later reap of that seat, including one by a\nsuccessor manager, reports how the child ended. When the custodian cannot write that record, it\nsays why in the seat's `custodian.log`, and a later reap of a child that ended on its own reports\nthe record as missing or unreadable. Optional runtimes are installed\nthrough the extension surface, for example `cotal ext add @cotal-ai/orca`, then selected with\n`--runtime orca` (similarly `@cotal-ai/tmux`, `@cotal-ai/cmux`, and `@cotal-ai/herdr`). They put teammates in native\nterminal surfaces rather than manager-owned PTYs. Those surfaces stream no exit, so the manager\nasks the runtime every five seconds whether a seat's process has ended, and frees a seat that ended\non its own once the runtime proves the exit. Its `seat reaped:` line says the exit detail is\nunavailable from that runtime. While a launch waits for readiness the manager asks every second and\nonce more when the readiness window closes, so a seat whose process ends before it joins the mesh is\nreported as exited on launch, with no last output, rather than as uncertain. A runtime that has not\nproved that last exit a second after the window closes leaves the launch uncertain. Runtime names are\nopen-ended and resolved from the registry; a missing provider or app throws, never silently falls back\n([architecture](architecture.md)).\n\n## Mesh registry\n\n`cotal up` records each running mesh in a machine-local registry\n(`~/.cotal/meshes/space.<key>.json`, named by a case-safe hex encoding of the space: broker URL, the project root holding its creds and\npersonas, and its mode). So a bare `cotal spawn <persona>` from *any* directory joins the\nrunning mesh with the right credentials instead of mistaking the cwd for a space:\n\n- `cotal use <name>` sets the default from every directory, including inside another mesh's\n project. `--space <name>` overrides it for one command.\n- When one broker has records for several spaces, `cotal up --space <name>` refreshes that named\n space.\n- A refresh rewrites only what that command decided: the server, root and mode, the user-auth\n endpoints, and an explicit `--host` or `--max-sessions`. Every other field, such as the TLS\n requirement, is kept as the record stands when the refresh writes it, so a change another\n command made during the refresh survives. If the record was removed during the refresh, `up`\n fails instead of writing it back.\n- A selected space stays the target when its broker is down, so the command reports it not up,\n and a selection that names a space no longer registered is refused with `TARGET_NOT_FOUND`.\n Neither reaches another mesh.\n- With nothing selected, a project with its own `.cotal/` resolves to that project's mesh;\n otherwise one running mesh is used automatically and several are an error.\n- `cotal meshes` lists them (a `*` marks the default); `cotal down` removes the entry.\n- `cotal space create <name> [--root <dir>]` names a space before it runs. It records the name\n and root apart from the registry and makes `<root>/.cotal/`, and `cotal up` in that root starts\n the space under that name. Until then a command that targets it refuses, naming the root.\n\nThe registry stores a *path*, never a secret; trust material stays in each project's\n`.cotal/auth`. If the mesh is down or won't take your creds, spawn fails with one\nsentence, never a raw NATS trace.\n\n### Meshes you did not start here\n\nA mesh running on another machine has no `cotal up` on this one, so register it by hand:\n\n```bash\ncotal space add optiplex --server nats://100.90.12.34:4222 --root ~/meshes/optiplex \\\n --allow-unencrypted-overlay # see below: an overlay address needs this\ncotal space forget optiplex\ncotal meshes add # guided: asks for the broker, probes it, offers what it finds\n```\n\n`space add` checks the broker and the trust before it writes, and on a terminal shows what it will\ntrust and asks first. On a terminal, a bare `cotal meshes add` walks you through it: it probes the broker you name and\nreports whether it is open or requires credentials, offers the spaces the folder already holds\ncredentials for, and shows the record before writing it. Scripts and agents keep the flag form -\nwithout a terminal nothing prompts.\n\n`--root` is the local folder holding that mesh's `.cotal/auth` and `.cotal/agents` (its personas);\nthe mode is inferred from what that folder holds.\n\nThe instance identities of the manager and the user-auth service are not part of that folder.\nEach root keeps its own in `.cotal/space.<hex>/`, so `cotal supervise` or `cotal up --user-auth` in\nthe root you copied the folder to starts an instance of its own. A root last run by an older Cotal\nstill holds them in `.cotal/auth`, as `manager-instance.<hex>.json`, `manager-siblings.<hex>.json`\nand `space.<hex>/.cotal/auth/auth-instance.<hex>.json`. Delete those files from a copy of such a\nfolder before the first `cotal supervise` or `cotal up` there. On the root itself, the first start\nmoves each record to `.cotal/space.<hex>/`. It refuses and keeps both entries when either place\nholds a symlink or anything else that is not a regular file, or when the two places hold different\nrecords.\n\n**Know what you are copying.** For an authenticated mesh that folder carries the space's account\n**signing seed**, which is the authority to mint any identity in the space. A machine holding it\nis a certificate authority for the mesh rather than a client of it: anyone who reads it can\nimpersonate any agent, read every retained channel and DM, change ACLs, and keep issuing\nthemselves credentials. There is no per-machine revocation; undoing it means rotating the signing\nkey and re-minting every credential in the space. Copy it only to machines you would trust with\nthe whole mesh. `cotal auth mint` on its own does not substitute here: registering an `auth` mesh needs\nsigning material that composes, which a minted user credential is not. The\nbroker is probed before the record is written, so a bad address or a credential that mesh will not\naccept fails at registration rather than at your first `spawn` (`--force` records it without verifying,\nuseful when the mesh is simply down right now).\n\n#### Which addresses you may register\n\nRegistering a mesh is how this machine starts sending agent credentials to a broker it does not\nrun. NATS announces itself in plaintext before anyone authenticates, so an attacker on the path\ncan pose as the broker and read the credential out of the connect unless the connection\n**requires TLS**, which is recorded on the entry and enforced on every dial through it.\n\nWhat the record will require decides what you may register:\n\n- **Without required TLS**, the address is the gate: **loopback** (`127.0.0.0/8`, `::1`), or\n **your private overlay** (`100.64.0.0/10`, `fd7a:115c:a1e0::/48`) with\n `--allow-unencrypted-overlay`. The tunnel provides the protection, and this command cannot check\n its state. Hostnames are refused because the lookup would choose which machine receives your\n credentials.\n- **With required TLS**, set `--tls` or use a `tls://` URL. The recorded scheme enforces the TLS\n requirement. A **hostname or public address** is accepted because the certificate chain and\n hostname check identify the peer. A registration whose broker cannot complete the handshake\n fails unless you pass `--force`, which records the entry without verification.\n\nOrdinary private ranges like `10.x` and `192.168.x` are refused in **both** modes. A caf\xE9's wifi\nis private but does not belong to you, and no public CA issues certificates for those ranges. An\naddress spelling changes nothing: `[::ffff:192.168.1.10]`, `3232235786`, `0300.0250.01.012`, and\n`192.168.257` all resolve to private addresses and receive the same refusal as the dotted form.\n`--force` exists for a mesh that is down. It never permits an unsafe credential destination.\n\n#### Registering a hosted user-auth mesh\n\nA user-auth space's IdP pins are established where the mesh runs and are never guessed. Register\none from **supplied** trust: `--user-auth-file bundle.json` (exported on the mesh's machine), or\n`--from https://auth.example`, which asks before it contacts the address at all, fetches the\ndiscovery document at `/.well-known/cotal-mesh` under that address over HTTPS, shows you the pins,\nand asks again before adopting them. A URL that already ends in `/.well-known/cotal-mesh` is\nfetched as given. Redirects are refused because a 302 can walk a pinned fetch down to\nplaintext or onto another host, and the pinned exchange must be an `https://` URL too. The one\nexception is an exchange on **this machine**, where nothing leaves the box: plain `http://` is\naccepted for a loopback *literal* (`127.0.0.1`, `::1`, and any spelling of them), but **not** for\n`localhost`, which a hosts entry or poisoned lookup could point elsewhere. Use the\nliteral. Registration checks that the pinned exchange\nanswers `/health` and `/jwks` as the pinned issuer. It also checks that the broker refuses a\nbare connect; that refusal is the pass. The bundle's sentinel credentials are written to a private (0600) file\nunder the entry's root; the registry itself never carries the secret.\n\n**Without required TLS**, an overlay address is **refused unless you accept the dependency\nexplicitly**, with `--allow-unencrypted-overlay`. The address is not the guarantee: it is protected\nwhile the tunnel is up, and if the tunnel is down that range is ordinary carrier-grade NAT and\nwhoever answers the dial receives your credentials. Only you can know which it is, so the command\nasks you to say so. Your acceptance is recorded on the mesh entry rather than printed and\nforgotten, and the guided form asks the same question instead of taking the flag.\n\n**With required TLS** (`--tls`, or a `tls://` URL) that consent is no longer asked for, and the\nflag is not needed: the handshake is what protects the connection, so the acceptance it stood in\nfor has been replaced by proof rather than promise. `cotal meshes add <space> --server\nnats://100.64.0.1 --tls` registers an overlay address with no prompt, no flag and no recorded\nacceptance. This is the \"the flag disappears once the broker can be served over TLS\" case, and it\nhas now arrived.\n\nThis gate is on **registration**. `cotal join --creds --server <url>` deliberately takes an\nexplicit connection at face value and does not consult the registry, so it is not covered. Join\nthat way only to an address you would have registered.\n\nThe connection is still probed first, with the same second try at the longer budget the registry\npreflight uses, so a slow link reads as a connect that did not finish within that budget and a\nrefused port reads as a broker that is not running.\n\nRecords added this way are removed only by something that names them. A failed liveness probe\ndoes not delete any record: an unreachable broker, local or registered by hand, is shown as\n`offline` in `cotal meshes`. A bare command does not count that offline record as running;\nname it with `--space` to restart it. `cotal down` / `cotal clean all` still drop an `up` record for the\nproject they are tearing down, and they leave a hand-registered one alone even when `--root`\npointed at that project. A `cotal up` for that space refuses outright unless it is that same\nendpoint: finding a broker already answering there is a refresh that starts nothing and leaves the\nrecord's provenance alone, while actually starting the broker for that space, server and root\nmakes this machine the one running it, so the record becomes an ordinary local one that\n`cotal down` clears. The refusal names `cotal supervise --space <s> --server <url>` (plus `cotal\ndeliver`) when the registered broker is on another host, and `cotal meshes rm` when it is local.\n`cotal meshes rm` drops it and re-registering with `--force` replaces it. `rm` only forgets a\nmesh. To stop one running here, use `cotal down`.\n\n## Watching\n\n`cotal console` is the terminal view (TUI on a real terminal, plain line stream when\npiped); the separately configured web service is the browser dashboard. Both observe without\nimplicitly starting dependencies; the walkthrough is [Watch a mesh](watch-a-mesh.md).\n\n## History\n\nRetained history is operator-owned. `cotal clean history --force` purges a space's\nretained channel history; `--dms` also purges DMs (`cotal history clear` is an alias).\nIt is deliberately **not** an agent tool: agents cannot wipe the record\n([identity & auth](identity-and-auth.md)). For a **stopped** mesh, `cotal clean store\n--force` deletes the on-disk JetStream store outright, and `cotal clean all --force`\nalso resets the space identity ([CLI reference](cli.md#clean)).\n\n## Offline backup\n\nFor a coherent durable cut, preserve the whole stack first, then create the artifact while it stays\ndown:\n\n```bash\ncotal down --preserve-state\ncotal backup create ./space-backup # full by default\n# later: deliberately resume the unchanged source\ncotal up --detach\n# or, from another preserved cut, restore before the normal listener opens\ncotal up --restore ./space-backup --detach\n```\n\nA refused cut leaves the mesh running and unfenced: fix what the refusal names and run\n`cotal down --preserve-state` again.\n\nA controller-owned stack, one whose root holds `.cotal/stack-controller.json`, cannot be preserved\nin this release. There, preserve, backup and restore refuse before any effect. Plain `cotal down`\nand `cotal up` keep that stack's store, channels and history\n([CLI reference](cli.md#backups)).\n\nUse `--store-dir` on both preservation and backup for a custom JetStream store. A store cap set\nwith `cotal up --max-file-store <bytes>` travels with the preserved state, and the resume renders it\nagain. nats-server reads the cap once at start and refuses a config reload that changes it, so a new\ncap always needs a restart. The cut records the chat stream's frontier per retained seat, so a\nresumed seat catches up from there instead of replaying its channels. `registry` is the\nonly partial selection (`backup create ... --only registry`; `up --restore ... --restore-only\nregistry`). Backup never stops or restarts a mesh implicitly, never opens the original store, and\ndoes not contain credentials or trust secrets. Backup/restore in every auth mode, open included,\nuses isolated, operation-specific maintenance logins; normal agent credentials cannot enter that\nlistener. Full\nrestore requires the same space and exact current local trust continuity, recreates conservative\nconsumer checkpoints bound to their snapshot stream sequence state, and resumes retained agents under\ntheir original principals. The trust commitment includes the cryptographically validated full\noperator/system/data-account root chain as well as static/user authority state. A registry-only\nrestore completes canonical empty infrastructure but leaves retained agents stopped because their\nDM/DLV/TASK/ACL state is outside that selection. Authenticated restore validates the complete space\ntrust bundle before staging or changing the preserved store. Interrupted ordinary resume retries the\nsame durable attempt after its prior listener is stopped. Restore re-entry can recover a surviving normal listener\nonly when its attempt nonce, NATS server name, process owner, endpoint, and target-store identity all\nmatch the fsynced proof. A provably dead uncommitted owner is retired under lock and replaced with a\nfresh attempt-bound listener; an occupied foreign listener or ambiguous owner is never adopted. The\nmanager commit validates while retained cleanup is still suppressed; the CLI durably records its\nattempt-bound 64-hex token in `manager-committed` / `resume-committed` before `finalizeResume` can\nrelease suppression. A new manager launch receives that token as a protected one-read file named by\npath and digest, never on its command line. A retry from either committed state goes straight to exact-token finalization;\nfailure preserves the committed gate and retained cleanup suppression. Missing commit evidence,\ninterrupted finalization, a live recorded endpoint despite missing pidfiles, or ambiguous proof fails closed. See the [CLI\nbackup and restore contract](cli.md#backups) for artifact, checkpoint, fallback,\ndisaster-consent, and degraded-recovery details.\n\n## Personas from the CLI\n\n`cotal persona` manages the selected local catalog offline: bare list, `show <name>`,\n`create <name> --file <persona.md>`, `edit <name>` and `remove <name> --yes`.\nEdits validate a private draft and refuse a changed source before promotion. Prepared spaces\nhave a catalog before startup. `--json` and `--plain` are supported for finite reports.\nThe legacy `personas` parser remains available during migration; its `--running` overlay is live.\nThe runtime write is `cotal_persona`; the runtime read is `cotal_personas`, both over the wire\nwith the manager's ownership checks. Fields: [agent files](agent-files.md).\n\n## Gate recovery\n\nA manager that dies mid-registration leaves its issuance gate *frozen* under that registration\nop. The freeze is correct: it stops two incarnations serving at once. The successor now completes\nthat dead op on boot, using the same guard as [`cotal reconcile-gate`](cli.md#reconcile-gate): it\nacts only when the freeze-holder is affirmatively gone under a complete CONNZ sweep (`gone` and\n`sweepComplete=true`). If the dead op's spec write committed, it finishes that same freeze\n(promote and reopen at the committed registration revision). If the spec did not advance, it\nabort-reopens the gate (generation+1, processEpoch unchanged) and continues the normal takeover.\nBoot heal and the following re-registration use separate one-shot executor windows, so a large\npredecessor family cannot spend the takeover's credential lifetime. If that later registration\nstill crosses a connection lifetime, it retries the same frozen operation with fresh authority\nand resumes verified-holder progress instead of freezing a new generation.\nA live holder, an incomplete sweep, or an unreachable delivery daemon still\nrefuses. Silence is never evidence of death, and there is no TTL. If holder verification is\ninterrupted, the frozen operation resumes from its durable, operation-and-gate-revision-bound\nprogress after liveness is checked again. A later freeze cannot reuse that progress: the cursor\nbinds the exact op, gate revision, and holder set. Use `cotal reconcile-gate` when the boot path cannot run\n(daemon down, a non-manager endpoint, or you want to lift the freeze without starting a manager). A spawn that hits the same frozen gate names that verb in the refusal\n(`blockedOp=registration`, the holding `opId`, `remedy=cotal reconcile-gate`) instead of a\nwait-timeout: the facts were always in the manager log; they now reach the spawn caller too.\n\nGive reconciliation a **quiet manager**. Suspend systemd restart policies, watchdogs, health-check\nrestart loops, and any other automation that can start or kill `cotal supervise` while boot healing or\n`cotal reconcile-gate` is running. Leave one recovery attempt in control until it finishes.\nRestarting the manager during the walk interrupts the current authority window. Durable progress makes\nthat interruption resumable, but a quiet manager is still the fastest and safest incident procedure.\n\n### Last-resort JetStream store replacement\n\nStore replacement is not normal gate recovery, is never automatic, and is destructive to mesh history.\nUse it only after the retained store cannot be reconciled and after deciding that losing its durable\ncontents is acceptable.\n\n1. Stop every actor touching the space: supervisor, watchdog, manager, delivery daemon, and broker.\n Confirm that no Cotal or NATS process still has the store open.\n2. Preserve the stopped store before changing anything. Move `.cotal/nats` aside to a dated backup and\n archive both `nats` and `auth`. Do not delete the only copy.\n3. Understand the loss: replacing the store removes JetStream message and control history and durable\n consumer state. Agent session files stored outside JetStream remain, but the mesh history they\n referenced does not.\n4. Start the broker against a new empty store, then start one manager. Wait until it reports\n serving successfully.\n5. Repopulate the mesh only after that manager is healthy. Re-enable supervisors, watchdogs, and other\n restart automation last.\n\nKeep the preserved store until the incident is reviewed and any required forensic or manual recovery is\ncomplete. Restoring it later restores the old durable state, including the fault that led to this last\nresort, so do not swap it back into a live mesh casually.\n\n## When something looks absent\n\nPermission denials are **loud, never silent**: an over-tight ACL rejects the endpoint call it\nrefuses instead of returning an empty or incomplete result that looks successful, and a denial no\ncall is waiting on, such as a refused subscription, shows up as a logged denial on the endpoint. Check\n`.cotal/manager.<key>.log`, `.cotal/delivery.<key>.log` (one pair per space, keyed as\n[Config](config.md#project-files) describes), and `.cotal/nats.log`; `cotal status` shows\nwhat is actually running. Those files live under the **project** `.cotal/`, not `~/.cotal`,\nunless the mesh root is the home directory. `cotal up --detach` redirects delivery and manager\nstdio onto those files, so an operator-created systemd unit around that launcher does not put\nthe child logs in that unit's journal. `journalctl -u <unit>` can be empty while the crash\nreason is already in the project log. Manager log lines start with the UTC time they were\nwritten. The access rules are collected in\n[Channels & permissions](channels-and-permissions.md).\n"
|
|
27440
|
+
"body": "# Run a mesh\n\n> **Guide** (informative) \xB7 **For:** operators \xB7 **Prereqs:** [Quickstart](getting-started.md)\n\nDay-to-day operation of a local mesh: what `cotal up` actually runs, how spawning\nresolves personas, harnesses, and models, how to reach a mesh from any directory, and the\noperator-only maintenance verbs. Every command's full flag set is in the\n[CLI reference](cli.md).\n\n## The stack\n\n`cotal up` brings up the whole local stack and bare `cotal down` stops it. Managed\nagents stay running as unmanaged OS processes; pass `--with-agents` to take them\nwith the stack. Seats of the built-in pty runtime run inside the manager process, so\nthey stop with the manager either way. On POSIX, a foreground broker has its own session and\nprocess group, so terminal Ctrl-C reaches the CLI owner before the broker. The owner stops the\nmanager through the same stop as bare down and prints the same report. When that stop is refused,\nfor example because the manager cannot prove it can spare, the stack stays running, including\nthe manager, broker, delivery and any auth service. End it with `cotal down --with-agents`.\nA current manager records what its stop does with its seats before\nbare down signals it. A pre-pin legacy manager instead receives a reduced-guarantee\nwarning and is signalled according to the documented upgrade contract. Its running binary\nmay still carry the older destructive SIGTERM handler, so the CLI does not claim its\npre-signal agent inventory was spared; those agents may have been reaped.\n\n- **Broker**: a local `nats-server` (logs to `.cotal/nats.log`).\n- **Delivery daemon**: the durable backstop, auth mode only\n ([what it does](delivery-daemon.md)).\n- **Manager**: a detached supervisor answering the control plane, so\n `cotal spawn --detach` and the `cotal_spawn` tool work right after `up`.\n\nCotal creates the presence bucket in memory storage. Its records are liveness that every endpoint\nrewrites each heartbeat, so nothing is lost when a broker restart empties it, and nats-server's file\nstore write latch cannot reach it. A broker stop removes the memory stream itself, so every `cotal up`,\nincluding the resume after `cotal down --preserve-state`, creates it again before any daemon starts.\nJetStream fixes a stream's storage class when it is created, so a presence bucket created file-backed\nby an older cotal stays file-backed until that stream is recreated.\n\nA file-backed presence bucket can remain open and watchable while refusing every write. A bound\nendpoint reports this as `presence-write-stuck` after one full presence TTL of consecutive failures.\nThe roster is last-known while that condition is active. Restarting the broker clears nats-server's\nin-memory store latch and preserves the JetStream root. Current credentials split the required stream\nauthority: the `cotal up` provisioner can create the presence stream but cannot delete it, while the\nteardown credential can delete it but cannot recreate it. Cotal therefore reports the condition but\ndoes not attempt an unsafe partial delete-and-recreate. Stop and restart the broker to recover.\nA broker below nats-server 2.14.5 carries the latch (nats-server fixed it in 2.14.5). When `cotal up` starts or finds such a broker and the space's presence bucket is file-backed, it says so. A memory-backed bucket gets no warning. A broker below the SPEC \xA713.12 floor of 2.12 is refused at connect with the floor sentence.\n\nThree modes:\n\n- **Default (static auth).** JWT-authed, on by default: sender authenticity and per-agent\n ACLs, enforced by the broker ([how](identity-and-auth.md)).\n- **`--user-auth --idp <url>`.** Per-user auth: people `cotal login` once, the operator\n grants their agents on the actor ledger, and every connect is authorized live against\n that grant. Starts the space's auth service alongside the broker\n ([how](identity-and-auth.md)).\n- **`--open`.** An unauthenticated, live-only dev mesh (no auth, no delivery daemon). For\n quick local experiments.\n\nThe broker and local services bind **loopback** by default. `--host 0.0.0.0` widens the broker\nbind independently of the auth mode, so \"network-reachable\" never silently means\n\"unauthenticated\". With no explicit `--server`, `cotal up` auto-selects a free local port when\nthe default address is already held by another project; an explicit `--server` fails loud on\ncollision.\n\n`--host` is a boot flag, not a live rebind. A fresh `cotal up` writes the generated\n`.cotal/auth/server.conf` (project-local, not `~/.cotal`) with that bind and starts nats against\nit. If anything is already answering at the mesh URL, `up` refreshes the recorded mesh and\nleaves the running nats listener alone, so passing `--host 0.0.0.0` on a live or orphaned\nbroker does not change who can connect. To change the bind: `cotal down`, then `cotal up --host\n<addr>` against a stopped broker so the generated file is rewritten. Do not edit `server.conf`\nby hand; the next real boot overwrites it.\n\nOn a stopped shared broker, `up` renders every persisted space account and every enabled\nspace's auth-callout account into the resolver preload, regardless of which space starts\nthe broker. A missing callout account for an enabled space stops the boot rather than\nstarting with a reduced resolver. An already-running broker is refreshed without rewriting\nits config.\n\nA broker-only host is a first-class `up` mode. `cotal up --no-manager` boots the broker and, in\nauth mode, the delivery daemon, and no local manager, so the broker host never has a manager to\nstop and never leaves a manager slot stale. A refresh under the flag of a mesh whose manager is\nlive refuses rather than 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. Without the flag,\nauth-mode `up` still starts nats, the delivery daemon, and a\nlocal manager. A space may run more than one manager, addressed by instance id\n([control surface](control-surface.md#instance-routing)); putting no manager on the broker host\nis a topology choice, not a singleton invariant. A manager whose boot inventory has no\navailable connector does not take unpinned `spawn`/`launch` on the class rail, so a sibling\nthat can launch the harness can. `describe` still rides the class rail, so an unpinned spawn\ncan bind-fence when that skip member answered describe; re-issue, or pin `--on`. Pin one\ninstance with `--on` when a partial inventory still answers with a harness refusal. The\nsupported split is:\n\n```bash\n# broker host (project root that owns the generated conf, pidfiles, and logs)\ncotal up --detach --host 0.0.0.0 --space main --no-manager\n# no local manager starts: the summary lists nats-server + delivery daemon, and there is no\n# `.cotal/manager.<spaceKey>.log` to wait for on this host\n\n# manager host (registered remote mesh, same space)\ncotal space add main --server nats://broker.example:4222 --root ~/meshes/main\ncotal supervise --space main --server nats://broker.example:4222\n```\n\nWait for `\u2713 manager up` in `.cotal/manager.<spaceKey>.log` on the manager host before spawning\nagents. On a broker host started without `--no-manager`, `cotal up --detach` prints `\u2713 running in\nthe background:` with `manager` listed once the manager pidfile is live; stop that local manager\nonly after the `\u2713 manager up` line. A host started WITH `--no-manager` never runs one, so neither\nthe wait nor the stop applies there. That detach stdout is not a safe teardown boundary: it is\npidfile liveness, not `\u2713 manager up`. `\u2713 manager up` is supervise's post-start line after\n`await mgr.start()`. Stopping the manager after only the detach line can still default-terminate\nthe child during registration after it has taken the governance slot. Stopping before that\npost-start log line can leave the endpoint governance slot held until the holder's gate\nreopens past the stamp (the successor's boot heal, or\n[`cotal reconcile-gate`](cli.md#reconcile-gate) when that boot cannot run). See\n[Gate recovery](#gate-recovery).\n\nStandalone `cotal deliver --creds` is not a repair for that split. Production renewal needs\nthe manager and the daemon to address one credential store. The manager renews its own service\ncredential inside that credential's own window and re-dials its service connection with the\nrenewed credential; if the connection closes and cannot be restored within about forty seconds\nit releases its lease and exits so a restart can serve, while a broker that is briefly gone is\nwaited out. Separate host filesystems still\nleave manager root A writing and the daemon reloading root B; that composition is refused\nwhile the daemon stays up. Before every remint the manager challenges the delivery daemon's\nstore identity, and the answer must come from the process holding the delivery lease: the\nreply names the answering endpoint and the manager reads the lease row itself under its own\ncredential, so a non-holder answering on the queue-grouped admin rail is refused instead of\ncounting as the daemon's store. A rail that reports no responder is also settled from the\nlease row, so a live holder on record makes that outcome a refusal rather than an absent\ndaemon. Keep delivery on the broker host under `up`, and share one store\nonly when you are composing a hosted pair ([embedding](embedding.md#supervisor-signing-authority)).\nOn the `--no-manager` split above, the manager host's manager stays off the daemon-credential\nrenewal lease once its store check finds the daemon on another store. A filesystem store is named\nby its root and by a random id in `.cotal/store.id`, which the copied `.cotal/auth` does not carry,\nso this holds when both hosts use the same root path. `cotal doctor auth --fix` on\nthe broker host then renews the daemon credentials once they pass their renewal point.\n\n### Split host bind\n\nA remote manager cannot reach a loopback broker. After changing `--host`, confirm the\ngenerated `host:` in `.cotal/auth/server.conf` and that nats is listening on that address\nbefore registering the mesh on the manager host. Detached child logs stay under the **project**\n`.cotal/` that `up` ran in (see [When something looks absent](#when-something-looks-absent));\nthey are not `~/.cotal` unless that directory is the mesh root.\n\nA user-auth mesh can expose only its credential exchange through an operator-owned HTTPS reverse\nproxy while leaving the existing local exchange untouched:\n\n```bash\ncotal up --user-auth --idp https://idp.example/api/auth \\\n --exchange-public-port 7443 \\\n --exchange-public-url https://auth.example\n```\n\nThe public listener itself still binds `127.0.0.1:7443`; configure the proxy to terminate TLS and\nforward to it. It serves only `/health`, `/jwks`, `/exchange`, `/manager-service-authority`,\n`/manager-instances`, and `/.well-known/cotal-mesh` with the documented methods. It needs no local file capability: the\nsigned IdP JWT or managed-agent actor token is the proof, while the original loopback listener\nremains capability-gated. Add\n`--exchange-trusted-proxy` only when that listener is reachable exclusively through your trusted\nproxy; it keys failure throttling by the last `X-Forwarded-For` hop instead of the socket address.\nThe well-known bundle includes IdP pins and a deny-all sentinel credential, so fetch it only from\nthe configured HTTPS origin. It tells participants to dial `--server` unless `--advertised-server`\nnames the address they can reach, such as a `wss://` address behind the proxy. Add\n`--agent-provisioning-url <https://\u2026>` to advertise an endpoint where a participant's `cotal spawn`\nprovisions its agent; without one, a participant on another machine needs a one-time enrollment,\ndescribed next. To change these listener flags, stop and restart the mesh; a refresh\nof an already-running service does not replace its bind or proxy policy. See\n[Identity & auth](identity-and-auth.md#per-user-authentication) for the trust boundary.\n\n### Remote supervised seats by enrollment\n\nA remote seat does not need to run `cotal login` when the mesh owner pre-mints a single-use\nenrollment for it. Mount the enrollment URL as a private file, place the seat persona on the remote\nmachine, and launch the foreground seat:\n\n```bash\nCOTAL_ENROLLMENT_FILE=/run/secrets/cotal-enrollment \\\n cotal spawn --config ./worker.md --space main\n```\n\nThe URL is redeemed once with an unauthenticated GET. Redirects, off-machine plain HTTP, retries,\nand login fallback are refused. If the seat has no mesh record yet, the enrollment response's stock\nuser-bundle fields register it before the launch. The returned actor token then uses the same remote\nauth-service exchange as a login-provisioned agent. The enrollment URL and file path do not enter the\npreflight or harness environment. A failed or reused enrollment leaves no actor material on disk; ask the owner\nfor a fresh enrollment. When the foreground seat exits, this machine's credential files are removed\nand the mesh-side grant stays until the mesh operator revokes it; the launch line says so. The exact\nserver contract is in\n[Enrollment redeem](identity-and-auth.md#enrollment-redeem).\n\n`cotal status` prints the detailed setup, process, registry, and live mesh status. Its Machine\nsection names the running CLI's source checkout, installed package root, or npx package root beside\nthe version. It has one row per installed connector, which reports whether the executables that\nconnector declares in `requires` are on PATH. Status, setup and the manager's preflight resolve them\nthe same way: an entry written as a path is checked as given, and a directory never counts as the\nexecutable. A connector whose setup provider reports health adds its\nown rows above those. The Claude Code connector reports its plugin and its skills plugin, and a stale\nskills row names the installed and CLI versions it compared.\n\nBefore reporting ready, the manager resolves every installed connector's declared harness\nbinaries against its own environment. A missing binary does not stop unrelated manager work: boot\ncontinues, but prints a named `connector <name> unavailable` line and records that reason in the\nmanager's `status` response. Available connector rows record the absolute paths boot resolved.\nA spawned seat and a seat resumed after `cotal down --preserve-state` both launch from those paths,\nand both are refused with the recorded reason when their connector's row is unavailable.\n`cotal models` takes the same rule and reports that reason in place of the catalog, so it agrees\nwith a launch about a harness installed or removed after boot. The manager looks again only when it\nrestarts. A connector registered after boot has no row, so spawn, resume and `cotal models` check\nits binaries on PATH when they run.\n\nOn an authenticated manager start, unfinished static lifecycle rows reconcile while the control\nendpoint is already serving. The manager `status` response reports\nthe `staticReconciliation` state, the last sweep counts, and each failed alias with its durable\nphase and literal disposition. `cotal status --components` reports the state and per-alias failure\ndetails. A sweep that stops before it plans any alias, for example on a slot row carrying a DEL or\nPURGE marker, reports the state `failed` with the reason in the last sweep's `error`, and\n`cotal status --components` prints it as `static sweep error=<reason>`. The next manager start\ncollects the rows again. A failed exact terminal is retried in the same process after 1, 5,\nand 30 seconds. Each attempt re-reads the durable slot and re-enters the same deterministic terminal\noperation; the delays only schedule work and never release the lifecycle fence. The terminal's\ncleanup removes the lifecycle's credential file and its broker durables and read-ACL row as separate\nsteps. A file that cannot be removed does not leave the broker footprint behind, and its failure\nkeeps the alias held for the next attempt.\n\nOn shutdown, the manager fences new reconciliation work and waits for an exact terminal that already\nstarted. The current serial sweep stops before its next alias, and startup cannot publish the manager\nservice after `stop()` completes.\n\nThe four-attempt budget is per manager process. An exhausted row stays held and reports\n`retry-exhausted` with the remedy to restart the manager. The next process derives a fresh budget\nfrom the still-authoritative durable row. A `recovered` row remains visible until the next static\nreconciliation sweep, then clears. This component reports reconciliation outcomes. It does not say\nwhether footprint cleanup completed independently of the terminal result; that separate durable\nprojection remains tracked by #1274.\n\n`cotal service install <name>` writes an owned systemd user unit for a saved direct-process\nservice definition ([CLI reference](cli.md#service)). It is Linux only, and a preset such as the\nmanager is not installed as a unit in this release. The units below remain the process models for\nthe stack's components, and they are still **examples of process models**: copy them only after\nyou decide which processes the unit should own.\n\n### Supervising the detached stack\n\n`cotal up` runs in the background by default; `cotal up --detach` names the same mode. It is a\nlauncher: it starts the broker, delivery daemon, and manager, reports what started, then exits. Do not wrap it in a systemd service with `Type=oneshot` and\n`RemainAfterExit=yes` and treat `systemctl is-active` as stack health. That unit becomes `active\n(exited)` when the launcher exits successfully and stays active even if every detached process dies.\nWhen background `up` can identify itself as the unit's main process under the current native\ninvocation, it prints a warning naming the unit and the `--foreground` remedy. The advisory does\nnot change startup or rewrite a unit. Inherited terminal/scope metadata is not enough to claim\nthat `up` is the unit's main process.\n\nFor a single-host stack, use `cotal up --foreground` so systemd tracks a long-running\nprocess and restarts the stack if that process fails:\n\n```ini\n[Service]\nType=simple\nWorkingDirectory=/srv/cotal-mesh\nExecStart=/usr/bin/cotal up --foreground --space main --host 0.0.0.0\nRestart=on-failure\nRestartSec=5s\n```\n\nAn active unit then proves the foreground launcher and broker are still running, but it still does\nnot prove that every child component serves. Pair it with the component check below. Also remember\nthat `cotal up` starts a local manager as well as the broker and delivery daemon; run\n`cotal up --no-manager` (add the flag to the unit's `ExecStart` too) on a host intended to be\nbroker-only, so the unit and the host agree.\n\nSeats spawned by the built-in `pty` runtime run with `oom_score_adj` 500, so under memory\npressure the kernel prefers a seat over the broker, manager and delivery daemon, which are left as\nthey were started; the extension runtimes do not own the seat's process and get no preference.\n\nThat `Type=simple` shape puts nats in the unit's cgroup with the foreground `up` process. A\n`Restart=always` (or `on-failure`) of **this** unit therefore restarts nats as well, so remote\nmanagers drop for the time it takes the broker to come back. Wrapping `cotal up --detach` in\n`Type=oneshot` with `RemainAfterExit=yes` does not move nats out of that cgroup. Detached\nspawn starts a new process group, not a new systemd cgroup, and the default\n`KillMode=control-group` still signals every process left in the service cgroup on stop or\nrestart, including the nats PID. Escaping that cgroup needs an explicit unit setting such as\n`KillMode=process`, or a separate nats unit; this CLI does not ship that escape. The\n`Type=oneshot` unit below is a `cotal status --components` liveness check, not a\n`--detach` launcher. Neither trade is universal from\n`Type=simple` alone; it follows from which processes the unit actually owns. `cotal service\ninstall` writes a Linux unit only for a saved direct-process service definition, so the manager\npreset, the broker and its siblings get no unit from it. Pick the example that matches the\nownership you want, and treat `systemctl is-active` as unit health, not mesh health.\n\nA broker that crashes under that foreground `up` keeps its mesh record and exits non-zero, so the\nunit's restart takes the repair path against the recorded store rather than starting a second one.\n\nA manager or delivery daemon that crashes keeps its lease until the bucket TTL removes it (10s for\nthe manager, 30s for delivery). A repair `up` inside that window waits out the lease when it can show\nthe holder is gone, so one run restores both processes. A lease that is renewed during the wait has\na live holder, and the repair refuses it. See [`up`](cli.md#up).\n\nIf the deployment deliberately uses `cotal up --detach` as a boot action, monitor observed state\ninstead of the launcher's exit:\n\n```ini\n[Unit]\nDescription=Check Cotal component liveness\n\n[Service]\nType=oneshot\nWorkingDirectory=/srv/cotal-mesh\nExecStart=/usr/bin/cotal status --components --space main\n```\n\nRun that check from a systemd timer or another monitor and alert on a nonzero exit. The command\ndistinguishes `absent`, `not-serving`, and `refused` components and never treats a sibling's health as\nproof. A dashboard whose extension is not installed reads `not-installed` and keeps the exit at `0`.\nIts delivery-process check is local to the broker host, so run it there. On a split topology,\nalso probe the broker URL from the manager host and monitor the manager's own service there. A remote\nmanager cannot observe the broker host's delivery PID, and an `active` unit on either host says\nnothing about the other host.\n\nA legacy stack has no command that stops one of its parts without tearing down the mesh. To stop\nparts on their own, use a controller stack: a space prepared by this release with `cotal setup` or\n`cotal space create` starts as one, and `cotal service stop <name> --space <s> --yes` stops one of\nits services. An existing legacy root keeps its own launcher, and nothing moves it in place. A named\n`cotal down <name>` stops a saved service definition, such as a controller stack's `stack-manager`\nor a `web` service, and never one legacy component. Bare `cotal down` stops the whole stack, on a\nlegacy root after first stopping the space's running services through the service controller, and\nleaves managed agents running as unmanaged OS processes, except pty seats, which stop with the\nmanager. `cotal down --with-agents` is the previous reap. If a pinned manager has no\nspare-capability record, stop its managed agents explicitly before running that whole-stack\ncommand. A current manager always publishes the record, so it is absent only for an older manager,\nwhich may not understand the reap request.\n\n## Remote supervised agents\n\nOn a remote user-auth mesh, foreground `cotal spawn` remains the default participant path. A\nparticipant can run detached agents only after the host advertises and operates the remote manager\nauthority service, and the participant's actor-ledger row includes `supervise`. This is not implied\nby `spawn` or `admin`.\n\nThe participant's loopback/operator exchange obtains one closed `manager-service` view for its\nordinary derived owner, a fixed server-selected manager actor, and one opaque manager instance.\nThe host, not the participant, issues the public-nkey JWT material via the replay-safe,\nlifecycle-bound prepare \u2192 activate \u2192 renew exchange, plus a one-shot target-pinned retirement\nrequest for a host-managed terminal. It never exports the space signer, a static\nprovisioner credential, or generic storage authority. Remote registration publishes its service\nstatus at the registered revision and current process epoch, so manager-caller selection can find it.\nEvery manager-authority request carries the participant's fresh login proof and refuses a redirect,\nso a 3xx answer from the pinned endpoint fails the request rather than sending the proof on.\nAn answer whose body is not JSON also fails the request, naming the endpoint and the HTTP status.\n\nStock participant supervision asks its host to enroll a detached agent and to prepare its terminal\nretirement, over the same manager-authority transport. The stock auth service answers both when it\nruns with a public exchange face: it grants the agent under the participant's owner at a lifecycle\nUID it picks, bounded by the participant actor's own grant, provisions that UID's durables, and on\nretirement releases them and revokes the grant before the manager's terminal rail. It refuses a\nsecond enrollment of a name whose grant still stands until that agent's retirement is prepared. A\nhost platform that keeps these writers in its own storage intercepts both requests on its own route\ninstead. Copying host secrets or actor-ledger files to a participant is not supported. Foreground\nspawning and operator-local hosted managers use their existing paths.\n\nThe remote manager that `cotal supervise` starts can host workflow runs through its host: the host\nadmits each run and signs only the run's own driver, mediator and operator credentials. A logged-in\nuser's `cotal run start` against it is admitted: the auth callout issues the user's manager\nconnection, and the host binds each run to the owner who registered the manager. The run spawns\nagents that user owns, enrolled by the host like any detached spawn, with the reach the user's own\nrow grants when the spawn runs. A spawn may be placed on that manager and on no other instance. The\nhost's own manager refuses user-auth runs by name.\n[User-auth run start](design/user-auth-run-start.md)\nrecords the path.\n\nThe registry entry decides the broker URL `supervise` dials, so a mesh published over `wss://` is\ndialed as a websocket. The manager-authority registration it runs first also takes its TLS\nrequirement from that entry, so the prepare credential is not exchanged over a plaintext\nconnection the record did not describe. `cotal meshes add` records both.\n\nWhen the authority service, login, or renewal is unavailable, the remote manager degrades\nfail-closed: it refuses new agents, restarts, and credential replacement rather than pretending\nlocal authority exists. Existing agents remain live only while their independent credentials are\nvalid. A hosted composition must revoke the managed grant and finish its resumable release before it\nrequests terminal retirement. Deleting DM or delivery consumers is not retirement and must not reset\na resumable lifecycle's frontier or pending state. The alias remains held until the terminal barrier\nconfirms. Restore service and renew successfully before asking it to recover an agent. See\n[Identity & auth](identity-and-auth.md#remote-manager-authority) and the [CLI\nreference](cli.md#supervise).\n\n## Spawning agents\n\n```bash\ncotal spawn # foreground: your default agent, in this terminal\ncotal spawn reviewer --detach # supervised: the manager runs it in a PTY\ncotal attach --name reviewer # watch/type into a detached agent (Ctrl-] detaches)\ncotal ps # what the manager is running\ncotal stop --name reviewer # stop one\n```\n\nHow a spawn resolves:\n\n- **Persona.** A bare `cotal spawn` uses `.cotal/agents/default.md`; a positional name\n picks `.cotal/agents/<name>.md`; `--config` takes an explicit ref or path. Set\n `COTAL_DEFAULT_PERSONA=<name-or-path>` to change the fallback. Fields and format:\n [agent files](agent-files.md).\n- **Harness.** Resolution order is an explicit `--agent` or `cotal_spawn` `agent` argument,\n then the persona file's `agent:` pin, then the invoking caller's `COTAL_DEFAULT_AGENT`,\n then the manager's `COTAL_DEFAULT_AGENT`, then the product default (Claude). Compared in\n [Connectors](connectors.md); per-connector guides:\n [Claude](connect-claude.md) \xB7 [OpenCode](connect-opencode.md) \xB7\n [Hermes](connect-hermes.md) \xB7 [pi](connect-pi.md).\n- **Model.** `--model` overrides the persona file's `model:` (Claude: `opus` / `sonnet` or\n a full id; OpenCode: `provider/model`). Connectors that expose a catalog report it via\n `cotal models --agent opencode`: model ids plus available variants; pick one with\n `--model provider/model --variant high`.\n- **Tools.** A spawned Claude Code agent gets the cotal tools plus the MCP servers the cotal\n config shares, which first-run `cotal setup` fills with your own; narrow them per spawn with\n `--share-tools` ([config](config.md)).\n- **Launch options.** `--opt key=value` (repeatable) passes a native harness flag straight\n through; a persona or manifest `launchOptions:` mapping does the same declaratively (a\n `--opt` wins per key). It is a **raw passthrough**, with no allow/deny list: Claude renders\n each as `--key value` (a bare `--key` for an empty value), OpenCode merges them into its\n agent config, and Hermes has no option surface so it fails loud. The trust boundary is the\n `spawn` capability itself, not the flag set, so granting `spawn` is host-launch authority\n ([security](security.md)). A key must be a plain flag name; malformed or prototype-polluting\n keys are refused.\n\nDetach from an attached PTY with **Ctrl-]** (the agent keeps running); rebind it with\n`COTAL_DETACH_KEY=ctrl-<char>` when it clashes with a keybinding inside the agent's TUI.\n\n**Runtimes.** The manager spawns into a **pty** by default. It spawns the PTY in-process on\nevery platform, so replacing the manager worker closes its seats and the pty runtime gives no hot\nupdate. Any manager stop, bare `cotal down` included, stops and deprovisions those seats. A stopping\nmanager refuses new spawns and first waits for the ones it already accepted, so their seats stop too. On Linux\nit can still adopt and reap seats that an earlier manager launched under a detached per-seat\ncustodian, so those seats drain under the new manager; it starts no new custodian. A custodian whose agent has exited exits a few seconds later on its own. `cotal seats`\nlists the custodians left on the machine, and `cotal seats --drain` retires the ones whose agent\nhas exited while keeping every seat whose agent still runs ([cli.md](cli.md#seats)). When a pty\nagent exits on its own, in-process or under a custodian, the manager logs a `seat reaped:` line\nwith the exit code and, for a signalled child, the signal number. The line ends with the last line\nthe child printed that starts with a connector's `[cotal-<name>]` or `[cotal-<name>/<part>]`\nprefix, cut to 240 characters, when it printed one. A custodian keeps the same record beside the\nseat's custody record, so a later reap of that seat, including one by a\nsuccessor manager, reports how the child ended. When the custodian cannot write that record, it\nsays why in the seat's `custodian.log`, and a later reap of a child that ended on its own reports\nthe record as missing or unreadable. Optional runtimes are installed\nthrough the extension surface, for example `cotal ext add @cotal-ai/orca`, then selected with\n`--runtime orca` (similarly `@cotal-ai/tmux`, `@cotal-ai/cmux`, and `@cotal-ai/herdr`). They put teammates in native\nterminal surfaces rather than manager-owned PTYs. Those surfaces stream no exit, so the manager\nasks the runtime every five seconds whether a seat's process has ended, and frees a seat that ended\non its own once the runtime proves the exit. Its `seat reaped:` line says the exit detail is\nunavailable from that runtime. While a launch waits for readiness the manager asks every second and\nonce more when the readiness window closes, so a seat whose process ends before it joins the mesh is\nreported as exited on launch, with no last output, rather than as uncertain. A runtime that has not\nproved that last exit a second after the window closes leaves the launch uncertain. Runtime names are\nopen-ended and resolved from the registry; a missing provider or app throws, never silently falls back\n([architecture](architecture.md)).\n\n## Mesh registry\n\n`cotal up` records each running mesh in a machine-local registry\n(`~/.cotal/meshes/space.<key>.json`, named by a case-safe hex encoding of the space: broker URL, the project root holding its creds and\npersonas, and its mode). So a bare `cotal spawn <persona>` from *any* directory joins the\nrunning mesh with the right credentials instead of mistaking the cwd for a space:\n\n- `cotal use <name>` sets the default from every directory, including inside another mesh's\n project. There a command warns that it targets the selection and names the `--space` for the\n folder's own space. `--space <name>` overrides the selection for one command.\n- When one broker has records for several spaces, `cotal up --space <name>` refreshes that named\n space.\n- A refresh rewrites only what that command decided: the server, root and mode, the user-auth\n endpoints, and an explicit `--host` or `--max-sessions`. Every other field, such as the TLS\n requirement, is kept as the record stands when the refresh writes it, so a change another\n command made during the refresh survives. If the record was removed during the refresh, `up`\n fails instead of writing it back.\n- A selected space stays the target when its broker is down, so the command reports it not up,\n and a selection that names a space no longer registered is refused with `TARGET_NOT_FOUND`.\n Neither reaches another mesh.\n- With nothing selected, a project with its own `.cotal/` resolves to that project's mesh;\n otherwise one running mesh is used automatically and several are an error.\n- `cotal meshes` lists them (a `*` marks the default); `cotal down` removes the entry. A\n controller stack's space stays prepared at its root, and a selection that names it stays.\n- `cotal space create <name> [--root <dir>]` names a space before it runs. It records the name\n and root apart from the registry and makes `<root>/.cotal/`, and `cotal up` in that root starts\n the space under that name. Until then a command that targets it refuses, naming the root.\n\nThe registry stores a *path*, never a secret; trust material stays in each project's\n`.cotal/auth`. If the mesh is down or won't take your creds, spawn fails with one\nsentence, never a raw NATS trace.\n\n### Meshes you did not start here\n\nA mesh running on another machine has no `cotal up` on this one, so register it by hand:\n\n```bash\ncotal space add optiplex --server nats://100.90.12.34:4222 --root ~/meshes/optiplex \\\n --allow-unencrypted-overlay # see below: an overlay address needs this\ncotal space forget optiplex\ncotal meshes add # guided: asks for the broker, probes it, offers what it finds\n```\n\n`space add` checks the broker and the trust before it writes, and on a terminal shows what it will\ntrust and asks first. On a terminal, a bare `cotal meshes add` walks you through it: it probes the broker you name and\nreports whether it is open or requires credentials, offers the spaces the folder already holds\ncredentials for, and shows the record before writing it. Scripts and agents keep the flag form -\nwithout a terminal nothing prompts.\n\n`--root` is the local folder holding that mesh's `.cotal/auth` and `.cotal/agents` (its personas);\nthe mode is inferred from what that folder holds.\n\nThe instance identities of the manager and the user-auth service are not part of that folder.\nEach root keeps its own in `.cotal/space.<hex>/`, so `cotal supervise` or `cotal up --user-auth` in\nthe root you copied the folder to starts an instance of its own. A root last run by an older Cotal\nstill holds them in `.cotal/auth`, as `manager-instance.<hex>.json`, `manager-siblings.<hex>.json`\nand `space.<hex>/.cotal/auth/auth-instance.<hex>.json`. Delete those files from a copy of such a\nfolder before the first `cotal supervise` or `cotal up` there. On the root itself, the first start\nmoves each record to `.cotal/space.<hex>/`. It refuses and keeps both entries when either place\nholds a symlink or anything else that is not a regular file, or when the two places hold different\nrecords.\n\n**Know what you are copying.** For an authenticated mesh that folder carries the space's account\n**signing seed**, which is the authority to mint any identity in the space. A machine holding it\nis a certificate authority for the mesh rather than a client of it: anyone who reads it can\nimpersonate any agent, read every retained channel and DM, change ACLs, and keep issuing\nthemselves credentials. There is no per-machine revocation; undoing it means rotating the signing\nkey and re-minting every credential in the space. Copy it only to machines you would trust with\nthe whole mesh. `cotal auth mint` on its own does not substitute here: registering an `auth` mesh needs\nsigning material that composes, which a minted user credential is not. The\nbroker is probed before the record is written, so a bad address or a credential that mesh will not\naccept fails at registration rather than at your first `spawn` (`--force` records it without verifying,\nuseful when the mesh is simply down right now).\n\n#### Which addresses you may register\n\nRegistering a mesh is how this machine starts sending agent credentials to a broker it does not\nrun. NATS announces itself in plaintext before anyone authenticates, so an attacker on the path\ncan pose as the broker and read the credential out of the connect unless the connection\n**requires TLS**, which is recorded on the entry and enforced on every dial through it.\n\nWhat the record will require decides what you may register:\n\n- **Without required TLS**, the address is the gate: **loopback** (`127.0.0.0/8`, `::1`), or\n **your private overlay** (`100.64.0.0/10`, `fd7a:115c:a1e0::/48`) with\n `--allow-unencrypted-overlay`. The tunnel provides the protection, and this command cannot check\n its state. Hostnames are refused because the lookup would choose which machine receives your\n credentials.\n- **With required TLS**, set `--tls` or use a `tls://` URL. The recorded scheme enforces the TLS\n requirement. A **hostname or public address** is accepted because the certificate chain and\n hostname check identify the peer. A registration whose broker cannot complete the handshake\n fails unless you pass `--force`, which records the entry without verification.\n\nOrdinary private ranges like `10.x` and `192.168.x` are refused in **both** modes. A caf\xE9's wifi\nis private but does not belong to you, and no public CA issues certificates for those ranges. An\naddress spelling changes nothing: `[::ffff:192.168.1.10]`, `3232235786`, `0300.0250.01.012`, and\n`192.168.257` all resolve to private addresses and receive the same refusal as the dotted form.\n`--force` exists for a mesh that is down. It never permits an unsafe credential destination.\n\n#### Registering a hosted user-auth mesh\n\nA user-auth space's IdP pins are established where the mesh runs and are never guessed. Register\none from **supplied** trust: `--user-auth-file bundle.json` (exported on the mesh's machine), or\n`--from https://auth.example`, which asks before it contacts the address at all, fetches the\ndiscovery document at `/.well-known/cotal-mesh` under that address over HTTPS, shows you the pins,\nand asks again before adopting them. A URL that already ends in `/.well-known/cotal-mesh` is\nfetched as given. Redirects are refused because a 302 can walk a pinned fetch down to\nplaintext or onto another host, and the pinned exchange must be an `https://` URL too. The one\nexception is an exchange on **this machine**, where nothing leaves the box: plain `http://` is\naccepted for a loopback *literal* (`127.0.0.1`, `::1`, and any spelling of them), but **not** for\n`localhost`, which a hosts entry or poisoned lookup could point elsewhere. Use the\nliteral. Registration checks that the pinned exchange\nanswers `/health` and `/jwks` as the pinned issuer. It also checks that the broker refuses a\nbare connect; that refusal is the pass. The bundle's sentinel credentials are written to a private (0600) file\nunder the entry's root; the registry itself never carries the secret.\n\n**Without required TLS**, an overlay address is **refused unless you accept the dependency\nexplicitly**, with `--allow-unencrypted-overlay`. The address is not the guarantee: it is protected\nwhile the tunnel is up, and if the tunnel is down that range is ordinary carrier-grade NAT and\nwhoever answers the dial receives your credentials. Only you can know which it is, so the command\nasks you to say so. Your acceptance is recorded on the mesh entry rather than printed and\nforgotten, and the guided form asks the same question instead of taking the flag.\n\n**With required TLS** (`--tls`, or a `tls://` URL) that consent is no longer asked for, and the\nflag is not needed: the handshake is what protects the connection, so the acceptance it stood in\nfor has been replaced by proof rather than promise. `cotal meshes add <space> --server\nnats://100.64.0.1 --tls` registers an overlay address with no prompt, no flag and no recorded\nacceptance. This is the \"the flag disappears once the broker can be served over TLS\" case, and it\nhas now arrived.\n\nThis gate is on **registration**. `cotal join --creds --server <url>` deliberately takes an\nexplicit connection at face value and does not consult the registry, so it is not covered. Join\nthat way only to an address you would have registered.\n\nThe connection is still probed first, with the same second try at the longer budget the registry\npreflight uses. A broker that accepts the connection and does not finish the connect within that\nbudget, because it is stopped, overloaded or behind a slow link, reads as not answering. A refused\nport reads as a broker that is not running.\n\nRecords added this way are removed only by something that names them. A failed liveness probe\ndoes not delete any record: an unreachable broker, local or registered by hand, is shown as\n`offline` in `cotal meshes`. A bare command does not count that offline record as running;\nname it with `--space` to restart it. `cotal down` / `cotal clean all` still drop an `up` record for the\nproject they are tearing down, and they leave a hand-registered one alone even when `--root`\npointed at that project. A `cotal up` for that space refuses outright unless it is that same\nendpoint: finding a broker already answering there is a refresh that starts nothing and leaves the\nrecord's provenance alone, while actually starting the broker for that space, server and root\nmakes this machine the one running it, so the record becomes an ordinary local one that\n`cotal down` clears. The refusal names `cotal supervise --space <s> --server <url>` (plus `cotal\ndeliver`) when the registered broker is on another host, and `cotal meshes rm` when it is local.\n`cotal meshes rm` drops it and re-registering with `--force` replaces it. `rm` only forgets a\nmesh. To stop one running here, use `cotal down`.\n\n## Watching\n\n`cotal console` is the terminal view (TUI on a real terminal, plain line stream when\npiped); the separately configured web service is the browser dashboard. Both observe without\nimplicitly starting dependencies; the walkthrough is [Watch a mesh](watch-a-mesh.md).\n\n## History\n\nRetained history is operator-owned. `cotal clean history --force` purges a space's\nretained channel history; `--dms` also purges DMs (`cotal history clear` is an alias).\nIt is deliberately **not** an agent tool: agents cannot wipe the record\n([identity & auth](identity-and-auth.md)). For a **stopped** mesh, `cotal clean store\n--force` deletes the on-disk JetStream store outright, and `cotal clean all --force`\nalso resets the space identity ([CLI reference](cli.md#clean)).\n\n## Offline backup\n\nFor a coherent durable cut, preserve the whole stack first, then create the artifact while it stays\ndown:\n\n```bash\ncotal down --preserve-state\ncotal backup create ./space-backup # full by default\n# later: deliberately resume the unchanged source\ncotal up --detach\n# or, from another preserved cut, restore before the normal listener opens\ncotal up --restore ./space-backup --detach\n```\n\nA refused cut leaves the mesh running and unfenced: fix what the refusal names and run\n`cotal down --preserve-state` again.\n\nA controller-owned stack, one whose root holds `.cotal/stack-controller.json`, cannot be preserved\nin this release. There, preserve, backup and restore refuse before any effect. Plain `cotal down`\nand `cotal up` keep that stack's store, channels and history\n([CLI reference](cli.md#backups)).\n\nUse `--store-dir` on both preservation and backup for a custom JetStream store. A store cap set\nwith `cotal up --max-file-store <bytes>` travels with the preserved state, and the resume renders it\nagain. nats-server reads the cap once at start and refuses a config reload that changes it, so a new\ncap always needs a restart. The cut records the chat stream's frontier per retained seat, so a\nresumed seat catches up from there instead of replaying its channels. `registry` is the\nonly partial selection (`backup create ... --only registry`; `up --restore ... --restore-only\nregistry`). Backup never stops or restarts a mesh implicitly, never opens the original store, and\ndoes not contain credentials or trust secrets. Backup/restore in every auth mode, open included,\nuses isolated, operation-specific maintenance logins; normal agent credentials cannot enter that\nlistener. Full\nrestore requires the same space and exact current local trust continuity, recreates conservative\nconsumer checkpoints bound to their snapshot stream sequence state, and resumes retained agents under\ntheir original principals. The trust commitment includes the cryptographically validated full\noperator/system/data-account root chain as well as static/user authority state. A registry-only\nrestore completes canonical empty infrastructure but leaves retained agents stopped because their\nDM/DLV/TASK/ACL state is outside that selection. Authenticated restore validates the complete space\ntrust bundle before staging or changing the preserved store. Interrupted ordinary resume retries the\nsame durable attempt after its prior listener is stopped. Restore re-entry can recover a surviving normal listener\nonly when its attempt nonce, NATS server name, process owner, endpoint, and target-store identity all\nmatch the fsynced proof. A provably dead uncommitted owner is retired under lock and replaced with a\nfresh attempt-bound listener; an occupied foreign listener or ambiguous owner is never adopted. The\nmanager commit validates while retained cleanup is still suppressed; the CLI durably records its\nattempt-bound 64-hex token in `manager-committed` / `resume-committed` before `finalizeResume` can\nrelease suppression. A new manager launch receives that token as a protected one-read file named by\npath and digest, never on its command line. A retry from either committed state goes straight to exact-token finalization;\nfailure preserves the committed gate and retained cleanup suppression. Missing commit evidence,\ninterrupted finalization, a live recorded endpoint despite missing pidfiles, or ambiguous proof fails closed. See the [CLI\nbackup and restore contract](cli.md#backups) for artifact, checkpoint, fallback,\ndisaster-consent, and degraded-recovery details.\n\n## Personas from the CLI\n\n`cotal persona` manages the selected local catalog offline: bare list, `show <name>`,\n`create <name> --file <persona.md>`, `edit <name>` and `remove <name> --yes`.\nEdits validate a private draft and refuse a changed source before promotion. Prepared spaces\nhave a catalog before startup. `--json` and `--plain` are supported for finite reports.\nThe legacy `personas` parser remains available during migration; its `--running` overlay is live.\nThe runtime write is `cotal_persona`; the runtime read is `cotal_personas`, both over the wire\nwith the manager's ownership checks. Fields: [agent files](agent-files.md).\n\n## Gate recovery\n\nA manager that dies mid-registration leaves its issuance gate *frozen* under that registration\nop. The freeze is correct: it stops two incarnations serving at once. The successor now completes\nthat dead op on boot, using the same guard as [`cotal reconcile-gate`](cli.md#reconcile-gate): it\nacts only when the freeze-holder is affirmatively gone under a complete CONNZ sweep (`gone` and\n`sweepComplete=true`). If the dead op's spec write committed, it finishes that same freeze\n(promote and reopen at the committed registration revision). If the spec did not advance, it\nabort-reopens the gate (generation+1, processEpoch unchanged) and continues the normal takeover.\nBoot heal and the following re-registration use separate one-shot executor windows, so a large\npredecessor family cannot spend the takeover's credential lifetime. If that later registration\nstill crosses a connection lifetime, it retries the same frozen operation with fresh authority\nand resumes verified-holder progress instead of freezing a new generation.\nA live holder, an incomplete sweep, or an unreachable delivery daemon still\nrefuses. Silence is never evidence of death, and there is no TTL. If holder verification is\ninterrupted, the frozen operation resumes from its durable, operation-and-gate-revision-bound\nprogress after liveness is checked again. A later freeze cannot reuse that progress: the cursor\nbinds the exact op, gate revision, and holder set. Use `cotal reconcile-gate` when the boot path cannot run\n(daemon down, a non-manager endpoint, or you want to lift the freeze without starting a manager). A spawn that hits the same frozen gate names that verb in the refusal\n(`blockedOp=registration`, the holding `opId`, `remedy=cotal reconcile-gate`) instead of a\nwait-timeout: the facts were always in the manager log; they now reach the spawn caller too.\n\nGive reconciliation a **quiet manager**. Suspend systemd restart policies, watchdogs, health-check\nrestart loops, and any other automation that can start or kill `cotal supervise` while boot healing or\n`cotal reconcile-gate` is running. Leave one recovery attempt in control until it finishes.\nRestarting the manager during the walk interrupts the current authority window. Durable progress makes\nthat interruption resumable, but a quiet manager is still the fastest and safest incident procedure.\n\n### Last-resort JetStream store replacement\n\nStore replacement is not normal gate recovery, is never automatic, and is destructive to mesh history.\nUse it only after the retained store cannot be reconciled and after deciding that losing its durable\ncontents is acceptable.\n\n1. Stop every actor touching the space: supervisor, watchdog, manager, delivery daemon, and broker.\n Confirm that no Cotal or NATS process still has the store open.\n2. Preserve the stopped store before changing anything. Move `.cotal/nats` aside to a dated backup and\n archive both `nats` and `auth`. Do not delete the only copy.\n3. Understand the loss: replacing the store removes JetStream message and control history and durable\n consumer state. Agent session files stored outside JetStream remain, but the mesh history they\n referenced does not.\n4. Start the broker against a new empty store, then start one manager. Wait until it reports\n serving successfully.\n5. Repopulate the mesh only after that manager is healthy. Re-enable supervisors, watchdogs, and other\n restart automation last.\n\nKeep the preserved store until the incident is reviewed and any required forensic or manual recovery is\ncomplete. Restoring it later restores the old durable state, including the fault that led to this last\nresort, so do not swap it back into a live mesh casually.\n\n## When something looks absent\n\nPermission denials are **loud, never silent**: an over-tight ACL rejects the endpoint call it\nrefuses instead of returning an empty or incomplete result that looks successful, and a denial no\ncall is waiting on, such as a refused subscription, shows up as a logged denial on the endpoint. Check\n`.cotal/manager.<key>.log`, `.cotal/delivery.<key>.log` (one pair per space, keyed as\n[Config](config.md#project-files) describes), and `.cotal/nats.log`; `cotal status` shows\nwhat is actually running. Those files live under the **project** `.cotal/`, not `~/.cotal`,\nunless the mesh root is the home directory. `cotal up --detach` redirects delivery and manager\nstdio onto those files, so an operator-created systemd unit around that launcher does not put\nthe child logs in that unit's journal. `journalctl -u <unit>` can be empty while the crash\nreason is already in the project log. Manager log lines start with the UTC time they were\nwritten. The access rules are collected in\n[Channels & permissions](channels-and-permissions.md).\n"
|
|
27441
27441
|
},
|
|
27442
27442
|
{
|
|
27443
27443
|
"slug": "security",
|
|
@@ -27451,7 +27451,7 @@ function loadDocsBundle() {
|
|
|
27451
27451
|
"title": "Setup internals (maintainer notes)",
|
|
27452
27452
|
"kind": "Project (non-normative maintainer notes)",
|
|
27453
27453
|
"summary": "cotal setup (implementations/cli/src/commands/setup.ts) saves configuration by default: it checks prerequisites, sets up the chosen application and runtimes, and seeds persona files.",
|
|
27454
|
-
"body": "# Setup internals (maintainer notes)\n\n> **Project** (non-normative maintainer notes) \xB7 **For:** maintainers changing how setup works\n>\n> How `cotal setup` works, and the cross-repo couplings it depends on. If you change one of\n> the things in the **Invariants** table, update the listed siblings in the same change, or\n> setup silently breaks for npx users.\n\n## The flow\n\n`cotal setup`\n([`implementations/cli/src/commands/setup.ts`](../implementations/cli/src/commands/setup.ts))\nsaves configuration by default: it checks prerequisites, sets up the chosen application and\nruntimes, and seeds persona files. Start is a separate terminal choice or `--start`; a script must\nalso pass `--detach`. The dashboard remains a separately configured `web` service preset. Every file it writes is announced (`\u2192 wrote \u2026` via `provenance.wrote`)\non stderr, or on stdout with the error when a stderr write fails. Each announcement is one line: a\ncontrol character or Unicode line separator in a path, such as a newline in `HOME`, is printed as\na `\\uXXXX` escape.\nEvery run takes the same inputs: destination, application, runtimes, project and space. With a\nterminal, `askInputs` asks for each one no flag gave and shows the answers before anything is\nwritten; Change asks again with the answers kept and Cancel saves nothing. Without one, or with\n`--yes`, `--no-input` or `--json`, `scriptedInputs` takes them from the flags and refuses a missing\none, and that refusal (`MissingFlags`) prints the scripted form without the command help.\n`--destination cloud` is refused before anything else with the route a Cloud space takes: `cotal\nlogin` registers it, then `--destination self-hosted`. `runtimeChoices`\nlists `pty`, the official runtimes with the host tools in `OFFICIAL_RUNTIME_TOOLS`, and each\ninstalled third-party runtime. A script's runtimes are the `--runtime` names, which `runtimeFlags`\nchecks before any write: an unknown name or an official one whose tool is not on PATH is refused.\n\n`validateSetupArguments` rejects conflicting startup flags and non-detached scripted startup\nbefore seeding. `planSetup` checks preparation before it writes: the space name, the application against the setup\nconnector surface, and the space at its destination. A local space goes through `checkLocalSpace`\nin [`space-registration.ts`](../implementations/cli/src/commands/space-registration.ts), the\nchecks `space create` makes, which with setup's `again` reports a space already prepared or started\nat that root instead of refusing it; a local space also needs a `nats-server` to locate. A\nself-hosted space goes through `checkSelfHostedSpace`: a registration whose root is the project, or\na connection file `addFromBundle` will register.\n\n`prepare` then writes, in order: the prepared space or the registration, the `default` persona in\n`<project>/.cotal/agents` (plus the demo personas with `--demo`), the space's `harness`\npreference and your own when you have none, the application's `connector` and `mcpServers`\nactions, every present connector's `skills` action, `~/.agents/skills` when it is not current, the\nchosen runtimes, and the onboarded stamp. `setUpRuntimes` adds a chosen official runtime that is\nnot installed through `addExtension` at the CLI's version, the spec `cotal update` replays, then asks every chosen provider's\n`available()`; a missing tool, a failed install or a provider that does not answer makes the result\n`partial`. It never removes a runtime. Each write is\nrecorded in the result's `written` list, so a rerun that changes nothing is a `no-op`. The default\npersona comes from `seedDefaultPersona` in\n[`lib/default-persona.ts`](../implementations/cli/src/lib/default-persona.ts), which\n`space create` and a local `up` also call. An\napplication whose `requires` is not on PATH runs none of its actions, and the result is `partial`\nwith the rest saved. The rich presentation narrates actions through `runSteps`; `--json` and\n`--plain` run them quietly and keep stdout for the result. The old `--auth` / `--open` flags are\ngone: mode is `cotal up [--open]`'s concern.\n\n**`--skills`** writes skills only: it asks installed connectors with a declared skills setup\nhook to reconcile their own harness, then reconciles `~/.agents/skills`. The base CLI passes only the\nvendor-neutral skills directory, version, and state directory; connector packages own native assets\nand commands. It does not seed personas, install the mesh\nconnector, offer a global install, or write the onboarded stamp. Combined with another setup\ninput, `--full` or `--demo` it is refused.\n\nThe seeded `default` persona has an empty active `subscribe` set and wildcard\n`allowSubscribe`/`allowPublish` ACLs. A fresh agent receives no channel traffic until it joins a\nchannel, but can join, create, read, and post to channels on demand. The guided demo personas keep\ntheir existing `welcome` read and post scope. Repeat setup replaces the prior default template only\nwhen its bytes still match the shipped legacy body with `allowPublish: []`; any user edit makes the\nfile ineligible and leaves it byte-identical.\n\nSteps run in-process via `runSteps`\n([`lib/steps.ts`](../implementations/cli/src/lib/steps.ts)). A step can be `optional` (asked\nY/n), carry a `confirm` consent prompt, or be `live` (it draws its own pane via\n[`lib/live-window.ts`](../implementations/cli/src/lib/live-window.ts)). On failure, an\ninteractive run offers a debug handoff for each connector whose setup provider declares an `assist`\nand whose executables are on PATH\n([`lib/assist.ts`](../implementations/cli/src/lib/assist.ts)). The provider owns the harness\nbinary and its flags; the CLI only builds the prompt. When no connector can host one, the menu says\nso in one line. A provider may also declare `status`, which returns read-only rows about what it\ninstalled. `cotal status` prints them, and the CLI passes only its own version and\nthe `cotal setup --skills` remedy. The extensions manifest caches each connector's setup ref, so\nstatus imports only connectors that declare a provider. The seed reconcile refreshes a seeded entry\nwhose cache predates that ref.\n\nThe **application question** offers the **setup connector surface**\n(`setupConnectorSurface`): every connector name the live registry or the installed extension\nmanifest advertises, materialized through the same loader the rest of the CLI uses. No connector\nname is written into `setup.ts`. `setupConnectorCandidates` turns that surface into choices and\nreads each hint off the connector's own declarations: `requires` names the executables a candidate\nstill needs on PATH, `setup` says whether it owns setup actions at all, and `pluginRoot` says\nwhether those actions install plugin assets. The chosen candidate runs its connector-owned\n`connector` action as a narrated step built by `actionStep`, which takes the action with the input its\ntype declares, so the compiler checks each pairing. The provider side is checked too. Each action's\n`run`, `assist.run` and `status` are function-typed properties, which TypeScript checks strictly, so a\nprovider whose callback narrows the input the CLI hands it does not compile. A candidate\nthat declares no provider is simply marked ready (OpenCode auto-wires at spawn, injecting its\nplugin via `buildLaunch` and never writing the user's config). The chosen candidate's `mcpServers` action runs next\nthe same way: the Claude provider reads the user-scope servers from Claude Code's config and\nrecords them through the `seed` input the CLI hands it. That is workspace's `seedConnectorServers`,\nwhich writes the operator-level cotal config under a lock and only when it declares no list for that\nconnector, so two setups run at once record one list. The `skills` action runs for every\npresent connector that declares one, selected or not, because Cotal's authored skills are\nindependent of mesh membership. An upgrade also supplies the current authored skills to each\nrefresh action, which updates an existing skills installation without adding a removed one.\nWith `--demo`, two experts (david, the engineer; sven, the guide) plus the operator's own driving\nsession (`me`) are written too, and `me` is the persona\n`cotal spawn me` drives.\n\n**`--yes`** asks nothing even on a TTY, so every input must be a flag. Optional and `confirm`\nsteps run, the global install takes its default, and a failed action makes the result `partial`.\nIt still launches nothing; the control plane comes up with `cotal up --detach`. The scripted form\nis the agent/CI contract; keep it working.\n\n## Invariants\n\n| Thing | Must stay in sync across | Why |\n|---|---|---|\n| Marketplace name **`cotal-mesh`** | `MARKETPLACE` in the claude connector's [`src/setup.ts`](../extensions/connector-claude-code/src/setup.ts) (materialized `marketplace.json`), repo [`.claude-plugin/marketplace.json`](../.claude-plugin/marketplace.json) | Setup installs and verifies the plugin as `cotal@cotal-mesh`. The wake channel does not use this name: `CHANNEL_REF` in [`extension.ts`](../extensions/connector-claude-code/src/extension.ts) is `server:cotal` |\n| Plugin assets | the claude connector's own [`src/setup.ts`](../extensions/connector-claude-code/src/setup.ts) copy list `COTAL_ASSETS` (`dist/mcp.cjs`, `dist/hook.cjs`, `dist/monitor.cjs`, `.claude-plugin/plugin.json`, `.mcp.json`, `hooks/hooks.json`, `hooks/native-wake.js`, `monitors/monitors.json`, plus the `dist/cli.json` that setup writes beside them), its `package.json` `files` field, and the release tree's list in [`scripts/materialize-claude-plugin.mjs`](../scripts/materialize-claude-plugin.mjs) | The connector materializes its own plugin; missing or renamed assets break the install, and the base CLI has no copy of the list. The release script refuses a plugin whose `.mcp.json` or hooks reference a file it did not copy |\n| `cotal` plugin copy | `materializeCotal` and `refresh` in the claude connector's `src/setup.ts`; `pluginVersion` stamps the release, `+`, then a digest of every file setup copies, `cli.json` included | A repeat `cotal setup` and `cotal update` replace the materialized payload and update existing registrations with Claude's own plugin command. Refresh preserves disabled settings and verifies the recorded version and resolved files. Status checks the same payload rather than trusting the marketplace alone. Running sessions need a plugin reload or restart |\n| Claude Desktop entry | `desktopConfigPath` and the `cotal` server name in the claude connector's `src/desktop-config.ts`, and `desktopEntry` in its `src/setup.ts` | Desktop reads its config only when it starts. The entry runs the materialized `dist/mcp.cjs` with `--desktop`, so the materialize path and the entry must agree |\n| `Connector.pluginRoot` | [`packages/core/src/connector.ts`](../packages/core/src/connector.ts) (contract) plus set in the claude connector's `extension.ts` | A connector's declaration that it ships installable plugin assets; the picker phrases its hint from it |\n| `BUNDLED_PKG_PREFIX` | [`lib/nats-bin.ts`](../implementations/cli/src/lib/nats-bin.ts) \u2194 the `@eplightning/nats-server-*` `optionalDependencies` in [`implementations/cli/package.json`](../implementations/cli/package.json) | The bundled NATS binary is resolved by `${prefix}-${platform}-${arch}`. (Future: swap the prefix to our own `@cotal-ai/nats-server-*`.) |\n| Onboard marker plus `ONBOARD_VERSION` | `~/.cotal/onboarded.json` in [`lib/onboard.ts`](../implementations/cli/src/lib/onboard.ts); version const in `setup.ts` | Setup shows the splash until it is written |\n| Demo-agent format | `DEMO_AGENTS` in `setup.ts` matches the frontmatter shape read by [`packages/core/src/agent-file.ts`](../packages/core/src/agent-file.ts) (same as `examples/01-lateral-coordination/agents/`) | `cotal spawn <name>` loads these |\n| Managed personas | each `DEMO_AGENTS` body carries a `# managed by cotal-setup` frontmatter marker; `writeDemoAgent` refreshes the file when the body changes, backing a marker-less (user-edited) file up to `<name>.md.bak` first | Edit `DEMO_AGENTS` plus re-run setup to update david/sven/me; delete the marker line to take ownership |\n| `DEFAULT_SERVER` | [`packages/core/src/endpoint.ts`](../packages/core/src/endpoint.ts) | The address `cotal up` starts |\n\n## Background processes (`cotal up`)\n\n`cotal up` owns native local stack startup. Opt-in setup startup uses that same entry through\n`__setup-start`, with the prepared root, space and current CLI version checked before effects.\nThe child passes that root to native `up --root`; it does not rely on an ambient workspace-root\nenvironment variable or an ancestor walk. It runs without prompts, and its output is not evidence. A nonzero or unconfirmed\nreturn retains uncertainty; timeout or interruption sends no unverified process signal. A successful\nreturn supplies only command completion and the root's recorded manager selector. Setup then calls\n`launchManagedPersona`, whose original native join receipt is the agent-readiness fact. The shared\nobserver lets setup retain one finite JSON receipt rather than embedding a second spawn envelope.\nSelf-hosted setup starts no local replacement stack. An explicit `--on` is validated\nbefore preparation, reaches the selected manager's credential issuance and native launch,\nand is refused for local startup. Preparation and startup failures remain separate.\nThis composition does not complete the new whole-stack controller or privileged schema writer. The control plane comes up in cutover order:\nold-manager preflight \u2192 **delivery daemon** (auth mode only) \u2192 **manager**, via\n`ensureControlPlane`\n([`lib/delivery-proc.ts`](../implementations/cli/src/lib/delivery-proc.ts)). The preflight reads\nthe manager record once and judges that one process: it stops a live manager whose\n`manager.<key>.delivery-aware` marker does not hold its pid, and leaves one whose marker does or\nthat exited while the preflight looked. The stop signals only that pid. A manager that replaced the\nrecord in the meantime is left running, and the delivery daemon's own check judges it. The detached\nprocesses, all stopped by `cotal down`:\n\nWith no explicit `--server`, `cotal up` auto-selects a free local port when the default broker\naddress is already held by another root or an unrecorded broker; an explicit `--server` remains\nfail-loud on collision.\n\n- **Mesh:** `startMeshDetached`\n ([`commands/up.ts`](../implementations/cli/src/commands/up.ts)) boots the background\n nats-server for `up --detach` and `up -f`, and writes `.cotal/nats.pid` and `.cotal/nats.log`.\n Foreground `up` runs the broker as its own child. Both modes write `.cotal/nats.pid` as soon as\n the broker spawns, so `cotal down` can stop it when `up` dies before the broker is ready. When\n that write fails, `up` stops the broker, leaves whatever is at `.cotal/nats.pid` in place and\n exits with the write error. Both then run one spawn-to-ready sequence: readiness, the broker\n version read and the server name check of a resumed or restored listener. Then they run one\n listener-ready sequence: space setup, user-auth service, mesh record, transport policy, control\n plane. When the listener never answers, its broker is below the version floor, or the space setup\n of a fresh boot fails, `up` stops the listener before it exits. It sends SIGKILL when SIGTERM\n does not stop it and removes `.cotal/nats.pid` only once the listener has exited.\n- **Delivery daemon:** `startDeliveryDetached` / `ensureDelivery`\n ([`lib/delivery-proc.ts`](../implementations/cli/src/lib/delivery-proc.ts)) re-execs `cotal\n deliver` detached with a pre-minted scoped `delivery.creds` (auth mode only, the durable\n backstop; open mode has none). Writes `.cotal/delivery.<key>.pid` and `.cotal/delivery.<key>.log`,\n where `<key>` is the space key ([Config](config.md#project-files)), so a root can serve a space per\n daemon.\n- **Manager:** `startManagerDetached` / `ensureManager`\n ([`lib/manager-proc.ts`](../implementations/cli/src/lib/manager-proc.ts)) re-execs `cotal\n supervise` detached (pty runtime); it answers the control plane\n (`cotal_spawn` / `cotal_despawn` / `cotal_persona`). Writes `.cotal/manager.<key>.log`;\n `managerUp(space)` checks that space's pid record. The **manager itself**\n writes `.cotal/manager.<key>.pid`, so a supervisor started by a container entrypoint, by cron, or\n by hand is recorded the same way a detached `cotal up` is. Readers verify the recorded pid is alive and is a\n supervisor before trusting it ([Config](config.md#project-files)).\n\nThe **web dashboard** is *not* part of `cotal up`. It ships inside `cotal-ai` as the `@cotal-ai/web`\nextension and is seeded automatically by the boot reconcile, the same durable, version-locked path as\nthe built-in connectors (`SEEDED_EXTENSIONS`), so it always matches the CLI version and needs no\nseparate install. Configure its service definition and explicit protected credential before\n`cotal service start web`. The native child owns its PID claim and private full-launch bootstrap\nrecord. The preset is explicitly read-only and retains the registered TLS requirement. Its\ninitialization phase does not prove a new authenticated health response; user-auth caller handoff\nand OS installation remain unsupported. See [dashboard setup](watch-a-mesh.md#web-dashboard).\n\nAll recorded local processes self-register `local-process` descriptors. Bare `cotal down` resolves\nthe full set and stops it in dependency order. No command selects one descriptor: a named\n`cotal down <name>` stops a saved service definition. Installed extensions cache their contributed\nregistry keys, so the\nbase CLI does not hardcode optional package pidfiles.\n\nEach recorded pidfile also carries a sibling `<pidfile>.identity` pin (pid plus the process's\nstart, where the OS reports one). The pin proves that the live process is still the recorded one.\nA reused pid and a torn or unreadable pin are refused and preserved. On Windows, manager and\ndelivery stops use a private process-bound local capability to reach their cleanup handlers.\nA daemon without that capability must be stopped through its original supervisor before relaunch.\nComponents using signals warn on a pre-pin record; the next launch writes a pin.\nA record clears only once death is confirmed. Local cooperative control files live under\n`.cotal/local-stop/`, with generation-specific names and private file permissions. Injected hosted\ndelivery services do not create these files.\n\nAll re-execs resolve this CLI via `selfArgv()` / `selfCotal()`\n([`lib/self-exec.ts`](../implementations/cli/src/lib/self-exec.ts)) = `[node, ...loaderFlags,\nentry]` (tsx loader in dev, compiled JS in prod), so they never need `cotal` on PATH; the stack\ncomes up identically via `npx`, `npm i -g`, and a dev clone.\n\n`selfArgv()` throws unless `process.argv[1]` resolves, through any symlink, to the bin the\n`cotal-ai` package declares (`dist/cotal.js`) or to the `cotal.ts` beside that package's manifest\n(a checkout's `bin/cotal.ts`). Started from any other file, such as a smoke suite under tsx, a\nre-exec would run that file again with a subcommand it ignores, and a file that reaches a starter\non load would spawn its own successor (#1629). The auth, manager and delivery starters ask before\nthey touch a pidfile or a log, and `seedOne` asks before it writes its cursor, stages a payload or\nwrites its child marker, so a refusal on those paths leaves none of them behind.\n\nFor ergonomics only, an npx run with no global `cotal` offers to `npm i -g cotal-ai`\n(`offerGlobalInstall`, pinned to the running version): gated on `isNpx()` plus a PATH scan\n(`cotalOnPath()`). `displayCmd()` runs the same scan for every user-facing hint, so the scan only\nresolves files. The `cotal --version` exec probe stays in `verifiedCotalExecutables()`, which the\nrefusal of an older cotal (below) uses to name a newer executable. The interactive prompt defaults\nto yes, the non-interactive path (`--yes` or no TTY) takes the default, and a failed install is\nnon-fatal (warn plus manual command). It is offered only in the rich presentation, so `--json` and\n`--plain` keep stdout for the result. The same `self-exec.ts`\nexposes `displayCmd()`, the prefix (`cotal` / `npx cotal-ai` / `pnpm cotal`) used in setup's next\ncommand so it matches how you ran it.\n\n## Built-in connectors are seeded extensions\n\nThe first-party connectors (`claude`, `opencode`, `codex`, `hermes`, `jcode`, `pi`) are **not** static-imported by\nthe binary. The composition root (`bin/cotal.ts`) registers no connector; they self-register only when\nimported, and they are imported only once installed. Before the first command of each boot that is\nnot a read, the CLI **seeds** them through the same `cotal ext add` path a third party uses, so they\nare ordinary extensions you can `cotal ext remove`. A command or subcommand that declares `reads`\ninstalls and refreshes nothing, and the CLI never imports a built-in whose installed version differs\nfrom the binary's: it names the package with `cotal ext seed` instead. Code lives in [`implementations/cli/src/seed/`](../implementations/cli/src/seed/);\nthe entry is `reconcileSeededConnectors()`, gated in `runCli` before the manifest overlay so\n`ext seed --repair` survives a corrupt manifest.\n\n**What ships where.** The connectors are `devDependencies` of `cotal-ai` (not runtime deps), and a\n`prepack` step ([`bin/scripts/copy-seeded-connectors.mjs`](../bin/scripts/copy-seeded-connectors.mjs))\n`npm pack`s each into `bin/seeded-connectors/<name>/` (honoring each connector's own `files`), added to\nthe package `files`. `SEEDED_EXTENSIONS` (`@cotal-ai/workspace`) is the shared list: the\nconnectors plus `web`. The prepack asserts that every bundled payload's `name` and `version` match\nthe umbrella (the\n`fixed` changeset group keeps them lockstep), so a version-skewed payload can never be published; `web`\nalso emits `dist/web/vendor/vendor-manifest.json` (name/version/license/sha512) as the auditable\ninventory of its vendored browser libs (marked/DOMPurify ship as opaque `dist` bytes, not runtime deps).\n`seed/paths.ts:shippedSourceDir` resolves the live `extensions/<pkg>` dir in a\nsource checkout and `<cotal-ai>/seeded-connectors/<name>` in a published install. The reconcile copies\nthat payload into the durable store `seed/store/<version>/<name>`. The version is validated as one safe\npath segment, and the destination is checked to stay inside the store before anything is written. `ext add --install-links` reifies\nthe `file:` dep from THAT stable path (a volatile source would fail to re-reify); `ext add` then\njunction-links each `@cotal-ai/*` peer to the binary's own copy. Before the first lazy import in each\nprocess, materialization rechecks those links by realpath and rebinds stale links under the extension\nlock. This lets the registry-facing imports of a global install, npx, and source worktrees share the\nmachine prefix while each process still gets its host's single `@cotal-ai/core` registry instance;\nlauncher artifacts are self-contained and do not resolve those mutable links later.\n\n**Reconcile policy** (generation = the `cotal-ai` version): a never-seeded built-in is seeded; a\nstill-installed one WE seeded (`source: \"seeded\"`) is refreshed only when the version bumps (semver\ncompare) or under `--force`; an operator-managed official entry (a manual `ext add` at a chosen\nversion, no seeded marker) is left untouched on upgrade; a deliberately-removed one stays removed. The\n`ever-seeded` **authority** (`seed/authority.json`, mirrored to a monotonic `.bak`) is the sole arbiter\nof removed-vs-never-seeded and is unioned with its backup on read, so a truncated authority never\nresurrects a removal. Before writing the generation stamp, setup verifies that every\n(re)installed extension is recorded in the manifest, present on disk with a resolvable entry file,\nand at the generation version. A version-skewed payload fails loud (`ext seed --repair`) rather than being stamped as current. A cotal\n**older** than the store's stamped generation refuses before writing anything, rather than stamping the\nstore back down to its own version while refreshing nothing: run the newer cotal, or `ext seed --force`\nto rebuild the store for the version you are running. `--reset` is not that recovery: it discards the\never-seeded authority and resurrects deliberately-removed connectors. The refusal names a concrete cotal executable\nonly after a bounded `--version` probe proves that executable is at least the store generation;\notherwise it retains the generic instruction. A generation advance records the exact\nrealpath-resolved CLI entry and an ISO timestamp in `seed/stamp.json`, then announces the migration\nafter that stamp commits. An older CLI includes those fields in its refusal when present; legacy\ngeneration-only stamps stay valid and retain the shorter refusal. A CLI whose package root is the\nrepo `bin/` (a source checkout, including a suite child of `bin/cotal.ts`) refuses that write, stamp,\nand generation GC rather than migrating the operator-global store. The refusal names\n`COTAL_SKIP_CONNECTOR_SEED=1` as the way to run other commands from a checkout, since an isolated\n`$XDG_CONFIG_HOME` alone does not lift it; `COTAL_HOME` does not relocate this store. Isolated\nin-tree seed smokes set `COTAL_ALLOW_CHECKOUT_SEED=1` after pointing `$XDG_CONFIG_HOME` at a scratch\ndir. An unproven entry is refused the same way: a missing identity answer is not treated as a\nreleased install.\n\nBefore an upgraded generation commits, reconciliation also refreshes already installed harness\ncopies through each connector's setup provider. The provider runs even when its native executable\nis unavailable: an installed integration that cannot be updated must fail, not disappear from the\nupgrade checks. A refresh failure leaves the older stamp and recovery cursor intact. A Claude\nplugin installation at local or project scope that its update does not reach produces a warning and lets\nthe generation commit, so one project's record cannot block every command. A user-scope installation\nthat does not update still fails. The reconcile\nlock stays held while the extension writer lock is released for provider loading and native refresh,\nthen reacquired. A manifest change during that interval refuses the generation commit. This keeps\nordinary first use after an npm upgrade on the same harness-refresh path as `cotal update`.\n\nConnector import and registration do not probe Claude. Setup discovers readiness through the\nprovider, including Desktop's bundled executable, rather than the managed launch requirements.\nClaude's version probes have a five-second kill timeout. An unavailable unrelated harness is\nskipped during setup. Refresh reads Cotal's native plugin records first: no installed integration\nmeans no native probe, while an installed integration with an unusable executable still refuses.\nSelected setup actions recheck readiness inside the recorded step. Losing it after discovery\nproduces a partial result; no skipped native action is reported as completed.\n\nClaude's generated MCP entry and classic hook commands use the recorded Node executable. Those\ngenerated bytes, the native module and the CLI record all contribute to the plugin version digest.\n\n**Crash safety.** One shared advisory lock ([`packages/workspace/src/advisory-lock.ts`](../packages/workspace/src/advisory-lock.ts):\natomic hard-link publish, PID + process-start liveness, bounded wait, dead-owner reclaim) guards the\nwhole reconcile and every `cotal ext` mutation; a live reconcile is waited on, not mistaken for a crash.\nA crash **cursor** is journaled before each connector mutation and cleared only at the final commit, so\na SIGKILL mid-run is detected on the next boot (fail loud \u2192 `ext seed --repair` re-installs the\ninterrupted connector before it clears the evidence). Seed children are authenticated (they carry the\nlive lock's nonce + parent PID, not a bare env flag) and record a liveness marker so a post-crash repair\nrefuses to race an orphaned installer. `ext seed --reset` quarantines corrupt manifest/authority state\naside and rebuilds. See [cli.md `ext`](cli.md#ext) for the operator-facing flags.\n"
|
|
27454
|
+
"body": "# Setup internals (maintainer notes)\n\n> **Project** (non-normative maintainer notes) \xB7 **For:** maintainers changing how setup works\n>\n> How `cotal setup` works, and the cross-repo couplings it depends on. If you change one of\n> the things in the **Invariants** table, update the listed siblings in the same change, or\n> setup silently breaks for npx users.\n\n## The flow\n\n`cotal setup`\n([`implementations/cli/src/commands/setup.ts`](../implementations/cli/src/commands/setup.ts))\nsaves configuration by default: it checks prerequisites, sets up the chosen application and\nruntimes, and seeds persona files. Start is a separate terminal choice or `--start`; a script must\nalso pass `--detach`. The dashboard remains a separately configured `web` service preset. Every file it writes is announced (`\u2192 wrote \u2026` via `provenance.wrote`)\non stderr, or on stdout with the error when a stderr write fails. Each announcement is one line: a\ncontrol character or Unicode line separator in a path, such as a newline in `HOME`, is printed as\na `\\uXXXX` escape.\nEvery run takes the same inputs: destination, application, runtimes, project and space. With a\nterminal, `askInputs` asks for each one no flag gave and shows the answers before anything is\nwritten; Change asks again with the answers kept and Cancel saves nothing. Without one, or with\n`--yes`, `--no-input` or `--json`, `scriptedInputs` takes them from the flags and refuses a missing\none, and that refusal (`MissingFlags`) prints the scripted form without the command help.\n`--destination cloud` is refused before anything else with the route a Cloud space takes: `cotal\nlogin` registers it, then `--destination self-hosted`. `runtimeChoices`\nlists `pty`, the official runtimes with the host tools in `OFFICIAL_RUNTIME_TOOLS`, and each\ninstalled third-party runtime. A script's runtimes are the `--runtime` names, which `runtimeFlags`\nchecks before any write: an unknown name or an official one whose tool is not on PATH is refused.\n\n`validateSetupArguments` rejects conflicting startup flags and non-detached scripted startup\nbefore seeding. `planSetup` checks preparation before it writes: the space name, the application against the setup\nconnector surface, and the space at its destination. A local space goes through `checkLocalSpace`\nin [`space-registration.ts`](../implementations/cli/src/commands/space-registration.ts), the\nchecks `space create` makes, which with setup's `again` reports a space already prepared or started\nat that root instead of refusing it; a local space also needs a `nats-server` to locate. A\nself-hosted space goes through `checkSelfHostedSpace`: a registration whose root is the project, or\na connection file `addFromBundle` will register.\n\n`prepare` then writes, in order: the prepared space or the registration, the `default` persona in\n`<project>/.cotal/agents` (plus the demo personas with `--demo`), the space's `harness`\npreference and your own when you have none, the application's `connector` and `mcpServers`\nactions, every present connector's `skills` action, `~/.agents/skills` when it is not current, the\nchosen runtimes, and the onboarded stamp. `setUpRuntimes` adds a chosen official runtime that is\nnot installed through `addExtension` at the CLI's version, the spec `cotal update` replays, then asks every chosen provider's\n`available()`; a missing tool, a failed install or a provider that does not answer makes the result\n`partial`. It never removes a runtime. Each write is\nrecorded in the result's `written` list, so a rerun that changes nothing is a `no-op`. The default\npersona comes from `seedDefaultPersona` in\n[`lib/default-persona.ts`](../implementations/cli/src/lib/default-persona.ts), which\n`space create` and a local `up` also call. An\napplication whose `requires` is not on PATH runs none of its actions, and the result is `partial`\nwith the rest saved. The rich presentation narrates actions through `runSteps`; `--json` and\n`--plain` run them quietly and keep stdout for the result. The old `--auth` / `--open` flags are\ngone: mode is `cotal up [--open]`'s concern.\n\n**`--skills`** writes skills only: it asks installed connectors with a declared skills setup\nhook to reconcile their own harness, then reconciles `~/.agents/skills`. The base CLI passes only the\nvendor-neutral skills directory, version, and state directory; connector packages own native assets\nand commands. It does not seed personas, install the mesh\nconnector, offer a global install, or write the onboarded stamp. Combined with another setup\ninput, `--full` or `--demo` it is refused.\n\nThe seeded `default` persona has an empty active `subscribe` set and wildcard\n`allowSubscribe`/`allowPublish` ACLs. A fresh agent receives no channel traffic until it joins a\nchannel, but can join, create, read, and post to channels on demand. The guided demo personas keep\ntheir existing `welcome` read and post scope. Repeat setup replaces the prior default template only\nwhen its bytes still match the shipped legacy body with `allowPublish: []`; any user edit makes the\nfile ineligible and leaves it byte-identical.\n\nSteps run in-process via `runSteps`\n([`lib/steps.ts`](../implementations/cli/src/lib/steps.ts)). A step can be `optional` (asked\nY/n), carry a `confirm` consent prompt, or be `live` (it draws its own pane via\n[`lib/live-window.ts`](../implementations/cli/src/lib/live-window.ts)). On failure, an\ninteractive run offers a debug handoff for each connector whose setup provider declares an `assist`\nand whose executables are on PATH\n([`lib/assist.ts`](../implementations/cli/src/lib/assist.ts)). The provider owns the harness\nbinary and its flags; the CLI only builds the prompt. When no connector can host one, the menu says\nso in one line. A provider may also declare `status`, which returns read-only rows about what it\ninstalled. `cotal status` prints them, and the CLI passes only its own version and\nthe `cotal setup --skills` remedy. The extensions manifest caches each connector's setup ref, so\nstatus imports only connectors that declare a provider. The seed reconcile refreshes a seeded entry\nwhose cache predates that ref.\n\nThe **application question** offers the **setup connector surface**\n(`setupConnectorSurface`): every connector name the live registry or the installed extension\nmanifest advertises, materialized through the same loader the rest of the CLI uses. No connector\nname is written into `setup.ts`. `setupConnectorCandidates` turns that surface into choices and\nreads each hint off the connector's own declarations: `requires` names the executables a candidate\nstill needs on PATH, `setup` says whether it owns setup actions at all, and `pluginRoot` says\nwhether those actions install plugin assets. The chosen candidate runs its connector-owned\n`connector` action as a narrated step built by `actionStep`, which takes the action with the input its\ntype declares, so the compiler checks each pairing. The provider side is checked too. Each action's\n`run`, `assist.run` and `status` are function-typed properties, which TypeScript checks strictly, so a\nprovider whose callback narrows the input the CLI hands it does not compile. A candidate\nthat declares no provider is simply marked ready (OpenCode auto-wires at spawn, injecting its\nplugin via `buildLaunch` and never writing the user's config). The chosen candidate's `mcpServers` action runs next\nthe same way: the Claude provider reads the user-scope servers from Claude Code's config and\nrecords them through the `seed` input the CLI hands it. That is workspace's `seedConnectorServers`,\nwhich writes the operator-level cotal config under a lock and only when it declares no list for that\nconnector, so two setups run at once record one list. The `skills` action runs for every\npresent connector that declares one, selected or not, because Cotal's authored skills are\nindependent of mesh membership. An upgrade also supplies the current authored skills to each\nrefresh action, which updates an existing skills installation without adding a removed one.\nWith `--demo`, two experts (david, the engineer; sven, the guide) plus the operator's own driving\nsession (`me`) are written too, and `me` is the persona\n`cotal spawn me` drives.\n\n**`--yes`** asks nothing even on a TTY, so every input must be a flag. Optional and `confirm`\nsteps run, the global install takes its default, and a failed action makes the result `partial`.\nIt still launches nothing; the control plane comes up with `cotal up --detach`. The scripted form\nis the agent/CI contract; keep it working.\n\n## Invariants\n\n| Thing | Must stay in sync across | Why |\n|---|---|---|\n| Marketplace name **`cotal-mesh`** | `MARKETPLACE` in the claude connector's [`src/setup.ts`](../extensions/connector-claude-code/src/setup.ts) (materialized `marketplace.json`), repo [`.claude-plugin/marketplace.json`](../.claude-plugin/marketplace.json) | Setup installs and verifies the plugin as `cotal@cotal-mesh`. The wake channel does not use this name: `CHANNEL_REF` in [`extension.ts`](../extensions/connector-claude-code/src/extension.ts) is `server:cotal` |\n| Plugin assets | the claude connector's own [`src/setup.ts`](../extensions/connector-claude-code/src/setup.ts) copy list `COTAL_ASSETS` (`dist/mcp.cjs`, `dist/hook.cjs`, `dist/monitor.cjs`, `.claude-plugin/plugin.json`, `.mcp.json`, `hooks/hooks.json`, `hooks/native-wake.js`, `monitors/monitors.json`, plus the `dist/cli.json` that setup writes beside them), its `package.json` `files` field, and the release tree's list in [`scripts/materialize-claude-plugin.mjs`](../scripts/materialize-claude-plugin.mjs) | The connector materializes its own plugin; missing or renamed assets break the install, and the base CLI has no copy of the list. The release script refuses a plugin whose `.mcp.json` or hooks reference a file it did not copy |\n| `cotal` plugin copy | `materializeCotal` and `refresh` in the claude connector's `src/setup.ts`; `pluginVersion` stamps the release, `+`, then a digest of every file setup copies, `cli.json` included | A repeat `cotal setup` and `cotal update` replace the materialized payload and update existing registrations with Claude's own plugin command. Refresh preserves disabled settings and verifies the recorded version and resolved files. Status checks the same payload rather than trusting the marketplace alone. Running sessions need a plugin reload or restart |\n| Claude Desktop entry | `desktopConfigPath`, `desktopRunning` and the `cotal` server name in the claude connector's `src/desktop-config.ts`, and `desktopEntry` in its `src/setup.ts` | Desktop reads its config only when it starts and writes that copy back on ordinary use, so on macOS `writeDesktopEntry` writes nothing while a process whose `ps -A -o comm=` path ends in `/Claude.app/Contents/MacOS/Claude` runs. Windows is unchanged. The entry runs the materialized `dist/mcp.cjs` with `--desktop`, so the materialize path and the entry must agree |\n| `Connector.pluginRoot` | [`packages/core/src/connector.ts`](../packages/core/src/connector.ts) (contract) plus set in the claude connector's `extension.ts` | A connector's declaration that it ships installable plugin assets; the picker phrases its hint from it |\n| `BUNDLED_PKG_PREFIX` | [`lib/nats-bin.ts`](../implementations/cli/src/lib/nats-bin.ts) \u2194 the `@eplightning/nats-server-*` `optionalDependencies` in [`implementations/cli/package.json`](../implementations/cli/package.json) | The bundled NATS binary is resolved by `${prefix}-${platform}-${arch}`. (Future: swap the prefix to our own `@cotal-ai/nats-server-*`.) |\n| Onboard marker plus `ONBOARD_VERSION` | `~/.cotal/onboarded.json` in [`lib/onboard.ts`](../implementations/cli/src/lib/onboard.ts); version const in `setup.ts` | Setup shows the splash until it is written |\n| Demo-agent format | `DEMO_AGENTS` in `setup.ts` matches the frontmatter shape read by [`packages/core/src/agent-file.ts`](../packages/core/src/agent-file.ts) (same as `examples/01-lateral-coordination/agents/`) | `cotal spawn <name>` loads these |\n| Managed personas | each `DEMO_AGENTS` body carries a `# managed by cotal-setup` frontmatter marker; `writeDemoAgent` refreshes the file when the body changes, backing a marker-less (user-edited) file up to `<name>.md.bak` first | Edit `DEMO_AGENTS` plus re-run setup to update david/sven/me; delete the marker line to take ownership |\n| `DEFAULT_SERVER` | [`packages/core/src/endpoint.ts`](../packages/core/src/endpoint.ts) | The address `cotal up` starts |\n\n## Background processes (`cotal up`)\n\n`cotal up` owns native local stack startup. Opt-in setup startup uses that same entry through\n`__setup-start`, with the prepared root, space and current CLI version checked before effects.\nThe child passes that root to native `up --root`; it does not rely on an ambient workspace-root\nenvironment variable or an ancestor walk. It runs without prompts, and its output is not evidence. A nonzero or unconfirmed\nreturn retains uncertainty; timeout or interruption sends no unverified process signal. A successful\nreturn supplies only command completion and the root's recorded manager selector. Setup then calls\n`launchManagedPersona`, whose original native join receipt is the agent-readiness fact. The shared\nobserver lets setup retain one finite JSON receipt rather than embedding a second spawn envelope.\nSelf-hosted setup starts no local replacement stack. An explicit `--on` is validated\nbefore preparation, reaches the selected manager's credential issuance and native launch,\nand is refused for local startup. Preparation and startup failures remain separate.\nThis composition does not complete the new whole-stack controller or privileged schema writer. The control plane comes up in cutover order:\nold-manager preflight \u2192 **delivery daemon** (auth mode only) \u2192 **manager**, via\n`ensureControlPlane`\n([`lib/delivery-proc.ts`](../implementations/cli/src/lib/delivery-proc.ts)). The preflight reads\nthe manager record once and judges that one process: it stops a live manager whose\n`manager.<key>.delivery-aware` marker does not hold its pid, and leaves one whose marker does or\nthat exited while the preflight looked. The stop signals only that pid. A manager that replaced the\nrecord in the meantime is left running, and the delivery daemon's own check judges it. The detached\nprocesses, all stopped by `cotal down`:\n\nWith no explicit `--server`, `cotal up` auto-selects a free local port when the default broker\naddress is already held by another root or an unrecorded broker; an explicit `--server` remains\nfail-loud on collision.\n\n- **Mesh:** `startMeshDetached`\n ([`commands/up.ts`](../implementations/cli/src/commands/up.ts)) boots the background\n nats-server for `up --detach` and `up -f`, and writes `.cotal/nats.pid` and `.cotal/nats.log`.\n Foreground `up` runs the broker as its own child. Both modes write `.cotal/nats.pid` as soon as\n the broker spawns, so `cotal down` can stop it when `up` dies before the broker is ready. When\n that write fails, `up` stops the broker, leaves whatever is at `.cotal/nats.pid` in place and\n exits with the write error. Both then run one spawn-to-ready sequence: readiness, the broker\n version read and the server name check of a resumed or restored listener. Then they run one\n listener-ready sequence: space setup, user-auth service, mesh record, transport policy, control\n plane. When the listener never answers, its broker is below the version floor, or the space setup\n of a fresh boot fails, `up` stops the listener before it exits. It sends SIGKILL when SIGTERM\n does not stop it and removes `.cotal/nats.pid` only once the listener has exited.\n- **Delivery daemon:** `startDeliveryDetached` / `ensureDelivery`\n ([`lib/delivery-proc.ts`](../implementations/cli/src/lib/delivery-proc.ts)) re-execs `cotal\n deliver` detached with a pre-minted scoped `delivery.creds` (auth mode only, the durable\n backstop; open mode has none). Writes `.cotal/delivery.<key>.pid` and `.cotal/delivery.<key>.log`,\n where `<key>` is the space key ([Config](config.md#project-files)), so a root can serve a space per\n daemon.\n- **Manager:** `startManagerDetached` / `ensureManager`\n ([`lib/manager-proc.ts`](../implementations/cli/src/lib/manager-proc.ts)) re-execs `cotal\n supervise` detached (pty runtime); it answers the control plane\n (`cotal_spawn` / `cotal_despawn` / `cotal_persona`). Writes `.cotal/manager.<key>.log`;\n `managerUp(space)` checks that space's pid record. The **manager itself**\n writes `.cotal/manager.<key>.pid`, so a supervisor started by a container entrypoint, by cron, or\n by hand is recorded the same way a detached `cotal up` is. Readers verify the recorded pid is alive and is a\n supervisor before trusting it ([Config](config.md#project-files)).\n\nThe **web dashboard** is *not* part of `cotal up`. It ships inside `cotal-ai` as the `@cotal-ai/web`\nextension and is seeded automatically by the boot reconcile, the same durable, version-locked path as\nthe built-in connectors (`SEEDED_EXTENSIONS`), so it always matches the CLI version and needs no\nseparate install. Configure its service definition and explicit protected credential before\n`cotal service start web`. The native child owns its PID claim and private full-launch bootstrap\nrecord. The preset is explicitly read-only and retains the registered TLS requirement. Its\ninitialization phase does not prove a new authenticated health response; user-auth caller handoff\nand OS installation remain unsupported. See [dashboard setup](watch-a-mesh.md#web-dashboard).\n\nAll recorded local processes self-register `local-process` descriptors. Bare `cotal down` resolves\nthe full set and stops it in dependency order. No command selects one descriptor: a named\n`cotal down <name>` stops a saved service definition. Installed extensions cache their contributed\nregistry keys, so the\nbase CLI does not hardcode optional package pidfiles.\n\nEach recorded pidfile also carries a sibling `<pidfile>.identity` pin (pid plus the process's\nstart, where the OS reports one). The pin proves that the live process is still the recorded one.\nA reused pid and a torn or unreadable pin are refused and preserved. On Windows, manager and\ndelivery stops use a private process-bound local capability to reach their cleanup handlers.\nA daemon without that capability must be stopped through its original supervisor before relaunch.\nComponents using signals warn on a pre-pin record; the next launch writes a pin.\nA record clears only once death is confirmed. Local cooperative control files live under\n`.cotal/local-stop/`, with generation-specific names and private file permissions. Injected hosted\ndelivery services do not create these files.\n\nAll re-execs resolve this CLI via `selfArgv()` / `selfCotal()`\n([`lib/self-exec.ts`](../implementations/cli/src/lib/self-exec.ts)) = `[node, ...loaderFlags,\nentry]` (tsx loader in dev, compiled JS in prod), so they never need `cotal` on PATH; the stack\ncomes up identically via `npx`, `npm i -g`, and a dev clone.\n\n`selfArgv()` throws unless `process.argv[1]` resolves, through any symlink, to the bin the\n`cotal-ai` package declares (`dist/cotal.js`) or to the `cotal.ts` beside that package's manifest\n(a checkout's `bin/cotal.ts`). Started from any other file, such as a smoke suite under tsx, a\nre-exec would run that file again with a subcommand it ignores, and a file that reaches a starter\non load would spawn its own successor (#1629). The auth, manager and delivery starters ask before\nthey touch a pidfile or a log, and `seedOne` asks before it writes its cursor, stages a payload or\nwrites its child marker, so a refusal on those paths leaves none of them behind.\n\nFor ergonomics only, an npx run with no global `cotal` offers to `npm i -g cotal-ai`\n(`offerGlobalInstall`, pinned to the running version): gated on `isNpx()` plus a PATH scan\n(`cotalOnPath()`). `displayCmd()` runs the same scan for every user-facing hint, so the scan only\nresolves files. The `cotal --version` exec probe stays in `verifiedCotalExecutables()`, which the\nrefusal of an older cotal (below) uses to name a newer executable. The interactive prompt defaults\nto yes, the non-interactive path (`--yes` or no TTY) takes the default, and a failed install is\nnon-fatal (warn plus manual command). It is offered only in the rich presentation, so `--json` and\n`--plain` keep stdout for the result. The same `self-exec.ts`\nexposes `displayCmd()`, the prefix (`cotal` / `npx cotal-ai` / `pnpm cotal`) used in setup's next\ncommand so it matches how you ran it.\n\n## Built-in connectors are seeded extensions\n\nThe first-party connectors (`claude`, `opencode`, `codex`, `hermes`, `jcode`, `pi`) are **not** static-imported by\nthe binary. The composition root (`bin/cotal.ts`) registers no connector; they self-register only when\nimported, and they are imported only once installed. Before the first command of each boot that is\nnot a read, the CLI **seeds** them through the same `cotal ext add` path a third party uses, so they\nare ordinary extensions you can `cotal ext remove`. A command or subcommand that declares `reads`\ninstalls and refreshes nothing, and the CLI never imports a built-in whose installed version differs\nfrom the binary's: it names the package with `cotal ext seed` instead. Code lives in [`implementations/cli/src/seed/`](../implementations/cli/src/seed/);\nthe entry is `reconcileSeededConnectors()`, gated in `runCli` before the manifest overlay so\n`ext seed --repair` survives a corrupt manifest.\n\n**What ships where.** The connectors are `devDependencies` of `cotal-ai` (not runtime deps), and a\n`prepack` step ([`bin/scripts/copy-seeded-connectors.mjs`](../bin/scripts/copy-seeded-connectors.mjs))\n`npm pack`s each into `bin/seeded-connectors/<name>/` (honoring each connector's own `files`), added to\nthe package `files`. `SEEDED_EXTENSIONS` (`@cotal-ai/workspace`) is the shared list: the\nconnectors plus `web`. The prepack asserts that every bundled payload's `name` and `version` match\nthe umbrella (the\n`fixed` changeset group keeps them lockstep), so a version-skewed payload can never be published; `web`\nalso emits `dist/web/vendor/vendor-manifest.json` (name/version/license/sha512) as the auditable\ninventory of its vendored browser libs (marked/DOMPurify ship as opaque `dist` bytes, not runtime deps).\n`seed/paths.ts:shippedSourceDir` resolves the live `extensions/<pkg>` dir in a\nsource checkout and `<cotal-ai>/seeded-connectors/<name>` in a published install. The reconcile copies\nthat payload into the durable store `seed/store/<version>/<name>`. The version is validated as one safe\npath segment, and the destination is checked to stay inside the store before anything is written. `ext add --install-links` reifies\nthe `file:` dep from THAT stable path (a volatile source would fail to re-reify); `ext add` then\njunction-links each `@cotal-ai/*` peer to the binary's own copy. Before the first lazy import in each\nprocess, materialization rechecks those links by realpath and rebinds stale links under the extension\nlock. This lets the registry-facing imports of a global install, npx, and source worktrees share the\nmachine prefix while each process still gets its host's single `@cotal-ai/core` registry instance;\nlauncher artifacts are self-contained and do not resolve those mutable links later.\n\n**Reconcile policy** (generation = the `cotal-ai` version): a never-seeded built-in is seeded; a\nstill-installed one WE seeded (`source: \"seeded\"`) is refreshed only when the version bumps (semver\ncompare) or under `--force`; an operator-managed official entry (a manual `ext add` at a chosen\nversion, no seeded marker) is left untouched on upgrade; a deliberately-removed one stays removed. The\n`ever-seeded` **authority** (`seed/authority.json`, mirrored to a monotonic `.bak`) is the sole arbiter\nof removed-vs-never-seeded and is unioned with its backup on read, so a truncated authority never\nresurrects a removal. Before writing the generation stamp, setup verifies that every\n(re)installed extension is recorded in the manifest, present on disk with a resolvable entry file,\nand at the generation version. A version-skewed payload fails loud (`ext seed --repair`) rather than being stamped as current. A cotal\n**older** than the store's stamped generation refuses before writing anything, rather than stamping the\nstore back down to its own version while refreshing nothing: run the newer cotal, or `ext seed --force`\nto rebuild the store for the version you are running. `--reset` is not that recovery: it discards the\never-seeded authority and resurrects deliberately-removed connectors. The refusal names a concrete cotal executable\nonly after a bounded `--version` probe proves that executable is at least the store generation;\notherwise it retains the generic instruction. A generation advance records the exact\nrealpath-resolved CLI entry and an ISO timestamp in `seed/stamp.json`, then announces the migration\nafter that stamp commits. An older CLI includes those fields in its refusal when present; legacy\ngeneration-only stamps stay valid and retain the shorter refusal. A CLI whose package root is the\nrepo `bin/` (a source checkout, including a suite child of `bin/cotal.ts`) refuses that write, stamp,\nand generation GC rather than migrating the operator-global store. The refusal names\n`COTAL_SKIP_CONNECTOR_SEED=1` as the way to run other commands from a checkout, since an isolated\n`$XDG_CONFIG_HOME` alone does not lift it; `COTAL_HOME` does not relocate this store. Isolated\nin-tree seed smokes set `COTAL_ALLOW_CHECKOUT_SEED=1` after pointing `$XDG_CONFIG_HOME` at a scratch\ndir. An unproven entry is refused the same way: a missing identity answer is not treated as a\nreleased install.\n\nBefore an upgraded generation commits, reconciliation also refreshes already installed harness\ncopies through each connector's setup provider. The provider runs even when its native executable\nis unavailable: an installed integration that cannot be updated must fail, not disappear from the\nupgrade checks. A refresh failure leaves the older stamp and recovery cursor intact. A Claude\nplugin installation at local or project scope that its update does not reach produces a warning and lets\nthe generation commit, so one project's record cannot block every command. A user-scope installation\nthat does not update still fails. The reconcile\nlock stays held while the extension writer lock is released for provider loading and native refresh,\nthen reacquired. A manifest change during that interval refuses the generation commit. This keeps\nordinary first use after an npm upgrade on the same harness-refresh path as `cotal update`.\n\nConnector import and registration do not probe Claude. Setup discovers readiness through the\nprovider, including Desktop's bundled executable, rather than the managed launch requirements.\nClaude's version probes have a five-second kill timeout. An unavailable unrelated harness is\nskipped during setup. Refresh reads Cotal's native plugin records first: no installed integration\nmeans no native probe, while an installed integration with an unusable executable still refuses.\nSelected setup actions recheck readiness inside the recorded step. Losing it after discovery\nproduces a partial result; no skipped native action is reported as completed.\n\nClaude's generated MCP entry and classic hook commands use the recorded Node executable. Those\ngenerated bytes, the native module and the CLI record all contribute to the plugin version digest.\n\n**Crash safety.** One shared advisory lock ([`packages/workspace/src/advisory-lock.ts`](../packages/workspace/src/advisory-lock.ts):\natomic hard-link publish, PID + process-start liveness, bounded wait, dead-owner reclaim) guards the\nwhole reconcile and every `cotal ext` mutation; a live reconcile is waited on, not mistaken for a crash.\nA crash **cursor** is journaled before each connector mutation and cleared only at the final commit, so\na SIGKILL mid-run is detected on the next boot (fail loud \u2192 `ext seed --repair` re-installs the\ninterrupted connector before it clears the evidence). Seed children are authenticated (they carry the\nlive lock's nonce + parent PID, not a bare env flag) and record a liveness marker so a post-crash repair\nrefuses to race an orphaned installer. `ext seed --reset` quarantines corrupt manifest/authority state\naside and rebuilds. See [cli.md `ext`](cli.md#ext) for the operator-facing flags.\n"
|
|
27455
27455
|
},
|
|
27456
27456
|
{
|
|
27457
27457
|
"slug": "spaces",
|
|
@@ -27472,7 +27472,7 @@ function loadDocsBundle() {
|
|
|
27472
27472
|
"title": "Telemetry",
|
|
27473
27473
|
"kind": "",
|
|
27474
27474
|
"summary": "The cotal CLI sends anonymous usage data so we can see which commands people use, which fail, and on which platforms.",
|
|
27475
|
-
"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, the failure\'s error text with values and paths masked, 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 the OS user\'s own Cotal directory, `~/.cotal/telemetry.json`\n(`%LOCALAPPDATA%\\Cotal\\telemetry.json` on Windows), mode `0600`: the level, a random install id\n(uuid v4), the file\'s creation time, and the times of the one-time notice and the last install\nevent. `COTAL_HOME` does not move this file, so every workspace one person runs under their own\naccount reports the same install id. A file that does not validate refuses every command with\n`SCHEMA_INVALID` and is never replaced.\n\n## Never sent\n\nArguments, flag values, paths, mesh, space, agent, user and host names, environment values,\nprompts, messages and any other content. The error text of a failure is sent at `regular` only\nafter masking (see `errorMessage` below). The install id identifies the settings file, not a person\nor machine. The `account` field at `regular` is a hash of the cached sign-in\'s IdP URL and subject\nid; the id itself, the email and the name behind it are never sent. The daemon, manager,\nconnectors and workflow runtime send nothing; neither do internal\nentries, hidden daemon runners, `agent-bearer` and `cotal telemetry` itself.\n\nThe intake sees the IP address every request comes from, as any web server does. It uses it for a\ncountry and region lookup and does not store it: the stored event carries the derived location only.\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| `account` | string or `null` | A sha256 hash of the IdP URL and subject id of the home\'s cached sign-in, cut to 32 hex characters, so one person counts once across machines; with several sign-ins, the first by IdP URL. `null` without a sign-in. |\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| `errorMessage` | string or `null`, at most 500 characters | The failure\'s message after masking: quoted words become `<value>` (the CLI quotes every user value it echoes), URLs `<url>`, email addresses `<email>`, IPv4 addresses `<ip>`, paths `<path>` and any run of 32 or more token characters `<token>`. `null` on success. |\n| `errorStack` | string or `null`, at most 2000 characters | At most 12 frames of the failure\'s stack, each file path cut down to its part inside `node_modules` (the installed package and file, never the directory it is installed under); a frame outside one reads `<path>`. `null` on success or when the failure had no stack. |\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| `account` | string or `null` | As in the `command` event. |\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'
|
|
27475
|
+
"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, the failure\'s error text with values and paths masked, 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. While the\nenvironment forces `off`, `cotal telemetry <level>` saves nothing and creates no file: it is a\n`no-op` with an `ENVIRONMENT_OPT_OUT` warning.\n\nThe settings live in the OS user\'s own Cotal directory, `~/.cotal/telemetry.json`\n(`%LOCALAPPDATA%\\Cotal\\telemetry.json` on Windows), mode `0600`: the level, a random install id\n(uuid v4), the file\'s creation time, and the times of the one-time notice and the last install\nevent. `COTAL_HOME` does not move this file, so every workspace one person runs under their own\naccount reports the same install id. A file that does not validate refuses every command with\n`SCHEMA_INVALID` and is never replaced.\n\n## Never sent\n\nArguments, flag values, paths, mesh, space, agent, user and host names, environment values,\nprompts, messages and any other content. The error text of a failure is sent at `regular` only\nafter masking (see `errorMessage` below). The install id identifies the settings file, not a person\nor machine. The `account` field at `regular` is a hash of the cached sign-in\'s IdP URL and subject\nid; the id itself, the email and the name behind it are never sent. The daemon, manager,\nconnectors and workflow runtime send nothing; neither do internal\nentries, hidden daemon runners, `agent-bearer` and `cotal telemetry` itself.\n\nThe intake sees the IP address every request comes from, as any web server does. It uses it for a\ncountry and region lookup and does not store it: the stored event carries the derived location only.\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| `account` | string or `null` | A sha256 hash of the IdP URL and subject id of the home\'s cached sign-in, cut to 32 hex characters, so one person counts once across machines; with several sign-ins, the first by IdP URL. `null` without a sign-in. |\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| `errorMessage` | string or `null`, at most 500 characters | The failure\'s message after masking: quoted words become `<value>` (the CLI quotes every user value it echoes), URLs `<url>`, email addresses `<email>`, IPv4 addresses `<ip>`, paths `<path>` and any run of 32 or more token characters `<token>`. `null` on success. |\n| `errorStack` | string or `null`, at most 2000 characters | At most 12 frames of the failure\'s stack, each file path cut down to its part inside `node_modules` (the installed package and file, never the directory it is installed under); a frame outside one reads `<path>`. `null` on success or when the failure had no stack. |\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| `account` | string or `null` | As in the `command` event. |\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'
|
|
27476
27476
|
},
|
|
27477
27477
|
{
|
|
27478
27478
|
"slug": "transport",
|
|
@@ -27486,7 +27486,7 @@ function loadDocsBundle() {
|
|
|
27486
27486
|
"title": "Upgrading a running deployment",
|
|
27487
27487
|
"kind": "Guide (informative)",
|
|
27488
27488
|
"summary": "Substrate stability tells you what the version numbers promise.",
|
|
27489
|
-
"body": "# Upgrading a running deployment\n\n> **Guide** (informative) \xB7 **For:** operators upgrading a mesh that already exists \xB7 **See also:** [Substrate stability](stability.md), [Run a mesh](run-a-mesh.md), [Identity and auth](identity-and-auth.md)\n\n[Substrate stability](stability.md) tells you what the version numbers promise. This page is the\nother half: what to actually do when the deployment already exists, has credentials in it, and\ncannot simply be recreated. Every release that breaks a running deployment gets a section here,\nnaming what migrates on its own, what does not, and the order to move the pieces in.\n\n## The pre-1.0 upgrade contract\n\nThe packages are pre-1.0, so a minor bump may break an API or an on-disk expectation. Four\ncommitments make that survivable for someone with a fleet:\n\n- **Pin an exact version.** `0.N.P`, never `^0.N.P`. A range can pull a breaking minor in during an\n unrelated reinstall.\n- **Every break that touches a running deployment gets a section on this page**, written in terms of\n what an operator does, not in terms of which module changed.\n- **Read the section before you start, not halfway through.** A section names the work up front\n precisely so the operation does not change shape once it is underway.\n- **A break that cannot be made automatic says so.** Where credentials or state must be recreated by\n hand, the section says which ones and when, rather than leaving you to discover it at the moment\n the first one stops working.\n- **A change to the shape of a credential, or to who may renew one, is breaking whatever the commit\n marker says.** This rule is stated because the marker is a judgement made while writing the code\n and the consequence is felt by someone running it a day later. A fleet that keeps authenticating\n looks compatible and is not, if nothing in it can renew. Any automated check of this rule would\n read commit markers, so a break recorded as a feature is the one case it could not see, which is\n why the rule is written for people first. **The marker held for this release: the 0.49.0 change\n that caused all of this, `36d177951 feat(core)!`, did carry its `!`.** The rule exists for the\n next one that does not.\n\nWhat this page does not promise is a rolling upgrade. Nothing in the current line dual-serves two\nauthority versions, so where broker and manager run separately there is a window in which the mesh\nis down. The sections below give that window's shape so it can be scheduled rather than endured.\n\n## Personal native connections in 0.92.0\n\nUpdate the CLI and its Claude plugin together, then reopen the native Code session. Existing static\nauth connections keep their credentials and gateway tools. Personal signed-in connections are new;\nthe plugin binds a native conversation through its ordinary command hooks before creating one.\nThis works without intercepting the `classic.*` events protected by Claude's built-in guard; keep\nthe guard enabled. Organization policies that disable command hooks or native modules still apply.\nRemote meshes must advertise\nconversation identity v1 on the selected registration's HTTPS origin. The existing account signs\nin through Cotal; do not copy login files or paste a token into Claude.\n\nEach signed-in conversation gets its own roleless actor, direct messages and an own-event-only\npublication grant. The space's required-event policy remains in force. Desktop Chat cannot use this\nnative path because it has no Code transcript binding.\n\nCreation persists the request and secret before contacting the authority. Retrying an interrupted\ncreation reuses them. Disconnect can report pending cleanup; retry it for the same connection.\nPrivate recovery state remains until the authority confirms that exact lifecycle is retired.\nNeither an upgrade nor a lost response silently creates replacement authority.\n\n## Cooperative workflow cancellation in 0.92.0\n\nThe manager adds the issued, instance-bound `run-cancel` command. Its cluster revision advances to\n37, retaining the vitals and keyed-submission commands. Cancellation is available through the\ngeneric endpoint client; this change adds no CLI or MCP cancellation shortcut.\n\n### What keeps working\n\nExisting run records, admissions, revocation markers and journals keep their formats. Upgrade does\nnot rewrite them, cancel a run or remove a marker. A released run without a marker can still resume\nunder its admission. A revoked run remains revoked. Manager shutdown still requests boundary\nrelease; it does not write a cancellation marker or prove an agent process stopped.\n\n### What needs an update\n\n- Custom `RunHost` implementations must return a drive with `cancel(reason)`, in addition to\n `done` and `release(reason)`. It must propagate cooperative root cancellation to in-flight\n effects. Do not substitute boundary release or process termination. Rebuild and qualify the\n custom host against the new core contract before enabling it on an upgraded manager.\n- The built-in runtime and language packages carry the root signal through both engines and the\n compiled worker. Keep core, auth, manager, runtime and language on the same release; copying one\n source file or replacing only the manager does not supply the new protocol and worker code.\n- Existing caller credentials do not gain a new publish permission on installation. Obtain fresh\n issued credentials through the supported issuer before using `run-cancel`. Static credentials\n and caller generations must still meet the original lifecycle and grant checks. Never edit a\n credential to add the permission.\n- The new command requires a fresh Describe-derived instance and epoch bind. An old issuer lacks\n its independent request observer and inspect/commit path, so a new signerless manager cannot use\n that issuer for cancellation. Do not treat missing support as a reason to bypass the issuer.\n\n### Order for a split topology\n\nSchedule a maintenance window for the version change; this is not a rolling-compatibility promise.\nTake the deployment's usual consistent backup of broker state, including run records, admissions\nand journals, and the issuer's private configuration. Upgrade the issuer, then the manager and its\nruntime dependencies to the same release, using their normal stop/start procedure. Reinstalling a\npackage does not replace modules already loaded by a running process. Reconnect callers through\nthe upgraded issuer and resolve the manager's current contract before invoking cancellation.\nThere is no broker format migration for this command and no automatic cancellation during upgrade.\n\n### What the result establishes\n\nA durable marker denies future admission but is not proof of cessation. Only `stopped` with the\ncaptured drive outcome establishes that local drive's result. If the holder recheck fails or the\nrequest budget expires, a parked effect can remain running beside a committed marker. Preserve an\nunknown result; do not blindly resend it or declare cleanup complete. To continue cancellation,\na caller can make a fresh explicit request, which observes the marker and current holder before\nreporting its own result. Pending cancellation intent can also block a local launch after a definite\nno-dispatch refusal if marker absence was not confirmed within the budget.\n\n`run show` now displays revocation independently of recorded state. An unreadable marker yields\na partial result and exit 4 in human and JSON output, rather than an apparently complete view.\nAutomation must handle that existing partial-result convention. None of these observations proves\nmodel or tool preemption, physical process cleanup or cessation on another host.\n\n## Service authority status selector in 0.92.0\n\nA service endpoint authority status query that reads one operation now names that operation:\n`target: { kind: \"operation\", operation, requestId }`, where `operation` is `register`,\n`activate`, `renew`, `ready` or `revoke`. `@cotal-ai/core` exports the operation names as\n`ServiceStatusOperation` and the whole selector as `ServiceStatusTarget`. The 0.91.0 selector,\n`{ kind: \"operation\", requestId }`, no longer parses. The change is in `parseServiceAuthorityRequest`,\n`parseServiceAuthorityEnvelope` and the `ServiceAuthorityRequest` type, so it reaches every client\nand server built on them.\n\nA `revoke` operation status parses, but the 0.92.0 auth service does not answer it yet: it is\n`denied` with reason `unsupported` (HTTP 422), as SPEC states. To follow a revocation, read\n`target: { kind: \"instance\" }`, which reports `revoking` with the revoke's operation id.\n\n### What keeps working\n\n- Every running mesh. 0.91.0 shipped the request types and the client (the auth provider's\n `serviceEndpointAuthority` hook and its HTTP client), but no 0.91.0 auth service serves\n `/service-endpoint-authority`, so no deployment has status traffic on this route to lose. In\n 0.92.0 only a context opened with `serviceEndpoints` serves the route. That option is off by\n default and has no CLI flag; without it the route answers 404.\n- `target: { kind: \"instance\" }` is unchanged, and so are the register, activate, renew, ready and\n revoke requests themselves.\n- No credential, broker or connection is involved. Nothing needs reminting and no broker restarts.\n\n### What does not migrate\n\n- **Code that builds an operation status query without `operation`.** The 0.92.0 provider hook and\n client parse each request before sending it, so the query fails locally with a `bad-request`\n error, `service-endpoint authority: <detail>`, whose detail is\n `object has missing or unsupported fields`, and nothing is sent. An operation name outside the\n five gets the detail `unsupported status operation`.\n- **A 0.91.0 client still sends the old selector.** A 0.92.0 auth service opened with\n `serviceEndpoints` answers it with HTTP 400, whose body carries the same parser message. The\n service reads and writes nothing for it and logs nothing. The 0.91.0 provider hook and client\n never show that message: they cannot parse the 400 body as a protocol result, so they throw a\n `ServiceAuthorityTransportError` whose message is\n `service authority returned an unconfirmed response; retain the existing request identity`, with\n `httpStatus` 400 and outcome `not-executed`. A status query is a read, so nothing ran and\n nothing needs replaying. An auth service without `serviceEndpoints` answers 404 before it reads\n the body.\n- **No legacy operation is inferred.** Neither the client nor the service maps an old selector onto\n `register` or any other operation. One requestId can key a different request under each\n operation, so a guessed operation could answer with another request's status. Send the operation\n the request was made as; the requestId you stored with it stays the same.\n\n### Durable data\n\nNothing stored changes on upgrade, and nothing is rewritten. The selector is a request shape and is\nnever stored. 0.91.0 wrote no service authority request intents or operation records, so there is\nnothing to convert. A 0.92.0 context opened with `serviceEndpoints` writes its own records from the\nmoment it is enabled: an admission record when it first opens, then request intents, the\nper-instance intent index, operation, proposal and assignment records, and endpoint gates. Each\nintent already records its operation. Without `serviceEndpoints` it writes none of them.\n\n### Order for a split topology\n\nUpgrade every client that builds status queries together with, or before, the first auth service\nyou open with `serviceEndpoints`. That means code using `ServiceAuthorityRequest` or\n`parseServiceAuthorityRequest`, and every process that calls the auth provider's\n`serviceEndpointAuthority`. This change adds no ordering for brokers, managers or connectors;\norder those as their own sections say.\n\nThere is no compatibility window to schedule. A 0.92.0 auth service never accepts the old selector,\nand a 0.91.0 auth service has no handler for the route at all, so a mixed pool is not partly\nworking: every 0.92.0 auth service opened with `serviceEndpoints` refuses an old client's\noperation status queries with 400, which that client reports as the unconfirmed-response error\nabove, and one without `serviceEndpoints` answers 404. Nothing else is interrupted, so the change\nhas no outage window of its own.\n\n### Before the upgrade\n\n- Nothing needs a snapshot for this change, because it writes nothing. If the same upgrade first\n enables `serviceEndpoints`, take the deployment's usual consistent backup of broker JetStream\n state and the auth host's private configuration before that step, because the service endpoint\n host then starts writing to the auth bucket.\n- Find the old selectors in your code, then name the operation in each:\n\n```sh\ngrep -rn 'kind: \"operation\"' --include='*.ts' --include='*.js' src/\n```\n\n```ts\n// 0.91.0\n{ ...coordinates, operation: \"status\", target: { kind: \"operation\", requestId } }\n// 0.92.0\n{ ...coordinates, operation: \"status\", target: { kind: \"operation\", operation: \"register\", requestId } }\n```\n\nWhat this section measured, and what it did not: the local refusal and the accepted and refused\nselectors are covered by the core parser's own test. The 400, 404 and `unsupported` answers, and\nthe absence of a log line, are read from the auth service's source. The 0.91.0 client's error is\nread from the 0.91.0 client and core parser source; the client file is unchanged since 0.91.0. No\nmixed-version deployment was run.\n\n## History reader deletes after 0.86.0\n\nObserver and admin credentials now carry a `CONSUMER.DELETE` row for each of the connection's eight\nhistory-reader names, `hist_<readerId>_<n>`, on CHAT (and on DM for admin). `channelHistory`,\n`multiChannelHistory` and `dmHistory` read under those names and delete each reader when its step\nends. Before this, every read left its consumers for five minutes, and a dashboard polling history\nfilled the stream's consumer limit until reads were refused with \"maximum consumers limit reached\".\n\n### What stops working\n\nA static observer or admin credential minted by 0.86.0 or earlier has no reader-delete rows. On it,\na current client's history read fails with \"it predates the observer and admin reader-delete grant,\nso remint it\", and leaves one reader per stream until the broker's five-minute inactive threshold.\nA credential the CLI mints at start (`cotal console`, `cotal web` on a static auth mesh) is current\nafter the upgrade.\n\n### Before the upgrade\n\n- Remint every observer or admin credential a process reads from a file: `cotal auth mint <name> --profile\n observer` or `--profile admin`, and any `--creds` file given to a dashboard.\n- On a user-auth mesh, the auth service mints these views per connection. Upgrade it before, or with,\n the hosts that read history, so no current client connects through an older auth service.\n\n## Native key issuance recording after 0.85.0\n\nBefore upgrading, take a consistent backup of broker JetStream state and the auth host's private\nconfiguration and signing material through the deployment's supported backup procedure. The auth\nbucket must use file storage. Allow space for one issuance record per new callout exchange until\nits minted expiry plus clock tolerance; static credentials have no such record.\n\nEmbedders that call `startAuthCallout` must supply the required `recordIssuance` callback. It must\nfinish a durable create-only write for the broker-generated user key and recheck the validated\nlifecycle and credential authority before returning. A missing callback refuses startup; a failed\nwrite or recheck denies the exchange. Do not use the isolated tests' no-op callbacks in a deployment.\nThe stock `startAuthService` authority plane wires the recorder and its pruning automatically.\nExisting auth records need no manual rewrite, and no tenant or participant grant is added.\n\nFor split deployments, pause new managed launches and stop every old auth-callout responder before\nstarting the matched new auth service. A mixed responder pool can still issue keys without records.\nNew connections and reconnects cannot authenticate while the callout service is down. Existing\nbroker connections keep their current JWT expiry; this change needs no broker restart. Upgrade the\nCLI, Manager and connector packages together before admitting new managed launches, using the\nruntime's supported Manager custody or recovery path. This source change does not promise that\nrunning seats survive a Manager restart.\n\nThe new service prunes expired issuance rows before opening and on a timer. A corrupt row refuses\nopening or fences the running authority plane; investigate the stored evidence through the normal\noperator recovery procedure. Do not erase the auth bucket or recreate signing keys to clear it.\nRows written before a later authority-check failure can remain until pruning; they do not prove\nthat a client received a JWT or established a connection.\n\nConnections admitted before the last old responder stopped, static JWTs and old responders have no\nissuance record. They are not backfilled or identified from names. Bearers without current lifecycle\nand credential claims cannot be recorded and are refused; use the supported enrollment or renewal\npath for the selected release. These records alone do not provide persistent native-key cutoff or\ncomplete revocation. Reintroducing an old responder also reopens the recording gap.\n\n## Native Claude MCP discovery after 0.82.2\n\nBefore upgrading, snapshot Cotal configuration and the Claude profile on each launch host.\nOrdinary Claude spawns now load the host's native MCP sources alongside declared Cotal overrides.\nAn empty sharing map no longer suppresses native servers. Audit enabled servers and their\ncredentials before admitting new seats. Use an explicit sharing selection, including\n`--share-tools none`, to retain isolation. Existing sessions keep their loaded configuration.\n\nNo broker restart, credential-format migration or cross-host profile copy is required. Upgrade\nthe CLI, core, manager and connector together on each launch host. Pause new managed spawns\nwhile adopting the matched packages; use the runtime's supported manager custody or recovery\npath. Any interruption is that manager transition, not a change to the broker wire contract.\nCarried private homes stay isolated and do not import the operator's native profile.\n\n## Connector-store protection after 0.82.2\n\nSnapshot the private Cotal state, connector store and mesh configuration before upgrading.\nInstall the new CLI on each CLI and manager host, then verify the selected executable before\nrunning setup or store maintenance. The POSIX installer prints its absolute launcher because\nit cannot change the parent shell's command cache, aliases or functions. An older executable\nalready on disk remains unsafe for a newer store; do not use its force advice to recover.\n\nForward setup and connector seeding migrate built-ins automatically. Same-generation force and\nreset rebuilds remain supported. An executable older than the store now refuses every backward\nmigration, including force and reset. Select a verified executable at least as new as the store\nand run normal seeding instead. No credential or mesh-record format needs manual migration.\n\nA split deployment needs no broker restart for this change. Keep each host's CLI and connectors\non the same release. Pause new managed spawns while adopting it; a running manager keeps its\nloaded code until restarted. Use the chosen runtime's supported custody or preserve-state path.\nThe interruption covers that manager transition and depends on its runtime, not a broker outage.\nDo not overwrite a live manager installation or stop an unidentified listener to force adoption.\n\nAn implicit foreground spawn now chooses a numbered actor when the requested actor is occupied.\nExisting grants, launch material and sessions stay in place. An explicit name remains pinned\nand refuses an occupied actor rather than renaming it. Signed enrollments and managed handoffs\nkeep their existing identity semantics.\n\n## Background startup in 0.91.0\n\n`cotal up` now returns after background startup by default. `cotal up --detach` retains that\nmeaning. Use `cotal up --foreground` when terminal lifetime should own the native stack.\n\nA systemd unit, launchd KeepAlive job, container entrypoint or script that supervises the `up`\nprocess must add `--foreground` before upgrading.\n[Persisted callers](#persisted-callers-in-0910) gives the literal\nreplacement for each.\n\nA project folder an earlier release started keeps its own launcher after the upgrade. Stop it with\n`cotal down` as usual, upgrade, then run plain `cotal up` in that folder: it starts on the retained\nlauncher with the same identity, store and history. It does not need `space create` or `space add`.\n\nRecorded recovery requests retain their original foreground/background intent. They do not\nreinterpret a prior request using the new default. This change does not migrate a whole stack to\nowned services.\n\nPrepared plain-loopback user-auth roots can use the controller's broker, auth service, delivery and\nmanager members. First enable may return a known partial result after the first three start: the\nmanager needs an existing signed-in, granted CLI login. Sign in and obtain that grant, then run\nbare `cotal up`; no static caller or new grant is substituted. Auth endpoints come from the\ncreation-pinned child's native provider readiness, not a guessed URL. The controller keeps unknown\noperations blocked. Existing three-member auth/open journals are read with an unattempted auth row\nand written with all four roles; old clients do not understand that new record shape. Upgrade the\ncomposed CLI together before using it. TLS-required, remote and public-exchange user-auth paths\nretain their native route.\n\n## Persisted callers in 0.91.0\n\nSome command lines outlive the install that wrote them: OS units, scripts and container commands.\nAfter `npm i -g cotal-ai@<new>` they reach the new binary before any `cotal` command has run. This\nsection says what each one does after the upgrade and what to change before it.\n\n### Command mapping\n\n| Earlier command | Command in 0.91.0 |\n|---|---|\n| `meshes`, `meshes add` | `space`, `space add <name>` |\n| `clean store`, `clean all` | `reset store <space>`, `reset space <space>` |\n| `mint <name>` | `auth mint <name>` |\n| `mint --signer` | `auth signer --out <private-file>` |\n| `actor list`, `actor grant <actor>`, `actor revoke <actor>` | `auth grants`, `auth grant <actor>`, `auth revoke <actor>` |\n| `join [channel]` | `chat [channel]` |\n| `console` | `watch` |\n| `channels` | `channel` |\n| `send dm <agent>`, `send msg <channel>`, `send ask <role>` | `send -t <agent>`, `send -c <channel>`, `send -r <role>` |\n| `personas` | `persona` |\n| `endpoints`, `describe`, `invoke` | `endpoint`, `endpoint describe <target>`, `endpoint invoke <target> <command>` |\n| `deregister-instance` | `endpoint deregister <class> <instance-id>` |\n| `reconcile-gate`, `seats` | `manager reconcile`, `manager seats` |\n| `topology view` | `team check <file> --graph` |\n| `down -f <manifest>` | `team remove <deployment-id>` |\n| `status --components` | `service` |\n| `run journal`, `run migrate`, `run ps` | `run show`, `run check`, `run` |\n| `deliver pending <name>` | `pending <agent>` |\n| `web` | `service add <name> --preset web`, then `service start <name>` |\n| The four daemon entries | Their service presets, under [deprecated daemon entries](#deprecated-daemon-entries) |\n\nThe sections on this page for each command family give the details of its change.\n\n### What stops working\n\n`cotal up` returns after background startup by default. An earlier line such as\n`cotal up --space <space> --server <url>` that a supervisor runs as its main process now exits\nonce the stack is started. systemd then treats a `Type=simple` unit as finished and can stop the\nprocesses that unit started, and a restart policy starts another attempt. A launchd `KeepAlive`\njob relaunches it the same way. Add `--foreground` before upgrading. For a systemd unit, replace\n`ExecStart=/usr/bin/cotal up --space <space> --server <url>` with:\n\n```ini\n[Service]\nType=simple\nExecStart=/usr/bin/cotal up --foreground --space <space> --server <url>\n```\n\nFor a launchd job, add `--foreground` after `up` in `ProgramArguments`:\n\n```xml\n<key>ProgramArguments</key>\n<array>\n <string>/usr/local/bin/cotal</string>\n <string>up</string>\n <string>--foreground</string>\n <string>--space</string>\n <string>main</string>\n <string>--server</string>\n <string>nats://127.0.0.1:4222</string>\n</array>\n```\n\nA container command or script makes the same change to its `up` line. When background `up` is the\nmain process of a systemd unit, it prints a warning naming the unit and the `--foreground` remedy\nafter it starts or refreshes the stack. `up -f`, which has no foreground form, and a dry run print\nnothing. A launchd job gets no warning. `cotal up --detach` keeps its meaning. The warning changes\nno mode and no unit, and no unit or script is rewritten or restarted.\n\n`cotal spawn <name>` now launches through a manager and attaches. A container or script that ran\nan agent directly with `spawn <name>` adds `--foreground`.\n\n### Deprecated daemon entries\n\n`supervise`, `deliver`, `auth-service` and `feedback-intake` stay accepted with their existing\nflags for every caller: units Cotal installed, units and scripts you wrote, and container commands.\nThey are deprecated and are removed in `1.0.0`, one release for all four. Help and completion no\nlonger list them. Each run prints one line on stderr naming its replacement and that release.\nStdout, behavior and exit codes are unchanged, so no unit restarts in a loop because of the\nupgrade. `deliver pending` warns the same way and names `cotal pending <agent>`.\n\n| Entry | Replacement |\n|---|---|\n| `supervise` | `cotal service add <name> --preset manager` |\n| `deliver` | `cotal service add <name> --preset delivery` |\n| `auth-service` | `cotal service add <name> --preset auth-service` |\n| `feedback-intake` | `cotal service add <name> --preset feedback-intake` |\n\n`cotal service add <name> --preset <preset> --help` lists the options a preset takes, and\n`cotal service start <name>` starts it. A `supervise --roster <file>` line maps to the manager\npreset's `--roster <file>`, with an absolute path, and `cotal service start <name> --foreground`\nkeeps the manager in the foreground as `supervise` did. The manager, delivery daemon and auth service that `up`\nstarts itself do not warn, so the line in a log always comes from a unit, script or container you\ncan change.\n\n### Owned units\n\nA manager unit that an earlier `cotal service install` wrote keeps calling `supervise`. After the\nupgrade it starts the new binary and prints the deprecation warning on each start. This release\ndoes not rewrite that unit.\n\nA unit that this release's `cotal service install <name>` writes runs the hidden `__service-unit`\nentry, which an earlier binary does not have. Before you downgrade, stop it with\n`cotal service stop <name>` and remove it with `cotal service uninstall <name>`. After the\ndowngrade, install the unit again with the earlier binary's `service install`.\n\n### Containers\n\nThe `solo` service in `deploy/docker/compose.yaml` runs `spawn scout --foreground`. The team\nservice and the image's default command still run `supervise` with `--roster`, so their logs show\nthe deprecation warning. An image you already built carries its own CLI and is unaffected until\nyou rebuild it.\n\n### Before the upgrade\n\n- Add `--foreground` to every unit, launchd job, container command or script that supervises\n `cotal up`.\n- Add `--foreground` to every container command or script that runs `cotal spawn <name>` directly.\n- Move units and scripts that call `supervise`, `deliver`, `auth-service` or `feedback-intake` to\n their service presets before `1.0.0`.\n- Before a downgrade, stop and uninstall every unit this release's `service install` wrote.\n\n## Team commands in 0.91.0\n\nThe new grammar names fresh intent `team boot <file>` and additive intent\n`team deploy <file>`. `team check <file> --graph` resolves declared access\nwithout a broker. `team` and `team show <id>` read local fresh summaries, additive ledgers and\nagent/channel returned receipts; `team remove <full-id>` is the ID-only additive\nteardown name. An edited manifest is not authority to remove an old deployment.\n\nBoot's `--dry-run` is a declaration view.\nDeploy's `--dry-run` reads native presence and channel registry for advisory\nclassification against a prior ledger. Remove's preview compares a legacy\nledger with native manager PS and display-only membership observations, or a\nreturned channel-only receipt with current KV revisions. No preview authorizes\na launch or stop. Bare `team boot <file>` enters guarded native fresh\nstartup only for a prepared, stopped local root; it refuses a leftover manager\nrecord and offers rich, JSON and plain receipts. User-auth boot records an intent before\nowner/trust preparation and retains uncertainty without retry. Windows boot\napply refuses, because protected service-record inventory is not supported there.\nBare `team deploy` applies all-new channel-only manifests with explicit live\ndelivery, or one to 100 all-new agents on a static/open mesh, optionally with\nup to 100 new live channel cards. A mixed deployment records one intent before\ncreating channels and launching agents through one pinned manager/runtime.\nEach channel revision and original launch request, allocation and matched join\nis recorded before continuing. A full receipt requires both sequences to match.\nUnknown effects are retained, not replayed; completed effects are not rolled back.\nBoth routes offer rich, JSON and plain output. Identical settled deployments can return a verified\nno-op without new effects. Named stale-agent waivers retain the old policy without restarting.\nUser-auth agent apply and existing-card mutation still refuse effects.\n\n`team remove <id>` can retire a returned static agent deployment through its original manager.\nReview `--dry-run` first; scripts require `--yes`. This needs the native slot query, original UIDs\nand native retired slot/head plus cleanup evidence, not just stop acknowledgements. A returned\nchannel-only deployment instead uses deletes conditioned on its original card revisions. A static\nmixed deployment retires agents first and needs a complete retirement receipt before checking and\ndeleting cards, under one confirmation and maintenance lock. Both receipts must exist before its\nblocker is released. Partial and unknown effects retain their journals without replay. History,\ngrants and readers are unchanged. Open/user-auth agent retirement and legacy removal still refuse.\nThe old `spawn -f` manifest spelling no longer deploys. Native `up -f` still\nstarts a fresh mesh; `down -f <file> --run <id>` remains the retained route\nonly for an older additive ledger. Move an automated agent deploy or teardown\nto the new spelling only when its auth mode and manifest are ones these routes accept.\n\n## Credential commands in 0.91.0\n\nCredential minting is `cotal auth mint <name>`. The old `cotal mint` spelling refuses before\nissuance and names its replacement. A stripped account signer is exported with the explicit\n`cotal auth signer --out <private-file>`, not `mint --signer`.\n\nThe default standing credential label is create-only. Repeated minting does not silently replace\nit. Choose a new label or a separate private output. An explicit existing output is replaced only\nwhen it is an owned private native credential matching the supplied `--identity`. Explicit\nexports cannot overwrite managed `.cotal` or workstation state.\n\nThe new receipt reports the actual principal, resulting lifecycle, private destination and\nnative expiry without credentials. Reusing an nkey does not promise reuse of its old lifecycle\nor bind-only inboxes. An agent-profile re-mint reports its newly allocated UID and warns about\nthat distinction; consuming clients must use the returned UID. Provisioning is explicit and is\nnot evidence that a client joined. Scripts should parse the finite `--json` document rather than\nscraping the former text lines. Secret and signer files must stay out of support bundles.\n\n`auth mint <name> --endpoint <class> --out <private-file>` is a separate caller mode. It uses the\nsame native static authority and a live digest-verified untargeted synchronous ephemeral surface.\nTargeted, journaled or action commands refuse the whole issuance; no unsupported row is dropped.\nThe native agent footprint and exact additional endpoint rows are provisioned with required TLS,\nwithout adding spawn/run/admin scope or starting a seat. `--channels` enters both explicit channel\nACLs and defaults empty; `--expires-in` remains native seconds.\n\nThe create-only private `<out>.issuance.json` intent is retained before footprint work and contains\npublic identity/lifecycle/scope metadata only. It is not a native grant or completion receipt. An\nexisting output or intent blocks replay, including when an interrupted operation left no output.\nKeep that intent with any original file while diagnosing an unknown result. Provisioner close is\nreported separately. No raw-file caller identity, managed launch, user-auth issuer or environment-\nexport onboarding is added by writing this credential.\n\n## Endpoint deregistration in 0.91.0\n\nReplace `deregister-instance --endpoint <class> --instance <id>` with\n`endpoint deregister <class> <full-instance-id>`. Both positional values are required.\nThe old spelling refuses before effects and has no implicit instance selection. Review the\nnew `--dry-run` output, then use `--yes` in scripts. JSON retains native revision receipts;\nunknown or superseded deletions are not retried. User-auth and open targets do not receive\na static-signing fallback.\n\n## DM pending inspection in 0.91.0\n\nUse `cotal pending <agent>` for a one-document JSON/plain read of native DM\ndurable counts and bounded candidate IDs. `--durable <exact-dm-durable>` targets\nan older lifecycle directly. The retained `deliver pending` spelling still\nuses its earlier output shape; scripts must migrate to `result` fields rather\nthan assuming the old per-line format. Candidate IDs are not proof of\nrecipient delivery or complete message history. This route has only the\nstatic/open admin read authority; user-auth has no corresponding view yet.\n\n## Live space deletion in 0.91.0\n\n`space forget <name>` changes only the local registration. It does not delete\nlive broker state. `space delete <name> --dry-run` now reads known first-party\nnative streams, bucket backing streams and presence entries on the registered\nbroker in open or static-auth mode. Static transfer-stream names are read under\nan additional exact-name grant on a second confirmed connection. This non-atomic\nread is not an apply plan or evidence that presence records are live. User-auth\ninspection and deletion remain unavailable. Bare deletion now requires a typed\nexact-name confirmation on a terminal or the exact positional name with `--yes`\nin a script. It rereads the native impact and checks the current target before\nsubmitting exact stream deletes, stopping on the first refused or unconfirmed\nreply with acknowledged and remaining names. A native post-read reports any\nsurvivor or newly observed transfer target without retrying. No process, registration or local\nfile is removed.\n\n## Stopped reset in 0.91.0\n\n`reset store <space>` and `reset space <space>` are the named replacements for\n`clean store` and `clean all`. `reset store --dry-run` provides a partial\nlocal-impact preview of the store path, recorded processes, maintenance journal\nand broker credless reachability. `reset store` requires confirmation,\nacquires the maintenance lock, rechecks its exact store identity, legacy\nprocesses and all recorded service instances for that space, and calls the\nexisting store-only cleanup. Any recorded supervisor blocks reset. It keeps trust and the\nregistration. TLS-required or TLS-first store targets refuse, because no trusted\nstopped-broker check covers them. `reset space` now routes a\nreviewed local identity reset through native clean-all. It requires Linux, an\nisolated locally started registration and plain-NATS loopback nonresponse.\nIts preview inventories provider-declared keys and native filesystem targets;\napply rechecks the scope under the maintenance lock and constrains each delete\nto that reviewed inventory. Trust is removed last, followed by the unchanged\neligible registration. Unknown effects are not replayed. Service definitions,\npersonas, packages, IdP logins and remote data stay. Unsupported providers,\nforeign material, TLS-first and non-Linux targets refuse.\n\n## Backup maintenance in 0.91.0\n\n`backup create` supports one-document `--json` and plain reports. The completion receipt follows\nnative attempt cleanup. A published artifact with unconfirmed cleanup is partial; retain its\npath and original attempt instead of repeating creation. `backup inspect` reads metadata and\n`backup verify` checks declared file sizes and hashes. Neither proves restoration or authority\ncontinuity, starts a broker or writes a staging tree.\n\nThe canonical stale pre-commit recovery is `backup recover <full-attempt-id>`. Healthy retained\nold-source cleanup is `backup cleanup-fallback <full-attempt-id>`. Review `--dry-run`, then use\n`--yes` in scripts. Neither flag bypasses owner, deadline, root, journal revision or store-identity\nchecks. Restore startup stays on its guarded existing route.\n\nA controller-owned stack cannot be preserved in 0.91.0. On a root with a stack journal,\n`down --preserve-state`, `backup create`, `backup restore` and `up --restore` refuse `UNSUPPORTED`\nbefore any effect. Stop such a stack with plain `cotal down`; it keeps its store, channels and\nhistory for the next `cotal up`. Preserve, backup and restore of a controller-owned stack return in\na later release. A root without a stack journal keeps its preserve, backup and restore.\n\n## Feedback input in 0.91.0\n\nUse a protected `feedback --key-file <path>` rather than placing an intake key on argv.\nThe old `--key` spelling refuses without sending or echoing its value. Public feedback still\nworks with a contact email and requires no environment setup. JSON/plain output is one public\nreceipt, not a copy of the submitted body. Unknown submission outcomes are not retried, and no\nredirect or alternate credential/intake is substituted.\n\n## Credential diagnosis in 0.91.0\n\n`doctor auth` uses JSON/plain reports and distinguishes local claims from live authorization.\nA plain diagnostic that encounters material requiring migration refuses without moving it.\nExplicit `--fix` retains the native static daemon repair, with separate migration/re-sign receipts\nand no user-auth signer fallback. A local re-sign is not daemon adoption or broker acceptance.\nUnhealthy, partial and unknown results retain their classified outcomes; scripts must read the\nnew result and error fields instead of scraping the old `auth: healthy` line. Never respawn from\na locally ordered older credential file alone.\n\n## Service adapter declarations in 0.91.0\n\nA local-process provider can expose the version-1 workstation service ABI alongside its existing\nprocess metadata. It declares a closed option schema, explicit foreground/multi-instance/log\ncapabilities and launch/readiness hooks. Re-add an extension after adding this ABI so its cached\nservice metadata and package fingerprints are recorded. An older descriptor is not imported to\nguess its schema. Plain process definitions keep their existing meaning.\n\nPresets are declarative registry contributions. Their option flags, help, completion and guided\nquestions derive from the adapter schema. Re-add a preset provider to populate its new cache;\nmissing or changed metadata refuses without importing code to guess the definition.\n\nThe top-level `feedback-intake` launcher is a [deprecated entry](#deprecated-daemon-entries). For a\nnew intake, save a definition with\n`service add <name> --preset feedback-intake --space <s> --root <absolute-root> --keys <private-keys.json> --creds <private-creds>`, then use `service start <name>` in that space. An explicit\n`--store` must be an existing private file. The default is private and instance-keyed. No broker\nor other service is started, no signer is minted and required TLS is preserved. Native startup\nreceipts are private, request-specific observations rather than native endpoint serving claims.\n\nA service instance pins its adapter name, package version and declaration fingerprint. A later\npackage update does not silently adopt a running or retained instance under a different adapter.\nStop the exact owned process and explicitly remove its stopped records before choosing changed\nadapter metadata. Logs remain private local files. Extension removal refuses live, foreign or\nunresolved service instances, including records outside the current mesh selection. None of\nthese declarations migrate legacy PID records or supply OS-unit ownership. Native manager\nimpact comes from its explicit adapter's attributed current read, not cached metadata.\n\nThe delivery implementation now registers its service adapter and legacy custody descriptor.\nCustom composition roots that want those inventory/control rows import `@cotal-ai/delivery`.\nThe full binary already composes it. CLI stop helpers share the workspace descriptor without an\nimplementation-to-implementation import. The `delivery` preset requires the recorded root and\nexisting scoped credential, keeps the native store/account/lease guards and does not adopt an\nexisting legacy daemon. Its hidden child entry is recognized by the same process-role predicate.\nThe old `deliver` machine-facing route remains for the stack callers that still use it.\n\nFresh static-auth and open `team boot` now use controller-owned stack members and accept\n`--foreground`. Their manager receives a digest-pinned manifest launch spec, with no additive\nownership ledger. A recorded stack can resume only after a completed whole-stack stop with its\nmanager never attempted, recorded broker inputs and each member's settled native evidence. Legacy\ncustody, prior team boots, supervisors and unresolved attempts still refuse. User-auth preparation\nuses ordinary `up --user-auth`, login and an explicit grant, then `down` before `team boot`. No\ncaller or owner grant is substituted.\n\nNew controller journals record the broker store path, storage cap request and bind host. A later\npreparation keeps those inputs and refuses differing explicit values. Older journals remain\nreadable. If their broker is stopped, restart requires the original store named with `--store-dir`\nand the original `--max-file-store` and `--host` values, if set. The store must already be a real\ndirectory. Do not omit the cap or host unless the original start set neither. That restart records\nthe inputs; run `down` before the stopped-stack team route. No store path is inferred from an\nabsent field, and bare restart never silently opens a default empty store.\n\nThe native manager status contract adds optional caller-scoped stop-impact metadata and advances\nthe cluster revision. Upgrade the composed manager and its compatible registration host together;\na host that does not accept the current contract must refuse registration. Older managers can\nomit the observation. Component status then says impact was not reported, rather than inferring\nthat seats survive. An observed policy does not migrate PID records, reserve a stop or confirm\ncleanup. The manager package adds a separate controller-only `manager-service` adapter and\n`manager` preset. Existing legacy manager records are not migrated or adopted. Update the\ncomposed packages and extension metadata together; an old cached declaration is not rewritten\nbehind a retained instance. Native stops require a fresh full-scope read and unchanged request-bound\npolicy, with native returned evidence plus original process exit. Missing terminal evidence retains\nits reservation and blocks replacement, without force kill. Cleanup remains not checked.\nThe direct service route currently refuses foreground mode. On an open mesh there is no\nauthenticated caller, so readiness and stop impact come from the pinned child's own request-bound\nlocal receipt. Its registration must equal the launch identity bound to that PID. Status shows it\nas a child-reported local receipt, not an attributed endpoint response. OS installation and\nnon-Linux protected process support are not available.\nRecorded systemd user units with an original invocation ID use that invocation's native journal\nfor finite/follow logs. Missing invocation evidence refuses instead of reading a stale direct-child\nfile. Native cursors and timestamps are retained; journal retention/access is not proof of complete\noutput or completed service work. No unit install/control support is implied by this read route.\n\nAuth providers may implement the optional read-only `preflightServer` hook. It checks public\nserver inputs without creating a directory, pin, key, grant or daemon. The controller's user-auth\nroute requires it before preparation; providers without it refuse rather than skipping validation.\n`prepareServer` remains authoritative and checks again before provisioning.\n\nThe auth package also declares an `auth-service` preset over its prepared native key projection.\nIt does not migrate or reclaim existing PID records. Its real child claims custody before opening\nplanes and publishes a private startup phase only after native initialization and signal handlers.\nThis route declares shared foreground control and still refuses remote broker and TLS-required\ntargets. The transport refusal is\nintentional, because not every native authority connection carries the registered transport\nrequirement. No plaintext substitution or new issuer is used.\n\nManager maintenance now supports finite JSON/plain reports. Gate repair and seat drain default\nto cancellation in a terminal and require `--yes` in scripts. Use `manager reconcile --dry-run`\nor `manager seats --drain --dry-run` first. Gate repair requires one registered root/space/broker\nand carries its TLS requirement through native liveness/eviction. It does not dial a default broker\nwhen that target is missing. An interrupted submitted repair or unresolved drain can return exit 3;\nconfirmed native results remain distinct from unconfirmed effects. No process is started.\n\n## Linear reports in 0.91.0\n\nUpdate the installed Linear extension with the matching CLI/workspace generation. Account add/show,\ninventory and direct call/resource/prompt JSON now use version-1 envelopes: read the native\ninventory from `result.inventory` and a native request reply from `result.reply`. Rich/plain reports\nbound displayed rows and lines without truncating JSON. MCP tool `isError` remains a result with\nexit 0 and a warning, not a work-success claim. Native not-executed refusals stay distinct from\nunknown submissions, and neither is replayed automatically.\n\nThe `linear` service preset runs the same native endpoint implementation from an explicit static-auth\nroot and existing account. It retains native registration, renewal and bounded deregistration; no\nnew generic caller is issued. Its private phase proves handler initialization only, not a current\nendpoint reply or completed deregistration. Foreground service start uses the shared terminal\nlifetime and native signal handlers, not the old serve parser. User-auth/open targets and\nOS-supervised service operation refuse. The older `linear serve` route remains available for\ninstalled callers.\n\nA bare Linear/account group opens no account. Wrong-mode flags or extra operands refuse before\naccount access. Interactive OAuth remains a human flow without JSON. Foreground serve stays on its native human-output path.\nStatic caller issuance keeps its native authority and now returns shared JSON/plain private-file\nreceipts. Use an existing protected physical parent outside managed state; an existing output\nrefuses before provisioning. Uncertain provisioning/publication is not replayed and no environment-\nexport launch recipe is printed. The supported `auth mint --endpoint` form resolves its scope live;\ntargeted/journaled/action issuance and consuming launch adapters are not supported. These changes do\nnot replace ownership, start a service or print secrets.\n\n## Setup inputs in 0.91.0\n\n`cotal setup` takes four inputs: destination, application, project folder and space. Nothing on a\nrunning mesh changes, and there is no outage window.\n\n### What stops working\n\n- A `cotal setup --yes` or a setup with no terminal exits 1 with `USAGE_INVALID` unless it names\n every input as a flag. It no longer sets up every detected connector and seeds the folder it\n runs in.\n- Setup sets up one application, the one `--harness` names, and records it as the space's\n `harness` preference. An application whose executable is not on PATH exits 4 with the rest\n saved; it used to be skipped with a warning.\n- The default persona lands in `<project>/.cotal/agents`. Setup no longer looks for a running mesh\n to seed into.\n- A repeat `cotal setup` no longer prints a status card. `cotal status` reports the same state.\n\n### Before the upgrade\n\nA script or image that ran `cotal setup --yes` names the four inputs:\n\n```bash\ncotal setup --destination local --harness claude --project <dir> --space <name> --no-input\n```\n\nFor a space that runs elsewhere, register it first with `cotal space add`, or pass the bundle its\noperator exported with `--connection-file`, and use `--destination self-hosted` with the\nregistration's root as `--project`.\n\nStartup remains opt-in. `--start` conflicts with `--configure-only`; a script, JSON or plain\ninvocation must add `--detach`. `--yes` by itself still saves configuration only. Startup failure\nleaves completed preparation in the result. Do not treat an unknown startup result as permission\nto submit another launch; inspect the selected space and recorded original request first.\n\n## Reads that install nothing in 0.91.0\n\nA read no longer installs or refreshes the built-in connectors and the web dashboard before it\nruns. Nothing on a running mesh changes, and there is no outage window.\n\n### What stops working\n\n- On a fresh machine, `cotal status`, `cotal ext list`, `cotal ps`, `cotal runtimes`, `cotal docs`\n and the other reads leave the extension root empty. `cotal setup`, or any command that is not a\n read, installs the built-ins as before.\n- After an upgrade, a read does not refresh the built-ins. Where it would load one still at the\n older version, such as a connector's setup row in `cotal status`, it names the package and its\n version with `cotal ext seed` instead of loading it.\n\n### Before the upgrade\n\nA script that ran a read to install the built-ins, such as `cotal ext list` right after\n`npm i -g cotal-ai`, runs `cotal ext seed` instead.\n\n## Space selection in 0.91.0\n\n`cotal use` and the target a command picks without `--space` change. Nothing on a running mesh\nchanges, and there is no outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal use <space>` no longer checks the broker. It selects any registered space, including a\n stopped one, and the command that targets it reports the space not up. A name that is not\n registered exits 2 with `TARGET_NOT_FOUND` instead of 1. Bare `cotal use` reports the selection\n instead of exiting 1 with its usage, and `--json` writes one envelope.\n- A command run without `--space` stays on the selected space when its broker is down and refuses\n with `TARGET_UNAVAILABLE`, exit 2. It used to fall back to the one other running mesh and print\n a note that the default mesh was down. A selection that names a space no longer registered now\n refuses with `TARGET_NOT_FOUND` instead of falling back to the local project or the one running\n mesh.\n- `cotal space` lists the registered spaces. `cotal meshes` still lists them, and its `--json`\n lines keep their shape.\n\n### Before the upgrade\n\nA script that relied on the fallback names the space it means with `--space`, or selects it with\n`cotal use <space>`. A script that read `cotal use` exiting 1 for a stopped space checks the\nbroker with `cotal space show <space> --json` and reads `result.check`.\n\n## Extension listing output in 0.91.0\n\n`cotal ext --json` and `cotal ext list --json` now write one JSON document instead of one JSON line\nper extension. Nothing on a running mesh changes, and there is no outage window. Upgrading the\n`cotal` binary is the whole operation.\n\n### What stops working\n\n- A script that reads `cotal ext list --json` line by line reads one envelope instead. The rows it\n read are now `result.items`, each with the same fields, and `result.root` is the install prefix.\n With `jq`, `cotal ext list --json | jq -c '.result.items[]'` prints the rows as before. An empty\n prefix writes an envelope with no items instead of nothing.\n- A usage error from `cotal ext` exits 1 as one `refused` envelope under `--json`, and `--plain` or\n `--json` on `ext add`, `ext remove`, `ext root` or `ext seed` is refused. `cotal ext root` still\n prints the path alone.\n- The human listing is a table now. Human output is not a parsing target; use `--json`.\n\n### Before the upgrade\n\nSearch scripts for `cotal ext list --json` and `cotal ext --json`, and change line-by-line reads to\nread `result.items` from the one document.\n\n## History reads in 0.91.0\n\n`cotal history [channel]` reads retained messages. Clearing is now `cotal history clear`, which asks\nin a terminal and takes `--yes` in a script.\nThis changes the CLI grammar only. Stored messages, credentials, agents and running services do not\nmigrate or restart, and there is no outage window. Upgrade the CLI and its first-party components\ntogether through the normal update path; a separate broker or manager can stay running.\n\n### What stops working\n\n- Replace a script's `cotal history --force` with `cotal history clear --yes`. `--force` is refused\n on every `history` form, `history clear` included, rather than clearing anything. Add `--dms` to\n the explicit clear command only when the purge should include direct messages and the identity\n holds that authority.\n- `history clear` accepts no channel argument, read filters or `--follow`. It purges every channel\n in the selected space; these inputs are refused rather than treated as a selective purge.\n- Bare `history` reads channels within the caller's read grant. It never clears stored messages.\n Use `--json` and `result.items` for scripts; the human report is not a parsing target.\n- A user-auth history read uses the connection's channel grant. DM audit history and stream-sequence\n cursors are not available through that ordinary human credential. Use a time with `--since`.\n\n### Before the upgrade\n\nFind scripts that pass `--force` to `history` and change their explicit purge invocation. No stored\nstate or credential backup is needed for this CLI-only transition. Keep the deployment's usual\nbackup procedure before an intentional history purge; reads do not make a backup or restore data\nthat retention has already removed.\n\n## Status output in 0.91.0\n\n`cotal status` writes one structured document with `--json`, takes `--plain` and `--all`, and exits by\nthe structured outcome table. Nothing on a running mesh changes, and there is no outage window.\nUpgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal status` exits 4 when it could not read part of what it reports, such as a PID record, the\n folder's trust material, a persona catalog, the mesh registry or one of its records, or a user-auth login. It exited\n 0. An unreachable broker, a stopped process\n or an unbound responder still exits 0.\n- The recorded meshes list checks only the selected mesh and reads `not checked` for the others. Pass\n `--all` to check every one.\n- The report no longer lists the roster or prints a `cotal status` title line, and its standalone\n hints moved to a `Hints` section at the end. Rows are labeled `name: value`. Human output is not a\n parsing target; use `--json`.\n- `--components` is refused with `--json`, `--plain` or `--all`. Its exit, `0` to `3` by component\n health, replaces the report's, so it does not exit 4 for a partial report.\n\n### Before the upgrade\n\nSearch scripts and monitors for `cotal status`. A check that treats any nonzero exit of bare\n`status` as an outage should treat 4 as a report with unreadable parts. A script that read the\nroster from `status` should use `cotal ps` for managed seats.\n\n## Read command output in 0.91.0\n\n`cotal ps --json` now writes one JSON document instead of one JSON line per seat, and `ps`, `models`\nand `runtimes` exit by the structured outcome table. Nothing on a running mesh changes, and there is\nno outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- A script that reads `cotal ps --json` line by line reads one envelope instead. The rows it read\n are now `result.items`, each still the manager's row unchanged, and `result.managers` names each\n manager instance and whether it answered. With `jq`, `cotal ps --json | jq -c '.result.items[]'`\n prints the rows as before. Instance headers no longer go to stderr under `--json`.\n- An incomplete manager census, where a registered manager did not answer, refused or serves a\n different contract, exits 4 instead of 1.\n- A mesh that `ps` or `models` cannot resolve or reach exits 2 instead of 1, and a space catalog\n refresh that `ps` or `models` needed and could not get exits 2 instead of 1. Under `--json` each is\n one `refused` envelope whose `error.code` names the reason.\n- `cotal models` exits 4 when a connector's catalog could not be read, and `cotal runtimes` exits 4\n when an installed runtime provider fails to load. Both exited 0. `cotal models --agent <name>`\n exits 5 when that one catalog could not be read. OpenCode 2.x, which has no catalog the CLI can\n list, is reported `supported: false` with the reason in `unavailable` and does not fail the read.\n- The human reports of `ps`, `models` and `runtimes` are tables now, and `ps --wide` prints one\n labeled record per seat instead of a facts line under each row. Human output is not a parsing\n target; use `--json`.\n\n### Before the upgrade\n\nSearch scripts for `cotal ps --json`, and for checks of the exit status of `ps`, `models` or\n`runtimes`. Change line-by-line reads of `ps --json` to read `result.items` from the one document.\n\n## Shell completion script in 0.91.0\n\n`cotal completion <shell>` is now `cotal completion script <shell>`, and bare `cotal completion`\nreports which shells have the stub instead of printing usage. Nothing on a running mesh changes, and\nthere is no outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\nA startup line written by hand that runs the old form, such as `source <(cotal completion bash)` or\nthe PowerShell profile line `cotal completion powershell | Out-String | Invoke-Expression`, now\nloads no completion. The command exits 1, prints nothing on stdout, so the shell sources nothing,\nand names the replacement on stderr. Replace the line with its new form:\n\n```bash\nsource <(cotal completion script bash) # bash\nsource <(cotal completion script zsh) # zsh\ncotal completion script fish | source # fish\ncotal completion script powershell | Out-String | Invoke-Expression # PowerShell $PROFILE\n```\n\nA cached stub from an earlier release keeps calling the hidden `__complete` entry. The new ownership-aware\ninstaller does not adopt it just because its text matches a generated script. Review unrecorded\nstubs and custom startup wiring manually before replacing them. Nothing automatically rewrites\na hand-edited startup file. On Linux and macOS, `completion install powershell` asks the\ninstalled shell for its current-user all-hosts profile before adding an owned block;\nit does not adopt an existing manual line. Windows profile editing still requires a\nmanual review and the printed script.\n\nNew installs record their stub hash and startup block. Reinstall and update refresh unchanged\nrecorded stubs at the same path. Update adds no startup block and does not restore a stub the user\nremoved. Uninstall removes only unchanged recorded material. Modified or unowned material is\npreserved with a diagnosis. Reopening the shell is necessary to stop using functions already\nloaded in its parent process.\n\n## CLI command resolution in 0.91.0\n\nThe CLI now resolves and validates a command before it seeds connectors or writes configuration.\nNothing on a running mesh changes. Brokers, managers, seats, credentials and mesh records need no\nmigration and no restart, and there is no outage window. Upgrading the `cotal` binary is the whole\noperation. What changes is what two kinds of command line do.\n\n### What stops working\n\n- `cotal -v` no longer prints the version. It exits 1 and names `cotal version` and `cotal -V`. A\n script that reads the version this way must use `cotal version`, `cotal --version` or `cotal -V`.\n- The words `sp`, `a`, `s`, `r`, `x` and `h` are now aliases of built-in commands (`spawn`,\n `attach`, `status`, `run`, `ext` and `history`). An installed extension that provides a command\n under one of those words still loads, but the built-in wins and every run prints a warning naming\n the extension. Remove or update that extension to clear it. `cotal ext add` refuses a new\n extension whose name or alias is already claimed.\n\n### Before the upgrade\n\nSearch scripts and shell aliases for `cotal -v`, and run `cotal ext list` to see whether an\ninstalled extension provides one of the words above. There is nothing to snapshot. The extension\nmanifest an older binary wrote is read as before.\n\n## Messaging commands in 0.91.0\n\n`cotal channels` is now `cotal channel`, and `cotal send` names its destination with a flag instead\nof a `dm`, `msg` or `ask` word. Nothing on a running mesh changes, and there is no outage window.\nUpgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal channels`, with or without `list`, `set` or `default`, exits 1 as an unknown command. Bare\n `cotal channel` lists the channels, `cotal channel set <name>` saves one channel's settings, and\n `cotal channel defaults` reads or updates the space-wide defaults.\n- `cotal send dm <agent> \"<text>\"`, `cotal send msg <channel> \"<text>\"` and\n `cotal send ask <role> \"<text>\"` exit 1 with `USAGE_INVALID`, because they name no destination\n flag. Use `cotal send -t <agent> \"<text>\"`, `cotal send -c <channel> \"<text>\"` and\n `cotal send -r <role> \"<text>\"`.\n\n### Before the upgrade\n\nSearch scripts and shell aliases for `cotal channels` and `cotal send`, and change each line to the\nnew form above.\n\n## Managed Claude workspace trust in 0.80.0\n\nBefore upgrading, snapshot the manager's private Cotal state and Claude configuration home.\nPause new managed spawns while upgrading the connector, core, manager and chosen terminal\nruntime together. For tmux, cmux, Herdr, Orca and custodial Linux PTY seats, use that runtime's\nsupported manager-reload path and observe its custody/release bounds. Default `auto` selects\nin-process `pty`, whose seats cannot survive a manager restart. Drain those seats at an approved\nidle window, or use the documented preserve-state cut and `up` recovery, which resumes them\nrather than keeping their processes running. This upgrade does not require a broker restart.\n\nManaged launches now accept the authorized workspace through Claude's native startup dialog. They\nno longer require a prior interactive visit to that directory. Claude retains its normal\nrepository and linked-worktree trust semantics, including any trust it persists for later\nsessions. Foreground launches still prompt. No tool-permission bypass flag is added, but\naccepting workspace trust enables that checkout's hooks and settings, including its default\npermission mode unless a launch flag overrides it. Authorize only reviewed workspaces.\n\nThe launch recipe includes optional startup choices before its final confirmation. A custom\nruntime must implement `LaunchSpec.confirmBefore`, or refuse a recipe carrying it. Ignoring\nit leaves an untrusted launch at the native dialog until startup times out. The new launch\nrecipe does not require restarting an existing seat; manager reload continuity remains\nruntime-dependent as described above. Adopt the new runtime before launching new seats.\nNo credentials, mesh records or operator configuration files need manual migration.\n\n## AG-UI `takeCodePoints` export in 0.79.0\n\n`@cotal-ai/connector-core` no longer exports `takeCodePoints`. The AG-UI preview splitter is its\nonly user, and the package exported it only so the emitter could shorten a `RUN_ERROR` failure\ndetail, a path that never ran and is now removed. Nothing about a running mesh changes, and no\nshipped connector or host imports it. Only code of your own that imports it from the package is\naffected.\n\n### What stops working\n\nAn import of `takeCodePoints` from `@cotal-ai/connector-core` no longer compiles, and an ES module\nthat names it fails to link with `does not provide an export named 'takeCodePoints'`.\n\n### Before the upgrade\n\nTake a string's first `n` code points with `Array.from(s).slice(0, n).join(\"\")`, which is what the\nhelper did.\n\n## SecretStore create-only write in 0.78.0\n\n`SecretStore` from `@cotal-ai/core` has a fourth method, `create(key, value)`. It stores the value\nonly while the key is absent and resolves `true`, or resolves `false` and leaves the stored value in\nplace. The auth secret kinds made once per space now write their first value through it: the callout\naccount, the issuer keys, the owner secret and a hosted auth plane's instance identity. Before, two\nfirst calls on one space each made a value, the later `put` replaced the earlier one, and the earlier\ncaller kept using a value the store no longer held. The filesystem store implements `create` with an\nexclusive create, so a CLI root needs nothing, and stored values do not change.\n\n### What stops working\n\nA store of your own without `create` no longer compiles as a `SecretStore`. Plain JavaScript gets a\n`TypeError` at the first call that makes one of those kinds, such as `prepareServer` on a new space\nor the first `startAuthService` of a hosted context.\n\n### Before the upgrade\n\nAdd `create` to your store, decided in one atomic step by the backend, such as a conditional create\nor an insert that fails on an existing key. A read followed by a write brings the race back.\n\n## Manager-service authority refusal budgets in 0.78.0\n\n`POST /manager-service-authority` now counts its refusals against the exchange face's budgets, as\n`POST /exchange` does. On the loopback face a missing or wrong capability counts in the\ninvalid-capability window, and a refused IdP token or request counts in the refused-exchange window.\nOn the public face a refusal counts in the peer's bucket. A running mesh needs nothing, and a\nrequest that succeeds is never throttled.\n\n### What stops working\n\n`ManagerServiceAuthorityCtx` from `@cotal-ai/auth` now requires `failures` and `badCaps`, so a host\nthat builds it in an object literal without them fails to compile. The handler now calls the\n`peerKey`, `throttled` and `recordFailure` of the policy it is given. After 30 refusals in a minute\non one face, a further refusal on either route answers 429 without its reason.\n\n### Before the upgrade\n\nPass the `failures` and `badCaps` arrays your `/exchange` route counts in, so both routes share one\nbudget per face. A host that serves only this route can pass new empty arrays.\n\n## Primitive scope kind in 0.78.0\n\n`PrimitiveSpec` from `@cotal-ai/lang` replaces `opensScope: boolean` with `scope: ScopeKind | null`,\nthe scope kind a primitive opens, in the way `kind` names the effect it journals. Both engines now\nread that kind from the table. Before, they cast the primitive's name to `ScopeKind`, so a scope row\nmissing from the union compiled and reached the journal as a kind its readers did not know. A\nrunning mesh and its journals need nothing.\n\n### What stops working\n\nCode that reads `opensScope` from `PRIMITIVES` or a `PrimitiveSpec` fails to compile, and plain\nJavaScript reads `undefined` there, so a check on it never holds.\n\n### Before the upgrade\n\nRead `spec.scope !== null` where you read `spec.opensScope`, or `spec.scope` when you need the kind.\n\n## Token verifier clock tolerance in 0.77.0\n\n`validateUserToken`, and the `IdpConfig` that `verifyIdpToken` and `createIdpBridge` take, from\n`@cotal-ai/auth` no longer accept `clockToleranceSec`. Both verifiers allow a fixed 5 seconds of\nclock skew on `exp`, `nbf` and `iat`, the default they already applied. Nothing in the repository\npassed the option, so a running mesh behaves as before. Only code of your own that passes it is\naffected.\n\n### What stops working\n\nA call or `IdpConfig` that passes `clockToleranceSec` in an object literal no longer compiles. Plain\nJavaScript that keeps it still runs, and the value is ignored. A `NaN` value, such as `Number()` of\nan unset environment variable, used to turn off the expiry, not-before and issued-at checks in both\nverifiers, so an expired or post-dated token was accepted. A large value widened those checks by\nthat many seconds. Both kinds of token are now refused.\n\n### Before the upgrade\n\nRemove `clockToleranceSec` from each call and each `IdpConfig`. A host whose clock is more than 5\nseconds off the issuer's needs its time synchronized.\n\n## Auth context closure in 0.71.0 (unreleased)\n\nExisting deployments need no credential migration or restart for these additive APIs. Embedded\nhosts can now inspect `handle.connections()` and await `handle.closed` after `close()` or `drain()`\nto prove every owned transport ended, including the callout, replaced readiness readers and\nshort-lived clients. The inventory is a detached snapshot.\n\nA transport close failure now rejects with its connection label. The terminal signal stays pending\nwhile any connection remains live. Repair the failure and retry `close()` before awaiting\n`handle.closed`. Closing one hosted context does not close another account's context.\n\nRead a space's claim with `readPlaneClaim(kv, space)` on that account's leader-only auth bucket.\nAn unclaimed space returns `undefined`; held and released rows retain their claim identity.\nDeleted, malformed and foreign-space rows refuse. `PlaneClaimRow` and `PLANE_CLAIM_KEY` are exported.\n\nUse `observeAccountLivenessWithCreds({ servers, observerCreds, accountId, options })` with the\naccount-scoped membership-observer credential to list that account's connections. It never widens\ncredentials or evicts connections. Zero rows prove absence only with a complete sweep and the\nsingle-server proof. An embedded endpoint's trusted composition can retain transport custody\nthrough `EndpointOptions.onConnection`.\n\n## Unreleased\n\nOn a per-user-auth mesh, a spawn-scoped caller can arm the event plane of a child under its own\nowner without `admin`. This fixes owned spawns in spaces whose registration policy requires the\nplane. Upgrade the manager to pick up the admission change. No credential or state migration is\nneeded. Cross-owner arming still requires `admin`, and the child's own-channel rule and ledger\nenvelope are unchanged. A silent non-owner caller in a space without the policy still has the\nplane disarmed, with a notice if provisioning succeeds.\n\n## Hosted auth plane identity in the store (unreleased)\n\nA hosted context started through `startAuthService` keeps its auth plane instance identity in the\ninjected `SecretStore` under `authInstanceKey(space)`, with the other auth secret kinds. It used to\nsit under `stateDir` at `.cotal/space.<hex>/auth-instance.json`, though `stateDir` holds non-secret\nstate and the record holds the plane's private serve seed. The first start of an upgraded context\nputs that record into the store, removes the file and keeps the instance. A CLI root is unchanged.\n\n### What stops working\n\nA start refuses when the store and `stateDir` hold different instance identities, and names both. A\nstore that refuses a put of the new key fails the start.\n\n### Before the upgrade\n\nLet the store accept a put of `authInstanceKey(space)`. A copy of `stateDir` taken before the upgrade\nstill holds the serve seed, so delete it or protect it as secret material.\n\n## Injected SecretStore naming in 0.76.0\n\nThe manager and the delivery daemon name an injected `SecretStore` through\n`injectedSecretStoreIdentity` from `@cotal-ai/core`. The rule is unchanged: the store's declared\nidentity, else the coordinate in `COTAL_SECRET_STORE`, else a refusal. A running mesh needs nothing.\n\n### What stops working\n\n`reloadStoreIdentityOf` from `@cotal-ai/delivery` takes `{ injected: true, store }` or\n`{ injected: false, identity }`. A call that passes `injected: true` with no store fails to compile,\nand plain JavaScript gets a `TypeError`. Both processes refuse an unnamed injected store with one\nmessage, which starts `an injected SecretStore must declare its identity`, so a log match on either\nold message no longer matches.\n\n### Before the upgrade\n\nPass the injected store to `reloadStoreIdentityOf`, or call `injectedSecretStoreIdentity(store)`.\n\n## Manager-service authority policy flag in 0.76.0\n\n`handleManagerServiceAuthority` from `@cotal-ai/auth` no longer reads `allowManagerAuthority` from\nits policy argument, and the field is removed. Both exchange faces set it to `true`, so it never\nrefused anything. A running mesh needs nothing: the public listener serves\n`POST /manager-service-authority` as it did before, and the docs now list that route.\n\n### What stops working\n\nA host that serves the route itself and passes a policy object literal with\n`allowManagerAuthority` fails to compile with TS2353. Plain JavaScript that set it to `false` got a\n403 from the handler, and its requests now reach the capability and IdP checks.\n\n### Before the upgrade\n\nRemove `allowManagerAuthority` from the policy you pass. Where you set it to `false`, leave the\nroute out of that listener's route table instead.\n\n## Hermes model from the environment in 0.68.0\n\nA connector now launches on the model and variant its launcher resolved (the `--model` or\n`--variant` flag, else the agent file's `model:` or `variant:`) and no longer reads them again from\nthe agent file. The Hermes connector also no longer takes a model from `HERMES_MODEL` in the\nenvironment of the process that spawns the seat, including when `spawn.env` lists it.\n\n### What stops working\n\nA Hermes spawn whose only model was `HERMES_MODEL` in the spawning environment is refused at launch,\nand the refusal names both ways to set a model. Spawns that set `--model` or `model:` are unchanged,\non every connector.\n\nCode that calls a connector's `buildLaunch` directly with only `configPath` now gets no model or\nvariant from that file. Pass them as `model` and `variant`.\n\n### Before the upgrade\n\nMove each Hermes seat's model from `HERMES_MODEL` onto its spawn with `--model`, or into its\npersona's `model:`.\n\n## Run answers on a participant manager in 0.68.0\n\nA participant manager now asks its issuing host for an answering credential by naming the run and\nstep it answers. The host reads the pause's token off that run's journal and no longer accepts a\ntoken from the manager. Runs on a mesh with no participant manager are unaffected.\n\n### What stops working\n\nWhile a participant manager and its issuing host run different sides of this release, the host\nrefuses every `cotal run answer` and every amendment that manager serves, because each side refuses\nthe other's request shape. Starting, resuming and reading runs is unchanged. A pause stays waiting\nthrough the window, or follows its timeout if it has one.\n\n### Before the upgrade\n\nUpgrade the auth service and every participant manager registered with it in the same window, then\nanswer the pauses that waited.\n\n## Headless OpenCode handshake in 0.69.0\n\nWith `COTAL_SERVE_HEADLESS=1`, the OpenCode launcher's `[cotal-serve]` line on stdout now carries\nonly `port` and `session`. The server password no longer appears in it, and the 1.x TUI no longer\nreceives the password on its command line.\n\n### What stops working\n\nA headless host that read `password` from that line has no password, and the server refuses its\nrequests. Seats with a TUI, and headless seats that no host drives, are unaffected.\n\n### Before the upgrade\n\nHave each headless host mint a password and pass it to the launcher as `OPENCODE_SERVER_PASSWORD`,\nthen use it for basic auth as before. Without that variable the launcher mints its own.\n\n## Filesystem store identity in 0.69.0\n\nThe delivery daemon's answer to the manager's store check now names a filesystem store by its root\nand by a random `id` that the store records once in `store.id` inside its own directory:\n`.cotal/store.id` for a workspace root, or the directory of the file for `cotal deliver --creds\n<file>`. A manager no longer counts the daemon's store as its own because the two roots have the\nsame path. On a split whose broker host and manager host use one root path, the manager host now\nstays off the daemon-credential renewal lease, so `cotal doctor auth --fix` on the broker host can\nrenew the daemon credentials.\n\n### What stops working\n\nA manager and a delivery daemon on different sides of this release refuse each other's answer to\nthe store check. The manager then remints no daemon credential, and a manager that is booting does\nnot start. This is read from the code and was not measured across two releases. A\n`cotal deliver --creds <file>` whose directory is a read-only mount and holds no `store.id` stops at\nstart. So does a `--creds` file that is its directory's `store.id` under any name, and a `store.id`\nthat is a symbolic link or holds anything but a lowercase UUID.\n\n### Before the upgrade\n\nUpgrade the broker host and every manager host of a space in the same window. For a `--creds` file\non a read-only mount, add a regular `store.id` file beside it that holds a new lowercase UUID and no newline,\nas `node -e 'process.stdout.write(crypto.randomUUID())' > store.id` writes. Move a `--creds` file\nnamed or linked as `store.id` to a file of its own.\n\n## Detached spawns with `--share-tools` in 0.69.0\n\nThe manager's `spawn` operation now takes `shareTools` as a list of MCP server names. The CLI parses\n`--share-tools` into that list before it sends the request, and the manager cluster document moves\nto revision 22. A cut taken with `cotal down --preserve-state` before the upgrade still resumes: the\nmanager reads its `cotal-manager-resume/v1` inventory and writes new cuts as\n`cotal-manager-resume/v2`.\n\n### What stops working\n\nA CLI and a manager on different sides of this release refuse a detached spawn that passes\n`--share-tools`, because the CLI checks each request against the contract the manager serves. This\nis read from the code and was not measured across two releases. A detached spawn without the flag,\na foreground spawn and a roster entry are unaffected. A manager older than this release cannot\nresume a cut that this release took.\n\n### Before the upgrade\n\nUpgrade the CLI on every host that runs `cotal spawn --detach` in the same window as the managers\nit reaches.\n\n## Shared MCP server checks in 0.69.0\n\nThe cotal config reader now checks each server under `connectors.<name>.mcpServers` when it reads\nthe file, and refuses one that cannot launch as written, naming the file and the field. The rules\nare in [the config file](config.md#the-config-file).\n\n### What stops working\n\nA config file that holds such a server refuses every Claude spawn that reads it, including one with\n`--share-tools none`. Before, a field of the wrong type failed each Claude spawn that shared the\nserver with a `TypeError` that named neither the file nor the server, a spawn that did not share it\nlaunched, and a server with no `command` or `url` was passed to `claude`, which never started it.\nRead from the code and not measured: spawns on other connectors, a manager resume and the step of\n`cotal setup` that records the shared list read the same files, so each stops at the same refusal.\n\n### Before the upgrade\n\nCheck `connectors.<name>.mcpServers` in the operator-level config file and in each space's\n`.cotal/config.json`. Give each server a string `command`, or a `type` of `http`, `sse` or `ws` with\na string `url`. Write `args` as a list of strings and `env` and `headers` as objects of strings, or\nremove the server.\n\n## Remote manager family eviction in 0.69.0\n\nA remote manager registered through its host now asks the host to evict up to 256 holders of its\ncredential family in one maintenance request, and the host reads the family once for the whole set.\nBefore, a restart sent one request per holder and the host read the whole family for each one.\nMeshes with no remote manager are unaffected.\n\n### What stops working\n\nWhile a remote manager and its issuing host run different sides of this release, each side refuses\nthe other's eviction request shape. A restart whose credential family already has holders then fails\nat its eviction step and leaves the manager's registration gate frozen. A first start, a clean stop\nand the host's reconciliation of a foreign slot holder are unchanged.\n\n### Before the upgrade\n\nUpgrade the auth service and every remote manager registered with it in the same window. A manager\nthat restarted inside the window resumes its frozen registration on its next start once both sides\nrun this release.\n\n## AG-UI emitter holder hooks in 0.69.0\n\n`AguiEmitterHolder` from `@cotal-ai/connector-core` now takes its hooks as one named object after\nthe emitter factory: `new AguiEmitterHolder(startEmitter, { onError, onRunClosed, waitLive, runMeta })`.\nOnly `onError` is required. Nothing about a running mesh changes, and every shipped connector passes\nits hooks by name. Only a connector of your own that builds a holder is affected.\n\n### What stops working\n\nA holder built with positional hooks, such as `new AguiEmitterHolder(start, onError, onRunClosed)`,\nno longer compiles, because the constructor takes two arguments. Plain JavaScript that keeps the\npositional form still runs, but the holder calls none of its hooks, so a failure never reaches\n`onError`.\n\n### Before the upgrade\n\nPass each hook by name, for example `new AguiEmitterHolder(start, { onError, onRunClosed })`, and\ndrop any `undefined` that filled an earlier slot to reach a later hook.\n\n## Worker run failure type in 0.70.0\n\n`WorkerRunFailed`, the failed result of `runInWorker` in `@cotal-ai/lang`, is now a union on\n`class`: `released`, `held`, `effect`, `too-large`, `rejected` or `error`. A running mesh needs\nnothing, because the runtime host and the engine thread ship in the same install. A run on the\ncompiled engine whose program throws an object with `code: \"L5012\"` or `code: \"L5025\"` used to end\nreleased and now ends failed, as it does on the walker.\n\n### What stops working\n\nTypeScript code that reads `code`, `reason`, `step`, `pending`, `kind`, `detail` or `tooLarge` on a\n`WorkerRunFailed` it has not narrowed fails with TS2339. A released, held, too-large or rejected\nresult no longer carries `code`, so JavaScript that branched on `L5012`, `L5025`, `L5006` or\n`L5010` stops matching with no error. `tooLarge` is gone.\n\n### Before the upgrade\n\nBranch on `class` where such code read `code`: `released` for L5012, `held` for L5025, `too-large`\nfor L5006 and `rejected` for L5010. An `effect` or `error` result keeps its `code`. Once narrowed to\n`too-large`, a result carries the `stepKey`, `bytes` and `bound` that `tooLarge` held.\n\n## Remote manager request builder in 0.70.0\n\n`remoteManagerClient.remoteManagerAuthorityRequest` from `@cotal-ai/manager` now takes an\noperation's coordinates as one object, and `remoteManagerRegistrationProof` from `@cotal-ai/core`\ncomputes the proof from the manager's identity state instead of a request. Nothing about a running\nmesh changes: the proof digest and the request on the wire are the same, so a manager and a host on\ndifferent sides of this release still accept each other. Only code that builds remote manager\nrequests itself is affected, in TypeScript and in plain JavaScript.\n\n### What stops working\n\nA call that passes the registration proof, contract artifacts, session, retirement or transfer\nreader as positional arguments after the operation no longer compiles. A call that passes a request\nto `remoteManagerRegistrationProof` no longer compiles either, because the second argument now names\nthe lifecycle `lifecycleUid`, as the identity state does.\n\nPlain JavaScript runs both old calls without an error. The builder drops the positional coordinates,\nso the host refuses the request with `requires a sha256 registrationProof`. A proof computed from a\nrequest leaves out the lifecycle, so the host refuses a request that carries it as a proof mismatch.\n\n### Before the upgrade\n\nName the coordinates, for example\n`remoteManagerAuthorityRequest(state, \"cli\", \"retire\", { registrationProof, retirement })`.\nCompute the proof as `remoteManagerRegistrationProof(owner, state)`, adding the contract artifacts\nas a third argument for activation only. A host that recomputes the proof from a received request\npasses `{ space, instanceId, lifecycleUid: managerLifecycleUid, identities }` from that request.\n\n## Bearer validator lifetime cap in 0.70.0\n\n`validateUserToken` from `@cotal-ai/auth` no longer takes `maxTtlSec`. It caps a bearer's lifetime\nat the cap of the bearer's view, the same cap the issuer applies when it mints: 900 seconds, or 300\nfor a `transfer-writer` bearer. The auth callout never passed the option, so a running mesh behaves\nas before. Only code of your own that calls the validator with `maxTtlSec` is affected.\n\n### What stops working\n\nA call that passes `maxTtlSec` in an object literal no longer compiles. Plain JavaScript that keeps\nit still runs, and the value is ignored. A `NaN` value, such as `Number()` of an unset environment\nvariable, used to turn the lifetime check off and accept a bearer of any lifetime. That bearer is\nnow refused at its view's cap.\n\n### Before the upgrade\n\nRemove `maxTtlSec` from each call. A test that needs a bearer to expire sooner mints one with a\nshorter lifetime.\n\n## Persisted identity records in 0.70.0\n\nThe manager instance identity, the manager sibling identities, the auth plane instance identity and\na participant manager's remote authority state now share one reader and one first mint in\n`@cotal-ai/workspace`, exported as `claimIdentityRecord` with the nkey check `identityOf`. Each\nrecord is read as a regular file, must hold non-empty nkeys and is created exclusively, so\nconcurrent first starts of a participant manager on one root now settle on one identity where each\nused to keep its own. `saveManagerInstanceIdentity` and `saveAuthInstanceIdentity` are gone. A\nrunning mesh whose records are plain files needs nothing.\n\n### What stops working\n\nA manager instance, auth instance or remote authority record that is a symlink, a directory or any\nother non-regular entry is refused where it used to be followed. The manager, the auth plane and a\nparticipant manager fail to start on it, and `cotal reconcile-gate` and `cotal deregister-instance`\nrefuse it. Retirement already refused it. A remote authority record with an empty nkey id or seed\nis refused too. A first mint that loses its race and cannot read the winner now refuses with\n`identity-record-create-lost` in place of `manager-instance-identity-create-lost` or\n`auth-instance-identity-create-lost`. Code that imports either `save` function no longer compiles.\n\n### Before the upgrade\n\nReplace a symlinked identity record with a copy of the file it points to. Code that wrote a record\nwith a `save` function plants it with `createManagerInstanceIdentity` or\n`createAuthInstanceIdentity`, which create the record when it is absent and otherwise return the\nstored one unchanged. Nothing replaces an overwrite of a stored identity.\n\n## Manager instance in user credentials in 0.70.0\n\n`AuthProvider.userCredentials` from `@cotal-ai/core` no longer returns `managerInstanceId`. A\n`manager-caller` credential's manager instance is the signed `act.managerInstanceId` claim in its\nbearer, which the broker verifies and the CLI already used. The reference provider in\n`@cotal-ai/auth` stops copying the exchange response's field into its result, where nothing\ncompared it with the bearer. The exchange still answers with the field, so a running mesh behaves as\nbefore.\n\n### What stops working\n\nCode of your own that reads `managerInstanceId` from a `userCredentials` result no longer compiles,\nand plain JavaScript reads `undefined` there.\n\n### Before the upgrade\n\nRead the instance from the bearer's `act.managerInstanceId` claim.\n\n## Auth plane identity location in 0.70.0\n\nThe user-auth service keeps its instance identity in the root's `.cotal/space.<hex>/auth-instance.json`,\nbeside the manager's. It used to sit inside `.cotal/auth`, at\n`space.<hex>/.cotal/auth/auth-instance.<hex>.json`, so a copy of that folder carried it. The first\nstart of an upgraded root moves the record and keeps the instance. A hosted context started through\n`startAuthService` has its record moved the same way inside its `stateDir`.\n\n### What stops working\n\nCode that calls `openAuthAuthorityPlane` without the new `identityRoot` option no longer compiles. A\nstart that finds a record both in `.cotal/space.<hex>/` and at its older place refuses and names the\ntwo files. A start also refuses when the older place of the auth or manager identity holds a symlink,\na directory or anything else that is not a regular file. The manager used to skip a dangling symlink\nthere and mint a new identity.\n\n### Before the upgrade\n\nPass `identityRoot` to `openAuthAuthorityPlane`. When `dir` is a workspace root's user-auth state\ndir, `<root>/.cotal/auth/space.<hex>`, pass that root. A plane with no workspace root, as\n`startAuthService` runs, passes `dir` itself. Either keeps the identity the plane already has: on the\nfirst start it moves from `<dir>/.cotal/auth/` to `<identityRoot>/.cotal/space.<hex>/`. Never pass a\ndirectory inside `.cotal/auth`: the record would land in the folder an operator copies and travel\nwith it again.\n\nA copy of `.cotal/auth` taken from a root last run by an older Cotal carries that root's record.\nDelete `.cotal/auth/space.<hex>/.cotal/auth/auth-instance.<hex>.json` from the root you copied it to\nbefore the first `cotal up --user-auth` there.\n\n## Per-seat `COTAL_` names in `spawn.env` in 0.71.0\n\n`spawn.env` in the cotal config no longer forwards a `COTAL_` name the launcher sets for each seat,\nsuch as `COTAL_ROLE`, `COTAL_MODEL` or `COTAL_SUBSCRIBE`. Before, a seat launched with no value of\nits own took the spawning process's value and ran under that role, model or read set. The\nmachine-wide knobs a seat already receives, such as `COTAL_HOME`, may still be listed.\n\n### What stops working\n\nEvery spawn and resume under a config whose `spawn.env` lists such a name is refused before\nlaunch, and the refusal names the entry. Code that calls `launchEnv` from `@cotal-ai/connector-core`\nwith such a name in `envAllow` gets the same error.\n\n### Before the upgrade\n\nRemove those names from `spawn.env`. Give each seat its role, model and channels with `--role`,\n`--model` and `--subscribe`, or in its persona's `role:`, `model:` and `subscribe:`.\n\n## Role addresses in 0.71.0\n\nA role must be one `[A-Za-z0-9_-]` token. Before 0.71.0 any other spelling was rewritten into one:\n` probe ` reached the `probe` queue and `pro.be` reached `pro_be`, while the message kept the\nspelling sent. An anycast to `*` was accepted and stored where no holder reads it.\n\n### What stops working\n\nAn agent whose role is outside the token set no longer starts, however it is launched:\n`cotal join --role`, `cotal spawn --role`, an agent file's `role:`, `COTAL_ROLE` and an embedded\nendpoint's `card.role` are all refused before the agent joins.\n\nA send to such a role, or to `*`, through `cotal send ask`, `/anycast` or `cotal_anycast` is refused,\nand nothing is stored.\n\n`routeToken` is no longer exported from `@cotal-ai/core`. A role routes as spelled, so code that\nused it to name a role's queue uses the role itself, and `assertValidRole` checks one.\n\n### What migrates on its own\n\nEvery task queue. A `svc_<role>` durable was always named from the rewritten token, so its pending\nrequests and its holders carry over.\n\n### Before the upgrade\n\nRename each role outside the token set to the token it already routed to: remove the surrounding\nspaces and replace every other character outside the set with `_`. Rename it where the holder is\nlaunched and in every script or prompt that sends to it.\n\n## IdP URLs on `localhost` in 0.73.0\n\nThe IdP URL that `cotal login --idp` and `cotal up --user-auth --idp` take, the JWKS URL the auth\nservice pins from it, the space-catalog link and `cotal sync` now share one rule: `https://`, or\n`http://` on a loopback IP literal. `localhost` is a name, so it no longer counts: a hosts entry\nwould choose the IdP, and with it the keys the callout trusts. Every loopback literal now passes, so\n`http://127.0.0.2/api/auth` and `http://[::ffff:127.0.0.1]/api/auth` are accepted where they were\nrefused before. A JWKS URL with any scheme other than `https:` or `http:` is refused. A mesh whose\nIdP uses HTTPS is unaffected.\n\n### What stops working\n\nOn a mesh whose IdP is pinned at `http://localhost:<port>/...`, the auth service builds its key\nresolver from the pinned JWKS URL at start, and that resolver now refuses it with\n`JWKS origin must be https (or http on a loopback IP literal for dev)`.\n\n`cotal login` and `cotal logout` with `--idp http://localhost:<port>/...` are refused with\n`idp url must be https (or http on a loopback IP literal such as 127.0.0.1 for local dev)`, and so is\nevery command that reads a session cached under that URL.\n\n### Before the upgrade\n\nUse `127.0.0.1` (or `::1`) in place of `localhost`. In the space's `idp.json` under the mesh's\n`.cotal/auth`, change `url` and `jwksUri` to the literal spelling and leave `issuer` and `audience`\nas they are. Owners derive from the issuer, so existing grants keep matching. Change a manifest's\n`broker.idp` the same way, because `cotal up` refuses an `--idp` that differs from the pin. Then\nhave each person run `cotal login --idp http://127.0.0.1:<port>/api/auth` again, because sessions\nare cached under the URL.\n\n## Terminal layout `Pane.confirm` in 0.73.0\n\n`Pane.confirm` is removed from `@cotal-ai/core`, and the tmux and cmux terminal-layout providers no\nlonger press Enter in the panes they open. The flag carried no prompt text, so both providers\npressed Enter five times, one second apart: the first dialog a pane showed was answered with its\ndefault, and a prompt that never appeared went unreported. Nothing in Cotal sets it, so a running\nmesh needs nothing. Spawned agents are unaffected, because their runtimes match the startup prompt\nby its text.\n\n### What stops working\n\nA `Pane` object literal that sets `confirm` fails to compile with TS2353. Plain JavaScript that sets\nit runs without an error, and the pane stays at its prompt.\n\n### Before the upgrade\n\nRemove `confirm` from each `Pane`. Start a command that shows a startup prompt with the option that\nskips it, or answer the prompt in the pane.\n\n## Carrying a resumed Claude session to another host in 0.67.0\n\n`cotal spawn --resume <id> --detach --on <instance>` now carries a Claude session held on the\noperator's host to the target manager instance. Both sides need this release: an older manager does\nnot serve `transcript-receive`, and the CLI then stops with that manager's refusal instead of\nlaunching. The manager cluster document moves to revision 21, and the `ps` row's `resume` object\ngains `host` and `transferredAt`.\n\nA manager host that runs carried seats needs `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_AUTH_TOKEN` or a\ncloud provider selection in its environment, because each carried seat runs in its own Claude home\nwith no stored login. On an authenticated mesh the CLI mints the transfer writer from the space's\nsigning seed, so the carrying host needs that seed, as for any other operator command. On a user-auth\nmesh it exchanges the operator's login for a `transfer-writer` view instead, so the operator's grant\nneeds scope `admin`, and the auth service must run this release. A remote manager receives a carry once\nits host serves the manager-service `transferReader` operation. A seat launched without carrying,\nincluding any `--resume` whose id this host does not hold, is unchanged.\n\n## Lifecycle head type in 0.67.0\n\n`LifecycleMapping`, the type `parseLifecycleHead` returns, is now a union on `state`. Nothing about\na running mesh changes: heads that parsed before parse the same way, and the refusals are\nunchanged. Only TypeScript code that compiles against `@cotal-ai/core` is affected.\n\n### What stops working\n\nAn `interface` that extends `LifecycleMapping` fails with TS2312, because an interface cannot extend\na union. Code that builds a head in memory no longer compiles when the head is `retiring` without\nits `op`, or `active` or `retired` with one. The parser already refused those heads.\n\n### Before the upgrade\n\nDeclare such an interface as an intersection instead, for example\n`type ActiveMapping = LifecycleMapping & { state: \"active\" }`. A reader that has checked\n`state === \"retiring\"` reads `op` without a guard.\n\n## Issuance gate types in 0.67.0\n\n`EpGateRow` and `EndpointGateRow`, which `parseIssuanceGate` and `parseEndpointGate` return, and\n`EpGateState`, which an `EpIssuanceGate` or `EpIssuanceBarrier` returns from `observe`, are now\nunions on `state`. Nothing about a running mesh changes: gates that parsed before parse the same\nway, and the refusals are unchanged. Only TypeScript code that compiles against `@cotal-ai/core`\nis affected.\n\n### What stops working\n\nAn `interface` that extends one of these types fails with TS2312, because an interface cannot\nextend a union. Code that builds a gate in memory, such as a custom barrier's `observe`, no longer\ncompiles when the gate is `frozen` or `retired` without its `op`, or `open` with one. The gate\nparsers already refused those rows.\n\n### Before the upgrade\n\nDeclare such an interface as an intersection instead, for example\n`type CustomGateRow = EpGateRow & { custom: string }`.\n\n## Lifecycle-blocked refusals in 0.66.0\n\nA refusal that carries `ai.cotal.ep.lifecycle-blocked` now reports only the lifecycle state it\nread. Nothing about a running mesh changes. A client that branches on the detail must read the new\nfield.\n\n### What stops working\n\nA refusal raised at the issuance gate used to carry `headState` without reading the head:\n`retiring` for a frozen gate and `retired` for a retired one. It now carries `gateState`\n(`frozen` or `retired`) and no `headState`. A client that treats `headState: \"retired\"` as a\nburned uid, or `headState: \"retiring\"` as a retirement in flight, no longer matches those\nrefusals, and the `[lifecycle ...]` suffix on the error string changes the same way. A custom\nissuance barrier whose `observe` returns a frozen gate without a valid `op` (a string `opId` and\none of the four op kinds) is now refused as `internal` by `registerServiceInstance`.\n\n### Before the upgrade\n\nUpdate such a client to read `gateState` for a gate refusal and `blockedOp` for the operation that\nholds the gate. `headState` is present only when the refusal read the head, for example an\nactivation refused because the head is still retiring.\n\n## Workflow programs that bind `once` in 0.65.0\n\n`once` is now a scope of the workflow language, so it is a reserved name. A program that declares\nits own `once` binding (`const once = ...`, a parameter or a function named `once`) is refused at\nvalidation with L2002. Nothing else about a running mesh changes.\n\n### What stops working\n\nA run whose recorded program binds `once` cannot be resumed after the upgrade, because a resume\nvalidates the recorded program again. A new `cotal run start` of such a program is refused before\nanything is recorded.\n\n### Before the upgrade\n\nList the runs with `cotal run ps` and check each program that is still running or held for a\nbinding named `once`. Let those runs finish on the old version before you upgrade the manager, and\nrename the binding in the program before you start it again.\n\n## From 0.58.0 to 0.59.0\n\nEvery connector now publishes a failed run's `RUN_ERROR` on `events.<owner>.<actor>` with the fixed\nmessage `run failed` and no `code` or `rawEvent`. The error text and error kind a harness reports\ncan echo a prompt, a peer message or tool output, and that channel has a different read ACL. A\nreader that showed the message or branched on `code` gets neither after the upgrade. Where a\nconnector reports the error kind as the agent's presence condition, that is unchanged.\n\n### Settle pending event frames before the upgrade\n\nEach session's events are frozen in its event write-ahead log before they are published. A session\nrestarted on 0.59.0 whose log still holds an unacknowledged frame with an older `RUN_ERROR` does not\nrepublish it: its event emitter halts with `egress-run-error` and publishes nothing further for that\nsession. The broker may or may not already hold that frame, so the halt cannot settle it.\n\n1. Stop the seats cleanly on 0.58.0, with the broker still up.\n2. List the logs that still hold a pending frame. The logs live under the events state root\n (`COTAL_WORKSPACE_ROOT`). Empty output means there is nothing to settle.\n\n ```sh\n find \"$COTAL_WORKSPACE_ROOT/.cotal/events\" -name wal.json \\\n -exec jq -r 'select(.pending != null) | input_filename' {} +\n ```\n\n3. For each session listed, start it again on 0.58.0 while the broker is reachable, let it recover,\n stop it, and run step 2 again. Recovery publishes the frame as 0.58.0 would have, error text\n included, so it only finishes what 0.58.0 had already started.\n\n If that start halts with `cas-loss` instead, the agent's subject is no longer at the sequence this\n log expects, and no restart settles that log, on 0.58.0 or later. A lost acknowledgement is one\n cause: the broker stored the frame, so it and its error text are already on the channel, and every\n retry halts the same way because the stream checks the frozen expectation before it deduplicates.\n The halt message names the other causes, such as a second emitter for the same agent under a\n different state root, a restored stream or frontier record, or a purged channel. With those the\n pending frame may never have reached the broker, so a `cas-loss` does not tell you whether it\n landed. Find and stop any second writer and rule out a restored state first. Clearing the halt\n then means purging the agent's event channel and removing the agent's directory under the events\n state root whole (see [Event plane](connect-claude.md#event-plane)). That abandons the pending\n frame whether or not the broker has it, and the purge also drops the earlier frames of every\n session of that agent.\n4. Upgrade once step 2 prints nothing.\n\nIf a session halts with `egress-run-error` after the upgrade, go back to step 3 for that session on\n0.58.0. Do not edit or delete `wal.json` on its own to get past either halt: clearing the pending\nframe abandons that epoch, an event the broker never received is lost, and removing part of the\ndirectory leaves a state the next start refuses.\n\n## Explicit actor grants in 0.59.0\n\n`cotal actor grant` no longer fills an omitted ACL flag with its wide default. A grant names\n`--scope`, `--allow-subscribe` and `--allow-publish`, or passes `--full` to give the ones it leaves\noff their wide defaults (`spawn,role:default`, `>` read, `>` post). Any other grant is refused. The\nbreak is in the CLI on the machine that holds the actor ledger, the one that ran\n`cotal up --user-auth --idp <url>`. No stored row, credential or wire message changes.\n\n### What keeps working\n\nExisting actor ledger rows keep the authority they were granted, and their users and agents connect\nas before. `actor revoke`, `actor list` and a `grant` that names all three ACL flags behave as they\ndid on 0.58.0. Nothing on disk is converted.\n\n### What stops working\n\nA grant that leaves off any of the three flags without `--full` exits 1 with\n`refusing to grant \"<actor>\" with --scope, --allow-subscribe, --allow-publish left off`, naming the\nflags it is missing, and then prints both accepted forms. It writes no row and does not retire the\nactor's current lifecycle. An existing row stays as it was, and an actor granted for the first time\nstays out until the grant is run again. This includes the bare grant printed on 0.58.0 by\n`cotal login`, `cotal status`, `actor list` and the not-granted refusal. Look for it in provisioning\nscripts, onboarding runbooks and anything that pastes those hints.\n\n### Upgrade order\n\nChange the scripts before the ledger machine is upgraded, and make each grant name all three flags.\n0.58.0 and 0.59.0 both accept that form. To keep a wide row, write its defaults out:\n\n```sh\ncotal actor grant <actor> --sub <IdP subject> \\\n --scope spawn,role:default --allow-subscribe '>' --allow-publish '>'\n```\n\nSwitch to `--full` only once the ledger machine runs 0.59.0. 0.58.0 refuses it with\n`Unknown option '--full'` before it reads the ledger. Brokers, managers and participant machines\nneed nothing for this break, so their order is the one the section above gives.\n\n### The window\n\nThis break has no outage. No process restarts for it, and a refused grant changes nothing. The\nexposure is a grant script that runs against 0.59.0 before it was changed: it fails and grants\nnothing.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. On the ledger machine, save the output\nof `cotal actor list` to compare rows after the changed scripts run, and list the scripts that call\n`cotal actor grant`.\n\n### The upgrade end to end\n\n```sh\n# on the ledger machine, still on 0.58.0\ncotal actor list > actors-before.txt\ngrep -rn 'actor grant' <your provisioning scripts>\n# make every grant name --scope, --allow-subscribe and --allow-publish, run them, then upgrade\nnpm i -g cotal-ai@0.59.0\ncotal actor list | diff actors-before.txt -\n```\n\nBoth refusals quoted here were run on 0.58.0 and on the 0.59.0 code. That brokers, managers and stored\nrows need nothing is read from the change, which touches only the CLI and its hints, and was not run\non a live split deployment.\n\n## Repeated flags refused in 0.59.0\n\nA `cotal` flag given more than once is now a usage error unless the command declares it repeatable.\nOn 0.58.0 the last value won with no message, so `cotal down web --space a --space b` acted on `b`\nwhile a wrapper that checked the first `--space` verified `a`. The break is in the command-line\nparser on the machine that runs the command, including commands added with `cotal ext add`. No\nstored state, credential or wire message changes.\n\n### What keeps working\n\nA command line that gives each flag once parses as it did on 0.58.0, in any order and in the\n`--flag=value` form. Flags whose help says repeatable, such as `--opt` and `down --session-store`,\nstill collect every value. A flag-shaped word after `--` is still a positional. The daemons, units\nand agents that `cotal` starts for itself are given each flag once, so a fleet driven only by `cotal`\ncommands typed by hand needs no action.\n\n### What stops working\n\nA command line that repeats any other flag exits 1 before the command runs. It prints\n`Option '--space' cannot be repeated`, or `Option '-f, --file' cannot be repeated` for a flag with a\nshort form, followed by the command's help. `-f` and `--file` count as the same flag. Look for it in\nscripts, aliases and wrappers that append a flag to override one set earlier, such as a fixed\n`--space` followed by `\"$@\"`.\n\n### Upgrade order\n\nChange those scripts first so each flag is given once. 0.58.0 and 0.59.0 both accept that form.\nBrokers, managers and participant machines need nothing for this break, and each machine's CLI\napplies it when that machine is upgraded, so their order is the one the sections above give.\n\n### The window\n\nThis break has no outage. No process restarts for it, and a refused command does nothing. The\nexposure is a script that still repeats a flag when it runs on 0.59.0: it exits 1 instead of acting on\nthe last value.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. List the scripts, aliases and wrappers\nthat call `cotal` so each one can be checked.\n\n### The upgrade end to end\n\n```sh\n# still on 0.58.0\ngrep -rn 'cotal ' <your scripts and wrappers>\n# give each non-repeatable flag once, then upgrade\nnpm i -g cotal-ai@0.59.0\n# run each changed script; a repeat left behind exits 1 with the usage error and does nothing\n```\n\nThe refusal and its messages were run against the 0.59.0 parser and `cotal topology view`. That the\nargument lists `cotal` builds for its own processes give each flag once is read from the code, and\nwas not run on a live split deployment.\n\n## Detached spawns from a seat's shell in 0.62.0\n\nOn a static or open mesh, `cotal spawn --detach` run inside a managed seat's shell now launches as\nthat seat. On 0.61.0 it minted a one-shot operator instrument, so the manager recorded that\ninstrument as the spawner and the seat's own `cotal_despawn` of the child was refused with\n`not authorized: <seat> was not spawned by <caller> (admin tier required)`. The break is in the CLI\non the machine where the seats run. No stored state, credential or wire message changes.\n\n### What keeps working\n\n`cotal spawn --detach` from an operator terminal or from a script outside any seat launches as\nbefore, and so does any call with `--creds`, one aimed at a space other than the seat's own, or a raw\nopen target named with `--server` and an unregistered `--space`. A user-auth mesh is unchanged. A seat with\n`capabilities: [spawn]` still spawns from its shell, and can now stop that child with\n`cotal_despawn`. `--on <instance>` from a seat's shell still lands on that manager instance, now as\nthe seat.\n\n### What stops working\n\n- On a static mesh, a seat without `capabilities: [spawn]` can no longer spawn from its shell. Its\n own credential holds no spawn subject, so the broker refuses the request and the command exits 1.\n- A child launched from a seat's shell is now that seat's child, so the manager stops it when the\n seat exits, as it does for a `cotal_spawn` child. A child that has to outlive the seat that\n started it now goes with the seat.\n- A seat launched without `COTAL_SPACE` is placed by its static credential. Every connector sets\n that variable, so this only reaches a hand-built launch: from such a seat's shell, a spawn aimed at\n a static space that holds no credential for the seat is refused instead of running as the operator.\n\n### Upgrade order\n\nOnly the CLI that seats run from their shell changes, which is the one installed on the host where\nthe seats run. Brokers and managers need nothing for this break, so their order is the one the\nsections above give.\n\n### The window\n\nThis break has no outage. No process restarts for it. A child already running when you upgrade\nkeeps the spawner the manager recorded at its launch.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. List the agent files whose seats run\n`cotal spawn --detach` from their shell, note which of them lack `capabilities: [spawn]`, and note\nwhich of their children must outlive the seat.\n\n### The upgrade end to end\n\n```sh\n# still on 0.61.0: find the seats that spawn from their shell\ngrep -rln 'cotal spawn' .cotal/agents\n# add `capabilities: [spawn]` to each of those agent files that lacks it, and launch any child\n# that must outlive its seat from an operator terminal instead\nnpm i -g cotal-ai@0.62.0\n```\n\nThe attribution, the despawn, the refusal of a seat without `spawn`, the stop on seat exit and a\nseat's `--on` spawn were run on a local static mesh, and the attribution and the despawn on a local\nopen mesh.\n\n## From 0.53.0 to 0.54.0\n\nManager calls now borrow an instance-bound `manager-caller` credential. Followed mutations require\n`manager.goal-result` on the selected manager, so a compatible issuer, manager and client must be\nloaded together. An older manager is refused before a followed mutation; upgrading an installed\nbinary alone does not replace code in a running manager, connector or embedded client.\n\n### Preserve state before changing processes\n\nSnapshot the broker's durable storage using its supported backup procedure, the host authority and\nactor ledgers, and each participant's manager identity, runtime custody records, credentials and\nsaved sessions. Include the embedding application's database and configuration under its supported\nbackup procedure. Record the loaded package versions and the CLI path used by bearer helpers.\nKeep these copies private. Do not change the IdP issuer, regenerate manager identities, rotate agent\ncredentials or recreate tenant storage to make the upgrade pass.\n\nNo ledger, goal-history or session conversion is required for this change. Existing ordinary\nmessaging credentials retain their normal expiry rules. New manager-caller credentials are obtained\non demand from the current grant; old manager-call credentials do not gain the new view automatically.\nExisting accepted goals remain durable and must not be submitted again merely because observation\nwas interrupted. Fresh remote registration publishes its service status at the current revision and\nepoch; do not seed that status manually.\n\n### Upgrade the split deployment\n\n1. Stage one pinned 0.54.0 package set for the host and participants, including the embedding SDKs.\n Pause new manager mutations and let accepted work settle where possible before reloading processes.\n2. Upgrade the host issuer and embedding first. Keep the broker, its account identities and durable\n storage in place. Then load the matching manager release on each participating machine.\n3. Preserve active seats through the runtime's supported update path. A Linux custodial runtime may\n release and re-adopt seats within its 600-second unattended window; verify the actual runtime,\n custody records and process identities before relying on it. A legacy PTY runtime without release\n support cannot preserve active seats through a generic manager restart. Drain it at an approved\n idle window instead of signalling the manager or replacing conversations.\n4. Reload the clients and connectors through their session-preserving host controls. Refresh any\n bearer helper captured from an older immutable CLI path. A transport-only reconnect does not reload\n JavaScript. Verify authenticated instance selection, a read-only manager command and canonical\n result recovery before allowing new followed mutations.\n\nTreat the interval from issuer reload through compatible manager/client reload as a manager-control\noutage. Mixed versions can refuse discovery or commands; there is no promised rolling transition.\nOrdinary agent sessions survive only where their runtime and credentials permit it. If verification\nfails, keep mutations paused and repair forward from the preserved state rather than resetting it.\nThis release does not add host-backed enrollment or terminal release for stock participant detached\nagents; see [Remote supervised agents](run-a-mesh.md#remote-supervised-agents).\n\n## From 0.48.2 to 0.49.0\n\n0.49.0 changes how a credential's authority is recorded. A credential is no longer only a signed\nfile: it is an *issuance*, with a generation the issuer chose and durable evidence of the ceiling it\nwas granted under. The important consequence for a running deployment is not at connect time. It is\nat renewal time.\n\n### What keeps working without any action\n\n- **Existing agent credentials keep authenticating.** A credential minted under 0.48.2 is not\n revoked and is not rejected at connect. Nothing needs to be re-issued to bring the fleet back up\n after the upgrade.\n- **The channel registry survives.** Channels, their replay settings, descriptions, and usage text\n are ordinary durable state and are not rewritten by the upgrade.\n- **`cotal deliver` is still a standalone command.** Running the delivery daemon as its own process\n remains supported; it is not restricted to being a child of `cotal up`.\n- **`cotal join` keeps its flags.** In particular `--lifecycle-uid` is not new in 0.49.0. It has\n been required alongside `--creds` since well before this release, and the pairing rule did not\n change here. A scripted external join that worked under 0.48.2 works unchanged.\n\n### What does not migrate\n\n**A credential minted before 0.49.0 cannot be renewed.** Managed agent credentials carry a\n24-hour lifetime and the manager re-signs one once it passes **75%** of its life, ticking every\nquarter of the TTL so a tick always lands inside that window. When the manager reaches a credential\nthat carries no issuance, it refuses to renew it and logs the agent by name:\n\n```\n! managed cred renewal <agent>: renewManagedStaticCred: <agent> carries no issuance;\n a static credential minted before SPEC 13.15 is not renewed under an unbound generation\n - respawn the agent\n - the agent dies loud at this cred's expiry unless it is reminted\n```\n\nSo the fleet comes up fine, runs normally, and then each agent stops at its own credential's\nexpiry, within roughly a day of the upgrade, one at a time rather than together. The refusal is\ndeliberate: the renewal would otherwise have to invent a generation nobody issued, which is the\nstate the release exists to remove.\n\n**Respawn the managed agents as the last step of the upgrade.** For this particular upgrade the\nrespawn is not optional: stopping a 0.48.2 manager ends its agent processes whichever CLI you use,\nfor the reason given under the outage window below. The respawn is how they come back, and it is\nalso what mints each credential as an issuance so it renews from then on. One planned pass over the\nfleet is the whole job. Skipping it leaves agents stopped and, for any credential that survived\ninto 0.49.0 unminted, brings the renewal cliff above a day later, one agent at a time.\n\n### Credentials you minted yourself\n\n**A credential you minted with `cotal mint` is a different case, and it very likely needs\nnothing.** The distinction that matters is not the word \"static\", which covers both. It is **what\nminted the credential and who owns its renewal**. A credential the **manager** minted for an agent\nit spawned carries a lifetime and is renewed by the manager, so it is the subject of everything\nabove. A credential **you** minted with `cotal mint` and handed to an external peer is issued with\n**no expiry at all**, and no manager renews it: it is not in the sweep, so there is no renewal to\nfail. It keeps working after the upgrade, and re-minting it would mean coordinating with a third\nparty for no gain.\n\nThe manager says which one it is holding. Where a credential has no expiry to reach, the sweep\nnames it and moves on rather than refusing:\n\n```\n! managed cred renewal <agent>: credential is unbounded - not renewed\n (a pre-TTL credential stays as minted until respawn)\n```\n\nRe-mint an external peer's credential only if you want it to carry a lifetime, and at a time you\nchoose.\n\n### How to read the boot log\n\nA 0.49.0 manager starting over an existing space may print lines like:\n\n```\n verified evicted: <holder-key> (3/12)\n already verified (durable): <holder-key>\n\u2713 boot self-heal: manager/<id> registration gate reopened at generation <n>\n```\n\nThese are **not** a credential migration, and reading them as one is the most likely way to\nconclude the fleet is fine when it is not. They come from the manager repairing **one** endpoint\nregistration gate that a previous restart left frozen, and they enumerate that single gate's\ncredential-family holders as it verifies each one evicted. `already verified (durable)` on a later\nstart is the repair cursor resuming, not a credential that became durable. The repair is real and\nuseful (it is what previously needed `cotal reconcile-gate` by hand), but it says nothing about\nwhether your agent credentials carry issuances. The renewal refusal above is the signal that does.\n\n### Which side to upgrade first in a split topology\n\nMove the manager first.\n\nThe stores 0.49.0 introduces are created by the **manager** at its own boot, not by the broker.\nThey are create-or-verify and idempotent, so a 0.49.0 manager brings the space's authority stores\nup to the new shape itself, and it does so against whichever broker is answering.\n\nBeing honest about the evidence behind each direction, because they are not equally established:\n\n- **Broker-first was measured on a live 30-agent deployment** (issue #1578). Upgrading the broker\n first locks the old manager out immediately: `cotal up` re-renders the broker's generated config\n from the trust record, and after the restart the still-0.48.2 manager is refused on every\n connection with an `authentication error` naming the Nkey, continuously. That text comes from the\n broker process, not from a Cotal command, so match on its shape rather than on an exact string.\n `cotal ps` reports zero agents while\n the agent processes are still alive, because the manager has lost its view of them, not because\n they died. Upgrading the manager clears it immediately.\n- **Manager-first is reasoned from where the new stores are provisioned**, not from a measured\n fleet upgrade. It is the recommended order because the manager is the component that creates what\n 0.49.0 adds, but it has not been run end to end on a production split topology at the time of\n writing. Treat it as the better-supported order rather than a guaranteed one, and keep the\n rollback below ready either way.\n\nWhichever order you pick, **this is not a rolling upgrade**. Between the two steps the mesh is down\nand the manager cannot see its agents. Go straight through rather than pausing between them, and\nschedule it as an outage window.\n\n### What the window looks like\n\n- **The managed agent processes do not survive step 1, in either order.** This is the one place\n where the obvious reordering does not rescue you, so it is worth understanding rather than\n working around. Sparing agents on a bare manager stop is a **handshake**: a 0.49.0 manager\n publishes a capability file proving it can release its agents, and a 0.49.0 `cotal down` refuses\n the stop unless it finds one. **A 0.48.2 manager never publishes that file**, because the\n mechanism ships in the release you are installing. So the old CLI against the old manager sends a\n plain stop and takes every seat with it, and the new CLI against the old manager either refuses\n (leaving `--with-agents`, which reaps deliberately) or falls to the legacy path, warns that it\n cannot verify the manager can spare its agents, and signals it anyway.\n- **You can confirm which side you are on in one command, without stopping anything.** The flag that\n marks the newer behaviour is absent from the older CLI, and its summary line makes the difference\n plain:\n\n ```\n $ cotal down --help # on 0.48.2\n cotal down - stop the whole local stack, or name only the components to stop\n\n $ cotal down --help # on 0.49.0\n cotal down - stop the whole local stack (managed agents stay running unless --with-agents), ...\n ```\n\n If your `cotal down --help` does not mention `--with-agents`, stopping the manager stops the\n agents with it.\n- **Therefore the respawn in step 5 is mandatory recovery for this upgrade, not an optional pass.**\n It is also the step that re-mints credentials as issuances, so it is the same action either way.\n Plan the window to include it rather than treating it as cleanup.\n- The **manager's view** of them is lost while the two sides disagree, so `cotal ps` reports zero\n and control commands do not reach seats.\n- **Messages are not delivered** while the mesh is down.\n- The window is as long as it takes to restart the second component, plus the manager's own start.\n It is minutes, not hours, provided you do not stop between the steps.\n- **Nothing self-heals if you stop halfway.** The refusal is continuous until both sides match.\n\n### Snapshot this before you start\n\nTake these while the deployment is still on 0.48.2. The two `cotal` reads are live reads and must\nhappen before anything stops.\n\n- **A filesystem or volume snapshot of both containers**, if your platform offers one. This is the\n only rollback that covers every case, and it is what the reporting deployment used.\n- **`cotal backup create <dir>`**, for the durable space state, **but read the next paragraph before\n you rely on it**: on a split broker and manager topology it is very likely unavailable to you, and\n the volume snapshot above is your actual rollback.\n- **The trust records and credential directory** under `.cotal/auth` on the manager host, including\n the per-space material directory. These are what a re-mint would otherwise have to replace.\n- **A copy of the channel registry**, so you can verify it came back rather than assuming it did:\n `cotal channels list` before and after.\n- **The output of `cotal ps`**, so you know how many seats you expect to see afterwards and can tell\n a lost view from a lost agent.\n\n#### `cotal backup` on a split topology\n\n**`cotal backup create` cannot read a running stack.** It requires a completed cut, and only\n`cotal down --preserve-state` publishes one:\n\n```\n$ cotal backup create ./backup.0482\n\u2717 backup requires a completed cut; run `cotal down --preserve-state` first\n```\n\n**And `cotal down --preserve-state` requires a manager alive on the host you run it from.** It uses\nthat manager to attest that every retained child stopped, and the check is deliberately fail-closed:\na manager that is dead or merely uncertain refuses rather than preserving an unproven cut. The check\nreads a local pidfile, so a **remote** manager does not satisfy it. On a split topology the broker\nhost has no local manager, which means the documented durable-backup path is not available there.\n\n**Measured rather than assumed, at 0.48.2**: the backup refusal above is executed output. The\npreservation requirement is read from `down.ts` at the same tag, where the preserve path asks a\nmanager to prepare an inventory and then requires that manager to be locally alive before it\ncommits. The part not executed end to end is a genuine two-host split, which needs two real hosts.\n\n**What to do instead.** Use the filesystem or volume snapshot of both containers. That is the\nrollback the reporting deployment actually used, it covers the broker's durable state and the\nmanager's credential material together, and it does not depend on either component being able to\nattest for the other. If you want `cotal backup` as well, take it from a host that does have a live\nlocal manager, and understand it is a second copy rather than the primary rollback.\n\n**This looks like a product limitation rather than a documentation gap**, and it is written here as\none so an operator is not left thinking they mis-typed a command. The upgrade path for the exact\ntopology this page is addressed to cannot use the documented backup command.\n\n### The upgrade end to end\n\n```bash\n# 0. on 0.48.2, STILL RUNNING: record what you expect to see afterwards.\n# These two are live reads, so they must happen before anything stops.\ncotal channels list > channels.before\ncotal ps > ps.before\n\n# 1. manager host. READ THE NOTE BELOW THE BLOCK FIRST: this step ends the\n# managed agent processes whichever order you choose, and the respawn in\n# step 5 is how they come back. It is recovery, not tidying.\n#\n# STOP THE MANAGER WITH THE 0.48.2 CLI, BEFORE INSTALLING 0.49.0. The\n# order matters and it is not recoverable once you install: a 0.49.0\n# `down manager` REFUSES to stop a 0.48.2 manager whose pid record carries\n# a start token, which is every manager on a platform that can read one\n# (Linux can):\n# refusing bare manager stop: ... does not prove this manager can detach\n# its agents; use --with-agents or stop the agents explicitly\n# The refusal names two remedies and NEITHER clears it for this case. The\n# check reads a capability file that only a 0.49.0 manager writes; it never\n# counts agents, so stopping them first changes nothing. And `--with-agents`\n# is whole-stack only, so `down manager --with-agents` is refused by its own\n# flag rule. See #1592.\ncotal down manager # the 0.48.2 CLI, still installed.\n # 0.48.2 has no --with-agents; this\n # is the whole route. On a host that\n # runs the whole stack, the 0.49.0\n # `cotal down --with-agents` after\n # installing is the alternative.\nnpm install -g cotal-ai@0.49.0 # ONLY after the stop above\n# `supervise` RUNS IN THE FOREGROUND and holds the terminal until you stop\n# it. There is no --detach on this command. Start it under whatever keeps\n# your manager alive normally (systemd unit, container entrypoint, or a\n# second terminal), and run the remaining steps from another shell.\ncotal supervise --space <space> --server nats://<broker>:4222\n\n# 2. broker host: stop the stack.\n# NOT `--preserve-state` on a split topology: it needs a manager alive on\n# THIS host to attest its children stopped, and yours is on the other one.\n# Your rollback is the volume snapshot from \"Snapshot this before you\n# start\", not `cotal backup`.\n# See \"cotal backup on a split topology\" above.\ncotal down\n\n# 3. broker host: install 0.49.0 and start it again\nnpm install -g cotal-ai@0.49.0\n# Record the manager log's size BEFORE starting, so step 3a can tell THIS\n# boot's output from every earlier one. It must be captured here, ahead of\n# the start: taken afterwards it sits past the new line and the wait hangs.\n# `<spaceKey>` is NOT the space name. It is lowercase hex of the name's\n# UTF-8 bytes, so space `prod` is `manager.70726f64.log`. Do not guess it:\n# `cotal up` prints the real path on its launch line. Substituting the\n# plain name points at a file that does not exist, and the wait below then\n# burns its full timeout before telling you.\nLOG=.cotal/manager.<spaceKey>.log\nOFF=$( [ -f \"$LOG\" ] && wc -c < \"$LOG\" || echo 0 )\ncotal up --detach --host 0.0.0.0 --space <space> --no-manager\n\n# 3a. SPLIT TOPOLOGY ONLY: `--no-manager` above boots the broker (and the\n# delivery daemon) with NO local manager on the broker host, so there is\n# no wait-and-stop step on a current cotal-ai. The rest of this step is\n# the OLDER-host recipe, kept because the flag is refused there and that\n# refusal is your signal you are on it: without the flag the `up` also\n# starts a local manager, and you must wait for the log to show it is up,\n# then stop it, or you finish the upgrade with two managers and the one\n# you did not intend is the one nobody is watching.\n# A bare `grep -q` does NOT wait: it reads once and exits 1 immediately\n# if the line has not been written yet. Bound the wait instead, so a\n# manager that never comes up fails loudly rather than reading as ready.\n# The log is opened APPEND-ONLY, so on any host that has run a manager\n# before, this file ALREADY carries a `manager up` line from an earlier\n# boot. Grepping the whole file therefore matches instantly and waits for\n# nothing. Read only what THIS boot appended, using the $OFF captured in\n# step 3 above (before the start, which is the only point it is correct):\ntimeout 60 bash -c \\\n \"until tail -c +$((OFF+1)) \\\"$LOG\\\" | grep -q '. manager up'; do sleep 1; done\"\n# exit 0 = THIS boot logged it; exit 124 = it never did, so STOP and look.\n# This manager is 0.49.0 and publishes its own spare-capability file, so\n# the bare stop below is NOT the refusal case from step 1.\ncotal down manager # broker + delivery remain\n# On a current cotal-ai the two commands above are unnecessary (nothing\n# to wait for, nothing to stop) and `cotal down manager` simply reports\n# no manager to stop.\n\n# 4. verify the mesh is whole again before touching the fleet.\n# Do NOT compare `cotal ps` against ps.before yet: step 1 ended the agent\n# processes, so at this point it is EXPECTED to be empty, and an empty\n# `ps` is also the signature of the broker/manager mismatch described\n# above. The two are indistinguishable here, so compare what the mesh\n# itself should have carried across instead:\ncotal channels list # compare against channels.before: this SHOULD match now\ncotal ps # expect it to be EMPTY here; ps.before is the target for\n # step 5, not for this step\n\n# 5. the step that is easy to skip: respawn the managed agents so their\n# credentials are re-minted as issuances and can renew. Persona is a\n# POSITIONAL argument here, unlike `cotal stop`, which requires --name.\n# One call per agent:\ncotal spawn <persona> --detach --name <n> --space <space>\n# then the comparison step 4 could not make:\ncotal ps # NOW compare against ps.before: seat count should match\n```\n\nThe mesh is down from step 2 until step 3 finishes. That is the window. On a split topology there is\nno cut and no backup inside it, so the window is the stop, the install and the restart, nothing more.\n\n## Adding a section for a future release\n\n**Every changeset marked breaking adds a section to this page.** A release that changes what an\noperator must do, in what order, or what stops working, is not finished until the section exists.\n`scripts/upgrade-section-gate.mjs` grades a commit range for this: run it as\n`pnpm upgrade-section-gate --base <ref>` and it reds when the range carries a breaking change and\nadds no new release section. The former Attribution workflow enforced this on each pull request;\nthat workflow and its required-status rule were removed on 22 October 2026. The script remains\navailable for manual checks, and reviewers still check upgrade instructions for breaking changes.\nBe precise about what the check proves. It proves a section for a release\n**was written here**. It cannot prove the section is **correct**, or that it describes the break\nthat actually landed, and it cannot see a breaking change that carries no marker at all. Reviewing\nthe words remains a person's job.\n\n**Mark the break, or the gate cannot see it.** Any one of these is enough, and they are the only\nthings it reads:\n\n- a `!` before the colon in the commit subject, as in `feat(core)!: bind hosted runs to the caller`\n- a `BREAKING CHANGE:` footer in the commit body\n- a changeset in `.changeset/` declaring a `major` bump for any package\n\nThe marker must survive the squash. A `!` that lives only in a commit you squash away is not in the\nrange the gate grades, so put it in the subject that lands on `main`.\n\n**The heading is a `##` and names the release**, like `## From 0.48.2 to 0.49.0`. Both matter, and\nneither is a style preference. Coverage is claimed by a heading, so a heading that names\nno release claims every release and distinguishes none: `## Notes` with a sentence under it would\notherwise satisfy the rule. Naming the release also makes the section the one an operator upgrading\nthat release will search for. Use `###` freely for detail inside a section. Subsections belong to\ntheir release rather than counting as separate coverage.\n\nName the release that first carries the change: the next version Changesets publishes, which\n`pnpm changeset status --verbose` lists. `bin/package.json` on `main` still reads the release already\npublished. If a release is cut while the change is open, the change ships in the release after it,\nso move the heading before merging. The gate accepts any version in a heading, so before merging a\nrelease pull request, check every heading added since the previous tag against the version it\npublishes.\n\nA section is written for the operator, not for the reviewer. It answers, in this order:\n\n1. What keeps working with no action at all.\n2. What does **not** migrate, and when that becomes visible. Name the log line if there is one.\n3. The order to move components in for a split topology, and why that order.\n4. What the outage window looks like, including what survives it.\n5. What to snapshot before starting.\n6. The commands, end to end.\n\n**Where an answer was not measured, say so in the document rather than guessing.** An operator who\nknows which half of a recommendation is reasoned and which is measured can plan around it; one who\nfinds out afterwards cannot.\n\n## Handle redemption in 0.91.0\n\n`auth redeem <private-handle-file>` selects the native auth target, then refuses unsupported\nconferring-handle redemption without reading the file or issuing credentials. It does not redirect\nthe artifact into agent enrollment or interactive-session redemption. Offline handle inspection\nstill labels signatures, revocation and holder currency not checked. An explicit `--space`\nrequests static-registry signature inspection with a native ephemeral operator reader. It checks\nsignatures, issuer scope, presented links and required revocation records without establishing\npresenter identity, live epochs or use authority. It confers no holder credential, persists no\nreader credential, and never turns missing or stale native records into verified offline success.\nOpen/user-auth inspection and conferring redemption remain unsupported.\n\n## Dashboard service transition in 0.91.0\n\nThe matching dashboard package now registers a service preset and hidden native child, not a\npublic web command. An older cached command descriptor is hidden and refuses with the matching\nextension-update remedy before importing provider code or repairing the install. `update` remains\nexplicit; `service start web` never installs a deliberately absent extension.\n\nConfigure the saved web definition with its existing registered static-auth root, one protected\ncredential file and explicit read-only intent. The service retains required TLS, creates no caller\nor channel-purger credential, opens no browser and prints no bootstrap secret into logs. The page\nmarks it read-only and hides delete; the server independently rejects that mutation before any\nalternate connection. The private session file holds the full-launch bootstrap; logs give its\nreference only. User-auth/open targets and OS installation remain unsupported in this route.\nExisting legacy process records are still observed, never silently adopted. An already-running\nlegacy dashboard is not restarted or converted by loading metadata. See [dashboard setup](watch-a-mesh.md#web-dashboard).\n\n## Registered lifecycle retirement in 0.91.0\n\n`auth retire <full-lifecycle-uid> [--yes]` has closed UID/space input, but the standalone CLI still\nlacks a supported native retirement instrument carrying its own current serve identity and gate.\nIt refuses without publishing a request. It does not borrow the manager's identity, treat --yes as\na grant, or call the host's interactive/managed loopback doors as a substitute. This remains an\nexplicit authority gate, not a completed retirement operation.\n\n## Local-process `rootedAt` removal in 0.91.0\n\nA local-process extension descriptor no longer has a `rootedAt` field. No stop has read it since\nthe dashboard became a service. Operators have nothing to migrate, snapshot or order, and there is\nno outage: a manifest that an older install cached with the field still loads, and the next\nmanifest rewrite drops it. An extension author removes the field from each `LocalProcess`\ndeclaration, where TypeScript now reports it as an unknown property.\n"
|
|
27489
|
+
"body": "# Upgrading a running deployment\n\n> **Guide** (informative) \xB7 **For:** operators upgrading a mesh that already exists \xB7 **See also:** [Substrate stability](stability.md), [Run a mesh](run-a-mesh.md), [Identity and auth](identity-and-auth.md)\n\n[Substrate stability](stability.md) tells you what the version numbers promise. This page is the\nother half: what to actually do when the deployment already exists, has credentials in it, and\ncannot simply be recreated. Every release that breaks a running deployment gets a section here,\nnaming what migrates on its own, what does not, and the order to move the pieces in.\n\n## The pre-1.0 upgrade contract\n\nThe packages are pre-1.0, so a minor bump may break an API or an on-disk expectation. Four\ncommitments make that survivable for someone with a fleet:\n\n- **Pin an exact version.** `0.N.P`, never `^0.N.P`. A range can pull a breaking minor in during an\n unrelated reinstall.\n- **Every break that touches a running deployment gets a section on this page**, written in terms of\n what an operator does, not in terms of which module changed.\n- **Read the section before you start, not halfway through.** A section names the work up front\n precisely so the operation does not change shape once it is underway.\n- **A break that cannot be made automatic says so.** Where credentials or state must be recreated by\n hand, the section says which ones and when, rather than leaving you to discover it at the moment\n the first one stops working.\n- **A change to the shape of a credential, or to who may renew one, is breaking whatever the commit\n marker says.** This rule is stated because the marker is a judgement made while writing the code\n and the consequence is felt by someone running it a day later. A fleet that keeps authenticating\n looks compatible and is not, if nothing in it can renew. Any automated check of this rule would\n read commit markers, so a break recorded as a feature is the one case it could not see, which is\n why the rule is written for people first. **The marker held for this release: the 0.49.0 change\n that caused all of this, `36d177951 feat(core)!`, did carry its `!`.** The rule exists for the\n next one that does not.\n\nWhat this page does not promise is a rolling upgrade. Nothing in the current line dual-serves two\nauthority versions, so where broker and manager run separately there is a window in which the mesh\nis down. The sections below give that window's shape so it can be scheduled rather than endured.\n\n## Endpoint caller files after 0.99.0\n\n`cotal auth mint <name> --endpoint <class> --out <file>` from 0.91.0 through 0.99.0 recorded the\nendpoint's wire token, such as `com_example_reviews`, in `<file>.issuance.json`. `ep describe`,\n`ep invoke` and `invoke` with `--creds <file>` then failed with \"the endpoint contract read did not\ncomplete\", so those files could not make a call. A current client refuses that intent with\n`SCHEMA_INVALID`. Issue a new caller to a new `--out` file; the old output and intent block reuse of\ntheir path.\n\n## Personal native connections in 0.92.0\n\nUpdate the CLI and its Claude plugin together, then reopen the native Code session. Existing static\nauth connections keep their credentials and gateway tools. Personal signed-in connections are new;\nthe plugin binds a native conversation through its ordinary command hooks before creating one.\nThis works without intercepting the `classic.*` events protected by Claude's built-in guard; keep\nthe guard enabled. Organization policies that disable command hooks or native modules still apply.\nRemote meshes must advertise\nconversation identity v1 on the selected registration's HTTPS origin. The existing account signs\nin through Cotal; do not copy login files or paste a token into Claude.\n\nEach signed-in conversation gets its own roleless actor, direct messages and an own-event-only\npublication grant. The space's required-event policy remains in force. Desktop Chat cannot use this\nnative path because it has no Code transcript binding.\n\nCreation persists the request and secret before contacting the authority. Retrying an interrupted\ncreation reuses them. Disconnect can report pending cleanup; retry it for the same connection.\nPrivate recovery state remains until the authority confirms that exact lifecycle is retired.\nNeither an upgrade nor a lost response silently creates replacement authority.\n\n## Cooperative workflow cancellation in 0.92.0\n\nThe manager adds the issued, instance-bound `run-cancel` command. Its cluster revision advances to\n37, retaining the vitals and keyed-submission commands. Cancellation is available through the\ngeneric endpoint client; this change adds no CLI or MCP cancellation shortcut.\n\n### What keeps working\n\nExisting run records, admissions, revocation markers and journals keep their formats. Upgrade does\nnot rewrite them, cancel a run or remove a marker. A released run without a marker can still resume\nunder its admission. A revoked run remains revoked. Manager shutdown still requests boundary\nrelease; it does not write a cancellation marker or prove an agent process stopped.\n\n### What needs an update\n\n- Custom `RunHost` implementations must return a drive with `cancel(reason)`, in addition to\n `done` and `release(reason)`. It must propagate cooperative root cancellation to in-flight\n effects. Do not substitute boundary release or process termination. Rebuild and qualify the\n custom host against the new core contract before enabling it on an upgraded manager.\n- The built-in runtime and language packages carry the root signal through both engines and the\n compiled worker. Keep core, auth, manager, runtime and language on the same release; copying one\n source file or replacing only the manager does not supply the new protocol and worker code.\n- Existing caller credentials do not gain a new publish permission on installation. Obtain fresh\n issued credentials through the supported issuer before using `run-cancel`. Static credentials\n and caller generations must still meet the original lifecycle and grant checks. Never edit a\n credential to add the permission.\n- The new command requires a fresh Describe-derived instance and epoch bind. An old issuer lacks\n its independent request observer and inspect/commit path, so a new signerless manager cannot use\n that issuer for cancellation. Do not treat missing support as a reason to bypass the issuer.\n\n### Order for a split topology\n\nSchedule a maintenance window for the version change; this is not a rolling-compatibility promise.\nTake the deployment's usual consistent backup of broker state, including run records, admissions\nand journals, and the issuer's private configuration. Upgrade the issuer, then the manager and its\nruntime dependencies to the same release, using their normal stop/start procedure. Reinstalling a\npackage does not replace modules already loaded by a running process. Reconnect callers through\nthe upgraded issuer and resolve the manager's current contract before invoking cancellation.\nThere is no broker format migration for this command and no automatic cancellation during upgrade.\n\n### What the result establishes\n\nA durable marker denies future admission but is not proof of cessation. Only `stopped` with the\ncaptured drive outcome establishes that local drive's result. If the holder recheck fails or the\nrequest budget expires, a parked effect can remain running beside a committed marker. Preserve an\nunknown result; do not blindly resend it or declare cleanup complete. To continue cancellation,\na caller can make a fresh explicit request, which observes the marker and current holder before\nreporting its own result. Pending cancellation intent can also block a local launch after a definite\nno-dispatch refusal if marker absence was not confirmed within the budget.\n\n`run show` now displays revocation independently of recorded state. An unreadable marker yields\na partial result and exit 4 in human and JSON output, rather than an apparently complete view.\nAutomation must handle that existing partial-result convention. None of these observations proves\nmodel or tool preemption, physical process cleanup or cessation on another host.\n\n## Service authority status selector in 0.92.0\n\nA service endpoint authority status query that reads one operation now names that operation:\n`target: { kind: \"operation\", operation, requestId }`, where `operation` is `register`,\n`activate`, `renew`, `ready` or `revoke`. `@cotal-ai/core` exports the operation names as\n`ServiceStatusOperation` and the whole selector as `ServiceStatusTarget`. The 0.91.0 selector,\n`{ kind: \"operation\", requestId }`, no longer parses. The change is in `parseServiceAuthorityRequest`,\n`parseServiceAuthorityEnvelope` and the `ServiceAuthorityRequest` type, so it reaches every client\nand server built on them.\n\nA `revoke` operation status parses, but the 0.92.0 auth service does not answer it yet: it is\n`denied` with reason `unsupported` (HTTP 422), as SPEC states. To follow a revocation, read\n`target: { kind: \"instance\" }`, which reports `revoking` with the revoke's operation id.\n\n### What keeps working\n\n- Every running mesh. 0.91.0 shipped the request types and the client (the auth provider's\n `serviceEndpointAuthority` hook and its HTTP client), but no 0.91.0 auth service serves\n `/service-endpoint-authority`, so no deployment has status traffic on this route to lose. In\n 0.92.0 only a context opened with `serviceEndpoints` serves the route. That option is off by\n default and has no CLI flag; without it the route answers 404.\n- `target: { kind: \"instance\" }` is unchanged, and so are the register, activate, renew, ready and\n revoke requests themselves.\n- No credential, broker or connection is involved. Nothing needs reminting and no broker restarts.\n\n### What does not migrate\n\n- **Code that builds an operation status query without `operation`.** The 0.92.0 provider hook and\n client parse each request before sending it, so the query fails locally with a `bad-request`\n error, `service-endpoint authority: <detail>`, whose detail is\n `object has missing or unsupported fields`, and nothing is sent. An operation name outside the\n five gets the detail `unsupported status operation`.\n- **A 0.91.0 client still sends the old selector.** A 0.92.0 auth service opened with\n `serviceEndpoints` answers it with HTTP 400, whose body carries the same parser message. The\n service reads and writes nothing for it and logs nothing. The 0.91.0 provider hook and client\n never show that message: they cannot parse the 400 body as a protocol result, so they throw a\n `ServiceAuthorityTransportError` whose message is\n `service authority returned an unconfirmed response; retain the existing request identity`, with\n `httpStatus` 400 and outcome `not-executed`. A status query is a read, so nothing ran and\n nothing needs replaying. An auth service without `serviceEndpoints` answers 404 before it reads\n the body.\n- **No legacy operation is inferred.** Neither the client nor the service maps an old selector onto\n `register` or any other operation. One requestId can key a different request under each\n operation, so a guessed operation could answer with another request's status. Send the operation\n the request was made as; the requestId you stored with it stays the same.\n\n### Durable data\n\nNothing stored changes on upgrade, and nothing is rewritten. The selector is a request shape and is\nnever stored. 0.91.0 wrote no service authority request intents or operation records, so there is\nnothing to convert. A 0.92.0 context opened with `serviceEndpoints` writes its own records from the\nmoment it is enabled: an admission record when it first opens, then request intents, the\nper-instance intent index, operation, proposal and assignment records, and endpoint gates. Each\nintent already records its operation. Without `serviceEndpoints` it writes none of them.\n\n### Order for a split topology\n\nUpgrade every client that builds status queries together with, or before, the first auth service\nyou open with `serviceEndpoints`. That means code using `ServiceAuthorityRequest` or\n`parseServiceAuthorityRequest`, and every process that calls the auth provider's\n`serviceEndpointAuthority`. This change adds no ordering for brokers, managers or connectors;\norder those as their own sections say.\n\nThere is no compatibility window to schedule. A 0.92.0 auth service never accepts the old selector,\nand a 0.91.0 auth service has no handler for the route at all, so a mixed pool is not partly\nworking: every 0.92.0 auth service opened with `serviceEndpoints` refuses an old client's\noperation status queries with 400, which that client reports as the unconfirmed-response error\nabove, and one without `serviceEndpoints` answers 404. Nothing else is interrupted, so the change\nhas no outage window of its own.\n\n### Before the upgrade\n\n- Nothing needs a snapshot for this change, because it writes nothing. If the same upgrade first\n enables `serviceEndpoints`, take the deployment's usual consistent backup of broker JetStream\n state and the auth host's private configuration before that step, because the service endpoint\n host then starts writing to the auth bucket.\n- Find the old selectors in your code, then name the operation in each:\n\n```sh\ngrep -rn 'kind: \"operation\"' --include='*.ts' --include='*.js' src/\n```\n\n```ts\n// 0.91.0\n{ ...coordinates, operation: \"status\", target: { kind: \"operation\", requestId } }\n// 0.92.0\n{ ...coordinates, operation: \"status\", target: { kind: \"operation\", operation: \"register\", requestId } }\n```\n\nWhat this section measured, and what it did not: the local refusal and the accepted and refused\nselectors are covered by the core parser's own test. The 400, 404 and `unsupported` answers, and\nthe absence of a log line, are read from the auth service's source. The 0.91.0 client's error is\nread from the 0.91.0 client and core parser source; the client file is unchanged since 0.91.0. No\nmixed-version deployment was run.\n\n## History reader deletes after 0.86.0\n\nObserver and admin credentials now carry a `CONSUMER.DELETE` row for each of the connection's eight\nhistory-reader names, `hist_<readerId>_<n>`, on CHAT (and on DM for admin). `channelHistory`,\n`multiChannelHistory` and `dmHistory` read under those names and delete each reader when its step\nends. Before this, every read left its consumers for five minutes, and a dashboard polling history\nfilled the stream's consumer limit until reads were refused with \"maximum consumers limit reached\".\n\n### What stops working\n\nA static observer or admin credential minted by 0.86.0 or earlier has no reader-delete rows. On it,\na current client's history read fails with \"it predates the observer and admin reader-delete grant,\nso remint it\", and leaves one reader per stream until the broker's five-minute inactive threshold.\nA credential the CLI mints at start (`cotal console`, `cotal web` on a static auth mesh) is current\nafter the upgrade.\n\n### Before the upgrade\n\n- Remint every observer or admin credential a process reads from a file: `cotal auth mint <name> --profile\n observer` or `--profile admin`, and any `--creds` file given to a dashboard.\n- On a user-auth mesh, the auth service mints these views per connection. Upgrade it before, or with,\n the hosts that read history, so no current client connects through an older auth service.\n\n## Native key issuance recording after 0.85.0\n\nBefore upgrading, take a consistent backup of broker JetStream state and the auth host's private\nconfiguration and signing material through the deployment's supported backup procedure. The auth\nbucket must use file storage. Allow space for one issuance record per new callout exchange until\nits minted expiry plus clock tolerance; static credentials have no such record.\n\nEmbedders that call `startAuthCallout` must supply the required `recordIssuance` callback. It must\nfinish a durable create-only write for the broker-generated user key and recheck the validated\nlifecycle and credential authority before returning. A missing callback refuses startup; a failed\nwrite or recheck denies the exchange. Do not use the isolated tests' no-op callbacks in a deployment.\nThe stock `startAuthService` authority plane wires the recorder and its pruning automatically.\nExisting auth records need no manual rewrite, and no tenant or participant grant is added.\n\nFor split deployments, pause new managed launches and stop every old auth-callout responder before\nstarting the matched new auth service. A mixed responder pool can still issue keys without records.\nNew connections and reconnects cannot authenticate while the callout service is down. Existing\nbroker connections keep their current JWT expiry; this change needs no broker restart. Upgrade the\nCLI, Manager and connector packages together before admitting new managed launches, using the\nruntime's supported Manager custody or recovery path. This source change does not promise that\nrunning seats survive a Manager restart.\n\nThe new service prunes expired issuance rows before opening and on a timer. A corrupt row refuses\nopening or fences the running authority plane; investigate the stored evidence through the normal\noperator recovery procedure. Do not erase the auth bucket or recreate signing keys to clear it.\nRows written before a later authority-check failure can remain until pruning; they do not prove\nthat a client received a JWT or established a connection.\n\nConnections admitted before the last old responder stopped, static JWTs and old responders have no\nissuance record. They are not backfilled or identified from names. Bearers without current lifecycle\nand credential claims cannot be recorded and are refused; use the supported enrollment or renewal\npath for the selected release. These records alone do not provide persistent native-key cutoff or\ncomplete revocation. Reintroducing an old responder also reopens the recording gap.\n\n## Native Claude MCP discovery after 0.82.2\n\nBefore upgrading, snapshot Cotal configuration and the Claude profile on each launch host.\nOrdinary Claude spawns now load the host's native MCP sources alongside declared Cotal overrides.\nAn empty sharing map no longer suppresses native servers. Audit enabled servers and their\ncredentials before admitting new seats. Use an explicit sharing selection, including\n`--share-tools none`, to retain isolation. Existing sessions keep their loaded configuration.\n\nNo broker restart, credential-format migration or cross-host profile copy is required. Upgrade\nthe CLI, core, manager and connector together on each launch host. Pause new managed spawns\nwhile adopting the matched packages; use the runtime's supported manager custody or recovery\npath. Any interruption is that manager transition, not a change to the broker wire contract.\nCarried private homes stay isolated and do not import the operator's native profile.\n\n## Connector-store protection after 0.82.2\n\nSnapshot the private Cotal state, connector store and mesh configuration before upgrading.\nInstall the new CLI on each CLI and manager host, then verify the selected executable before\nrunning setup or store maintenance. The POSIX installer prints its absolute launcher because\nit cannot change the parent shell's command cache, aliases or functions. An older executable\nalready on disk remains unsafe for a newer store; do not use its force advice to recover.\n\nForward setup and connector seeding migrate built-ins automatically. Same-generation force and\nreset rebuilds remain supported. An executable older than the store now refuses every backward\nmigration, including force and reset. Select a verified executable at least as new as the store\nand run normal seeding instead. No credential or mesh-record format needs manual migration.\n\nA split deployment needs no broker restart for this change. Keep each host's CLI and connectors\non the same release. Pause new managed spawns while adopting it; a running manager keeps its\nloaded code until restarted. Use the chosen runtime's supported custody or preserve-state path.\nThe interruption covers that manager transition and depends on its runtime, not a broker outage.\nDo not overwrite a live manager installation or stop an unidentified listener to force adoption.\n\nAn implicit foreground spawn now chooses a numbered actor when the requested actor is occupied.\nExisting grants, launch material and sessions stay in place. An explicit name remains pinned\nand refuses an occupied actor rather than renaming it. Signed enrollments and managed handoffs\nkeep their existing identity semantics.\n\n## Background startup in 0.91.0\n\n`cotal up` now returns after background startup by default. `cotal up --detach` retains that\nmeaning. Use `cotal up --foreground` when terminal lifetime should own the native stack.\n\nA systemd unit, launchd KeepAlive job, container entrypoint or script that supervises the `up`\nprocess must add `--foreground` before upgrading.\n[Persisted callers](#persisted-callers-in-0910) gives the literal\nreplacement for each.\n\nA project folder an earlier release started keeps its own launcher after the upgrade. Stop it with\n`cotal down` as usual, upgrade, then run plain `cotal up` in that folder: it starts on the retained\nlauncher with the same identity, store and history. It does not need `space create` or `space add`.\n\nRecorded recovery requests retain their original foreground/background intent. They do not\nreinterpret a prior request using the new default. This change does not migrate a whole stack to\nowned services.\n\nPrepared plain-loopback user-auth roots can use the controller's broker, auth service, delivery and\nmanager members. First enable may return a known partial result after the first three start: the\nmanager needs an existing signed-in, granted CLI login. Sign in and obtain that grant, then run\nbare `cotal up`; no static caller or new grant is substituted. Auth endpoints come from the\ncreation-pinned child's native provider readiness, not a guessed URL. The controller keeps unknown\noperations blocked. Existing three-member auth/open journals are read with an unattempted auth row\nand written with all four roles; old clients do not understand that new record shape. Upgrade the\ncomposed CLI together before using it. TLS-required, remote and public-exchange user-auth paths\nretain their native route.\n\n## Persisted callers in 0.91.0\n\nSome command lines outlive the install that wrote them: OS units, scripts and container commands.\nAfter `npm i -g cotal-ai@<new>` they reach the new binary before any `cotal` command has run. This\nsection says what each one does after the upgrade and what to change before it.\n\n### Command mapping\n\n| Earlier command | Command in 0.91.0 |\n|---|---|\n| `meshes`, `meshes add` | `space`, `space add <name>` |\n| `clean store`, `clean all` | `reset store <space>`, `reset space <space>` |\n| `mint <name>` | `auth mint <name>` |\n| `mint --signer` | `auth signer --out <private-file>` |\n| `actor list`, `actor grant <actor>`, `actor revoke <actor>` | `auth grants`, `auth grant <actor>`, `auth revoke <actor>` |\n| `join [channel]` | `chat [channel]` |\n| `console` | `watch` |\n| `channels` | `channel` |\n| `send dm <agent>`, `send msg <channel>`, `send ask <role>` | `send -t <agent>`, `send -c <channel>`, `send -r <role>` |\n| `personas` | `persona` |\n| `endpoints`, `describe`, `invoke` | `endpoint`, `endpoint describe <target>`, `endpoint invoke <target> <command>` |\n| `deregister-instance` | `endpoint deregister <class> <instance-id>` |\n| `reconcile-gate`, `seats` | `manager reconcile`, `manager seats` |\n| `topology view` | `team check <file> --graph` |\n| `down -f <manifest>` | `team remove <deployment-id>` |\n| `status --components` | `service` |\n| `run journal`, `run migrate`, `run ps` | `run show`, `run check`, `run` |\n| `deliver pending <name>` | `pending <agent>` |\n| `web` | `service add <name> --preset web`, then `service start <name>` |\n| The four daemon entries | Their service presets, under [deprecated daemon entries](#deprecated-daemon-entries) |\n\nThe sections on this page for each command family give the details of its change.\n\n### What stops working\n\n`cotal up` returns after background startup by default. An earlier line such as\n`cotal up --space <space> --server <url>` that a supervisor runs as its main process now exits\nonce the stack is started. systemd then treats a `Type=simple` unit as finished and can stop the\nprocesses that unit started, and a restart policy starts another attempt. A launchd `KeepAlive`\njob relaunches it the same way. Add `--foreground` before upgrading. For a systemd unit, replace\n`ExecStart=/usr/bin/cotal up --space <space> --server <url>` with:\n\n```ini\n[Service]\nType=simple\nExecStart=/usr/bin/cotal up --foreground --space <space> --server <url>\n```\n\nFor a launchd job, add `--foreground` after `up` in `ProgramArguments`:\n\n```xml\n<key>ProgramArguments</key>\n<array>\n <string>/usr/local/bin/cotal</string>\n <string>up</string>\n <string>--foreground</string>\n <string>--space</string>\n <string>main</string>\n <string>--server</string>\n <string>nats://127.0.0.1:4222</string>\n</array>\n```\n\nA container command or script makes the same change to its `up` line. When background `up` is the\nmain process of a systemd unit, it prints a warning naming the unit and the `--foreground` remedy\nafter it starts or refreshes the stack. `up -f`, which has no foreground form, and a dry run print\nnothing. A launchd job gets no warning. `cotal up --detach` keeps its meaning. The warning changes\nno mode and no unit, and no unit or script is rewritten or restarted.\n\n`cotal spawn <name>` now launches through a manager and attaches. A container or script that ran\nan agent directly with `spawn <name>` adds `--foreground`.\n\n### Deprecated daemon entries\n\n`supervise`, `deliver`, `auth-service` and `feedback-intake` stay accepted with their existing\nflags for every caller: units Cotal installed, units and scripts you wrote, and container commands.\nThey are deprecated and are removed in `1.0.0`, one release for all four. Help and completion no\nlonger list them. Each run prints one line on stderr naming its replacement and that release.\nStdout, behavior and exit codes are unchanged, so no unit restarts in a loop because of the\nupgrade. `deliver pending` warns the same way and names `cotal pending <agent>`.\n\n| Entry | Replacement |\n|---|---|\n| `supervise` | `cotal service add <name> --preset manager` |\n| `deliver` | `cotal service add <name> --preset delivery` |\n| `auth-service` | `cotal service add <name> --preset auth-service` |\n| `feedback-intake` | `cotal service add <name> --preset feedback-intake` |\n\n`cotal service add <name> --preset <preset> --help` lists the options a preset takes, and\n`cotal service start <name>` starts it. A `supervise --roster <file>` line maps to the manager\npreset's `--roster <file>`, with an absolute path, and `cotal service start <name> --foreground`\nkeeps the manager in the foreground as `supervise` did. The manager, delivery daemon and auth service that `up`\nstarts itself do not warn, so the line in a log always comes from a unit, script or container you\ncan change.\n\n### Owned units\n\nA manager unit that an earlier `cotal service install` wrote keeps calling `supervise`. After the\nupgrade it starts the new binary and prints the deprecation warning on each start. This release\ndoes not rewrite that unit.\n\nA unit that this release's `cotal service install <name>` writes runs the hidden `__service-unit`\nentry, which an earlier binary does not have. Before you downgrade, stop it with\n`cotal service stop <name>` and remove it with `cotal service uninstall <name>`. After the\ndowngrade, install the unit again with the earlier binary's `service install`.\n\n### Containers\n\nThe `solo` service in `deploy/docker/compose.yaml` runs `spawn scout --foreground`. The team\nservice and the image's default command still run `supervise` with `--roster`, so their logs show\nthe deprecation warning. An image you already built carries its own CLI and is unaffected until\nyou rebuild it.\n\n### Before the upgrade\n\n- Add `--foreground` to every unit, launchd job, container command or script that supervises\n `cotal up`.\n- Add `--foreground` to every container command or script that runs `cotal spawn <name>` directly.\n- Move units and scripts that call `supervise`, `deliver`, `auth-service` or `feedback-intake` to\n their service presets before `1.0.0`.\n- Before a downgrade, stop and uninstall every unit this release's `service install` wrote.\n\n## Team commands in 0.91.0\n\nThe new grammar names fresh intent `team boot <file>` and additive intent\n`team deploy <file>`. `team check <file> --graph` resolves declared access\nwithout a broker. `team` and `team show <id>` read local fresh summaries, additive ledgers and\nagent/channel returned receipts; `team remove <full-id>` is the ID-only additive\nteardown name. An edited manifest is not authority to remove an old deployment.\n\nBoot's `--dry-run` is a declaration view.\nDeploy's `--dry-run` reads native presence and channel registry for advisory\nclassification against a prior ledger. Remove's preview compares a legacy\nledger with native manager PS and display-only membership observations, or a\nreturned channel-only receipt with current KV revisions. No preview authorizes\na launch or stop. Bare `team boot <file>` enters guarded native fresh\nstartup only for a prepared, stopped local root; it refuses a leftover manager\nrecord and offers rich, JSON and plain receipts. User-auth boot records an intent before\nowner/trust preparation and retains uncertainty without retry. Windows boot\napply refuses, because protected service-record inventory is not supported there.\nBare `team deploy` applies all-new channel-only manifests with explicit live\ndelivery, or one to 100 all-new agents on a static/open mesh, optionally with\nup to 100 new live channel cards. A mixed deployment records one intent before\ncreating channels and launching agents through one pinned manager/runtime.\nEach channel revision and original launch request, allocation and matched join\nis recorded before continuing. A full receipt requires both sequences to match.\nUnknown effects are retained, not replayed; completed effects are not rolled back.\nBoth routes offer rich, JSON and plain output. Identical settled deployments can return a verified\nno-op without new effects. Named stale-agent waivers retain the old policy without restarting.\nUser-auth agent apply and existing-card mutation still refuse effects.\n\n`team remove <id>` can retire a returned static agent deployment through its original manager.\nReview `--dry-run` first; scripts require `--yes`. This needs the native slot query, original UIDs\nand native retired slot/head plus cleanup evidence, not just stop acknowledgements. A returned\nchannel-only deployment instead uses deletes conditioned on its original card revisions. A static\nmixed deployment retires agents first and needs a complete retirement receipt before checking and\ndeleting cards, under one confirmation and maintenance lock. Both receipts must exist before its\nblocker is released. Partial and unknown effects retain their journals without replay. History,\ngrants and readers are unchanged. Open/user-auth agent retirement and legacy removal still refuse.\nThe old `spawn -f` manifest spelling no longer deploys. Native `up -f` still\nstarts a fresh mesh; `down -f <file> --run <id>` remains the retained route\nonly for an older additive ledger. Move an automated agent deploy or teardown\nto the new spelling only when its auth mode and manifest are ones these routes accept.\n\n## Credential commands in 0.91.0\n\nCredential minting is `cotal auth mint <name>`. The old `cotal mint` spelling refuses before\nissuance and names its replacement. A stripped account signer is exported with the explicit\n`cotal auth signer --out <private-file>`, not `mint --signer`.\n\nThe default standing credential label is create-only. Repeated minting does not silently replace\nit. Choose a new label or a separate private output. An explicit existing output is replaced only\nwhen it is an owned private native credential matching the supplied `--identity`. Explicit\nexports cannot overwrite managed `.cotal` or workstation state.\n\nThe new receipt reports the actual principal, resulting lifecycle, private destination and\nnative expiry without credentials. Reusing an nkey does not promise reuse of its old lifecycle\nor bind-only inboxes. An agent-profile re-mint reports its newly allocated UID and warns about\nthat distinction; consuming clients must use the returned UID. Provisioning is explicit and is\nnot evidence that a client joined. Scripts should parse the finite `--json` document rather than\nscraping the former text lines. Secret and signer files must stay out of support bundles.\n\n`auth mint <name> --endpoint <class> --out <private-file>` is a separate caller mode. It uses the\nsame native static authority and a live digest-verified untargeted synchronous ephemeral surface.\nTargeted, journaled or action commands refuse the whole issuance; no unsupported row is dropped.\nThe native agent footprint and exact additional endpoint rows are provisioned with required TLS,\nwithout adding spawn/run/admin scope or starting a seat. `--channels` enters both explicit channel\nACLs and defaults empty; `--expires-in` remains native seconds.\n\nThe create-only private `<out>.issuance.json` intent is retained before footprint work and contains\npublic identity/lifecycle/scope metadata only. It is not a native grant or completion receipt. An\nexisting output or intent blocks replay, including when an interrupted operation left no output.\nKeep that intent with any original file while diagnosing an unknown result. Provisioner close is\nreported separately. No raw-file caller identity, managed launch, user-auth issuer or environment-\nexport onboarding is added by writing this credential.\n\n## Endpoint deregistration in 0.91.0\n\nReplace `deregister-instance --endpoint <class> --instance <id>` with\n`endpoint deregister <class> <full-instance-id>`. Both positional values are required.\nThe old spelling refuses before effects and has no implicit instance selection. Review the\nnew `--dry-run` output, then use `--yes` in scripts. JSON retains native revision receipts;\nunknown or superseded deletions are not retried. User-auth and open targets do not receive\na static-signing fallback.\n\n## DM pending inspection in 0.91.0\n\nUse `cotal pending <agent>` for a one-document JSON/plain read of native DM\ndurable counts and bounded candidate IDs. `--durable <exact-dm-durable>` targets\nan older lifecycle directly. The retained `deliver pending` spelling still\nuses its earlier output shape; scripts must migrate to `result` fields rather\nthan assuming the old per-line format. Candidate IDs are not proof of\nrecipient delivery or complete message history. This route has only the\nstatic/open admin read authority; user-auth has no corresponding view yet.\n\n## Live space deletion in 0.91.0\n\n`space forget <name>` changes only the local registration. It does not delete\nlive broker state. `space delete <name> --dry-run` now reads known first-party\nnative streams, bucket backing streams and presence entries on the registered\nbroker in open or static-auth mode. Static transfer-stream names are read under\nan additional exact-name grant on a second confirmed connection. This non-atomic\nread is not an apply plan or evidence that presence records are live. User-auth\ninspection and deletion remain unavailable. Bare deletion now requires a typed\nexact-name confirmation on a terminal or the exact positional name with `--yes`\nin a script. It rereads the native impact and checks the current target before\nsubmitting exact stream deletes, stopping on the first refused or unconfirmed\nreply with acknowledged and remaining names. A native post-read reports any\nsurvivor or newly observed transfer target without retrying. No process, registration or local\nfile is removed.\n\n## Stopped reset in 0.91.0\n\n`reset store <space>` and `reset space <space>` are the named replacements for\n`clean store` and `clean all`. `reset store --dry-run` provides a partial\nlocal-impact preview of the store path, recorded processes, maintenance journal\nand broker credless reachability. `reset store` requires confirmation,\nacquires the maintenance lock, rechecks its exact store identity, legacy\nprocesses and all recorded service instances for that space, and calls the\nexisting store-only cleanup. Any recorded supervisor blocks reset. It keeps trust and the\nregistration. TLS-required or TLS-first store targets refuse, because no trusted\nstopped-broker check covers them. `reset space` now routes a\nreviewed local identity reset through native clean-all. It requires Linux, an\nisolated locally started registration and plain-NATS loopback nonresponse.\nIts preview inventories provider-declared keys and native filesystem targets;\napply rechecks the scope under the maintenance lock and constrains each delete\nto that reviewed inventory. Trust is removed last, followed by the unchanged\neligible registration. Unknown effects are not replayed. Service definitions,\npersonas, packages, IdP logins and remote data stay. Unsupported providers,\nforeign material, TLS-first and non-Linux targets refuse.\n\n## Backup maintenance in 0.91.0\n\n`backup create` supports one-document `--json` and plain reports. The completion receipt follows\nnative attempt cleanup. A published artifact with unconfirmed cleanup is partial; retain its\npath and original attempt instead of repeating creation. `backup inspect` reads metadata and\n`backup verify` checks declared file sizes and hashes. Neither proves restoration or authority\ncontinuity, starts a broker or writes a staging tree.\n\nThe canonical stale pre-commit recovery is `backup recover <full-attempt-id>`. Healthy retained\nold-source cleanup is `backup cleanup-fallback <full-attempt-id>`. Review `--dry-run`, then use\n`--yes` in scripts. Neither flag bypasses owner, deadline, root, journal revision or store-identity\nchecks. Restore startup stays on its guarded existing route.\n\nA controller-owned stack cannot be preserved in 0.91.0. On a root with a stack journal,\n`down --preserve-state`, `backup create`, `backup restore` and `up --restore` refuse `UNSUPPORTED`\nbefore any effect. Stop such a stack with plain `cotal down`; it keeps its store, channels and\nhistory for the next `cotal up`. Preserve, backup and restore of a controller-owned stack return in\na later release. A root without a stack journal keeps its preserve, backup and restore.\n\n## Feedback input in 0.91.0\n\nUse a protected `feedback --key-file <path>` rather than placing an intake key on argv.\nThe old `--key` spelling refuses without sending or echoing its value. Public feedback still\nworks with a contact email and requires no environment setup. JSON/plain output is one public\nreceipt, not a copy of the submitted body. Unknown submission outcomes are not retried, and no\nredirect or alternate credential/intake is substituted.\n\n## Credential diagnosis in 0.91.0\n\n`doctor auth` uses JSON/plain reports and distinguishes local claims from live authorization.\nA plain diagnostic that encounters material requiring migration refuses without moving it.\nExplicit `--fix` retains the native static daemon repair, with separate migration/re-sign receipts\nand no user-auth signer fallback. A local re-sign is not daemon adoption or broker acceptance.\nUnhealthy, partial and unknown results retain their classified outcomes; scripts must read the\nnew result and error fields instead of scraping the old `auth: healthy` line. Never respawn from\na locally ordered older credential file alone.\n\n## Service adapter declarations in 0.91.0\n\nA local-process provider can expose the version-1 workstation service ABI alongside its existing\nprocess metadata. It declares a closed option schema, explicit foreground/multi-instance/log\ncapabilities and launch/readiness hooks. Re-add an extension after adding this ABI so its cached\nservice metadata and package fingerprints are recorded. An older descriptor is not imported to\nguess its schema. Plain process definitions keep their existing meaning.\n\nPresets are declarative registry contributions. Their option flags, help, completion and guided\nquestions derive from the adapter schema. Re-add a preset provider to populate its new cache;\nmissing or changed metadata refuses without importing code to guess the definition.\n\nThe top-level `feedback-intake` launcher is a [deprecated entry](#deprecated-daemon-entries). For a\nnew intake, save a definition with\n`service add <name> --preset feedback-intake --space <s> --root <absolute-root> --keys <private-keys.json> --creds <private-creds>`, then use `service start <name>` in that space. An explicit\n`--store` must be an existing private file. The default is private and instance-keyed. No broker\nor other service is started, no signer is minted and required TLS is preserved. Native startup\nreceipts are private, request-specific observations rather than native endpoint serving claims.\n\nA service instance pins its adapter name, package version and declaration fingerprint. A later\npackage update does not silently adopt a running or retained instance under a different adapter.\nStop the exact owned process and explicitly remove its stopped records before choosing changed\nadapter metadata. Logs remain private local files. Extension removal refuses live, foreign or\nunresolved service instances, including records outside the current mesh selection. None of\nthese declarations migrate legacy PID records or supply OS-unit ownership. Native manager\nimpact comes from its explicit adapter's attributed current read, not cached metadata.\n\nThe delivery implementation now registers its service adapter and legacy custody descriptor.\nCustom composition roots that want those inventory/control rows import `@cotal-ai/delivery`.\nThe full binary already composes it. CLI stop helpers share the workspace descriptor without an\nimplementation-to-implementation import. The `delivery` preset requires the recorded root and\nexisting scoped credential, keeps the native store/account/lease guards and does not adopt an\nexisting legacy daemon. Its hidden child entry is recognized by the same process-role predicate.\nThe old `deliver` machine-facing route remains for the stack callers that still use it.\n\nFresh static-auth and open `team boot` now use controller-owned stack members and accept\n`--foreground`. Their manager receives a digest-pinned manifest launch spec, with no additive\nownership ledger. A recorded stack can resume only after a completed whole-stack stop with its\nmanager never attempted, recorded broker inputs and each member's settled native evidence. Legacy\ncustody, prior team boots, supervisors and unresolved attempts still refuse. User-auth preparation\nuses ordinary `up --user-auth`, login and an explicit grant, then `down` before `team boot`. No\ncaller or owner grant is substituted.\n\nNew controller journals record the broker store path, storage cap request and bind host. A later\npreparation keeps those inputs and refuses differing explicit values. Older journals remain\nreadable. If their broker is stopped, restart requires the original store named with `--store-dir`\nand the original `--max-file-store` and `--host` values, if set. The store must already be a real\ndirectory. Do not omit the cap or host unless the original start set neither. That restart records\nthe inputs; run `down` before the stopped-stack team route. No store path is inferred from an\nabsent field, and bare restart never silently opens a default empty store.\n\nThe native manager status contract adds optional caller-scoped stop-impact metadata and advances\nthe cluster revision. Upgrade the composed manager and its compatible registration host together;\na host that does not accept the current contract must refuse registration. Older managers can\nomit the observation. Component status then says impact was not reported, rather than inferring\nthat seats survive. An observed policy does not migrate PID records, reserve a stop or confirm\ncleanup. The manager package adds a separate controller-only `manager-service` adapter and\n`manager` preset. Existing legacy manager records are not migrated or adopted. Update the\ncomposed packages and extension metadata together; an old cached declaration is not rewritten\nbehind a retained instance. Native stops require a fresh full-scope read and unchanged request-bound\npolicy, with native returned evidence plus original process exit. Missing terminal evidence retains\nits reservation and blocks replacement, without force kill. Cleanup remains not checked.\nThe direct service route currently refuses foreground mode. On an open mesh there is no\nauthenticated caller, so readiness and stop impact come from the pinned child's own request-bound\nlocal receipt. Its registration must equal the launch identity bound to that PID. Status shows it\nas a child-reported local receipt, not an attributed endpoint response. OS installation and\nnon-Linux protected process support are not available.\nRecorded systemd user units with an original invocation ID use that invocation's native journal\nfor finite/follow logs. Missing invocation evidence refuses instead of reading a stale direct-child\nfile. Native cursors and timestamps are retained; journal retention/access is not proof of complete\noutput or completed service work. No unit install/control support is implied by this read route.\n\nAuth providers may implement the optional read-only `preflightServer` hook. It checks public\nserver inputs without creating a directory, pin, key, grant or daemon. The controller's user-auth\nroute requires it before preparation; providers without it refuse rather than skipping validation.\n`prepareServer` remains authoritative and checks again before provisioning.\n\nThe auth package also declares an `auth-service` preset over its prepared native key projection.\nIt does not migrate or reclaim existing PID records. Its real child claims custody before opening\nplanes and publishes a private startup phase only after native initialization and signal handlers.\nThis route declares shared foreground control and still refuses remote broker and TLS-required\ntargets. The transport refusal is\nintentional, because not every native authority connection carries the registered transport\nrequirement. No plaintext substitution or new issuer is used.\n\nManager maintenance now supports finite JSON/plain reports. Gate repair and seat drain default\nto cancellation in a terminal and require `--yes` in scripts. Use `manager reconcile --dry-run`\nor `manager seats --drain --dry-run` first. Gate repair requires one registered root/space/broker\nand carries its TLS requirement through native liveness/eviction. It does not dial a default broker\nwhen that target is missing. An interrupted submitted repair or unresolved drain can return exit 3;\nconfirmed native results remain distinct from unconfirmed effects. No process is started.\n\n## Linear reports in 0.91.0\n\nUpdate the installed Linear extension with the matching CLI/workspace generation. Account add/show,\ninventory and direct call/resource/prompt JSON now use version-1 envelopes: read the native\ninventory from `result.inventory` and a native request reply from `result.reply`. Rich/plain reports\nbound displayed rows and lines without truncating JSON. MCP tool `isError` remains a result with\nexit 0 and a warning, not a work-success claim. Native not-executed refusals stay distinct from\nunknown submissions, and neither is replayed automatically.\n\nThe `linear` service preset runs the same native endpoint implementation from an explicit static-auth\nroot and existing account. It retains native registration, renewal and bounded deregistration; no\nnew generic caller is issued. Its private phase proves handler initialization only, not a current\nendpoint reply or completed deregistration. Foreground service start uses the shared terminal\nlifetime and native signal handlers, not the old serve parser. User-auth/open targets and\nOS-supervised service operation refuse. The older `linear serve` route remains available for\ninstalled callers.\n\nA bare Linear/account group opens no account. Wrong-mode flags or extra operands refuse before\naccount access. Interactive OAuth remains a human flow without JSON. Foreground serve stays on its native human-output path.\nStatic caller issuance keeps its native authority and now returns shared JSON/plain private-file\nreceipts. Use an existing protected physical parent outside managed state; an existing output\nrefuses before provisioning. Uncertain provisioning/publication is not replayed and no environment-\nexport launch recipe is printed. The supported `auth mint --endpoint` form resolves its scope live;\ntargeted/journaled/action issuance and consuming launch adapters are not supported. These changes do\nnot replace ownership, start a service or print secrets.\n\n## Setup inputs in 0.91.0\n\n`cotal setup` takes four inputs: destination, application, project folder and space. Nothing on a\nrunning mesh changes, and there is no outage window.\n\n### What stops working\n\n- A `cotal setup --yes` or a setup with no terminal exits 1 with `USAGE_INVALID` unless it names\n every input as a flag. It no longer sets up every detected connector and seeds the folder it\n runs in.\n- Setup sets up one application, the one `--harness` names, and records it as the space's\n `harness` preference. An application whose executable is not on PATH exits 4 with the rest\n saved; it used to be skipped with a warning.\n- The default persona lands in `<project>/.cotal/agents`. Setup no longer looks for a running mesh\n to seed into.\n- A repeat `cotal setup` no longer prints a status card. `cotal status` reports the same state.\n\n### Before the upgrade\n\nA script or image that ran `cotal setup --yes` names the four inputs:\n\n```bash\ncotal setup --destination local --harness claude --project <dir> --space <name> --no-input\n```\n\nFor a space that runs elsewhere, register it first with `cotal space add`, or pass the bundle its\noperator exported with `--connection-file`, and use `--destination self-hosted` with the\nregistration's root as `--project`.\n\nStartup remains opt-in. `--start` conflicts with `--configure-only`; a script, JSON or plain\ninvocation must add `--detach`. `--yes` by itself still saves configuration only. Startup failure\nleaves completed preparation in the result. Do not treat an unknown startup result as permission\nto submit another launch; inspect the selected space and recorded original request first.\n\n## Reads that install nothing in 0.91.0\n\nA read no longer installs or refreshes the built-in connectors and the web dashboard before it\nruns. Nothing on a running mesh changes, and there is no outage window.\n\n### What stops working\n\n- On a fresh machine, `cotal status`, `cotal ext list`, `cotal ps`, `cotal runtimes`, `cotal docs`\n and the other reads leave the extension root empty. `cotal setup`, or any command that is not a\n read, installs the built-ins as before.\n- After an upgrade, a read does not refresh the built-ins. Where it would load one still at the\n older version, such as a connector's setup row in `cotal status`, it names the package and its\n version with `cotal ext seed` instead of loading it.\n\n### Before the upgrade\n\nA script that ran a read to install the built-ins, such as `cotal ext list` right after\n`npm i -g cotal-ai`, runs `cotal ext seed` instead.\n\n## Space selection in 0.91.0\n\n`cotal use` and the target a command picks without `--space` change. Nothing on a running mesh\nchanges, and there is no outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal use <space>` no longer checks the broker. It selects any registered space, including a\n stopped one, and the command that targets it reports the space not up. A name that is not\n registered exits 2 with `TARGET_NOT_FOUND` instead of 1. Bare `cotal use` reports the selection\n instead of exiting 1 with its usage, and `--json` writes one envelope.\n- A command run without `--space` stays on the selected space when its broker is down and refuses\n with `TARGET_UNAVAILABLE`, exit 2. It used to fall back to the one other running mesh and print\n a note that the default mesh was down. A selection that names a space no longer registered now\n refuses with `TARGET_NOT_FOUND` instead of falling back to the local project or the one running\n mesh.\n- `cotal space` lists the registered spaces. `cotal meshes` still lists them, and its `--json`\n lines keep their shape.\n\n### Before the upgrade\n\nA script that relied on the fallback names the space it means with `--space`, or selects it with\n`cotal use <space>`. A script that read `cotal use` exiting 1 for a stopped space checks the\nbroker with `cotal space show <space> --json` and reads `result.check`.\n\n## Extension listing output in 0.91.0\n\n`cotal ext --json` and `cotal ext list --json` now write one JSON document instead of one JSON line\nper extension. Nothing on a running mesh changes, and there is no outage window. Upgrading the\n`cotal` binary is the whole operation.\n\n### What stops working\n\n- A script that reads `cotal ext list --json` line by line reads one envelope instead. The rows it\n read are now `result.items`, each with the same fields, and `result.root` is the install prefix.\n With `jq`, `cotal ext list --json | jq -c '.result.items[]'` prints the rows as before. An empty\n prefix writes an envelope with no items instead of nothing.\n- A usage error from `cotal ext` exits 1 as one `refused` envelope under `--json`, and `--plain` or\n `--json` on `ext add`, `ext remove`, `ext root` or `ext seed` is refused. `cotal ext root` still\n prints the path alone.\n- The human listing is a table now. Human output is not a parsing target; use `--json`.\n\n### Before the upgrade\n\nSearch scripts for `cotal ext list --json` and `cotal ext --json`, and change line-by-line reads to\nread `result.items` from the one document.\n\n## History reads in 0.91.0\n\n`cotal history [channel]` reads retained messages. Clearing is now `cotal history clear`, which asks\nin a terminal and takes `--yes` in a script.\nThis changes the CLI grammar only. Stored messages, credentials, agents and running services do not\nmigrate or restart, and there is no outage window. Upgrade the CLI and its first-party components\ntogether through the normal update path; a separate broker or manager can stay running.\n\n### What stops working\n\n- Replace a script's `cotal history --force` with `cotal history clear --yes`. `--force` is refused\n on every `history` form, `history clear` included, rather than clearing anything. Add `--dms` to\n the explicit clear command only when the purge should include direct messages and the identity\n holds that authority.\n- `history clear` accepts no channel argument, read filters or `--follow`. It purges every channel\n in the selected space; these inputs are refused rather than treated as a selective purge.\n- Bare `history` reads channels within the caller's read grant. It never clears stored messages.\n Use `--json` and `result.items` for scripts; the human report is not a parsing target.\n- A user-auth history read uses the connection's channel grant. DM audit history and stream-sequence\n cursors are not available through that ordinary human credential. Use a time with `--since`.\n\n### Before the upgrade\n\nFind scripts that pass `--force` to `history` and change their explicit purge invocation. No stored\nstate or credential backup is needed for this CLI-only transition. Keep the deployment's usual\nbackup procedure before an intentional history purge; reads do not make a backup or restore data\nthat retention has already removed.\n\n## Status output in 0.91.0\n\n`cotal status` writes one structured document with `--json`, takes `--plain` and `--all`, and exits by\nthe structured outcome table. Nothing on a running mesh changes, and there is no outage window.\nUpgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal status` exits 4 when it could not read part of what it reports, such as a PID record, the\n folder's trust material, a persona catalog, the mesh registry or one of its records, or a user-auth login. It exited\n 0. An unreachable broker, a stopped process\n or an unbound responder still exits 0.\n- The recorded meshes list checks only the selected mesh and reads `not checked` for the others. Pass\n `--all` to check every one.\n- The report no longer lists the roster or prints a `cotal status` title line, and its standalone\n hints moved to a `Hints` section at the end. Rows are labeled `name: value`. Human output is not a\n parsing target; use `--json`.\n- `--components` is refused with `--json`, `--plain` or `--all`. Its exit, `0` to `3` by component\n health, replaces the report's, so it does not exit 4 for a partial report.\n\n### Before the upgrade\n\nSearch scripts and monitors for `cotal status`. A check that treats any nonzero exit of bare\n`status` as an outage should treat 4 as a report with unreadable parts. A script that read the\nroster from `status` should use `cotal ps` for managed seats.\n\n## Read command output in 0.91.0\n\n`cotal ps --json` now writes one JSON document instead of one JSON line per seat, and `ps`, `models`\nand `runtimes` exit by the structured outcome table. Nothing on a running mesh changes, and there is\nno outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- A script that reads `cotal ps --json` line by line reads one envelope instead. The rows it read\n are now `result.items`, each still the manager's row unchanged, and `result.managers` names each\n manager instance and whether it answered. With `jq`, `cotal ps --json | jq -c '.result.items[]'`\n prints the rows as before. Instance headers no longer go to stderr under `--json`.\n- An incomplete manager census, where a registered manager did not answer, refused or serves a\n different contract, exits 4 instead of 1.\n- A mesh that `ps` or `models` cannot resolve or reach exits 2 instead of 1, and a space catalog\n refresh that `ps` or `models` needed and could not get exits 2 instead of 1. Under `--json` each is\n one `refused` envelope whose `error.code` names the reason.\n- `cotal models` exits 4 when a connector's catalog could not be read, and `cotal runtimes` exits 4\n when an installed runtime provider fails to load. Both exited 0. `cotal models --agent <name>`\n exits 5 when that one catalog could not be read. OpenCode 2.x, which has no catalog the CLI can\n list, is reported `supported: false` with the reason in `unavailable` and does not fail the read.\n- The human reports of `ps`, `models` and `runtimes` are tables now, and `ps --wide` prints one\n labeled record per seat instead of a facts line under each row. Human output is not a parsing\n target; use `--json`.\n\n### Before the upgrade\n\nSearch scripts for `cotal ps --json`, and for checks of the exit status of `ps`, `models` or\n`runtimes`. Change line-by-line reads of `ps --json` to read `result.items` from the one document.\n\n## Shell completion script in 0.91.0\n\n`cotal completion <shell>` is now `cotal completion script <shell>`, and bare `cotal completion`\nreports which shells have the stub instead of printing usage. Nothing on a running mesh changes, and\nthere is no outage window. Upgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\nA startup line written by hand that runs the old form, such as `source <(cotal completion bash)` or\nthe PowerShell profile line `cotal completion powershell | Out-String | Invoke-Expression`, now\nloads no completion. The command exits 1, prints nothing on stdout, so the shell sources nothing,\nand names the replacement on stderr. Replace the line with its new form:\n\n```bash\nsource <(cotal completion script bash) # bash\nsource <(cotal completion script zsh) # zsh\ncotal completion script fish | source # fish\ncotal completion script powershell | Out-String | Invoke-Expression # PowerShell $PROFILE\n```\n\nA cached stub from an earlier release keeps calling the hidden `__complete` entry. The new ownership-aware\ninstaller does not adopt it just because its text matches a generated script. Review unrecorded\nstubs and custom startup wiring manually before replacing them. Nothing automatically rewrites\na hand-edited startup file. On Linux and macOS, `completion install powershell` asks the\ninstalled shell for its current-user all-hosts profile before adding an owned block;\nit does not adopt an existing manual line. Windows profile editing still requires a\nmanual review and the printed script.\n\nNew installs record their stub hash and startup block. Reinstall and update refresh unchanged\nrecorded stubs at the same path. Update adds no startup block and does not restore a stub the user\nremoved. Uninstall removes only unchanged recorded material. Modified or unowned material is\npreserved with a diagnosis. Reopening the shell is necessary to stop using functions already\nloaded in its parent process.\n\n## CLI command resolution in 0.91.0\n\nThe CLI now resolves and validates a command before it seeds connectors or writes configuration.\nNothing on a running mesh changes. Brokers, managers, seats, credentials and mesh records need no\nmigration and no restart, and there is no outage window. Upgrading the `cotal` binary is the whole\noperation. What changes is what two kinds of command line do.\n\n### What stops working\n\n- `cotal -v` no longer prints the version. It exits 1 and names `cotal version` and `cotal -V`. A\n script that reads the version this way must use `cotal version`, `cotal --version` or `cotal -V`.\n- The words `sp`, `a`, `s`, `r`, `x` and `h` are now aliases of built-in commands (`spawn`,\n `attach`, `status`, `run`, `ext` and `history`). An installed extension that provides a command\n under one of those words still loads, but the built-in wins and every run prints a warning naming\n the extension. Remove or update that extension to clear it. `cotal ext add` refuses a new\n extension whose name or alias is already claimed.\n\n### Before the upgrade\n\nSearch scripts and shell aliases for `cotal -v`, and run `cotal ext list` to see whether an\ninstalled extension provides one of the words above. There is nothing to snapshot. The extension\nmanifest an older binary wrote is read as before.\n\n## Messaging commands in 0.91.0\n\n`cotal channels` is now `cotal channel`, and `cotal send` names its destination with a flag instead\nof a `dm`, `msg` or `ask` word. Nothing on a running mesh changes, and there is no outage window.\nUpgrading the `cotal` binary is the whole operation.\n\n### What stops working\n\n- `cotal channels`, with or without `list`, `set` or `default`, exits 1 as an unknown command. Bare\n `cotal channel` lists the channels, `cotal channel set <name>` saves one channel's settings, and\n `cotal channel defaults` reads or updates the space-wide defaults.\n- `cotal send dm <agent> \"<text>\"`, `cotal send msg <channel> \"<text>\"` and\n `cotal send ask <role> \"<text>\"` exit 1 with `USAGE_INVALID`, because they name no destination\n flag. Use `cotal send -t <agent> \"<text>\"`, `cotal send -c <channel> \"<text>\"` and\n `cotal send -r <role> \"<text>\"`.\n\n### Before the upgrade\n\nSearch scripts and shell aliases for `cotal channels` and `cotal send`, and change each line to the\nnew form above.\n\n## Managed Claude workspace trust in 0.80.0\n\nBefore upgrading, snapshot the manager's private Cotal state and Claude configuration home.\nPause new managed spawns while upgrading the connector, core, manager and chosen terminal\nruntime together. For tmux, cmux, Herdr, Orca and custodial Linux PTY seats, use that runtime's\nsupported manager-reload path and observe its custody/release bounds. Default `auto` selects\nin-process `pty`, whose seats cannot survive a manager restart. Drain those seats at an approved\nidle window, or use the documented preserve-state cut and `up` recovery, which resumes them\nrather than keeping their processes running. This upgrade does not require a broker restart.\n\nManaged launches now accept the authorized workspace through Claude's native startup dialog. They\nno longer require a prior interactive visit to that directory. Claude retains its normal\nrepository and linked-worktree trust semantics, including any trust it persists for later\nsessions. Foreground launches still prompt. No tool-permission bypass flag is added, but\naccepting workspace trust enables that checkout's hooks and settings, including its default\npermission mode unless a launch flag overrides it. Authorize only reviewed workspaces.\n\nThe launch recipe includes optional startup choices before its final confirmation. A custom\nruntime must implement `LaunchSpec.confirmBefore`, or refuse a recipe carrying it. Ignoring\nit leaves an untrusted launch at the native dialog until startup times out. The new launch\nrecipe does not require restarting an existing seat; manager reload continuity remains\nruntime-dependent as described above. Adopt the new runtime before launching new seats.\nNo credentials, mesh records or operator configuration files need manual migration.\n\n## AG-UI `takeCodePoints` export in 0.79.0\n\n`@cotal-ai/connector-core` no longer exports `takeCodePoints`. The AG-UI preview splitter is its\nonly user, and the package exported it only so the emitter could shorten a `RUN_ERROR` failure\ndetail, a path that never ran and is now removed. Nothing about a running mesh changes, and no\nshipped connector or host imports it. Only code of your own that imports it from the package is\naffected.\n\n### What stops working\n\nAn import of `takeCodePoints` from `@cotal-ai/connector-core` no longer compiles, and an ES module\nthat names it fails to link with `does not provide an export named 'takeCodePoints'`.\n\n### Before the upgrade\n\nTake a string's first `n` code points with `Array.from(s).slice(0, n).join(\"\")`, which is what the\nhelper did.\n\n## SecretStore create-only write in 0.78.0\n\n`SecretStore` from `@cotal-ai/core` has a fourth method, `create(key, value)`. It stores the value\nonly while the key is absent and resolves `true`, or resolves `false` and leaves the stored value in\nplace. The auth secret kinds made once per space now write their first value through it: the callout\naccount, the issuer keys, the owner secret and a hosted auth plane's instance identity. Before, two\nfirst calls on one space each made a value, the later `put` replaced the earlier one, and the earlier\ncaller kept using a value the store no longer held. The filesystem store implements `create` with an\nexclusive create, so a CLI root needs nothing, and stored values do not change.\n\n### What stops working\n\nA store of your own without `create` no longer compiles as a `SecretStore`. Plain JavaScript gets a\n`TypeError` at the first call that makes one of those kinds, such as `prepareServer` on a new space\nor the first `startAuthService` of a hosted context.\n\n### Before the upgrade\n\nAdd `create` to your store, decided in one atomic step by the backend, such as a conditional create\nor an insert that fails on an existing key. A read followed by a write brings the race back.\n\n## Manager-service authority refusal budgets in 0.78.0\n\n`POST /manager-service-authority` now counts its refusals against the exchange face's budgets, as\n`POST /exchange` does. On the loopback face a missing or wrong capability counts in the\ninvalid-capability window, and a refused IdP token or request counts in the refused-exchange window.\nOn the public face a refusal counts in the peer's bucket. A running mesh needs nothing, and a\nrequest that succeeds is never throttled.\n\n### What stops working\n\n`ManagerServiceAuthorityCtx` from `@cotal-ai/auth` now requires `failures` and `badCaps`, so a host\nthat builds it in an object literal without them fails to compile. The handler now calls the\n`peerKey`, `throttled` and `recordFailure` of the policy it is given. After 30 refusals in a minute\non one face, a further refusal on either route answers 429 without its reason.\n\n### Before the upgrade\n\nPass the `failures` and `badCaps` arrays your `/exchange` route counts in, so both routes share one\nbudget per face. A host that serves only this route can pass new empty arrays.\n\n## Primitive scope kind in 0.78.0\n\n`PrimitiveSpec` from `@cotal-ai/lang` replaces `opensScope: boolean` with `scope: ScopeKind | null`,\nthe scope kind a primitive opens, in the way `kind` names the effect it journals. Both engines now\nread that kind from the table. Before, they cast the primitive's name to `ScopeKind`, so a scope row\nmissing from the union compiled and reached the journal as a kind its readers did not know. A\nrunning mesh and its journals need nothing.\n\n### What stops working\n\nCode that reads `opensScope` from `PRIMITIVES` or a `PrimitiveSpec` fails to compile, and plain\nJavaScript reads `undefined` there, so a check on it never holds.\n\n### Before the upgrade\n\nRead `spec.scope !== null` where you read `spec.opensScope`, or `spec.scope` when you need the kind.\n\n## Token verifier clock tolerance in 0.77.0\n\n`validateUserToken`, and the `IdpConfig` that `verifyIdpToken` and `createIdpBridge` take, from\n`@cotal-ai/auth` no longer accept `clockToleranceSec`. Both verifiers allow a fixed 5 seconds of\nclock skew on `exp`, `nbf` and `iat`, the default they already applied. Nothing in the repository\npassed the option, so a running mesh behaves as before. Only code of your own that passes it is\naffected.\n\n### What stops working\n\nA call or `IdpConfig` that passes `clockToleranceSec` in an object literal no longer compiles. Plain\nJavaScript that keeps it still runs, and the value is ignored. A `NaN` value, such as `Number()` of\nan unset environment variable, used to turn off the expiry, not-before and issued-at checks in both\nverifiers, so an expired or post-dated token was accepted. A large value widened those checks by\nthat many seconds. Both kinds of token are now refused.\n\n### Before the upgrade\n\nRemove `clockToleranceSec` from each call and each `IdpConfig`. A host whose clock is more than 5\nseconds off the issuer's needs its time synchronized.\n\n## Auth context closure in 0.71.0 (unreleased)\n\nExisting deployments need no credential migration or restart for these additive APIs. Embedded\nhosts can now inspect `handle.connections()` and await `handle.closed` after `close()` or `drain()`\nto prove every owned transport ended, including the callout, replaced readiness readers and\nshort-lived clients. The inventory is a detached snapshot.\n\nA transport close failure now rejects with its connection label. The terminal signal stays pending\nwhile any connection remains live. Repair the failure and retry `close()` before awaiting\n`handle.closed`. Closing one hosted context does not close another account's context.\n\nRead a space's claim with `readPlaneClaim(kv, space)` on that account's leader-only auth bucket.\nAn unclaimed space returns `undefined`; held and released rows retain their claim identity.\nDeleted, malformed and foreign-space rows refuse. `PlaneClaimRow` and `PLANE_CLAIM_KEY` are exported.\n\nUse `observeAccountLivenessWithCreds({ servers, observerCreds, accountId, options })` with the\naccount-scoped membership-observer credential to list that account's connections. It never widens\ncredentials or evicts connections. Zero rows prove absence only with a complete sweep and the\nsingle-server proof. An embedded endpoint's trusted composition can retain transport custody\nthrough `EndpointOptions.onConnection`.\n\n## Unreleased\n\nOn a per-user-auth mesh, a spawn-scoped caller can arm the event plane of a child under its own\nowner without `admin`. This fixes owned spawns in spaces whose registration policy requires the\nplane. Upgrade the manager to pick up the admission change. No credential or state migration is\nneeded. Cross-owner arming still requires `admin`, and the child's own-channel rule and ledger\nenvelope are unchanged. A silent non-owner caller in a space without the policy still has the\nplane disarmed, with a notice if provisioning succeeds.\n\n## Hosted auth plane identity in the store (unreleased)\n\nA hosted context started through `startAuthService` keeps its auth plane instance identity in the\ninjected `SecretStore` under `authInstanceKey(space)`, with the other auth secret kinds. It used to\nsit under `stateDir` at `.cotal/space.<hex>/auth-instance.json`, though `stateDir` holds non-secret\nstate and the record holds the plane's private serve seed. The first start of an upgraded context\nputs that record into the store, removes the file and keeps the instance. A CLI root is unchanged.\n\n### What stops working\n\nA start refuses when the store and `stateDir` hold different instance identities, and names both. A\nstore that refuses a put of the new key fails the start.\n\n### Before the upgrade\n\nLet the store accept a put of `authInstanceKey(space)`. A copy of `stateDir` taken before the upgrade\nstill holds the serve seed, so delete it or protect it as secret material.\n\n## Injected SecretStore naming in 0.76.0\n\nThe manager and the delivery daemon name an injected `SecretStore` through\n`injectedSecretStoreIdentity` from `@cotal-ai/core`. The rule is unchanged: the store's declared\nidentity, else the coordinate in `COTAL_SECRET_STORE`, else a refusal. A running mesh needs nothing.\n\n### What stops working\n\n`reloadStoreIdentityOf` from `@cotal-ai/delivery` takes `{ injected: true, store }` or\n`{ injected: false, identity }`. A call that passes `injected: true` with no store fails to compile,\nand plain JavaScript gets a `TypeError`. Both processes refuse an unnamed injected store with one\nmessage, which starts `an injected SecretStore must declare its identity`, so a log match on either\nold message no longer matches.\n\n### Before the upgrade\n\nPass the injected store to `reloadStoreIdentityOf`, or call `injectedSecretStoreIdentity(store)`.\n\n## Manager-service authority policy flag in 0.76.0\n\n`handleManagerServiceAuthority` from `@cotal-ai/auth` no longer reads `allowManagerAuthority` from\nits policy argument, and the field is removed. Both exchange faces set it to `true`, so it never\nrefused anything. A running mesh needs nothing: the public listener serves\n`POST /manager-service-authority` as it did before, and the docs now list that route.\n\n### What stops working\n\nA host that serves the route itself and passes a policy object literal with\n`allowManagerAuthority` fails to compile with TS2353. Plain JavaScript that set it to `false` got a\n403 from the handler, and its requests now reach the capability and IdP checks.\n\n### Before the upgrade\n\nRemove `allowManagerAuthority` from the policy you pass. Where you set it to `false`, leave the\nroute out of that listener's route table instead.\n\n## Hermes model from the environment in 0.68.0\n\nA connector now launches on the model and variant its launcher resolved (the `--model` or\n`--variant` flag, else the agent file's `model:` or `variant:`) and no longer reads them again from\nthe agent file. The Hermes connector also no longer takes a model from `HERMES_MODEL` in the\nenvironment of the process that spawns the seat, including when `spawn.env` lists it.\n\n### What stops working\n\nA Hermes spawn whose only model was `HERMES_MODEL` in the spawning environment is refused at launch,\nand the refusal names both ways to set a model. Spawns that set `--model` or `model:` are unchanged,\non every connector.\n\nCode that calls a connector's `buildLaunch` directly with only `configPath` now gets no model or\nvariant from that file. Pass them as `model` and `variant`.\n\n### Before the upgrade\n\nMove each Hermes seat's model from `HERMES_MODEL` onto its spawn with `--model`, or into its\npersona's `model:`.\n\n## Run answers on a participant manager in 0.68.0\n\nA participant manager now asks its issuing host for an answering credential by naming the run and\nstep it answers. The host reads the pause's token off that run's journal and no longer accepts a\ntoken from the manager. Runs on a mesh with no participant manager are unaffected.\n\n### What stops working\n\nWhile a participant manager and its issuing host run different sides of this release, the host\nrefuses every `cotal run answer` and every amendment that manager serves, because each side refuses\nthe other's request shape. Starting, resuming and reading runs is unchanged. A pause stays waiting\nthrough the window, or follows its timeout if it has one.\n\n### Before the upgrade\n\nUpgrade the auth service and every participant manager registered with it in the same window, then\nanswer the pauses that waited.\n\n## Headless OpenCode handshake in 0.69.0\n\nWith `COTAL_SERVE_HEADLESS=1`, the OpenCode launcher's `[cotal-serve]` line on stdout now carries\nonly `port` and `session`. The server password no longer appears in it, and the 1.x TUI no longer\nreceives the password on its command line.\n\n### What stops working\n\nA headless host that read `password` from that line has no password, and the server refuses its\nrequests. Seats with a TUI, and headless seats that no host drives, are unaffected.\n\n### Before the upgrade\n\nHave each headless host mint a password and pass it to the launcher as `OPENCODE_SERVER_PASSWORD`,\nthen use it for basic auth as before. Without that variable the launcher mints its own.\n\n## Filesystem store identity in 0.69.0\n\nThe delivery daemon's answer to the manager's store check now names a filesystem store by its root\nand by a random `id` that the store records once in `store.id` inside its own directory:\n`.cotal/store.id` for a workspace root, or the directory of the file for `cotal deliver --creds\n<file>`. A manager no longer counts the daemon's store as its own because the two roots have the\nsame path. On a split whose broker host and manager host use one root path, the manager host now\nstays off the daemon-credential renewal lease, so `cotal doctor auth --fix` on the broker host can\nrenew the daemon credentials.\n\n### What stops working\n\nA manager and a delivery daemon on different sides of this release refuse each other's answer to\nthe store check. The manager then remints no daemon credential, and a manager that is booting does\nnot start. This is read from the code and was not measured across two releases. A\n`cotal deliver --creds <file>` whose directory is a read-only mount and holds no `store.id` stops at\nstart. So does a `--creds` file that is its directory's `store.id` under any name, and a `store.id`\nthat is a symbolic link or holds anything but a lowercase UUID.\n\n### Before the upgrade\n\nUpgrade the broker host and every manager host of a space in the same window. For a `--creds` file\non a read-only mount, add a regular `store.id` file beside it that holds a new lowercase UUID and no newline,\nas `node -e 'process.stdout.write(crypto.randomUUID())' > store.id` writes. Move a `--creds` file\nnamed or linked as `store.id` to a file of its own.\n\n## Detached spawns with `--share-tools` in 0.69.0\n\nThe manager's `spawn` operation now takes `shareTools` as a list of MCP server names. The CLI parses\n`--share-tools` into that list before it sends the request, and the manager cluster document moves\nto revision 22. A cut taken with `cotal down --preserve-state` before the upgrade still resumes: the\nmanager reads its `cotal-manager-resume/v1` inventory and writes new cuts as\n`cotal-manager-resume/v2`.\n\n### What stops working\n\nA CLI and a manager on different sides of this release refuse a detached spawn that passes\n`--share-tools`, because the CLI checks each request against the contract the manager serves. This\nis read from the code and was not measured across two releases. A detached spawn without the flag,\na foreground spawn and a roster entry are unaffected. A manager older than this release cannot\nresume a cut that this release took.\n\n### Before the upgrade\n\nUpgrade the CLI on every host that runs `cotal spawn --detach` in the same window as the managers\nit reaches.\n\n## Shared MCP server checks in 0.69.0\n\nThe cotal config reader now checks each server under `connectors.<name>.mcpServers` when it reads\nthe file, and refuses one that cannot launch as written, naming the file and the field. The rules\nare in [the config file](config.md#the-config-file).\n\n### What stops working\n\nA config file that holds such a server refuses every Claude spawn that reads it, including one with\n`--share-tools none`. Before, a field of the wrong type failed each Claude spawn that shared the\nserver with a `TypeError` that named neither the file nor the server, a spawn that did not share it\nlaunched, and a server with no `command` or `url` was passed to `claude`, which never started it.\nRead from the code and not measured: spawns on other connectors, a manager resume and the step of\n`cotal setup` that records the shared list read the same files, so each stops at the same refusal.\n\n### Before the upgrade\n\nCheck `connectors.<name>.mcpServers` in the operator-level config file and in each space's\n`.cotal/config.json`. Give each server a string `command`, or a `type` of `http`, `sse` or `ws` with\na string `url`. Write `args` as a list of strings and `env` and `headers` as objects of strings, or\nremove the server.\n\n## Remote manager family eviction in 0.69.0\n\nA remote manager registered through its host now asks the host to evict up to 256 holders of its\ncredential family in one maintenance request, and the host reads the family once for the whole set.\nBefore, a restart sent one request per holder and the host read the whole family for each one.\nMeshes with no remote manager are unaffected.\n\n### What stops working\n\nWhile a remote manager and its issuing host run different sides of this release, each side refuses\nthe other's eviction request shape. A restart whose credential family already has holders then fails\nat its eviction step and leaves the manager's registration gate frozen. A first start, a clean stop\nand the host's reconciliation of a foreign slot holder are unchanged.\n\n### Before the upgrade\n\nUpgrade the auth service and every remote manager registered with it in the same window. A manager\nthat restarted inside the window resumes its frozen registration on its next start once both sides\nrun this release.\n\n## AG-UI emitter holder hooks in 0.69.0\n\n`AguiEmitterHolder` from `@cotal-ai/connector-core` now takes its hooks as one named object after\nthe emitter factory: `new AguiEmitterHolder(startEmitter, { onError, onRunClosed, waitLive, runMeta })`.\nOnly `onError` is required. Nothing about a running mesh changes, and every shipped connector passes\nits hooks by name. Only a connector of your own that builds a holder is affected.\n\n### What stops working\n\nA holder built with positional hooks, such as `new AguiEmitterHolder(start, onError, onRunClosed)`,\nno longer compiles, because the constructor takes two arguments. Plain JavaScript that keeps the\npositional form still runs, but the holder calls none of its hooks, so a failure never reaches\n`onError`.\n\n### Before the upgrade\n\nPass each hook by name, for example `new AguiEmitterHolder(start, { onError, onRunClosed })`, and\ndrop any `undefined` that filled an earlier slot to reach a later hook.\n\n## Worker run failure type in 0.70.0\n\n`WorkerRunFailed`, the failed result of `runInWorker` in `@cotal-ai/lang`, is now a union on\n`class`: `released`, `held`, `effect`, `too-large`, `rejected` or `error`. A running mesh needs\nnothing, because the runtime host and the engine thread ship in the same install. A run on the\ncompiled engine whose program throws an object with `code: \"L5012\"` or `code: \"L5025\"` used to end\nreleased and now ends failed, as it does on the walker.\n\n### What stops working\n\nTypeScript code that reads `code`, `reason`, `step`, `pending`, `kind`, `detail` or `tooLarge` on a\n`WorkerRunFailed` it has not narrowed fails with TS2339. A released, held, too-large or rejected\nresult no longer carries `code`, so JavaScript that branched on `L5012`, `L5025`, `L5006` or\n`L5010` stops matching with no error. `tooLarge` is gone.\n\n### Before the upgrade\n\nBranch on `class` where such code read `code`: `released` for L5012, `held` for L5025, `too-large`\nfor L5006 and `rejected` for L5010. An `effect` or `error` result keeps its `code`. Once narrowed to\n`too-large`, a result carries the `stepKey`, `bytes` and `bound` that `tooLarge` held.\n\n## Remote manager request builder in 0.70.0\n\n`remoteManagerClient.remoteManagerAuthorityRequest` from `@cotal-ai/manager` now takes an\noperation's coordinates as one object, and `remoteManagerRegistrationProof` from `@cotal-ai/core`\ncomputes the proof from the manager's identity state instead of a request. Nothing about a running\nmesh changes: the proof digest and the request on the wire are the same, so a manager and a host on\ndifferent sides of this release still accept each other. Only code that builds remote manager\nrequests itself is affected, in TypeScript and in plain JavaScript.\n\n### What stops working\n\nA call that passes the registration proof, contract artifacts, session, retirement or transfer\nreader as positional arguments after the operation no longer compiles. A call that passes a request\nto `remoteManagerRegistrationProof` no longer compiles either, because the second argument now names\nthe lifecycle `lifecycleUid`, as the identity state does.\n\nPlain JavaScript runs both old calls without an error. The builder drops the positional coordinates,\nso the host refuses the request with `requires a sha256 registrationProof`. A proof computed from a\nrequest leaves out the lifecycle, so the host refuses a request that carries it as a proof mismatch.\n\n### Before the upgrade\n\nName the coordinates, for example\n`remoteManagerAuthorityRequest(state, \"cli\", \"retire\", { registrationProof, retirement })`.\nCompute the proof as `remoteManagerRegistrationProof(owner, state)`, adding the contract artifacts\nas a third argument for activation only. A host that recomputes the proof from a received request\npasses `{ space, instanceId, lifecycleUid: managerLifecycleUid, identities }` from that request.\n\n## Bearer validator lifetime cap in 0.70.0\n\n`validateUserToken` from `@cotal-ai/auth` no longer takes `maxTtlSec`. It caps a bearer's lifetime\nat the cap of the bearer's view, the same cap the issuer applies when it mints: 900 seconds, or 300\nfor a `transfer-writer` bearer. The auth callout never passed the option, so a running mesh behaves\nas before. Only code of your own that calls the validator with `maxTtlSec` is affected.\n\n### What stops working\n\nA call that passes `maxTtlSec` in an object literal no longer compiles. Plain JavaScript that keeps\nit still runs, and the value is ignored. A `NaN` value, such as `Number()` of an unset environment\nvariable, used to turn the lifetime check off and accept a bearer of any lifetime. That bearer is\nnow refused at its view's cap.\n\n### Before the upgrade\n\nRemove `maxTtlSec` from each call. A test that needs a bearer to expire sooner mints one with a\nshorter lifetime.\n\n## Persisted identity records in 0.70.0\n\nThe manager instance identity, the manager sibling identities, the auth plane instance identity and\na participant manager's remote authority state now share one reader and one first mint in\n`@cotal-ai/workspace`, exported as `claimIdentityRecord` with the nkey check `identityOf`. Each\nrecord is read as a regular file, must hold non-empty nkeys and is created exclusively, so\nconcurrent first starts of a participant manager on one root now settle on one identity where each\nused to keep its own. `saveManagerInstanceIdentity` and `saveAuthInstanceIdentity` are gone. A\nrunning mesh whose records are plain files needs nothing.\n\n### What stops working\n\nA manager instance, auth instance or remote authority record that is a symlink, a directory or any\nother non-regular entry is refused where it used to be followed. The manager, the auth plane and a\nparticipant manager fail to start on it, and `cotal reconcile-gate` and `cotal deregister-instance`\nrefuse it. Retirement already refused it. A remote authority record with an empty nkey id or seed\nis refused too. A first mint that loses its race and cannot read the winner now refuses with\n`identity-record-create-lost` in place of `manager-instance-identity-create-lost` or\n`auth-instance-identity-create-lost`. Code that imports either `save` function no longer compiles.\n\n### Before the upgrade\n\nReplace a symlinked identity record with a copy of the file it points to. Code that wrote a record\nwith a `save` function plants it with `createManagerInstanceIdentity` or\n`createAuthInstanceIdentity`, which create the record when it is absent and otherwise return the\nstored one unchanged. Nothing replaces an overwrite of a stored identity.\n\n## Manager instance in user credentials in 0.70.0\n\n`AuthProvider.userCredentials` from `@cotal-ai/core` no longer returns `managerInstanceId`. A\n`manager-caller` credential's manager instance is the signed `act.managerInstanceId` claim in its\nbearer, which the broker verifies and the CLI already used. The reference provider in\n`@cotal-ai/auth` stops copying the exchange response's field into its result, where nothing\ncompared it with the bearer. The exchange still answers with the field, so a running mesh behaves as\nbefore.\n\n### What stops working\n\nCode of your own that reads `managerInstanceId` from a `userCredentials` result no longer compiles,\nand plain JavaScript reads `undefined` there.\n\n### Before the upgrade\n\nRead the instance from the bearer's `act.managerInstanceId` claim.\n\n## Auth plane identity location in 0.70.0\n\nThe user-auth service keeps its instance identity in the root's `.cotal/space.<hex>/auth-instance.json`,\nbeside the manager's. It used to sit inside `.cotal/auth`, at\n`space.<hex>/.cotal/auth/auth-instance.<hex>.json`, so a copy of that folder carried it. The first\nstart of an upgraded root moves the record and keeps the instance. A hosted context started through\n`startAuthService` has its record moved the same way inside its `stateDir`.\n\n### What stops working\n\nCode that calls `openAuthAuthorityPlane` without the new `identityRoot` option no longer compiles. A\nstart that finds a record both in `.cotal/space.<hex>/` and at its older place refuses and names the\ntwo files. A start also refuses when the older place of the auth or manager identity holds a symlink,\na directory or anything else that is not a regular file. The manager used to skip a dangling symlink\nthere and mint a new identity.\n\n### Before the upgrade\n\nPass `identityRoot` to `openAuthAuthorityPlane`. When `dir` is a workspace root's user-auth state\ndir, `<root>/.cotal/auth/space.<hex>`, pass that root. A plane with no workspace root, as\n`startAuthService` runs, passes `dir` itself. Either keeps the identity the plane already has: on the\nfirst start it moves from `<dir>/.cotal/auth/` to `<identityRoot>/.cotal/space.<hex>/`. Never pass a\ndirectory inside `.cotal/auth`: the record would land in the folder an operator copies and travel\nwith it again.\n\nA copy of `.cotal/auth` taken from a root last run by an older Cotal carries that root's record.\nDelete `.cotal/auth/space.<hex>/.cotal/auth/auth-instance.<hex>.json` from the root you copied it to\nbefore the first `cotal up --user-auth` there.\n\n## Per-seat `COTAL_` names in `spawn.env` in 0.71.0\n\n`spawn.env` in the cotal config no longer forwards a `COTAL_` name the launcher sets for each seat,\nsuch as `COTAL_ROLE`, `COTAL_MODEL` or `COTAL_SUBSCRIBE`. Before, a seat launched with no value of\nits own took the spawning process's value and ran under that role, model or read set. The\nmachine-wide knobs a seat already receives, such as `COTAL_HOME`, may still be listed.\n\n### What stops working\n\nEvery spawn and resume under a config whose `spawn.env` lists such a name is refused before\nlaunch, and the refusal names the entry. Code that calls `launchEnv` from `@cotal-ai/connector-core`\nwith such a name in `envAllow` gets the same error.\n\n### Before the upgrade\n\nRemove those names from `spawn.env`. Give each seat its role, model and channels with `--role`,\n`--model` and `--subscribe`, or in its persona's `role:`, `model:` and `subscribe:`.\n\n## Role addresses in 0.71.0\n\nA role must be one `[A-Za-z0-9_-]` token. Before 0.71.0 any other spelling was rewritten into one:\n` probe ` reached the `probe` queue and `pro.be` reached `pro_be`, while the message kept the\nspelling sent. An anycast to `*` was accepted and stored where no holder reads it.\n\n### What stops working\n\nAn agent whose role is outside the token set no longer starts, however it is launched:\n`cotal join --role`, `cotal spawn --role`, an agent file's `role:`, `COTAL_ROLE` and an embedded\nendpoint's `card.role` are all refused before the agent joins.\n\nA send to such a role, or to `*`, through `cotal send ask`, `/anycast` or `cotal_anycast` is refused,\nand nothing is stored.\n\n`routeToken` is no longer exported from `@cotal-ai/core`. A role routes as spelled, so code that\nused it to name a role's queue uses the role itself, and `assertValidRole` checks one.\n\n### What migrates on its own\n\nEvery task queue. A `svc_<role>` durable was always named from the rewritten token, so its pending\nrequests and its holders carry over.\n\n### Before the upgrade\n\nRename each role outside the token set to the token it already routed to: remove the surrounding\nspaces and replace every other character outside the set with `_`. Rename it where the holder is\nlaunched and in every script or prompt that sends to it.\n\n## IdP URLs on `localhost` in 0.73.0\n\nThe IdP URL that `cotal login --idp` and `cotal up --user-auth --idp` take, the JWKS URL the auth\nservice pins from it, the space-catalog link and `cotal sync` now share one rule: `https://`, or\n`http://` on a loopback IP literal. `localhost` is a name, so it no longer counts: a hosts entry\nwould choose the IdP, and with it the keys the callout trusts. Every loopback literal now passes, so\n`http://127.0.0.2/api/auth` and `http://[::ffff:127.0.0.1]/api/auth` are accepted where they were\nrefused before. A JWKS URL with any scheme other than `https:` or `http:` is refused. A mesh whose\nIdP uses HTTPS is unaffected.\n\n### What stops working\n\nOn a mesh whose IdP is pinned at `http://localhost:<port>/...`, the auth service builds its key\nresolver from the pinned JWKS URL at start, and that resolver now refuses it with\n`JWKS origin must be https (or http on a loopback IP literal for dev)`.\n\n`cotal login` and `cotal logout` with `--idp http://localhost:<port>/...` are refused with\n`idp url must be https (or http on a loopback IP literal such as 127.0.0.1 for local dev)`, and so is\nevery command that reads a session cached under that URL.\n\n### Before the upgrade\n\nUse `127.0.0.1` (or `::1`) in place of `localhost`. In the space's `idp.json` under the mesh's\n`.cotal/auth`, change `url` and `jwksUri` to the literal spelling and leave `issuer` and `audience`\nas they are. Owners derive from the issuer, so existing grants keep matching. Change a manifest's\n`broker.idp` the same way, because `cotal up` refuses an `--idp` that differs from the pin. Then\nhave each person run `cotal login --idp http://127.0.0.1:<port>/api/auth` again, because sessions\nare cached under the URL.\n\n## Terminal layout `Pane.confirm` in 0.73.0\n\n`Pane.confirm` is removed from `@cotal-ai/core`, and the tmux and cmux terminal-layout providers no\nlonger press Enter in the panes they open. The flag carried no prompt text, so both providers\npressed Enter five times, one second apart: the first dialog a pane showed was answered with its\ndefault, and a prompt that never appeared went unreported. Nothing in Cotal sets it, so a running\nmesh needs nothing. Spawned agents are unaffected, because their runtimes match the startup prompt\nby its text.\n\n### What stops working\n\nA `Pane` object literal that sets `confirm` fails to compile with TS2353. Plain JavaScript that sets\nit runs without an error, and the pane stays at its prompt.\n\n### Before the upgrade\n\nRemove `confirm` from each `Pane`. Start a command that shows a startup prompt with the option that\nskips it, or answer the prompt in the pane.\n\n## Carrying a resumed Claude session to another host in 0.67.0\n\n`cotal spawn --resume <id> --detach --on <instance>` now carries a Claude session held on the\noperator's host to the target manager instance. Both sides need this release: an older manager does\nnot serve `transcript-receive`, and the CLI then stops with that manager's refusal instead of\nlaunching. The manager cluster document moves to revision 21, and the `ps` row's `resume` object\ngains `host` and `transferredAt`.\n\nA manager host that runs carried seats needs `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_AUTH_TOKEN` or a\ncloud provider selection in its environment, because each carried seat runs in its own Claude home\nwith no stored login. On an authenticated mesh the CLI mints the transfer writer from the space's\nsigning seed, so the carrying host needs that seed, as for any other operator command. On a user-auth\nmesh it exchanges the operator's login for a `transfer-writer` view instead, so the operator's grant\nneeds scope `admin`, and the auth service must run this release. A remote manager receives a carry once\nits host serves the manager-service `transferReader` operation. A seat launched without carrying,\nincluding any `--resume` whose id this host does not hold, is unchanged.\n\n## Lifecycle head type in 0.67.0\n\n`LifecycleMapping`, the type `parseLifecycleHead` returns, is now a union on `state`. Nothing about\na running mesh changes: heads that parsed before parse the same way, and the refusals are\nunchanged. Only TypeScript code that compiles against `@cotal-ai/core` is affected.\n\n### What stops working\n\nAn `interface` that extends `LifecycleMapping` fails with TS2312, because an interface cannot extend\na union. Code that builds a head in memory no longer compiles when the head is `retiring` without\nits `op`, or `active` or `retired` with one. The parser already refused those heads.\n\n### Before the upgrade\n\nDeclare such an interface as an intersection instead, for example\n`type ActiveMapping = LifecycleMapping & { state: \"active\" }`. A reader that has checked\n`state === \"retiring\"` reads `op` without a guard.\n\n## Issuance gate types in 0.67.0\n\n`EpGateRow` and `EndpointGateRow`, which `parseIssuanceGate` and `parseEndpointGate` return, and\n`EpGateState`, which an `EpIssuanceGate` or `EpIssuanceBarrier` returns from `observe`, are now\nunions on `state`. Nothing about a running mesh changes: gates that parsed before parse the same\nway, and the refusals are unchanged. Only TypeScript code that compiles against `@cotal-ai/core`\nis affected.\n\n### What stops working\n\nAn `interface` that extends one of these types fails with TS2312, because an interface cannot\nextend a union. Code that builds a gate in memory, such as a custom barrier's `observe`, no longer\ncompiles when the gate is `frozen` or `retired` without its `op`, or `open` with one. The gate\nparsers already refused those rows.\n\n### Before the upgrade\n\nDeclare such an interface as an intersection instead, for example\n`type CustomGateRow = EpGateRow & { custom: string }`.\n\n## Lifecycle-blocked refusals in 0.66.0\n\nA refusal that carries `ai.cotal.ep.lifecycle-blocked` now reports only the lifecycle state it\nread. Nothing about a running mesh changes. A client that branches on the detail must read the new\nfield.\n\n### What stops working\n\nA refusal raised at the issuance gate used to carry `headState` without reading the head:\n`retiring` for a frozen gate and `retired` for a retired one. It now carries `gateState`\n(`frozen` or `retired`) and no `headState`. A client that treats `headState: \"retired\"` as a\nburned uid, or `headState: \"retiring\"` as a retirement in flight, no longer matches those\nrefusals, and the `[lifecycle ...]` suffix on the error string changes the same way. A custom\nissuance barrier whose `observe` returns a frozen gate without a valid `op` (a string `opId` and\none of the four op kinds) is now refused as `internal` by `registerServiceInstance`.\n\n### Before the upgrade\n\nUpdate such a client to read `gateState` for a gate refusal and `blockedOp` for the operation that\nholds the gate. `headState` is present only when the refusal read the head, for example an\nactivation refused because the head is still retiring.\n\n## Workflow programs that bind `once` in 0.65.0\n\n`once` is now a scope of the workflow language, so it is a reserved name. A program that declares\nits own `once` binding (`const once = ...`, a parameter or a function named `once`) is refused at\nvalidation with L2002. Nothing else about a running mesh changes.\n\n### What stops working\n\nA run whose recorded program binds `once` cannot be resumed after the upgrade, because a resume\nvalidates the recorded program again. A new `cotal run start` of such a program is refused before\nanything is recorded.\n\n### Before the upgrade\n\nList the runs with `cotal run ps` and check each program that is still running or held for a\nbinding named `once`. Let those runs finish on the old version before you upgrade the manager, and\nrename the binding in the program before you start it again.\n\n## From 0.58.0 to 0.59.0\n\nEvery connector now publishes a failed run's `RUN_ERROR` on `events.<owner>.<actor>` with the fixed\nmessage `run failed` and no `code` or `rawEvent`. The error text and error kind a harness reports\ncan echo a prompt, a peer message or tool output, and that channel has a different read ACL. A\nreader that showed the message or branched on `code` gets neither after the upgrade. Where a\nconnector reports the error kind as the agent's presence condition, that is unchanged.\n\n### Settle pending event frames before the upgrade\n\nEach session's events are frozen in its event write-ahead log before they are published. A session\nrestarted on 0.59.0 whose log still holds an unacknowledged frame with an older `RUN_ERROR` does not\nrepublish it: its event emitter halts with `egress-run-error` and publishes nothing further for that\nsession. The broker may or may not already hold that frame, so the halt cannot settle it.\n\n1. Stop the seats cleanly on 0.58.0, with the broker still up.\n2. List the logs that still hold a pending frame. The logs live under the events state root\n (`COTAL_WORKSPACE_ROOT`). Empty output means there is nothing to settle.\n\n ```sh\n find \"$COTAL_WORKSPACE_ROOT/.cotal/events\" -name wal.json \\\n -exec jq -r 'select(.pending != null) | input_filename' {} +\n ```\n\n3. For each session listed, start it again on 0.58.0 while the broker is reachable, let it recover,\n stop it, and run step 2 again. Recovery publishes the frame as 0.58.0 would have, error text\n included, so it only finishes what 0.58.0 had already started.\n\n If that start halts with `cas-loss` instead, the agent's subject is no longer at the sequence this\n log expects, and no restart settles that log, on 0.58.0 or later. A lost acknowledgement is one\n cause: the broker stored the frame, so it and its error text are already on the channel, and every\n retry halts the same way because the stream checks the frozen expectation before it deduplicates.\n The halt message names the other causes, such as a second emitter for the same agent under a\n different state root, a restored stream or frontier record, or a purged channel. With those the\n pending frame may never have reached the broker, so a `cas-loss` does not tell you whether it\n landed. Find and stop any second writer and rule out a restored state first. Clearing the halt\n then means purging the agent's event channel and removing the agent's directory under the events\n state root whole (see [Event plane](connect-claude.md#event-plane)). That abandons the pending\n frame whether or not the broker has it, and the purge also drops the earlier frames of every\n session of that agent.\n4. Upgrade once step 2 prints nothing.\n\nIf a session halts with `egress-run-error` after the upgrade, go back to step 3 for that session on\n0.58.0. Do not edit or delete `wal.json` on its own to get past either halt: clearing the pending\nframe abandons that epoch, an event the broker never received is lost, and removing part of the\ndirectory leaves a state the next start refuses.\n\n## Explicit actor grants in 0.59.0\n\n`cotal actor grant` no longer fills an omitted ACL flag with its wide default. A grant names\n`--scope`, `--allow-subscribe` and `--allow-publish`, or passes `--full` to give the ones it leaves\noff their wide defaults (`spawn,role:default`, `>` read, `>` post). Any other grant is refused. The\nbreak is in the CLI on the machine that holds the actor ledger, the one that ran\n`cotal up --user-auth --idp <url>`. No stored row, credential or wire message changes.\n\n### What keeps working\n\nExisting actor ledger rows keep the authority they were granted, and their users and agents connect\nas before. `actor revoke`, `actor list` and a `grant` that names all three ACL flags behave as they\ndid on 0.58.0. Nothing on disk is converted.\n\n### What stops working\n\nA grant that leaves off any of the three flags without `--full` exits 1 with\n`refusing to grant \"<actor>\" with --scope, --allow-subscribe, --allow-publish left off`, naming the\nflags it is missing, and then prints both accepted forms. It writes no row and does not retire the\nactor's current lifecycle. An existing row stays as it was, and an actor granted for the first time\nstays out until the grant is run again. This includes the bare grant printed on 0.58.0 by\n`cotal login`, `cotal status`, `actor list` and the not-granted refusal. Look for it in provisioning\nscripts, onboarding runbooks and anything that pastes those hints.\n\n### Upgrade order\n\nChange the scripts before the ledger machine is upgraded, and make each grant name all three flags.\n0.58.0 and 0.59.0 both accept that form. To keep a wide row, write its defaults out:\n\n```sh\ncotal actor grant <actor> --sub <IdP subject> \\\n --scope spawn,role:default --allow-subscribe '>' --allow-publish '>'\n```\n\nSwitch to `--full` only once the ledger machine runs 0.59.0. 0.58.0 refuses it with\n`Unknown option '--full'` before it reads the ledger. Brokers, managers and participant machines\nneed nothing for this break, so their order is the one the section above gives.\n\n### The window\n\nThis break has no outage. No process restarts for it, and a refused grant changes nothing. The\nexposure is a grant script that runs against 0.59.0 before it was changed: it fails and grants\nnothing.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. On the ledger machine, save the output\nof `cotal actor list` to compare rows after the changed scripts run, and list the scripts that call\n`cotal actor grant`.\n\n### The upgrade end to end\n\n```sh\n# on the ledger machine, still on 0.58.0\ncotal actor list > actors-before.txt\ngrep -rn 'actor grant' <your provisioning scripts>\n# make every grant name --scope, --allow-subscribe and --allow-publish, run them, then upgrade\nnpm i -g cotal-ai@0.59.0\ncotal actor list | diff actors-before.txt -\n```\n\nBoth refusals quoted here were run on 0.58.0 and on the 0.59.0 code. That brokers, managers and stored\nrows need nothing is read from the change, which touches only the CLI and its hints, and was not run\non a live split deployment.\n\n## Repeated flags refused in 0.59.0\n\nA `cotal` flag given more than once is now a usage error unless the command declares it repeatable.\nOn 0.58.0 the last value won with no message, so `cotal down web --space a --space b` acted on `b`\nwhile a wrapper that checked the first `--space` verified `a`. The break is in the command-line\nparser on the machine that runs the command, including commands added with `cotal ext add`. No\nstored state, credential or wire message changes.\n\n### What keeps working\n\nA command line that gives each flag once parses as it did on 0.58.0, in any order and in the\n`--flag=value` form. Flags whose help says repeatable, such as `--opt` and `down --session-store`,\nstill collect every value. A flag-shaped word after `--` is still a positional. The daemons, units\nand agents that `cotal` starts for itself are given each flag once, so a fleet driven only by `cotal`\ncommands typed by hand needs no action.\n\n### What stops working\n\nA command line that repeats any other flag exits 1 before the command runs. It prints\n`Option '--space' cannot be repeated`, or `Option '-f, --file' cannot be repeated` for a flag with a\nshort form, followed by the command's help. `-f` and `--file` count as the same flag. Look for it in\nscripts, aliases and wrappers that append a flag to override one set earlier, such as a fixed\n`--space` followed by `\"$@\"`.\n\n### Upgrade order\n\nChange those scripts first so each flag is given once. 0.58.0 and 0.59.0 both accept that form.\nBrokers, managers and participant machines need nothing for this break, and each machine's CLI\napplies it when that machine is upgraded, so their order is the one the sections above give.\n\n### The window\n\nThis break has no outage. No process restarts for it, and a refused command does nothing. The\nexposure is a script that still repeats a flag when it runs on 0.59.0: it exits 1 instead of acting on\nthe last value.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. List the scripts, aliases and wrappers\nthat call `cotal` so each one can be checked.\n\n### The upgrade end to end\n\n```sh\n# still on 0.58.0\ngrep -rn 'cotal ' <your scripts and wrappers>\n# give each non-repeatable flag once, then upgrade\nnpm i -g cotal-ai@0.59.0\n# run each changed script; a repeat left behind exits 1 with the usage error and does nothing\n```\n\nThe refusal and its messages were run against the 0.59.0 parser and `cotal topology view`. That the\nargument lists `cotal` builds for its own processes give each flag once is read from the code, and\nwas not run on a live split deployment.\n\n## Detached spawns from a seat's shell in 0.62.0\n\nOn a static or open mesh, `cotal spawn --detach` run inside a managed seat's shell now launches as\nthat seat. On 0.61.0 it minted a one-shot operator instrument, so the manager recorded that\ninstrument as the spawner and the seat's own `cotal_despawn` of the child was refused with\n`not authorized: <seat> was not spawned by <caller> (admin tier required)`. The break is in the CLI\non the machine where the seats run. No stored state, credential or wire message changes.\n\n### What keeps working\n\n`cotal spawn --detach` from an operator terminal or from a script outside any seat launches as\nbefore, and so does any call with `--creds`, one aimed at a space other than the seat's own, or a raw\nopen target named with `--server` and an unregistered `--space`. A user-auth mesh is unchanged. A seat with\n`capabilities: [spawn]` still spawns from its shell, and can now stop that child with\n`cotal_despawn`. `--on <instance>` from a seat's shell still lands on that manager instance, now as\nthe seat.\n\n### What stops working\n\n- On a static mesh, a seat without `capabilities: [spawn]` can no longer spawn from its shell. Its\n own credential holds no spawn subject, so the broker refuses the request and the command exits 1.\n- A child launched from a seat's shell is now that seat's child, so the manager stops it when the\n seat exits, as it does for a `cotal_spawn` child. A child that has to outlive the seat that\n started it now goes with the seat.\n- A seat launched without `COTAL_SPACE` is placed by its static credential. Every connector sets\n that variable, so this only reaches a hand-built launch: from such a seat's shell, a spawn aimed at\n a static space that holds no credential for the seat is refused instead of running as the operator.\n\n### Upgrade order\n\nOnly the CLI that seats run from their shell changes, which is the one installed on the host where\nthe seats run. Brokers and managers need nothing for this break, so their order is the one the\nsections above give.\n\n### The window\n\nThis break has no outage. No process restarts for it. A child already running when you upgrade\nkeeps the spawner the manager recorded at its launch.\n\n### Snapshot this first\n\nNothing is rewritten, so this break has no state to back up. List the agent files whose seats run\n`cotal spawn --detach` from their shell, note which of them lack `capabilities: [spawn]`, and note\nwhich of their children must outlive the seat.\n\n### The upgrade end to end\n\n```sh\n# still on 0.61.0: find the seats that spawn from their shell\ngrep -rln 'cotal spawn' .cotal/agents\n# add `capabilities: [spawn]` to each of those agent files that lacks it, and launch any child\n# that must outlive its seat from an operator terminal instead\nnpm i -g cotal-ai@0.62.0\n```\n\nThe attribution, the despawn, the refusal of a seat without `spawn`, the stop on seat exit and a\nseat's `--on` spawn were run on a local static mesh, and the attribution and the despawn on a local\nopen mesh.\n\n## From 0.53.0 to 0.54.0\n\nManager calls now borrow an instance-bound `manager-caller` credential. Followed mutations require\n`manager.goal-result` on the selected manager, so a compatible issuer, manager and client must be\nloaded together. An older manager is refused before a followed mutation; upgrading an installed\nbinary alone does not replace code in a running manager, connector or embedded client.\n\n### Preserve state before changing processes\n\nSnapshot the broker's durable storage using its supported backup procedure, the host authority and\nactor ledgers, and each participant's manager identity, runtime custody records, credentials and\nsaved sessions. Include the embedding application's database and configuration under its supported\nbackup procedure. Record the loaded package versions and the CLI path used by bearer helpers.\nKeep these copies private. Do not change the IdP issuer, regenerate manager identities, rotate agent\ncredentials or recreate tenant storage to make the upgrade pass.\n\nNo ledger, goal-history or session conversion is required for this change. Existing ordinary\nmessaging credentials retain their normal expiry rules. New manager-caller credentials are obtained\non demand from the current grant; old manager-call credentials do not gain the new view automatically.\nExisting accepted goals remain durable and must not be submitted again merely because observation\nwas interrupted. Fresh remote registration publishes its service status at the current revision and\nepoch; do not seed that status manually.\n\n### Upgrade the split deployment\n\n1. Stage one pinned 0.54.0 package set for the host and participants, including the embedding SDKs.\n Pause new manager mutations and let accepted work settle where possible before reloading processes.\n2. Upgrade the host issuer and embedding first. Keep the broker, its account identities and durable\n storage in place. Then load the matching manager release on each participating machine.\n3. Preserve active seats through the runtime's supported update path. A Linux custodial runtime may\n release and re-adopt seats within its 600-second unattended window; verify the actual runtime,\n custody records and process identities before relying on it. A legacy PTY runtime without release\n support cannot preserve active seats through a generic manager restart. Drain it at an approved\n idle window instead of signalling the manager or replacing conversations.\n4. Reload the clients and connectors through their session-preserving host controls. Refresh any\n bearer helper captured from an older immutable CLI path. A transport-only reconnect does not reload\n JavaScript. Verify authenticated instance selection, a read-only manager command and canonical\n result recovery before allowing new followed mutations.\n\nTreat the interval from issuer reload through compatible manager/client reload as a manager-control\noutage. Mixed versions can refuse discovery or commands; there is no promised rolling transition.\nOrdinary agent sessions survive only where their runtime and credentials permit it. If verification\nfails, keep mutations paused and repair forward from the preserved state rather than resetting it.\nThis release does not add host-backed enrollment or terminal release for stock participant detached\nagents; see [Remote supervised agents](run-a-mesh.md#remote-supervised-agents).\n\n## From 0.48.2 to 0.49.0\n\n0.49.0 changes how a credential's authority is recorded. A credential is no longer only a signed\nfile: it is an *issuance*, with a generation the issuer chose and durable evidence of the ceiling it\nwas granted under. The important consequence for a running deployment is not at connect time. It is\nat renewal time.\n\n### What keeps working without any action\n\n- **Existing agent credentials keep authenticating.** A credential minted under 0.48.2 is not\n revoked and is not rejected at connect. Nothing needs to be re-issued to bring the fleet back up\n after the upgrade.\n- **The channel registry survives.** Channels, their replay settings, descriptions, and usage text\n are ordinary durable state and are not rewritten by the upgrade.\n- **`cotal deliver` is still a standalone command.** Running the delivery daemon as its own process\n remains supported; it is not restricted to being a child of `cotal up`.\n- **`cotal join` keeps its flags.** In particular `--lifecycle-uid` is not new in 0.49.0. It has\n been required alongside `--creds` since well before this release, and the pairing rule did not\n change here. A scripted external join that worked under 0.48.2 works unchanged.\n\n### What does not migrate\n\n**A credential minted before 0.49.0 cannot be renewed.** Managed agent credentials carry a\n24-hour lifetime and the manager re-signs one once it passes **75%** of its life, ticking every\nquarter of the TTL so a tick always lands inside that window. When the manager reaches a credential\nthat carries no issuance, it refuses to renew it and logs the agent by name:\n\n```\n! managed cred renewal <agent>: renewManagedStaticCred: <agent> carries no issuance;\n a static credential minted before SPEC 13.15 is not renewed under an unbound generation\n - respawn the agent\n - the agent dies loud at this cred's expiry unless it is reminted\n```\n\nSo the fleet comes up fine, runs normally, and then each agent stops at its own credential's\nexpiry, within roughly a day of the upgrade, one at a time rather than together. The refusal is\ndeliberate: the renewal would otherwise have to invent a generation nobody issued, which is the\nstate the release exists to remove.\n\n**Respawn the managed agents as the last step of the upgrade.** For this particular upgrade the\nrespawn is not optional: stopping a 0.48.2 manager ends its agent processes whichever CLI you use,\nfor the reason given under the outage window below. The respawn is how they come back, and it is\nalso what mints each credential as an issuance so it renews from then on. One planned pass over the\nfleet is the whole job. Skipping it leaves agents stopped and, for any credential that survived\ninto 0.49.0 unminted, brings the renewal cliff above a day later, one agent at a time.\n\n### Credentials you minted yourself\n\n**A credential you minted with `cotal mint` is a different case, and it very likely needs\nnothing.** The distinction that matters is not the word \"static\", which covers both. It is **what\nminted the credential and who owns its renewal**. A credential the **manager** minted for an agent\nit spawned carries a lifetime and is renewed by the manager, so it is the subject of everything\nabove. A credential **you** minted with `cotal mint` and handed to an external peer is issued with\n**no expiry at all**, and no manager renews it: it is not in the sweep, so there is no renewal to\nfail. It keeps working after the upgrade, and re-minting it would mean coordinating with a third\nparty for no gain.\n\nThe manager says which one it is holding. Where a credential has no expiry to reach, the sweep\nnames it and moves on rather than refusing:\n\n```\n! managed cred renewal <agent>: credential is unbounded - not renewed\n (a pre-TTL credential stays as minted until respawn)\n```\n\nRe-mint an external peer's credential only if you want it to carry a lifetime, and at a time you\nchoose.\n\n### How to read the boot log\n\nA 0.49.0 manager starting over an existing space may print lines like:\n\n```\n verified evicted: <holder-key> (3/12)\n already verified (durable): <holder-key>\n\u2713 boot self-heal: manager/<id> registration gate reopened at generation <n>\n```\n\nThese are **not** a credential migration, and reading them as one is the most likely way to\nconclude the fleet is fine when it is not. They come from the manager repairing **one** endpoint\nregistration gate that a previous restart left frozen, and they enumerate that single gate's\ncredential-family holders as it verifies each one evicted. `already verified (durable)` on a later\nstart is the repair cursor resuming, not a credential that became durable. The repair is real and\nuseful (it is what previously needed `cotal reconcile-gate` by hand), but it says nothing about\nwhether your agent credentials carry issuances. The renewal refusal above is the signal that does.\n\n### Which side to upgrade first in a split topology\n\nMove the manager first.\n\nThe stores 0.49.0 introduces are created by the **manager** at its own boot, not by the broker.\nThey are create-or-verify and idempotent, so a 0.49.0 manager brings the space's authority stores\nup to the new shape itself, and it does so against whichever broker is answering.\n\nBeing honest about the evidence behind each direction, because they are not equally established:\n\n- **Broker-first was measured on a live 30-agent deployment** (issue #1578). Upgrading the broker\n first locks the old manager out immediately: `cotal up` re-renders the broker's generated config\n from the trust record, and after the restart the still-0.48.2 manager is refused on every\n connection with an `authentication error` naming the Nkey, continuously. That text comes from the\n broker process, not from a Cotal command, so match on its shape rather than on an exact string.\n `cotal ps` reports zero agents while\n the agent processes are still alive, because the manager has lost its view of them, not because\n they died. Upgrading the manager clears it immediately.\n- **Manager-first is reasoned from where the new stores are provisioned**, not from a measured\n fleet upgrade. It is the recommended order because the manager is the component that creates what\n 0.49.0 adds, but it has not been run end to end on a production split topology at the time of\n writing. Treat it as the better-supported order rather than a guaranteed one, and keep the\n rollback below ready either way.\n\nWhichever order you pick, **this is not a rolling upgrade**. Between the two steps the mesh is down\nand the manager cannot see its agents. Go straight through rather than pausing between them, and\nschedule it as an outage window.\n\n### What the window looks like\n\n- **The managed agent processes do not survive step 1, in either order.** This is the one place\n where the obvious reordering does not rescue you, so it is worth understanding rather than\n working around. Sparing agents on a bare manager stop is a **handshake**: a 0.49.0 manager\n publishes a capability file proving it can release its agents, and a 0.49.0 `cotal down` refuses\n the stop unless it finds one. **A 0.48.2 manager never publishes that file**, because the\n mechanism ships in the release you are installing. So the old CLI against the old manager sends a\n plain stop and takes every seat with it, and the new CLI against the old manager either refuses\n (leaving `--with-agents`, which reaps deliberately) or falls to the legacy path, warns that it\n cannot verify the manager can spare its agents, and signals it anyway.\n- **You can confirm which side you are on in one command, without stopping anything.** The flag that\n marks the newer behaviour is absent from the older CLI, and its summary line makes the difference\n plain:\n\n ```\n $ cotal down --help # on 0.48.2\n cotal down - stop the whole local stack, or name only the components to stop\n\n $ cotal down --help # on 0.49.0\n cotal down - stop the whole local stack (managed agents stay running unless --with-agents), ...\n ```\n\n If your `cotal down --help` does not mention `--with-agents`, stopping the manager stops the\n agents with it.\n- **Therefore the respawn in step 5 is mandatory recovery for this upgrade, not an optional pass.**\n It is also the step that re-mints credentials as issuances, so it is the same action either way.\n Plan the window to include it rather than treating it as cleanup.\n- The **manager's view** of them is lost while the two sides disagree, so `cotal ps` reports zero\n and control commands do not reach seats.\n- **Messages are not delivered** while the mesh is down.\n- The window is as long as it takes to restart the second component, plus the manager's own start.\n It is minutes, not hours, provided you do not stop between the steps.\n- **Nothing self-heals if you stop halfway.** The refusal is continuous until both sides match.\n\n### Snapshot this before you start\n\nTake these while the deployment is still on 0.48.2. The two `cotal` reads are live reads and must\nhappen before anything stops.\n\n- **A filesystem or volume snapshot of both containers**, if your platform offers one. This is the\n only rollback that covers every case, and it is what the reporting deployment used.\n- **`cotal backup create <dir>`**, for the durable space state, **but read the next paragraph before\n you rely on it**: on a split broker and manager topology it is very likely unavailable to you, and\n the volume snapshot above is your actual rollback.\n- **The trust records and credential directory** under `.cotal/auth` on the manager host, including\n the per-space material directory. These are what a re-mint would otherwise have to replace.\n- **A copy of the channel registry**, so you can verify it came back rather than assuming it did:\n `cotal channels list` before and after.\n- **The output of `cotal ps`**, so you know how many seats you expect to see afterwards and can tell\n a lost view from a lost agent.\n\n#### `cotal backup` on a split topology\n\n**`cotal backup create` cannot read a running stack.** It requires a completed cut, and only\n`cotal down --preserve-state` publishes one:\n\n```\n$ cotal backup create ./backup.0482\n\u2717 backup requires a completed cut; run `cotal down --preserve-state` first\n```\n\n**And `cotal down --preserve-state` requires a manager alive on the host you run it from.** It uses\nthat manager to attest that every retained child stopped, and the check is deliberately fail-closed:\na manager that is dead or merely uncertain refuses rather than preserving an unproven cut. The check\nreads a local pidfile, so a **remote** manager does not satisfy it. On a split topology the broker\nhost has no local manager, which means the documented durable-backup path is not available there.\n\n**Measured rather than assumed, at 0.48.2**: the backup refusal above is executed output. The\npreservation requirement is read from `down.ts` at the same tag, where the preserve path asks a\nmanager to prepare an inventory and then requires that manager to be locally alive before it\ncommits. The part not executed end to end is a genuine two-host split, which needs two real hosts.\n\n**What to do instead.** Use the filesystem or volume snapshot of both containers. That is the\nrollback the reporting deployment actually used, it covers the broker's durable state and the\nmanager's credential material together, and it does not depend on either component being able to\nattest for the other. If you want `cotal backup` as well, take it from a host that does have a live\nlocal manager, and understand it is a second copy rather than the primary rollback.\n\n**This looks like a product limitation rather than a documentation gap**, and it is written here as\none so an operator is not left thinking they mis-typed a command. The upgrade path for the exact\ntopology this page is addressed to cannot use the documented backup command.\n\n### The upgrade end to end\n\n```bash\n# 0. on 0.48.2, STILL RUNNING: record what you expect to see afterwards.\n# These two are live reads, so they must happen before anything stops.\ncotal channels list > channels.before\ncotal ps > ps.before\n\n# 1. manager host. READ THE NOTE BELOW THE BLOCK FIRST: this step ends the\n# managed agent processes whichever order you choose, and the respawn in\n# step 5 is how they come back. It is recovery, not tidying.\n#\n# STOP THE MANAGER WITH THE 0.48.2 CLI, BEFORE INSTALLING 0.49.0. The\n# order matters and it is not recoverable once you install: a 0.49.0\n# `down manager` REFUSES to stop a 0.48.2 manager whose pid record carries\n# a start token, which is every manager on a platform that can read one\n# (Linux can):\n# refusing bare manager stop: ... does not prove this manager can detach\n# its agents; use --with-agents or stop the agents explicitly\n# The refusal names two remedies and NEITHER clears it for this case. The\n# check reads a capability file that only a 0.49.0 manager writes; it never\n# counts agents, so stopping them first changes nothing. And `--with-agents`\n# is whole-stack only, so `down manager --with-agents` is refused by its own\n# flag rule. See #1592.\ncotal down manager # the 0.48.2 CLI, still installed.\n # 0.48.2 has no --with-agents; this\n # is the whole route. On a host that\n # runs the whole stack, the 0.49.0\n # `cotal down --with-agents` after\n # installing is the alternative.\nnpm install -g cotal-ai@0.49.0 # ONLY after the stop above\n# `supervise` RUNS IN THE FOREGROUND and holds the terminal until you stop\n# it. There is no --detach on this command. Start it under whatever keeps\n# your manager alive normally (systemd unit, container entrypoint, or a\n# second terminal), and run the remaining steps from another shell.\ncotal supervise --space <space> --server nats://<broker>:4222\n\n# 2. broker host: stop the stack.\n# NOT `--preserve-state` on a split topology: it needs a manager alive on\n# THIS host to attest its children stopped, and yours is on the other one.\n# Your rollback is the volume snapshot from \"Snapshot this before you\n# start\", not `cotal backup`.\n# See \"cotal backup on a split topology\" above.\ncotal down\n\n# 3. broker host: install 0.49.0 and start it again\nnpm install -g cotal-ai@0.49.0\n# Record the manager log's size BEFORE starting, so step 3a can tell THIS\n# boot's output from every earlier one. It must be captured here, ahead of\n# the start: taken afterwards it sits past the new line and the wait hangs.\n# `<spaceKey>` is NOT the space name. It is lowercase hex of the name's\n# UTF-8 bytes, so space `prod` is `manager.70726f64.log`. Do not guess it:\n# `cotal up` prints the real path on its launch line. Substituting the\n# plain name points at a file that does not exist, and the wait below then\n# burns its full timeout before telling you.\nLOG=.cotal/manager.<spaceKey>.log\nOFF=$( [ -f \"$LOG\" ] && wc -c < \"$LOG\" || echo 0 )\ncotal up --detach --host 0.0.0.0 --space <space> --no-manager\n\n# 3a. SPLIT TOPOLOGY ONLY: `--no-manager` above boots the broker (and the\n# delivery daemon) with NO local manager on the broker host, so there is\n# no wait-and-stop step on a current cotal-ai. The rest of this step is\n# the OLDER-host recipe, kept because the flag is refused there and that\n# refusal is your signal you are on it: without the flag the `up` also\n# starts a local manager, and you must wait for the log to show it is up,\n# then stop it, or you finish the upgrade with two managers and the one\n# you did not intend is the one nobody is watching.\n# A bare `grep -q` does NOT wait: it reads once and exits 1 immediately\n# if the line has not been written yet. Bound the wait instead, so a\n# manager that never comes up fails loudly rather than reading as ready.\n# The log is opened APPEND-ONLY, so on any host that has run a manager\n# before, this file ALREADY carries a `manager up` line from an earlier\n# boot. Grepping the whole file therefore matches instantly and waits for\n# nothing. Read only what THIS boot appended, using the $OFF captured in\n# step 3 above (before the start, which is the only point it is correct):\ntimeout 60 bash -c \\\n \"until tail -c +$((OFF+1)) \\\"$LOG\\\" | grep -q '. manager up'; do sleep 1; done\"\n# exit 0 = THIS boot logged it; exit 124 = it never did, so STOP and look.\n# This manager is 0.49.0 and publishes its own spare-capability file, so\n# the bare stop below is NOT the refusal case from step 1.\ncotal down manager # broker + delivery remain\n# On a current cotal-ai the two commands above are unnecessary (nothing\n# to wait for, nothing to stop) and `cotal down manager` simply reports\n# no manager to stop.\n\n# 4. verify the mesh is whole again before touching the fleet.\n# Do NOT compare `cotal ps` against ps.before yet: step 1 ended the agent\n# processes, so at this point it is EXPECTED to be empty, and an empty\n# `ps` is also the signature of the broker/manager mismatch described\n# above. The two are indistinguishable here, so compare what the mesh\n# itself should have carried across instead:\ncotal channels list # compare against channels.before: this SHOULD match now\ncotal ps # expect it to be EMPTY here; ps.before is the target for\n # step 5, not for this step\n\n# 5. the step that is easy to skip: respawn the managed agents so their\n# credentials are re-minted as issuances and can renew. Persona is a\n# POSITIONAL argument here, unlike `cotal stop`, which requires --name.\n# One call per agent:\ncotal spawn <persona> --detach --name <n> --space <space>\n# then the comparison step 4 could not make:\ncotal ps # NOW compare against ps.before: seat count should match\n```\n\nThe mesh is down from step 2 until step 3 finishes. That is the window. On a split topology there is\nno cut and no backup inside it, so the window is the stop, the install and the restart, nothing more.\n\n## Adding a section for a future release\n\n**Every changeset marked breaking adds a section to this page.** A release that changes what an\noperator must do, in what order, or what stops working, is not finished until the section exists.\n`scripts/upgrade-section-gate.mjs` grades a commit range for this: run it as\n`pnpm upgrade-section-gate --base <ref>` and it reds when the range carries a breaking change and\nadds no new release section. The former Attribution workflow enforced this on each pull request;\nthat workflow and its required-status rule were removed on 22 October 2026. The script remains\navailable for manual checks, and reviewers still check upgrade instructions for breaking changes.\nBe precise about what the check proves. It proves a section for a release\n**was written here**. It cannot prove the section is **correct**, or that it describes the break\nthat actually landed, and it cannot see a breaking change that carries no marker at all. Reviewing\nthe words remains a person's job.\n\n**Mark the break, or the gate cannot see it.** Any one of these is enough, and they are the only\nthings it reads:\n\n- a `!` before the colon in the commit subject, as in `feat(core)!: bind hosted runs to the caller`\n- a `BREAKING CHANGE:` footer in the commit body\n- a changeset in `.changeset/` declaring a `major` bump for any package\n\nThe marker must survive the squash. A `!` that lives only in a commit you squash away is not in the\nrange the gate grades, so put it in the subject that lands on `main`.\n\n**The heading is a `##` and names the release**, like `## From 0.48.2 to 0.49.0`. Both matter, and\nneither is a style preference. Coverage is claimed by a heading, so a heading that names\nno release claims every release and distinguishes none: `## Notes` with a sentence under it would\notherwise satisfy the rule. Naming the release also makes the section the one an operator upgrading\nthat release will search for. Use `###` freely for detail inside a section. Subsections belong to\ntheir release rather than counting as separate coverage.\n\nName the release that first carries the change: the next version Changesets publishes, which\n`pnpm changeset status --verbose` lists. `bin/package.json` on `main` still reads the release already\npublished. If a release is cut while the change is open, the change ships in the release after it,\nso move the heading before merging. The gate accepts any version in a heading, so before merging a\nrelease pull request, check every heading added since the previous tag against the version it\npublishes.\n\nA section is written for the operator, not for the reviewer. It answers, in this order:\n\n1. What keeps working with no action at all.\n2. What does **not** migrate, and when that becomes visible. Name the log line if there is one.\n3. The order to move components in for a split topology, and why that order.\n4. What the outage window looks like, including what survives it.\n5. What to snapshot before starting.\n6. The commands, end to end.\n\n**Where an answer was not measured, say so in the document rather than guessing.** An operator who\nknows which half of a recommendation is reasoned and which is measured can plan around it; one who\nfinds out afterwards cannot.\n\n## Handle redemption in 0.91.0\n\n`auth redeem <private-handle-file>` selects the native auth target, then refuses unsupported\nconferring-handle redemption without reading the file or issuing credentials. It does not redirect\nthe artifact into agent enrollment or interactive-session redemption. Offline handle inspection\nstill labels signatures, revocation and holder currency not checked. An explicit `--space`\nrequests static-registry signature inspection with a native ephemeral operator reader. It checks\nsignatures, issuer scope, presented links and required revocation records without establishing\npresenter identity, live epochs or use authority. It confers no holder credential, persists no\nreader credential, and never turns missing or stale native records into verified offline success.\nOpen/user-auth inspection and conferring redemption remain unsupported.\n\n## Dashboard service transition in 0.91.0\n\nThe matching dashboard package now registers a service preset and hidden native child, not a\npublic web command. An older cached command descriptor is hidden and refuses with the matching\nextension-update remedy before importing provider code or repairing the install. `update` remains\nexplicit; `service start web` never installs a deliberately absent extension.\n\nConfigure the saved web definition with its existing registered static-auth root, one protected\ncredential file and explicit read-only intent. The service retains required TLS, creates no caller\nor channel-purger credential, opens no browser and prints no bootstrap secret into logs. The page\nmarks it read-only and hides delete; the server independently rejects that mutation before any\nalternate connection. The private session file holds the full-launch bootstrap; logs give its\nreference only. User-auth/open targets and OS installation remain unsupported in this route.\nExisting legacy process records are still observed, never silently adopted. An already-running\nlegacy dashboard is not restarted or converted by loading metadata. See [dashboard setup](watch-a-mesh.md#web-dashboard).\n\n## Registered lifecycle retirement in 0.91.0\n\n`auth retire <full-lifecycle-uid> [--yes]` has closed UID/space input, but the standalone CLI still\nlacks a supported native retirement instrument carrying its own current serve identity and gate.\nIt refuses without publishing a request. It does not borrow the manager's identity, treat --yes as\na grant, or call the host's interactive/managed loopback doors as a substitute. This remains an\nexplicit authority gate, not a completed retirement operation.\n\n## Local-process `rootedAt` removal in 0.91.0\n\nA local-process extension descriptor no longer has a `rootedAt` field. No stop has read it since\nthe dashboard became a service. Operators have nothing to migrate, snapshot or order, and there is\nno outage: a manifest that an older install cached with the field still loads, and the next\nmanifest rewrite drops it. An extension author removes the field from each `LocalProcess`\ndeclaration, where TypeScript now reports it as an unknown property.\n"
|
|
27490
27490
|
},
|
|
27491
27491
|
{
|
|
27492
27492
|
"slug": "watch-a-mesh",
|
|
@@ -27500,7 +27500,7 @@ function loadDocsBundle() {
|
|
|
27500
27500
|
"title": "Workflow runs",
|
|
27501
27501
|
"kind": "Concept (informative)",
|
|
27502
27502
|
"summary": "The new run-start-keyed command binds an envelope submission ID to one host-minted run.",
|
|
27503
|
-
"body": "# Workflow runs\n\n## Keyed submission\n\nThe new `run-start-keyed` command binds an envelope submission ID to one host-minted run.\n`run-submission-status` reads that binding for the authenticated caller incarnation. Both need\nfresh issued reach and a digest-verified description pinned to one manager instance and epoch.\nLegacy `run-start` is unchanged. An unsupported keyed command never falls back to it.\n\nThe scheduler must persist the submission ID, semantic fingerprint, caller incarnation,\ncontract digests and instance/epoch before dispatch. The client helpers prepare that snapshot\nand send one attempt at a time. They pass on only `signal` and `deadlineMs`, which a keyed\nrequest keeps at or below 30000. They refuse a reply that names another submission, instance,\nepoch or fingerprint, and that outcome stays unknown. They do not schedule retries, replace\nunknown work or re-pin it to another manager. Their prepared handle is process-local and WeakMap-bound: serialized JSON\ncannot be rehydrated through these helpers. Persisted scheduler recovery needs separate reviewed\nintegration, not a cast or an automatic fresh Describe. Keep the original work reservation\nwhile reconciling any unknown outcome.\n\nThe binding winner may make at most three admission-create attempts in its original callback\ndeadline, with the same admission bytes and authority. Initial authorization precedes binding,\nbut fresh authority rechecks before each admission attempt can fail after binding and retain\nbinding-only debt. The manager refuses a start while it is stopping or already at its launch\nlimit before it reads or writes a binding, so that refusal writes nothing. It says nothing about\nan earlier attempt under the same key, which may already be bound or running.\n`run-submission-status` takes no launch slot, so read the outcome there. A launch that fails after\nadmission leaves `admitted` with no run, and no later call launches it. Later duplicates only\nread. A `binding-only` result is a visible reconciliation hold, not a queue or permission to repair.\nAn absent admission is an observation at read time, not proof that the original callback cannot\nstill write. A changed caller generation or manager epoch refuses rather than adopting the run.\n\nThe receiving manager or issuing host measures the request budget on its own monotonic clock,\nand checks the caller's authority expiry on the wall clock. Wall-clock changes do not extend or\nspend the monotonic request budget. Authority expiry remains a wall-clock check. The callback\ndeadline a status read returns is a wall-clock record of that window, not something a caller or\nhost enforces.\n\nThe original callback timer bounds waiting and starting more attempts. It does not cancel an\nalready-issued broker publish, which may land later with the same frozen admission bytes.\nNo new admission attempt starts after the request budget or authority window ends. Once an\nadmission exists, the activation check uses only the manager's remaining monotonic request budget;\nit does not recheck the caller's wall-clock expiry. Keep the reservation even if an earlier read\nobserved no admission.\n\nSubmission state `activated` reports evidence of activation, including completed and failed\nruns. It is not a running-state or active-lane signal. Use native `run-status` for lifecycle\nstate. Terminal submission bindings remain permanent, and duplicate starts never relaunch them.\n\nThis implementation is under validation. Static native and signerless issuer paths require\nseparate acceptance evidence before release or integration.\n\n> **Concept** (informative) \xB7 **For:** people writing a durable multi-agent workflow, and implementers hosting one \xB7 **Normative:** [SPEC \xA714](../SPEC.md#14-workflow-runs-v05) and the language reference [`spec/cotal-lang.md`](../spec/cotal-lang.md)\n\nA **workflow run** is a program that coordinates agents over hours or days and survives the\nprocess that started it. The program is written in **Cotal Lang**, a small subset of JavaScript in\nwhich every interaction with the world is one of a dozen **effects** (`spawn`, `turn`, `ask`,\n`checkpoint`, `sleep`, `wait`, `notify`, `monitor`, and the four concurrency scopes) and everything\nelse is ordinary, pure JavaScript. Every effect is written into the run's **step journal** before\nit is performed and settled after, keyed by where in the program it happened rather than by when,\nso a run that dies is resumed on any host by **re-running the program from the top** with recorded\neffects returning their recorded results. Nothing about the interpreter is ever serialized: the\njournal and the program are the whole state.\n\n## A first program\n\n```js\nconst planner = await spawn(\"planner\")\nconst builder = await spawn(\"builder\", { worktree: \"wt-1\" })\n\nconst plan = await ask(planner, { name: \"plan\", schema: { steps: \"array\" } })\nconst ok = await checkpoint(\"approve-plan\", \"Approve the plan?\", { timeout: \"4h\", onExpiry: \"proceed\" })\nif (ok.status !== \"resolved\") {\n await notify([planner], { decision: \"approve-plan\", outcome: \"expired\" })\n}\n\nconst r = await turn(builder, { name: \"build\", deadline: \"30m\" })\nif (r.status === \"blocked\") {\n await turn(planner, { name: \"unblock\" })\n}\n\nconst outcome = await race({\n reply: () => wait(replied(builder), { timeout: \"20m\" }),\n giveUp: () => sleep(\"1h\"),\n}, { name: \"await-or-move-on\" })\nlog(\"outcome\", outcome.index)\n```\n\nRead it as the flowchart it is. `spawn` brings agents in; `ask` is the narrow case where the\nprogram itself needs a value (`schema` is a record the program hands the handler unchanged; the\nlanguage hashes it and gives it no meaning, and the handlers in this repository enforce it as the\nshorthand of the language reference \xA76.5);\n`checkpoint` is a durable pause a human resolves from anywhere, raced against a durable timer; `turn`\nwakes an agent for one turn and returns how it yielded; `race` runs two branches and keeps the one\nwhose recorded clock is earliest. Agents talk to each other in channels as they always do; the\nprogram never speaks in a channel, and the one thing it can put in front of an agent (`notify`) is a\nbounded decision record, not prose.\n\n## The mental model\n\n- **Pure code is JavaScript.** Loops, records, arrays, closures, template literals, destructuring,\n `try`/`catch`, arithmetic, `switch`, compound assignment, optional chaining, spread and rest: what\n you would write anyway, with the parts that hide effects or make meaning depend on the host removed\n (`class`, `this`, `new`, `for...in`, `==`, labels, regex literals, `Math`/`Date`/`JSON`, promises,\n generators). Every refusal names its code and the edit that fixes it. The builtins are a short list\n (`keys`, `map`, `sort`, `json.stringify`, `now()`, `random()`), and arrays, strings and numbers\n answer their usual methods (`xs.map`, `s.trim()`, `n.toFixed()`) and nothing outside that table.\n Records and arrays you build are yours to change until they cross an effect boundary; a member you\n do not own, a host prototype, or a value another branch built is refused with a code, never a\n surprise.\n- **Every effect is journalled and hashed.** A step is keyed `(scope path, kind, name, occurrence)`\n and its inputs are hashed. Reorder your program, add a step, rename a variable: recorded steps\n still match. Change what a step asks (a checkpoint's prompt, a sleep's duration, a turn's\n deadline) and the resume stops with a **divergence** naming the step, rather than replaying an\n answer to a question the program no longer asks.\n- **Concurrency is visible.** `parallel`, `race`, `fanOut` and `conclave` are the only ways to do\n two things at once, each branch gets its own journal namespace, and the scope writes its own\n entry saying how it settled: which arm won a race is a recorded fact, decided by the arms'\n recorded clocks and declaration order, never by a scheduler. A failure the program itself caused\n inside the scope settles the entry under its own catalog code (`fanOut` without a stable key is\n `L3021`, kind `runtime`), so a resume reads the same code the live run threw; a plain failure\n from the handler records the generic `L4000` `scope-fault`. A value the program throws inside a\n branch is recorded with that fault, so `catch` receives the same value on resume, and in a\n migration's dry walk, as it did live.\n A branch may not write to anything declared outside it; return the value and read it out of the\n scope's result.\n- **Time and randomness are tamed.** `now()` is the branch's run clock, the end of the last effect\n it awaited; `random()` is a seeded stream derived per scope. Both replay identically.\n- **Values freeze at the boundary.** What crossed into or out of an effect is what the journal\n recorded, and it cannot change afterwards; build a new value.\n- **The journal is the debugger.** Every entry carries its key, its inputs' hash, its outcome and\n its timing, and every error is in the program's own coordinates. A run can be **simulated** with a\n scripted handler and **dry-run** to a plan before it touches an agent. The simulator is\n discrete-event: timed effects park at their wake times and are delivered in wake order on one\n virtual clock, so concurrent branches accumulate the durations they wrote and a simulated `race`\n is decided by the same rule a live handler produces (least recorded clock, ties by declaration\n order). A `sleep(\"1m\")` arm beats a `sleep(\"1h\")` arm whatever their declaration order. Behind the\n worker bridge the simulator delivers its next wake only after the thread reports it has reacted\n to the last one, so a bridged simulation settles a race the same way, also when the dry run's\n recorder wraps the simulator.\n\nFull rules, with every code: [`spec/cotal-lang.md`](../spec/cotal-lang.md).\n\n## Continuing a run\n\n**Resume** is re-execution: the driver replays the journal, the program runs from the top, recorded\nsteps return instantly, and the first unrecorded step is performed live. It refuses a journal that\nbelongs to another run, a pin that differs from the recorded ones, and a different language version.\n\nA failed journal entry replays its error, while a pending entry lets the handler recover the\nexternal work it bound before the interruption.\n\nA relative `sleep` completes if its duration elapses during authorized arming before a pause\nis minted. It creates no past-due checkpoint. An existing pause keeps its recorded deadline and\nsettlement, and cancellation still rejects the sleep. Checkpoint and ask expiry rules are unchanged.\n\nA step that writes to a far side that honors no idempotency key (posting a comment, sending a mail)\nbelongs in `once`. A resume that finds a step inside `once` begun and never settled does not\ndispatch it again: it opens a hold, a checkpoint under a token derived from the step's recorded\nrequest id, whose prompt names that id. Answer it with\n`cotal run answer <run> <step-key> --value <json>`, and the value becomes the step's result. An\nexpired hold fails the step with the catchable L4027. Only `ask` runs inside `once`, so wrap just\nthe step that writes: a host restart while it is in flight costs a settle.\n\n```js\nconst publisher = await spawn(\"publisher\")\nconst res = await once(async () => {\n return await ask(publisher, { name: \"publish\", schema: { commentId: \"number\" } })\n}, { name: \"publish-360\" })\n```\n\n**Migrate** moves a run onto edited source. A dry walk of the new program over the recorded journal\nfinds every recorded step the edit changed (a divergence) and every one it no longer reaches (an\norphan), and the orphan table says what each means: a removed `sleep` is nothing, a removed `turn`\nalready happened, a removed `spawn` is a live agent you must adopt or release, a removed resolved\n`checkpoint` is a human decision you must explicitly discard. The decision is filed as a\n`migration` record with the actor's name on it. An adopted seat (`--adopt <name>#<uid>`) goes to\nthe edited program's next `spawn` of that persona, which returns the recorded handle and mints\nnothing, so the agent keeps its identity, its worktree and its turn history across the edit. A\nreleased seat (`--release <name>#<uid>`) is despawned when the migration commits, through the same\ndischarge a cancelled branch's seat leaves by, so the record never claims a release nothing did.\nThe spawn that adopts a seat binds the orphaned spawn's goal as its own, so a resume of that step\nreads the same seat back and a cancellation of it despawns the seat it holds.\n\n**Fork** starts a new run from a named step of an old one, copying the prefix under the parent's\npins (seed included, so the copied history's pure draws are the same draws). The child is a new run\nunder a new id whose record names the parent and the cut step (`forkedFrom`); the parent is\nuntouched. A spawn inside the copied prefix is honoured by its `onFork`: `\"adopt\"` copies it, and\nthe child shares the parent's agent (the manager shows that seat one turn at a time across both\nruns); `\"respawn\"`, the default, would mint a fresh identity the copied turns do not address, so\nthis host refuses that cut (L5019) rather than rewriting the parent's history.\n\nFork planning and migration inspection use the recorded language version. Version-1 history uses\nthe interpreter. Version-2 history is inspected inside a locked-down worker with a read-only\njournal, without a live effect handler or durable store. Inspection stops before the fork's cut\nstep or any effect that needs new work. Program catch and finally blocks cannot extend the cut.\nThe recorded pins are preserved.\n\n## From an agent session\n\nFresh `cotal setup` defaults declare `capabilities: [spawn, run]`. On an authenticated mesh,\nthat exposes `cotal_run` alongside the teammate tools. The selected manager must support execution.\nRead `cotal_docs` pages `lang-card` and `workflows`, then try:\n\n```json\n{\n \"verb\": \"start\",\n \"source\": \"await sleep(\\\"1s\\\", { name: \\\"first-run\\\" });\",\n \"file\": \"first-run.cotal.js\"\n}\n```\n\nPass this object to `cotal_run`. `source` contains the program; `file` only labels diagnostics\nand reads nothing from disk. The response returns a run ID before execution finishes. Call\n`cotal_run` with `verb: \"status\"` and that `runId` to inspect the state and step journal.\nA completed timer records its sleep step as `ok`.\n\n### If `cotal_run` is missing\n\n1. Call `cotal_orientation` and check the connector version, capabilities and tool list.\n Upgrade an older installation using the [upgrade guide](https://github.com/Cotal-AI/Cotal/blob/main/docs/UPGRADING.md).\n2. Have the operator add `run` to the persona's existing `capabilities` list, for example\n `capabilities: [spawn, run]`. `spawn` alone does not expose `cotal_run`. Setup leaves existing\n personas unchanged except for its [byte-exact legacy migration](getting-started.md).\n Peer persona-definition tools cannot grant capabilities.\n3. Relaunch the agent through the manager from the updated persona so it receives newly issued\n credentials and a fresh connector configuration. Editing the file or reconnecting with the\n old credential does not grant new broker permissions. If the launch sets `COTAL_CAPABILITIES`,\n update that override too; it takes precedence over the file.\n4. Check `cotal_orientation` again, then call `cotal_run` with `verb: \"ps\"` before starting work.\n\nTool visibility alone does not establish execution support. Hosted runs require issued caller\nauthority. Static authentication issues it to its credentials. On a user-auth mesh, a registered\nparticipant manager hosts authorized runs. A managed agent invokes `cotal_run` through its own\nlauncher-provided bearer command and immutable manager instance selection. Its current managed\ngrant and its direct parent's current grant must both carry `run`. A removed grant, stale lifecycle\nor wrong manager selection refuses rather than borrowing a signed-in user's authority.\n\nOpen meshes may expose the tool but refuse hosted runs. A local user-mode host without remote\nrun authority and an `execution: \"none\"` control manager also refuse the family. A legacy\ncredential without issued authority must be replaced through the current issuance path.\n\n## Operating a run\n\nThe manager hosts runs. `cotal run start` hands the program to the manager of the resolved mesh\n(the usual `--space` / `--server` / `--creds` flags), which validates it, mints the run id, drives\nit in its own process, and answers with the id once the run is recorded. The terminal is free the\nmoment the id prints; the run continues on the manager through every pause, and a manager restart\ntakes back every run it had recorded running, from the journal, under the next epoch. `resume`\nnames a run the manager recorded and is refused while the manager is already driving it. `ps` and\n`journal` read; `answer` resolves an open checkpoint, or an open `ask` attempt, from any terminal\nor agent that holds the `run` capability.\n\n`journal` prints an open pause's question under its step. Once a checkpoint or `ask` settles with an\naccepted answer, it instead prints the answer value as JSON, who answered, the artifact when one was\ncited, the recorded time, and the accepted answer id. Expired pauses and ordinary steps print no\nanswer line.\n\nA settled step is never answered twice, so a participant who changes their mind uses `amend`. It\nfiles a new answer beside the accepted one, naming the answer it supersedes, and `journal` prints\neach amendment under the step as an `amended` line, in the order the store committed them. The last\nline is the current position, whatever clock each amender's `at` came from. The pause stays settled\nand the run keeps the answer it acted on. A step that is still open, or that settled with no answer,\nrefuses an amend. The manager records the amender from the credential, as for an answer, and a\nspawned seat may amend only an answer recorded under its own name.\n\n```bash\ncotal run start --file build.cotal.js # the manager starts it; the minted id is printed\ncotal run ps # list run records: state, holder, lineage\ncotal run journal run-3f2a90c41b7e0d5a6c884e19b02df4a1 # print the durable step journal\ncotal run resume run-3f2a90c41b7e0d5a6c884e19b02df4a1 # the manager takes the run back\ncotal run answer run-3f2a90c41b7e0d5a6c884e19b02df4a1 \"/checkpoint:approve#0\" --value '\"yes\"'\ncotal run amend run-3f2a90c41b7e0d5a6c884e19b02df4a1 \"/checkpoint:approve#0\" --value '\"no\"' # record a changed position\ncotal run migrate run-3f2a90c41b7e0d5a6c884e19b02df4a1 --local --file build-v2.cotal.js # check an edited program against the journal\n```\n\nA program that does not validate is refused before anything is recorded, with every problem in the\nanswer as the validator would print it. The driver records the program beside the run, so `resume`\ntakes the run id alone and the manager reads the source back; an edited program is a `migrate` or a\n`fork`, never a resume. `cotal run migrate <runId> --local --file <program>` is that check: it\nreplays the run's journal and walks the edited program over it, prints whether the migration is\nadmissible, how many journal rows the walk accounted for, every orphaned step with its verdict and\ncode, and exits 0 on admissible and non-zero on not. It reads only, under the same credential\n`journal` reads on, and the commit side is not reachable yet: the report itself says what a commit\nwould file and that this invocation filed nothing. An answer is recorded under the answerer the\nmanager knows from the caller's credential: a managed agent by its name, anyone else by their\nprincipal. The request\ncarries no name. An agent with `capabilities: [run]` has the same five verbs as the `cotal_run`\ntool ([MCP tools](mcp-tools.md)), so a program can be written and started from inside a session.\nA `start` or `resume` answers once the run's record is written, within a bounded wait; a manager\nthat is still taking back a predecessor's runs at boot refuses both with `unavailable`, and a\nretry a moment later is the whole remedy.\n\n`--local` drives the run in this process instead: `start`, `resume` and `answer` exit when the\ndrive settles, `--by <who>` names the answerer, and `cotal run resume <runId> --local --file\n<program>` is how a run with no recorded program is continued. On a static mesh the local drive\nmints the run's own credential from the folder's trust material, so it runs from the mesh's\nproject folder. A local start also names the run's channel ceiling itself:\n`--admit-read <channels> --admit-publish <channels>`, comma-separated patterns or `none`, both\nrequired. The record it writes says an operator admitted the run and why, and the host checks\nit the same way it checks a hosted admission. A registered remote user-auth manager can host a\nrun through its issuing host. The host resolves a versioned caller against live issuance,\nadmits the run, and signs only the run's fixed driver, mediator and one-shot operator credentials\nfor manager-held nkeys. Renewal checks the activated attempt; the manager holds no signer.\nLocal user-auth runs remain unavailable because a user bearer holds no run rows. An open mesh\nhosts none either, since it issues no caller authority to admit a run under.\n\nA logged-in user starts runs on that remote manager with `cotal run start`. The auth callout issues\nthe user's manager connection against the user's actor-ledger row, the CLI reads the generation\nback from the connection's accepted row, and every `run` verb rides the versioned rail. The issuing\nhost admits a run only for the owner who registered the manager, and on every resume and answer it\nchecks that owner and that the caller's issuance is still live. It also watches the start, resume\nand answer requests on the broker itself, and admits or issues for one the manager forwards only if\nit saw that request, and only once. A resume or answer gets issuance only for the run, step, endpoint\nand amendment that request named. An answer's\ncredential reaches one pause, and the host reads which one off the run's own journal, for a run\nadmitted on that manager's instance. A request bound to another manager instance or epoch gets nothing, and so does one whose class or pinned contract is not the one the manager registered. Another user's start, answer or resume is refused, and so is a revoked actor's.\nSuch a run spawns, turns and despawns agents owned by that user. A spawn has the reach of the user\nwho started the run, as their actor-ledger row reads at that moment, and the host enrolls each agent\nthrough its managed-agent enrollment. A spawn may be placed on the manager that hosts the run and on\nno other instance. [User-auth run start](design/user-auth-run-start.md)\nrecords the path.\n\nRun admission accepts canonical space names such as `cotal_cloud` and `u_example-name`\nwithout rewriting them. Endpoint names keep their separate DNS-label grammar.\n\nA hosted run is **admitted** under the caller that started it. The caller's credential is an\nissuance ([identity and auth](identity-and-auth.md#issued-authority)): its requests ride a\nversioned rail that carries the credential's generation, and the manager resolves that\ngeneration's recorded permission ceiling and writes it beside the run before the driver starts.\nThat ceiling, the caller's own channel scope as it was issued, is what the run may read and post\nin channels; the manager's own reach never stands in for it. A request from a credential minted\nwithout an issuance is refused with `permission-denied` and a detail naming the caller. A run\nwhose caller had no channels can still sleep, checkpoint and turn agents; its `wait` on a channel\nis refused at the effect.\n\nThe manager endpoint serves `run-cancel` with `{ runId, reason }`. Call it through an issued\ncredential on a handle pinned to the manager instance, retaining the Describe-derived epoch bind.\nThe reason must encode to 1 through 512 UTF-8 bytes, and the request deadline is at most 30 seconds.\nThere is no new CLI or MCP convenience verb in this change.\n\nA signer-holding manager accepts the original caller's owner, actor and lifecycle under a live\nissued generation. On a signerless manager, the issuing host also accepts an interactive actor of\nthat owner with `run`, or an interactive admin. It must observe the caller's request itself before\na manager can forward it. Managed-agent names and platform assignments do not grant this authority.\n\nThe reply separates the durable marker from the local observation: `stopped` means a local drive\noutcome was observed; `stop-requested` means it was not observed within the request budget.\n`launch-abandoned`, `terminal` and `not-driven-here` describe their respective local states.\n`duplicate` is false for a confirmed new marker, true for a confirmed existing one, and null after\nan unknown write followed by marker readback. Preserve unknown outcomes. Neither a marker nor a\nreply proves process cleanup or that another host stopped. Revoked runs remain readable through\n`run show`, but cannot receive new drive or answering authority. The endpoint's `run-status`\nreply carries `revoked: { by, reason }` independently of the recorded run state. A running record\nwith that marker is not proof of a stopped holder. `revocationUnreadable` means the marker could\nnot be checked; do not read that as unrevoked or resumable. A released record without a marker can\nbe resumed under its original admission; a released record with a marker cannot. The human\n`run show` view displays admission revocation separately from recorded state. An unreadable marker\nproduces a partial result with exit 4, in human and JSON output, as it does in the run list.\n\nA cancellation can retain a local pending intent even after a definite no-dispatch refusal if its\nbudget ends before the manager can confirm marker absence. That intent blocks a new local launch\nor resume; it does not stop an existing drive. A later cancellation that observes a marker reconciles\nthe retained intent. Replaying the same request without a marker reports that it remains pending and\ndoes not dispatch again. A manager process restart loses the local intent, but does not settle the\nunknown write or prove cessation. An absent marker after an unknown write does not clear the intent\nin the running process. A `not-executed` write outcome therefore does not promise that a local launch\nis available.\n\nIssuer observation custody has finite lifetime budgets per caller, owner and issuer. Exhaustion\nlogs the budget class once per issuer process, without caller identities or request contents.\nRepeated refusals do not repeat that diagnostic. Custody entries are not evicted to make room.\n\nEmbedding hosts can call a drive's `cancel(reason)` handle to signal cooperative root cancellation.\nThe language's `RootCancellation` source carries the first reason through the walker and compiled\nworker to in-flight effect contexts. It is process-local and carries no authorization. The host must\nestablish permission and observe the durable revocation marker before calling it. A root cancellation\nends the drive as `released`; the status projection can lag if its write fails. Read the recorded\nstate separately. A completion or failure that wins the race keeps its actual outcome. It does not stop an agent process or preempt already-pulled tool work.\n`release(reason)` remains a separate next-effect-boundary request, used for host shutdown.\n\nThe host also supplies `cancellationObserved` for channel-effect classification. A fresh revocation\nread together with an observed local cancellation intent ends that effect as cancelled, even while\nthe manager is still reading the current holder before signalling the root. Pending intents alone\nnever do this. An ordinary local revoke without a cancellation intent keeps its existing failure\nbehavior. The shared observation neither signals the root nor replaces the holder check.\n\nIf the post-marker holder read fails or exceeds the original budget, the command does not signal\nthe root without that check and returns no stopped receipt. A parked checkpoint or sleep can\nremain running beside the marker. Preserve the unknown outcome. A later explicit cancellation\nrequest can read the same marker and a current holder, then report its own observed result.\n\n`cotal run revoke <runId> --local --by <who> --reason <text>` writes the run's revocation marker\nfrom the project folder. An empty `--by` or `--reason` is refused before anything is written. The\nadmission itself is never rewritten. Every host reads the marker\nbefore its next channel effect, so an open `wait` refuses at its next poll, and no resume,\ntakeover or manager restart continues the run. Revoking twice is not an error, and the first\nreason stands. A run whose admission is missing or revoked is left parked by the manager's boot\nreconcile, named in its log.\n\n`run ps`, hosted or `--local`, reads the marker beside each run record and prints `revoked` for a\nrun that carries one, whatever state the record itself holds, with the revoker and the reason\nunder the table. The record is display only here: a revoke writes no terminal state, because no host drove\nthe run to one and the journal owns the facts. A marker the listing cannot read, whether the store\nis unreachable or the marker has a version or shape it does not know, prints `unchecked` in the\n`STATE` column. The reason and the state the record carries go to stderr, and the command exits 1\nonce every row is printed. The hosted `run-ps` rows carry the marker as `revoked` (`by` and\n`reason`) or a failed read as `revocationUnreadable`, beside the record's own `state`.\n\nA run whose step was refused (L5016) stays held; a\nresume on a host that can perform the step performs it live and continues from there.\n`journal` prints what an open pause asks beneath its step key, which is the address `answer` takes\nback. Checkpoint expiry rides the mediated timer writer, which the delivery daemon pumps on a live\nmesh; on a bare broker a pause still resolves, it just cannot expire.\n\n## Diagnostic inventory\n\nA large inventory can exceed the broker's reply limit. A host that supports page mode can return\none bounded JSON page. Page mode is not in release 0.88.0 or earlier: the calling CLI or connector and\nthe manager both need a later version.\n\n```bash\ncotal run ps --page --on <instance> --limit 8\ncotal run ps --page --on <instance> --cursor <cursor>\n```\n\nFor the MCP surface, use `cotal_run` with `verb: \"ps\"`, `instance` and `page: { limit: 8 }`.\nPass a returned `nextCursor` as `page.cursor` to continue. A managed session can use its existing\nmanager binding. Keep the same space, endpoint filter, instance and process epoch throughout a scan.\nThe CLI requires `--on`; page mode is not available with `--local` or other verbs. An older host\nrefuses page mode rather than returning a truncated legacy list.\n\nEach page has at most 16 rows (default 8), and the complete reply fits 65536 bytes and the broker's\npayload limit. The reader issues at most 64 storage calls and stops issuing them after 2 seconds or\nthe smaller request duration. These are application limits, not bounds on broker CPU, disk work,\nclient-library retries or total elapsed time. One row that cannot fit is refused, never split.\n\nThe response carries `semantics: \"diagnostic\"`, the observed sequence cut, responder, storage-read\ncount and `stopReason`. Rows contain spec/status revisions and whether a revocation marker was\nobserved. Programs, journal contents and revocation reasons are omitted. Legacy requests without\npage mode retain their existing array output.\n\n`endObserved` means the walk reached its cut or received a no-match response. It does not prove an\natomic snapshot, absence, ownership, terminal work or worker inactivity. Cursors are unsigned\ncontinuation data. Deleted history and restores can be invisible. Keep partial results incomplete,\nand retain uncertain-start reservations and ownership holds. A continuation that makes no progress\nis reported as incomplete; do not retry it in a loop or use it to release work.\n\n## What is on the wire\n\nThe run's wire footprint is [SPEC \xA714](../SPEC.md#14-workflow-runs-v05):\n\n| Thing | Where | What it is |\n| --- | --- | --- |\n| the run | `run.<endpoint>.<runId>` record | the resolved **pins** (seed, logical epoch, budgets, language version) on the immutable half; holder, lease and `journalHigh` on the status half |\n| the program | `program.<endpoint>.<runId>` record | the source the run was started from, verbatim, written once by the driver that pinned the run; what a resume reads and what a migration is measured against |\n| the step journal | `WFJ_<space>` stream, one subject per run | append-only, no age eviction, no Direct Get; every append fenced by the run subject's own sequence; takeover is replay-then-activate |\n| a checkpoint answer | `answer.<endpoint>.<token>.<answerId>` | the payload beside the one-use settle fact; the settle names the answer it accepted |\n| a notice | `notice.<endpoint>.<runId>.<addresseeId>.<noticeId>` | one bounded decision told to one agent, rendered ahead of its next turn |\n| a migration | `migration.<endpoint>.<runId>.<migrationId>` | the report and who applied it, keyed by the report's own digest |\n| the admission | `admission.v1.<endpoint>.<runId>` in `cotal_admission_<space>` | the caller the run was admitted for, its channel ceiling and its provenance; written once before the driver starts, and the store refuses a second write on the key |\n| a revocation | `revoked.v1.<endpoint>.<runId>` in the same store | who revoked the run and why; create-only, idempotent, permanent at the broker, read by every host before its next channel effect |\n\nThe driver writes `journalHigh` at activation and again after each journal append, before the\nprogram acts on the entry. A successor whose replay ends below it refuses the run with\n`RunJournalTailTruncated`. That holds for records appended since the last activation too.\n\nA run's **driver** connects on a `run-driver` credential minted for one run and takeover\nattempt. It can append to its journal, use its replay durable, and write its own `run`, `program`,\n`notice` and `migration` records. It has no store point reads, checkpoint writes, chat consumers,\nor channel and membership registry grants.\n\nThe hosting process keeps a separate `run-mediator` connection for effects and reads. The driver\nreceives methods and data from that host; it never receives the mediator credential or connection.\nThe host checks the journal's current activation and step identity before dispatch, and checks\npause and wait authority again at each operation. Cancellation cleanup also admits losing steps\nnamed by a settled parent whose `cancel.issued` is still false. That permission allows cleanup;\nit cannot mint or rearm a cancelled pause. Wait acknowledgements consume host-held delivery\nreceipts. A recorded match can be reread only at its bound sequence and channel. A conclave's\nrecorded ownership flag must match its step-derived channel before registry writes or cleanup.\n\nThe mediator retains endpoint-wide checkpoint rights and stream-wide leader reads as trusted\nhost authority. Record reads exposed to the driver are restricted to its own run's keys. Reads\nthat decide writes remain leader-served. A read of the journal uses the run's filtered replay\ndurable, including the diagnostic for a journal with no run record. That durable is named after\nthe takeover, and an attempt reads it many times, so reads under one takeover run one at a time in\nthe hosting process and a replay removes a durable of its own name that an interrupted earlier read\nleft behind. A durable that survives a replay's own delete belongs to a reader the process cannot\naccount for, and reading its tail is refused. A drive handles that refusal as a takeover does: the\nreads behind its steps, and the diagnostic for a journal with no run record, replay up to three\ntimes before the refusal is raised. An operator read runs under a takeover minted for that read and\nreports the refusal on its first read.\n\nA served read uses a one-shot `run-operator` credential. An answer uses a read to find the open\npause, then a second credential pinned to that token for the answer and settlement.\n`cotal run --local` uses the same driver/mediator split. On an authenticated mesh it needs the\nlocally recorded space signer to mint both credentials; a single `--creds` file is refused.\nDirect library users supplying broker clients to `MeshHandler` are constructing a trusted effect\nhost. A hosted driver receives its closed effect interface instead.\n\nThis split confines broker credentials; it is not process isolation for injected host code.\nThe runtime and its effect host share the manager process. A run's channel reach is the admitted\nceiling ([SPEC \xA714.8](../SPEC.md#148-run-admission)): the starting caller's issued channel scope,\nrecorded once in `cotal_admission_<space>` under a per-run `run-admitter` credential the driver\nnever holds, and re-read by the host before every channel effect. Spawn and turn keep their own\ndelegated checks; `notify` writes agent-addressed notices and is not channel publication. Treat\n`run` as program-execution authority bounded by that ceiling, not as sandboxing of the program.\n\nA version-1 fork can replay its settled parent history through the host. Inherited checkpoint\nidentifiers carry no authority to read, rearm or claim the parent's pauses. New child effects use\nchild-derived identifiers. A fork is a new run and takes a new admission under the caller who\nforks it; the parent's ceiling is not inherited.\n\n\n## What ships today\n\nThe language, its validator, interpreter, simulator and dry run are `@cotal-ai/lang`\n(`packages/lang`), usable in-process with your own effect handler and with no broker: `validate(src)`,\nthen `run(src, { runId, handler })`, and `resume(src, journal, { runId, pins, handler })` to pick a\nrun up from its journal (the package README has the snippet, with `SimHandler` as the handler). That\nis the in-process route, yours to drive with your own handler; a run the driver starts executes on\nthe compiled engine, as the engine paragraph below says. The wire\nsubstrate of \xA714 (the `WFJ_<space>` stream, the five record kinds, the activation barrier, the\nper-run grants) is in `@cotal-ai/core`, and the run driver, journal store, migrate and fork are\n`@cotal-ai/runtime` (`implementations/runtime`). The migrate check is reachable as\n`cotal run migrate <runId> --local --file <program>`; committing a migration it judged admissible\nis not reachable from any surface yet. On the mesh handler, `sleep`, `checkpoint`,\n`wait(message(...))`, `wait(idle(...))`, `wait(down(...))`, `wait(replied(...))`, `notify`,\n`spawn`, `conclave`, `ask`, `monitor` and `turn` are durable.\n`spawn` is\nthe manager's spawn action submitted under the step's own identity: the goal binds under the step's\nrequest id, so a resumed run re-attaches to the same seat instead of allocating a second one, a\nfailed or refused spawn is catchable as L4002 with the manager's recorded reason, and a spawn on a\nrace branch that loses is despawned by the run's own cancellation sweep. A seat belongs to the run\nthat spawned it: when the run completes, it despawns every seat it spawned, including a race\nwinner's and one whose spawn failed while its process stayed up. A spawn marked `onFork: \"adopt\"`\nis the exception: a fork can share that seat and no run can see whether another still uses it, so\nthe seat stays up until you stop it with `cotal stop` once every run sharing it is done. A seat a\nmigration handed to a later spawn follows that spawn's policy, and it stays up if any spawn that held\nit was marked `onFork: \"adopt\"`, because a fork taken before the migration may still share it. In a\nspace with several managers, a despawn counts a seat as already gone only when the manager that\nallocated it says so. A run that\nfails or is released keeps its seats until a resume completes it or you stop them with\n`cotal stop`. Start a seat with `cotal spawn` when it should outlive any run. `permits` are the budgets\nthis host meters: `turns`, how many turns the run may dispatch to the agent, and `wallClock`, a\nduration from the spawn after which no turn is admitted. The turn that would exceed one is the\ncatchable L4001 (kind `permit-turns` or `permit-wall-clock`; a deadline the remaining wall clock\ncannot hold counts as exceeding it), an adopted run counts the turns its journal recorded, and a\nbudget the host has no meter for, such as `tokens` or `spend`, is refused at the spawn rather than\naccepted and ignored. `supervise` is the restart policy this host asks the manager to enforce:\n`restarts`, how many in-window process deaths may come back under the same handle, and `window`,\nthe duration those deaths are counted in (default `10m`). The manager restarts the process in\nplace under the same name, lifecycle uid, persona, worktree and permits; `monitor` does not fire\nfor a restart, and `wait(down)` fires only when the seat is gone for good. Spending the budget\nretires the seat, and the next `turn` is the catchable L4002. The policy also covers the manager's\nown death. On a static mesh, when the manager ends without stopping the seat (a crash, or a pty seat\nthat ended with its manager), the next manager of the same instance restarts the seat the same way\nand counts that as one restart. A turn the dead manager had accepted for the seat is served to the\nrestarted seat, so the interrupted turn runs again and finished turns do not. A seat whose budget\nis spent, or whose persona file or credential changed or expired while no manager ran, is retired instead. A policy this host cannot enforce\n(an unknown key, a user-mode seat, or a runtime that cannot respawn a name in place) is refused\nat the spawn rather than accepted and ignored. `events: false` is the workflow form of\n`cotal spawn --no-events`: the seat starts without its AG-UI event plane. A connector that\npublishes none, such as Hermes, needs it, because an omitted `events` arms the plane and the\nmanager refuses that connector at the spawn. A value that is not a boolean is refused at the\nspawn. `conclave` joins its\nmembers to a real channel as durable membership rows: the channel derives from the step's own\nrequest id when the program names none (a program-named channel is borrowed, never torn down, and\na membership that predates the conclave survives its close), each member handle resolves to its\nprincipal through the seat's own presence row (an absent member is catchable as L4002), and a\nconclave cancelled on a losing branch is released by the same cancellation sweep. `ask` parks one\ncheckpoint-plane pause per attempt, answered through `cotal run answer` as a checkpoint is, and\ntells the agent through the same relay `turn` uses: one relay per attempt under the attempt's own\ntoken, carrying the schema, the attempt count, the deadline and the previous refusal, which the\nseat's connector renders as the record wanted and the hosted command that answers it. The relay\nnames the ask's absolute deadline, so the manager stops serving an attempt at the instant its\npause expires and refuses a relay already past it. When that\nliteral command is run inside the managed seat, the CLI reuses the seat's lifecycle credential and\nissued caller identity rather than minting an operator instrument. The command\ndoes not take `--by`: the manager records the authenticated caller as the answerer. A spawned seat's\nbaseline credential carries only the self-targeted `run-answer` row, and the manager accepts it only\nfor the open ask or escalation relayed to that exact incarnation. It cannot answer another seat's\nask, an unrelayed checkpoint, another run, or start and resume commands. An ask addresses\nan agent the run spawned (anything else refuses before an attempt opens), a resumed attempt tells\nthe seat nothing twice, and a seat gone at the relay is L4002. On the pause itself:\nthe shorthand of the language reference \xA76.5 is enforced (an unreadable schema is L4022), a\nnon-conforming answer costs one attempt and its refusal reason is recorded on the entry for the\nanswerer to read, exhausted attempts (default one) are the catchable L4006, and so is the one\nabsolute deadline for the whole ask passing with no conforming record (its kind is `ask-deadline`),\nincluding one that passes before an attempt's pause is armed.\n`checkpoint` binds what it asks on its own entry, so `cotal run journal` prints the question under\nthe step key an answer is addressed by while the pause is open: the address alone left whoever was\nasked reading the source to find out what \"approve\" meant. The entry also records the open\nattempt's deadline and the `onExpiry` it was armed with, which `run journal --json` reports; what\nan expiry does is still decided from the program's source. An escalation's attempt records its own\ndeadline, one `timeout` after it opens, and a resumed attempt keeps the deadline it recorded. After a checkpoint or `ask` accepts an\nanswer, the journal prints that accepted answer's recorded value and attribution under the settled\nstep. A checkpoint's comes from its frozen result, and the journal never substitutes another filed\nanswer or invents fields that result does not hold. An `ask`'s result is the value alone, so its line\nis read from the answer record its last attempt's settle named, even when that value is a record with\nfields named like a checkpoint's. Amendments print on their own lines after it.\nAn `escalate` addressed to an agent this\nrun spawned is relayed to that seat through the same turn relay an `ask` uses, carrying the prompt\nand the token to answer under, held to the pause's own deadline; a `to` naming anyone else is a\nperson, and their pause stays the one anybody can answer, with the addressee recorded and rendered\nbeside the question.\n`monitor` registers interest in an agent, and the\nregistration is the journal entry itself, carrying the handle it registered: monitoring an agent\nthat is already dead succeeds, and the death is the wait's to observe. `wait(down(...))` observes\na monitored agent, and refuses one the run never performed `monitor` on. It reads the death off presence liveness, the\nsame witness a conclave join resolves members through: the value carries the handle, the reason\n(`lapsed` when nothing live holds the name any more, `superseded` when a live row holds it under\na different incarnation) and the time of observation. Presence liveness skips any value without a\nstring `card.id` and `card.name`, so a participant that publishes a malformed row under its own key\ncannot fail another run's wait, turn or conclave join. A superseded incarnation is down at once. A\nlapsed one is down only after its presence row has stayed gone for 30 seconds, because a seat whose\nconnector stalls past the row's 6-second TTL, under host load or across a reconnect, renews it under\nthe same incarnation and is still working. The 30 seconds count only across presence reads that\neach end within 6 seconds of the previous one starting, so a slow read or a run of failed reads, which\ncould hide a renewal, starts the count over. Even then the run asks the manager first: a seat whose\nprocess the manager still runs is alive however long its row has been gone, so the count starts over.\nOnly the manager that spawned the seat can report it gone, because another manager in the space knows\nonly its own seats, so the run asks again until that one answers. When it does not answer, the presence\nrow is the only witness and the lapse stands. The asking stops at the wait's timeout or cancellation,\nand at a turn's deadline, yield or cancellation, which then decide. A wait that begins\nafter the death resolves once that holds, and a timeout resolves null on one absolute deadline a\nresumed run re-attaches to.\n`turn` wakes one seat for one host turn through the manager as a pull-shaped relay: the run\nsubmits the turn under the step's own identity, the manager holds it as a goal pinned to the\nseat's incarnation, and the seat pulls it under its own reach ahead of its next host turn, so\nnothing is pushed into a session mid-thought. The payload the seat reads names the run and the\nstep and carries the rendered run context, plus any pending notices addressed to it, which the\nturn consumes. The seat yields through `cotal_yield` (`done`, `blocked`, or `handoff` with an\naddressee), and ending its host turn yields `done` for every turn it was shown. A `handoff` names\nanother seat the same run spawned: the next `turn` in the same scope to that seat records the\nlink, a handoff to a name the run never spawned is the catchable L4005, and one to a seat bound\nto a different worktree is L4004. The deadline elapsing before any yield is the catchable L4003:\nthe acceptance names the instant, the manager's goal-bound hold denies at it, and the run arms its\nown pause on that same instant, so either side outliving the other still converges on the same\nanswer. A seat that pulled the turn earlier stops showing it to its session at that same\ninstant. A seat that dies mid-turn is read off its own presence row by the run itself, with the\nsame 30-second confirmation for a lapsed row, and is the catchable L4002, and a death the manager\nmarked on the deadline terminal reads the same way. Two\nturns on one seat, from two branches or from two runs, reach it one at a time: the language\ndispatches the second when the first settles, and the manager shows a seat the oldest unsettled\nturn alone. A turn's goal id belongs to the caller that submitted it, so two callers may each submit\na turn under the same id. A seat yields by goal id alone, so the manager refuses to hand one seat\na second caller's turn under an id it already holds. On an auth mesh the relay needs no extra grant: every spawned seat's baseline\ncredential carries its own pull, yield, and caller-bound answer rows, the run driver's operator instrument carries the\nturn request and the `inspect` read that confirms a lapse, and the manager arms the deadline hold over its own serve grant and expires it\nitself once due. An accept the manager cannot finish is unwound to a failed terminal on the goal\nit bound, and a retry of that submission is refused naming the terminal rather than accepted a\nsecond time.\n`wait(replied(...))` observes those turns from another branch: a completed turn is a reply, and\nthe wait resolves with the observation record (the handle, the yield's status and note, the\nyield's own stamp). It reads as a level, the way `wait(down)` does: a reply that already exists\nresolves the wait at once, and two replies resolve to the latest by the yield's stamp. A denied\nor cancelled turn is never a reply, so an unanswered wait rides its own mediated timeout to\n`null`, and a handle the run never spawned or turned refuses loudly, since only this run's turns\nare observable. A turn the run itself ended without an accepted yield (its deadline, a\ncancellation, a refused handoff) is never a reply, whatever the seat yields to the relay later.\nA cancelled branch also withdraws what it relayed to a seat before its cancellation completes: its\nturn, its ask attempt or its escalation ends `cancelled` through the manager's reserved `cancel`\n(SPEC \xA713.6), so the seat is not shown it after the branch's scope settles and the next turn to\nthat seat does not wait behind it. Only the manager that accepted the relay holds it, so in a\nspace with more than one manager the run sends the cancel again while another manager refuses it,\nuntil the accepting one answers. A cancel refused as `unavailable`, such as by a manager still\nreconciling at boot, or one whose reply is lost (`deadline-exceeded`) is sent again until it\nlands or the relay's deadline passes, so the branch's cancellation does not complete while the\nseat can still be shown the relay. Any other refusal fails the branch's step at once under its\ncode, since sending it again changes nothing: a manager that serves no `cancel` is refused\n`not-found` before anything is sent, and an accepting manager with no such relay pending refuses\n`failed-precondition`. The run reads the relay's deadline from its goal and retries a failed\nread, since the relay may still be served until the deadline is known. A goal record\nthat can never yield a deadline, such as one that is not JSON, fails the branch's step at once,\nand so does a refusal that lasts past the deadline.\nA `spawn` may bind its agent to a **logical worktree** (`spawn(\"builder\", { worktree: \"wt-1\" })`):\nthe handle carries the id, and the run enforces the one rule the language states about it: two\nagents never share a worktree concurrently. The validator rejects the literal case up front\n(L3022: two branches of one concurrent scope spawning into one literal worktree, named branch\nfunctions included), and the runtime guards the rest, computed ids included: a spawn claims its\ntree before it submits, so a second spawn into a tree held by a live seat or by a spawn still\nbringing one up is the catchable L4008, a spawn that ends without a handle gives the tree back,\nand the tree is reusable the moment a holder's presence row is gone, so a discharged race loser\nor a crashed seat releases its tree with no bookkeeping. A spawn the endpoint refuses at accept\nis the catchable L4000 (L4001 when the refusal is the endpoint's seat capacity), and one whose\nseat never came up is L4002. A refusal that states the command did not run is answered\nbefore it gets that far. In a space served by more than one manager the resolve and the invoke\nare separate trips through the same anycast queue, so a run's call can reach an instance it did\nnot resolve against, and that instance refuses ahead of any effect. The run drops its resolved\nhandle, re-describes and re-issues, for a bounded number of attempts; after them the refusal\nsurfaces as the effect's own failure and still states that nothing ran. A spawn that names a\n`placement` addresses one instance by name, so a refusal from it is that incarnation answering\nabout itself and is never re-issued. When the spawn also names `cwd`, that manager resolves the\nexisting absolute directory to its canonical host path before accepting the spawn. A missing,\nrelative or non-directory path refuses with no seat and never falls back to the manager workspace.\nOne hosted run may name one placement instance; a program naming several is refused instead of\nwidening one run credential across hosts. Placement is accepted only from an object-literal spawn\noption bag whose `endpoint` and `instanceId` are string literals. A computed option bag, a computed\nplacement field, or a spread in either object refuses at `run-start`. The option argument may be\nabsent, and an object-literal bag without placement is accepted.\nA turn handoff across worktrees is the L4004 described above. Recovery keeps these honest: a resumed run\nreseeds its roster, holders and handoff memos from its own journal, and the driver re-issues any\nrecorded-but-undischarged cancellation at adoption, before the engine performs a new step, so a\nloser a crash left alive does not keep its seat or its tree while the resumed run works on. The\nsame sweep withdraws a cancelled branch's relays and undelivered notices: a notice waits on the run for its\naddressee's next turn, so a decision the run cancelled would otherwise arrive at an agent with\nnothing to distinguish it from one that stood.\n\nEvery effect the language defines performs on the mesh handler; nothing is refused as\nnot-yet-durable any more. The operator surface over the driver is `cotal run`; the section above has the verbs.\n\n**Two engines, and which one runs your program.** The tree-walker is language version `1` and the\ncompiled engine is version `2`, two languages rather than two speeds of one (`spec/cotal-lang.md`\n\xA78.4 lists what differs). The driver hosts both: **every run a driver starts is stamped `2` and\nexecuted by the compiled engine**. The program runs in its own locked-down worker thread with\nnothing in its global scope, while the effects and the durable journal stay in the driver's process,\nbridged over a message port. No socket or credential enters the isolate holding the program,\nand **every version-`1` record keeps replaying on the walker**, which is the walker's job. On either\nengine the driver bounds an effect's `ok` result at the broker's `max_payload` less 4096 bytes: a\nlarger result is refused ahead of the settling append (**L5006**), the step stays pending, and the\nrun is released. The driver serves a declared set of versions, and a record whose version it does\nnot serve is refused by name (**L5023**) with the run left untouched, instead of being replayed by\nwhichever engine happens to be present. Records do not cross between versions in either\ndirection; the repair is to resume on the recorded version, or to fork.\n\n**The engine needs node 22 or newer** and refuses below it as `EngineUnavailable`, which is an\nimplementation limit and not a language error: it carries no `L` code, so there is nothing to look\nup in the catalog. It is a floor rather than a warning because the engine's frame plumbing rests on\n`AsyncLocalStorage`, and 22 is the lowest node it has been measured on. The walker has no such floor.\n"
|
|
27503
|
+
"body": "# Workflow runs\n\n## Keyed submission\n\nThe new `run-start-keyed` command binds an envelope submission ID to one host-minted run.\n`run-submission-status` reads that binding for the authenticated caller incarnation. Both need\nfresh issued reach and a digest-verified description pinned to one manager instance and epoch.\nLegacy `run-start` is unchanged. An unsupported keyed command never falls back to it.\n\nThe scheduler must persist the submission ID, semantic fingerprint, caller incarnation,\ncontract digests and instance/epoch before dispatch. The client helpers prepare that snapshot\nand send one attempt at a time. They pass on only `signal` and `deadlineMs`, which a keyed\nrequest keeps at or below 30000. They refuse a reply that names another submission, instance,\nepoch or fingerprint, and that outcome stays unknown. They do not schedule retries, replace\nunknown work or re-pin it to another manager. Their prepared handle is process-local and WeakMap-bound: serialized JSON\ncannot be rehydrated through these helpers. Persisted scheduler recovery needs separate reviewed\nintegration, not a cast or an automatic fresh Describe. Keep the original work reservation\nwhile reconciling any unknown outcome.\n\nThe binding winner may make at most three admission-create attempts in its original callback\ndeadline, with the same admission bytes and authority. Initial authorization precedes binding,\nbut fresh authority rechecks before each admission attempt can fail after binding and retain\nbinding-only debt. The manager refuses a start while it is stopping or already at its launch\nlimit before it reads or writes a binding, so that refusal writes nothing. It says nothing about\nan earlier attempt under the same key, which may already be bound or running.\n`run-submission-status` takes no launch slot, so read the outcome there. A launch that fails after\nadmission leaves `admitted` with no run, and no later call launches it. Later duplicates only\nread. A `binding-only` result is a visible reconciliation hold, not a queue or permission to repair.\nAn absent admission is an observation at read time, not proof that the original callback cannot\nstill write. A changed caller generation or manager epoch refuses rather than adopting the run.\n\nThe receiving manager or issuing host measures the request budget on its own monotonic clock,\nand checks the caller's authority expiry on the wall clock. Wall-clock changes do not extend or\nspend the monotonic request budget. Authority expiry remains a wall-clock check. The callback\ndeadline a status read returns is a wall-clock record of that window, not something a caller or\nhost enforces.\n\nThe original callback timer bounds waiting and starting more attempts. It does not cancel an\nalready-issued broker publish, which may land later with the same frozen admission bytes.\nNo new admission attempt starts after the request budget or authority window ends. Once an\nadmission exists, the activation check uses only the manager's remaining monotonic request budget;\nit does not recheck the caller's wall-clock expiry. Keep the reservation even if an earlier read\nobserved no admission.\n\nSubmission state `activated` reports evidence of activation, including completed and failed\nruns. It is not a running-state or active-lane signal. Use native `run-status` for lifecycle\nstate. Terminal submission bindings remain permanent, and duplicate starts never relaunch them.\n\nThis implementation is under validation. Static native and signerless issuer paths require\nseparate acceptance evidence before release or integration.\n\n> **Concept** (informative) \xB7 **For:** people writing a durable multi-agent workflow, and implementers hosting one \xB7 **Normative:** [SPEC \xA714](../SPEC.md#14-workflow-runs-v05) and the language reference [`spec/cotal-lang.md`](../spec/cotal-lang.md)\n\nA **workflow run** is a program that coordinates agents over hours or days and survives the\nprocess that started it. The program is written in **Cotal Lang**, a small subset of JavaScript in\nwhich every interaction with the world is one of a dozen **effects** (`spawn`, `turn`, `ask`,\n`checkpoint`, `sleep`, `wait`, `notify`, `monitor`, and the four concurrency scopes) and everything\nelse is ordinary, pure JavaScript. Every effect is written into the run's **step journal** before\nit is performed and settled after, keyed by where in the program it happened rather than by when,\nso a run that dies is resumed on any host by **re-running the program from the top** with recorded\neffects returning their recorded results. Nothing about the interpreter is ever serialized: the\njournal and the program are the whole state.\n\n## A first program\n\n```js\nconst planner = await spawn(\"planner\")\nconst builder = await spawn(\"builder\", { worktree: \"wt-1\" })\n\nconst plan = await ask(planner, { name: \"plan\", schema: { steps: \"array\" } })\nconst ok = await checkpoint(\"approve-plan\", \"Approve the plan?\", { timeout: \"4h\", onExpiry: \"proceed\" })\nif (ok.status !== \"resolved\") {\n await notify([planner], { decision: \"approve-plan\", outcome: \"expired\" })\n}\n\nconst r = await turn(builder, { name: \"build\", deadline: \"30m\" })\nif (r.status === \"blocked\") {\n await turn(planner, { name: \"unblock\" })\n}\n\nconst outcome = await race({\n reply: () => wait(replied(builder), { timeout: \"20m\" }),\n giveUp: () => sleep(\"1h\"),\n}, { name: \"await-or-move-on\" })\nlog(\"outcome\", outcome.index)\n```\n\nRead it as the flowchart it is. `spawn` brings agents in; `ask` is the narrow case where the\nprogram itself needs a value (`schema` is a record the program hands the handler unchanged; the\nlanguage hashes it and gives it no meaning, and the handlers in this repository enforce it as the\nshorthand of the language reference \xA76.5);\n`checkpoint` is a durable pause a human resolves from anywhere, raced against a durable timer; `turn`\nwakes an agent for one turn and returns how it yielded; `race` runs two branches and keeps the one\nwhose recorded clock is earliest. Agents talk to each other in channels as they always do; the\nprogram never speaks in a channel, and the one thing it can put in front of an agent (`notify`) is a\nbounded decision record, not prose.\n\n## The mental model\n\n- **Pure code is JavaScript.** Loops, records, arrays, closures, template literals, destructuring,\n `try`/`catch`, arithmetic, `switch`, compound assignment, optional chaining, spread and rest: what\n you would write anyway, with the parts that hide effects or make meaning depend on the host removed\n (`class`, `this`, `new`, `for...in`, `==`, labels, regex literals, `Math`/`Date`/`JSON`, promises,\n generators). Every refusal names its code and the edit that fixes it. The builtins are a short list\n (`keys`, `map`, `sort`, `json.stringify`, `now()`, `random()`), and arrays, strings and numbers\n answer their usual methods (`xs.map`, `s.trim()`, `n.toFixed()`) and nothing outside that table.\n Records and arrays you build are yours to change until they cross an effect boundary; a member you\n do not own, a host prototype, or a value another branch built is refused with a code, never a\n surprise.\n- **Every effect is journalled and hashed.** A step is keyed `(scope path, kind, name, occurrence)`\n and its inputs are hashed. Reorder your program, add a step, rename a variable: recorded steps\n still match. Change what a step asks (a checkpoint's prompt, a sleep's duration, a turn's\n deadline) and the resume stops with a **divergence** naming the step, rather than replaying an\n answer to a question the program no longer asks.\n- **Concurrency is visible.** `parallel`, `race`, `fanOut` and `conclave` are the only ways to do\n two things at once, each branch gets its own journal namespace, and the scope writes its own\n entry saying how it settled: which arm won a race is a recorded fact, decided by the arms'\n recorded clocks and declaration order, never by a scheduler. A failure the program itself caused\n inside the scope settles the entry under its own catalog code (`fanOut` without a stable key is\n `L3021`, kind `runtime`), so a resume reads the same code the live run threw; a plain failure\n from the handler records the generic `L4000` `scope-fault`. A value the program throws inside a\n branch is recorded with that fault, so `catch` receives the same value on resume, and in a\n migration's dry walk, as it did live.\n A branch may not write to anything declared outside it; return the value and read it out of the\n scope's result.\n- **Time and randomness are tamed.** `now()` is the branch's run clock, the end of the last effect\n it awaited; `random()` is a seeded stream derived per scope. Both replay identically.\n- **Values freeze at the boundary.** What crossed into or out of an effect is what the journal\n recorded, and it cannot change afterwards; build a new value.\n- **The journal is the debugger.** Every entry carries its key, its inputs' hash, its outcome and\n its timing, and every error is in the program's own coordinates. A run can be **simulated** with a\n scripted handler and **dry-run** to a plan before it touches an agent. The simulator is\n discrete-event: timed effects park at their wake times and are delivered in wake order on one\n virtual clock, so concurrent branches accumulate the durations they wrote and a simulated `race`\n is decided by the same rule a live handler produces (least recorded clock, ties by declaration\n order). A `sleep(\"1m\")` arm beats a `sleep(\"1h\")` arm whatever their declaration order. Behind the\n worker bridge the simulator delivers its next wake only after the thread reports it has reacted\n to the last one, so a bridged simulation settles a race the same way, also when the dry run's\n recorder wraps the simulator.\n\nFull rules, with every code: [`spec/cotal-lang.md`](../spec/cotal-lang.md).\n\n## Continuing a run\n\n**Resume** is re-execution: the driver replays the journal, the program runs from the top, recorded\nsteps return instantly, and the first unrecorded step is performed live. It refuses a journal that\nbelongs to another run, a pin that differs from the recorded ones, and a different language version.\n\nA failed journal entry replays its error, while a pending entry lets the handler recover the\nexternal work it bound before the interruption.\n\nA relative `sleep` completes if its duration elapses during authorized arming before a pause\nis minted. It creates no past-due checkpoint. An existing pause keeps its recorded deadline and\nsettlement, and cancellation still rejects the sleep. Checkpoint and ask expiry rules are unchanged.\n\nA step that writes to a far side that honors no idempotency key (posting a comment, sending a mail)\nbelongs in `once`. A resume that finds a step inside `once` begun and never settled does not\ndispatch it again: it opens a hold, a checkpoint under a token derived from the step's recorded\nrequest id, whose prompt names that id. Answer it with\n`cotal run answer <run> <step-key> --value <json>`, and the value becomes the step's result. An\nexpired hold fails the step with the catchable L4027. Only `ask` runs inside `once`, so wrap just\nthe step that writes: a host restart while it is in flight costs a settle.\n\n```js\nconst publisher = await spawn(\"publisher\")\nconst res = await once(async () => {\n return await ask(publisher, { name: \"publish\", schema: { commentId: \"number\" } })\n}, { name: \"publish-360\" })\n```\n\n**Migrate** moves a run onto edited source. A dry walk of the new program over the recorded journal\nfinds every recorded step the edit changed (a divergence) and every one it no longer reaches (an\norphan), and the orphan table says what each means: a removed `sleep` is nothing, a removed `turn`\nalready happened, a removed `spawn` is a live agent you must adopt or release, a removed resolved\n`checkpoint` is a human decision you must explicitly discard. The decision is filed as a\n`migration` record with the actor's name on it. An adopted seat (`--adopt <name>#<uid>`) goes to\nthe edited program's next `spawn` of that persona, which returns the recorded handle and mints\nnothing, so the agent keeps its identity, its worktree and its turn history across the edit. A\nreleased seat (`--release <name>#<uid>`) is despawned when the migration commits, through the same\ndischarge a cancelled branch's seat leaves by, so the record never claims a release nothing did.\nThe spawn that adopts a seat binds the orphaned spawn's goal as its own, so a resume of that step\nreads the same seat back and a cancellation of it despawns the seat it holds.\n\n**Fork** starts a new run from a named step of an old one, copying the prefix under the parent's\npins (seed included, so the copied history's pure draws are the same draws). The child is a new run\nunder a new id whose record names the parent and the cut step (`forkedFrom`); the parent is\nuntouched. A spawn inside the copied prefix is honoured by its `onFork`: `\"adopt\"` copies it, and\nthe child shares the parent's agent (the manager shows that seat one turn at a time across both\nruns); `\"respawn\"`, the default, would mint a fresh identity the copied turns do not address, so\nthis host refuses that cut (L5019) rather than rewriting the parent's history.\n\nFork planning and migration inspection use the recorded language version. Version-1 history uses\nthe interpreter. Version-2 history is inspected inside a locked-down worker with a read-only\njournal, without a live effect handler or durable store. Inspection stops before the fork's cut\nstep or any effect that needs new work. Program catch and finally blocks cannot extend the cut.\nThe recorded pins are preserved.\n\n## From an agent session\n\nFresh `cotal setup` defaults declare `capabilities: [spawn, run]`. On an authenticated mesh,\nthat exposes `cotal_run` alongside the teammate tools. The selected manager must support execution.\nRead `cotal_docs` pages `lang-card` and `workflows`, then try:\n\n```json\n{\n \"verb\": \"start\",\n \"source\": \"await sleep(\\\"1s\\\", { name: \\\"first-run\\\" });\",\n \"file\": \"first-run.cotal.js\"\n}\n```\n\nPass this object to `cotal_run`. `source` contains the program; `file` only labels diagnostics\nand reads nothing from disk. The response returns a run ID before execution finishes. Call\n`cotal_run` with `verb: \"status\"` and that `runId` to inspect the state and step journal.\nA completed timer records its sleep step as `ok`.\n\n### If `cotal_run` is missing\n\n1. Call `cotal_orientation` and check the connector version, capabilities and tool list.\n Upgrade an older installation using the [upgrade guide](https://github.com/Cotal-AI/Cotal/blob/main/docs/UPGRADING.md).\n2. Have the operator add `run` to the persona's existing `capabilities` list, for example\n `capabilities: [spawn, run]`. `spawn` alone does not expose `cotal_run`. Setup leaves existing\n personas unchanged except for its [byte-exact legacy migration](getting-started.md).\n Peer persona-definition tools cannot grant capabilities.\n3. Relaunch the agent through the manager from the updated persona so it receives newly issued\n credentials and a fresh connector configuration. Editing the file or reconnecting with the\n old credential does not grant new broker permissions. If the launch sets `COTAL_CAPABILITIES`,\n update that override too; it takes precedence over the file.\n4. Check `cotal_orientation` again, then call `cotal_run` with `verb: \"ps\"` before starting work.\n\nTool visibility alone does not establish execution support. Hosted runs require issued caller\nauthority. Static authentication issues it to its credentials. On a user-auth mesh, a registered\nparticipant manager hosts authorized runs. A managed agent invokes `cotal_run` through its own\nlauncher-provided bearer command and immutable manager instance selection. Its current managed\ngrant and its direct parent's current grant must both carry `run`. A removed grant, stale lifecycle\nor wrong manager selection refuses rather than borrowing a signed-in user's authority.\n\nOpen meshes may expose the tool but refuse hosted runs. A local user-mode host without remote\nrun authority and an `execution: \"none\"` control manager also refuse the family. A legacy\ncredential without issued authority must be replaced through the current issuance path.\n\n## Operating a run\n\nThe manager hosts runs. `cotal run start` hands the program to the manager of the resolved mesh\n(the usual `--space` / `--server` / `--creds` flags), which validates it, mints the run id, drives\nit in its own process, and answers with the id once the run is recorded. The terminal is free the\nmoment the id prints; the run continues on the manager through every pause, and a manager restart\ntakes back every run it had recorded running, from the journal, under the next epoch. `resume`\nnames a run the manager recorded and is refused while the manager is already driving it. `ps` and\n`journal` read; `answer` resolves an open checkpoint, or an open `ask` attempt, from any terminal\nor agent that holds the `run` capability.\n\n`journal` prints an open pause's question under its step. Once a checkpoint or `ask` settles with an\naccepted answer, it instead prints the answer value as JSON, who answered, the artifact when one was\ncited, the recorded time, and the accepted answer id. Expired pauses and ordinary steps print no\nanswer line.\n\nA settled step is never answered twice, so a participant who changes their mind uses `amend`. It\nfiles a new answer beside the accepted one, naming the answer it supersedes, and `journal` prints\neach amendment under the step as an `amended` line, in the order the store committed them. The last\nline is the current position, whatever clock each amender's `at` came from. The pause stays settled\nand the run keeps the answer it acted on. A step that is still open, or that settled with no answer,\nrefuses an amend. The manager records the amender from the credential, as for an answer, and a\nspawned seat may amend only an answer recorded under its own name.\n\n```bash\ncotal run start --file build.cotal.js # the manager starts it; the minted id is printed\ncotal run ps # list run records: state, holder, lineage\ncotal run journal run-3f2a90c41b7e0d5a6c884e19b02df4a1 # print the durable step journal\ncotal run resume run-3f2a90c41b7e0d5a6c884e19b02df4a1 # the manager takes the run back\ncotal run answer run-3f2a90c41b7e0d5a6c884e19b02df4a1 \"/checkpoint:approve#0\" --value '\"yes\"'\ncotal run amend run-3f2a90c41b7e0d5a6c884e19b02df4a1 \"/checkpoint:approve#0\" --value '\"no\"' # record a changed position\ncotal run migrate run-3f2a90c41b7e0d5a6c884e19b02df4a1 --local --file build-v2.cotal.js # check an edited program against the journal\n```\n\nA program that does not validate is refused before anything is recorded, with every problem in the\nanswer as the validator would print it. The driver records the program beside the run, so `resume`\ntakes the run id alone and the manager reads the source back; an edited program is a `migrate` or a\n`fork`, never a resume. `cotal run migrate <runId> --local --file <program>` is that check: it\nreplays the run's journal and walks the edited program over it, prints whether the migration is\nadmissible, how many journal rows the walk accounted for, every orphaned step with its verdict and\ncode, and exits 0 on admissible and non-zero on not. It reads only, under the same credential\n`journal` reads on, and the commit side is not reachable yet: the report itself says what a commit\nwould file and that this invocation filed nothing. An answer is recorded under the answerer the\nmanager knows from the caller's credential: a managed agent by its name, anyone else by their\nprincipal. The request\ncarries no name. An agent with `capabilities: [run]` has the same five verbs as the `cotal_run`\ntool ([MCP tools](mcp-tools.md)), so a program can be written and started from inside a session.\nA `start` or `resume` answers once the run's record is written, within a bounded wait; a manager\nthat is still taking back a predecessor's runs at boot refuses both with `unavailable`, and a\nretry a moment later is the whole remedy.\n\n`--local` drives the run in this process instead: `start`, `resume` and `answer` exit when the\ndrive settles, `--by <who>` names the answerer, and `cotal run resume <runId> --local --file\n<program>` is how a run with no recorded program is continued. On a static mesh the local drive\nmints the run's own credential from the folder's trust material, so it runs from the mesh's\nproject folder. A local start also names the run's channel ceiling itself:\n`--admit-read <channels> --admit-publish <channels>`, comma-separated patterns or `none`, both\nrequired. The record it writes says an operator admitted the run and why, and the host checks\nit the same way it checks a hosted admission. A registered remote user-auth manager can host a\nrun through its issuing host. The host resolves a versioned caller against live issuance,\nadmits the run, and signs only the run's fixed driver, mediator and one-shot operator credentials\nfor manager-held nkeys. Renewal checks the activated attempt; the manager holds no signer.\nLocal user-auth runs remain unavailable because a user bearer holds no run rows. An open mesh\nhosts none either, since it issues no caller authority to admit a run under.\n\nA logged-in user starts runs on that remote manager with `cotal run start`. The auth callout issues\nthe user's manager connection against the user's actor-ledger row, the CLI reads the generation\nback from the connection's accepted row, and every `run` verb rides the versioned rail. The issuing\nhost admits a run only for the owner who registered the manager, and on every resume and answer it\nchecks that owner and that the caller's issuance is still live. It also watches the start, resume\nand answer requests on the broker itself, and admits or issues for one the manager forwards only if\nit saw that request, and only once. A resume or answer gets issuance only for the run, step, endpoint\nand amendment that request named. An answer's\ncredential reaches one pause, and the host reads which one off the run's own journal, for a run\nadmitted on that manager's instance. A request bound to another manager instance or epoch gets nothing, and so does one whose class or pinned contract is not the one the manager registered. Another user's start, answer or resume is refused, and so is a revoked actor's.\nSuch a run spawns, turns and despawns agents owned by that user. A spawn has the reach of the user\nwho started the run, as their actor-ledger row reads at that moment, and the host enrolls each agent\nthrough its managed-agent enrollment. A spawn may be placed on the manager that hosts the run and on\nno other instance. [User-auth run start](design/user-auth-run-start.md)\nrecords the path.\n\nRun admission accepts canonical space names such as `cotal_cloud` and `u_example-name`\nwithout rewriting them. Endpoint names keep their separate DNS-label grammar.\n\nA hosted run is **admitted** under the caller that started it. The caller's credential is an\nissuance ([identity and auth](identity-and-auth.md#issued-authority)): its requests ride a\nversioned rail that carries the credential's generation, and the manager resolves that\ngeneration's recorded permission ceiling and writes it beside the run before the driver starts.\nThat ceiling, the caller's own channel scope as it was issued, is what the run may read and post\nin channels; the manager's own reach never stands in for it. A request from a credential minted\nwithout an issuance is refused with `permission-denied` and a detail naming the caller. A run\nwhose caller had no channels can still sleep, checkpoint and turn agents; its `wait` on a channel\nis refused at the effect.\n\nThe manager endpoint serves `run-cancel` with `{ runId, reason }`. Call it through an issued\ncredential on a handle pinned to the manager instance, retaining the Describe-derived epoch bind.\nThe reason must encode to 1 through 512 UTF-8 bytes, and the request deadline is at most 30 seconds.\nThere is no new CLI or MCP convenience verb in this change.\n\nA signer-holding manager accepts the original caller's owner, actor and lifecycle under a live\nissued generation. On a signerless manager, the issuing host also accepts an interactive actor of\nthat owner with `run`, or an interactive admin. It must observe the caller's request itself before\na manager can forward it. Managed-agent names and platform assignments do not grant this authority.\n\nThe reply separates the durable marker from the local observation: `stopped` means a local drive\noutcome was observed; `stop-requested` means it was not observed within the request budget.\n`launch-abandoned`, `terminal` and `not-driven-here` describe their respective local states.\n`duplicate` is false for a confirmed new marker, true for a confirmed existing one, and null after\nan unknown write followed by marker readback. Preserve unknown outcomes. Neither a marker nor a\nreply proves process cleanup or that another host stopped. Revoked runs remain readable through\n`run show`, but cannot receive new drive or answering authority. The endpoint's `run-status`\nreply carries `revoked: { by, reason }` independently of the recorded run state. A running record\nwith that marker is not proof of a stopped holder. `revocationUnreadable` means the marker could\nnot be checked; do not read that as unrevoked or resumable. A released record without a marker can\nbe resumed under its original admission; a released record with a marker cannot. The human\n`run show` view displays admission revocation separately from recorded state. An unreadable marker\nproduces a partial result with exit 4, in human and JSON output, as it does in the run list.\n\nA cancellation can retain a local pending intent even after a definite no-dispatch refusal if its\nbudget ends before the manager can confirm marker absence. That intent blocks a new local launch\nor resume; it does not stop an existing drive. A later cancellation that observes a marker reconciles\nthe retained intent. Replaying the same request without a marker reports that it remains pending and\ndoes not dispatch again. A manager process restart loses the local intent, but does not settle the\nunknown write or prove cessation. An absent marker after an unknown write does not clear the intent\nin the running process. A `not-executed` write outcome therefore does not promise that a local launch\nis available.\n\nIssuer observation custody has finite lifetime budgets per caller, owner and issuer. Exhaustion\nlogs the budget class once per issuer process, without caller identities or request contents.\nRepeated refusals do not repeat that diagnostic. Custody entries are not evicted to make room.\n\nEmbedding hosts can call a drive's `cancel(reason)` handle to signal cooperative root cancellation.\nThe language's `RootCancellation` source carries the first reason through the walker and compiled\nworker to in-flight effect contexts. It is process-local and carries no authorization. The host must\nestablish permission and observe the durable revocation marker before calling it. A root cancellation\nends the drive as `released`; the status projection can lag if its write fails. Read the recorded\nstate separately. A completion or failure that wins the race keeps its actual outcome. It does not stop an agent process or preempt already-pulled tool work.\n`release(reason)` remains a separate next-effect-boundary request, used for host shutdown.\n\nThe host also supplies `cancellationObserved` for channel-effect classification. A fresh revocation\nread together with an observed local cancellation intent ends that effect as cancelled, even while\nthe manager is still reading the current holder before signalling the root. Pending intents alone\nnever do this. An ordinary local revoke without a cancellation intent keeps its existing failure\nbehavior. The shared observation neither signals the root nor replaces the holder check.\n\nIf the post-marker holder read fails or exceeds the original budget, the command does not signal\nthe root without that check and returns no stopped receipt. A parked checkpoint or sleep can\nremain running beside the marker. Preserve the unknown outcome. A later explicit cancellation\nrequest can read the same marker and a current holder, then report its own observed result.\n\n`cotal run revoke <runId> --local --by <who> --reason <text>` writes the run's revocation marker\nfrom the project folder. An empty `--by` or `--reason` is refused before anything is written. The\nadmission itself is never rewritten. Every host reads the marker\nbefore its next channel effect, so an open `wait` refuses at its next poll, and no resume,\ntakeover or manager restart continues the run. Revoking twice is not an error, and the first\nreason stands. A run whose admission is missing or revoked is left parked by the manager's boot\nreconcile, named in its log.\n\n`run ps`, hosted or `--local`, reads the marker beside each run record and prints `revoked` for a\nrun that carries one, whatever state the record itself holds, with the revoker and the reason\nunder the table. The record is display only here: a revoke writes no terminal state, because no host drove\nthe run to one and the journal owns the facts. A marker the listing cannot read, whether the store\nis unreachable or the marker has a version or shape it does not know, prints `unchecked` in the\n`STATE` column. The reason and the state the record carries go to stderr, and the command exits 1\nonce every row is printed. The hosted `run-ps` rows carry the marker as `revoked` (`by` and\n`reason`) or a failed read as `revocationUnreadable`, beside the record's own `state`.\n\nA run whose step was refused (L5016) stays held; a\nresume on a host that can perform the step performs it live and continues from there.\n`journal` prints what an open pause asks beneath its step key, which is the address `answer` takes\nback. Checkpoint expiry rides the mediated timer writer, which the delivery daemon pumps on a live\nmesh; on a bare broker a pause still resolves, it just cannot expire.\n\nA live pause that nobody answers, such as a `sleep` or a checkpoint timeout, is observed by\npolling: every 2 seconds the driver looks for the broker's timer fire, and every 2 seconds for the\nsettlement that fire records. A step therefore ends after its deadline, usually by 2 to 4 seconds\nand later on a loaded host: a live `sleep(\"1s\")` or `sleep(\"2s\")` usually takes about 4 seconds,\nand `sleep(\"5s\")` about 8. Timers whose deadlines fall within one poll can end on the same poll, so\na live `race` between them is not decided by their written durations: a `sleep(\"2s\")` branch\ndeclared before a `sleep(\"1s\")` branch can win. The simulator uses virtual time and reports the\ndurations as written.\n\n## Diagnostic inventory\n\nA large inventory can exceed the broker's reply limit. A host that supports page mode can return\none bounded JSON page. Page mode is not in release 0.88.0 or earlier: the calling CLI or connector and\nthe manager both need a later version.\n\n```bash\ncotal run ps --page --on <instance> --limit 8\ncotal run ps --page --on <instance> --cursor <cursor>\n```\n\nFor the MCP surface, use `cotal_run` with `verb: \"ps\"`, `instance` and `page: { limit: 8 }`.\nPass a returned `nextCursor` as `page.cursor` to continue. A managed session can use its existing\nmanager binding. Keep the same space, endpoint filter, instance and process epoch throughout a scan.\nThe CLI requires `--on`; page mode is not available with `--local` or other verbs. An older host\nrefuses page mode rather than returning a truncated legacy list.\n\nEach page has at most 16 rows (default 8), and the complete reply fits 65536 bytes and the broker's\npayload limit. The reader issues at most 64 storage calls and stops issuing them after 2 seconds or\nthe smaller request duration. These are application limits, not bounds on broker CPU, disk work,\nclient-library retries or total elapsed time. One row that cannot fit is refused, never split.\n\nThe response carries `semantics: \"diagnostic\"`, the observed sequence cut, responder, storage-read\ncount and `stopReason`. Rows contain spec/status revisions and whether a revocation marker was\nobserved. Programs, journal contents and revocation reasons are omitted. Legacy requests without\npage mode retain their existing array output.\n\n`endObserved` means the walk reached its cut or received a no-match response. It does not prove an\natomic snapshot, absence, ownership, terminal work or worker inactivity. Cursors are unsigned\ncontinuation data. Deleted history and restores can be invisible. Keep partial results incomplete,\nand retain uncertain-start reservations and ownership holds. A continuation that makes no progress\nis reported as incomplete; do not retry it in a loop or use it to release work.\n\n## What is on the wire\n\nThe run's wire footprint is [SPEC \xA714](../SPEC.md#14-workflow-runs-v05):\n\n| Thing | Where | What it is |\n| --- | --- | --- |\n| the run | `run.<endpoint>.<runId>` record | the resolved **pins** (seed, logical epoch, budgets, language version) on the immutable half; holder, lease and `journalHigh` on the status half |\n| the program | `program.<endpoint>.<runId>` record | the source the run was started from, verbatim, written once by the driver that pinned the run; what a resume reads and what a migration is measured against |\n| the step journal | `WFJ_<space>` stream, one subject per run | append-only, no age eviction, no Direct Get; every append fenced by the run subject's own sequence; takeover is replay-then-activate |\n| a checkpoint answer | `answer.<endpoint>.<token>.<answerId>` | the payload beside the one-use settle fact; the settle names the answer it accepted |\n| a notice | `notice.<endpoint>.<runId>.<addresseeId>.<noticeId>` | one bounded decision told to one agent, rendered ahead of its next turn |\n| a migration | `migration.<endpoint>.<runId>.<migrationId>` | the report and who applied it, keyed by the report's own digest |\n| the admission | `admission.v1.<endpoint>.<runId>` in `cotal_admission_<space>` | the caller the run was admitted for, its channel ceiling and its provenance; written once before the driver starts, and the store refuses a second write on the key |\n| a revocation | `revoked.v1.<endpoint>.<runId>` in the same store | who revoked the run and why; create-only, idempotent, permanent at the broker, read by every host before its next channel effect |\n\nThe driver writes `journalHigh` at activation and again after each journal append, before the\nprogram acts on the entry. A successor whose replay ends below it refuses the run with\n`RunJournalTailTruncated`. That holds for records appended since the last activation too.\n\nA run's **driver** connects on a `run-driver` credential minted for one run and takeover\nattempt. It can append to its journal, use its replay durable, and write its own `run`, `program`,\n`notice` and `migration` records. It has no store point reads, checkpoint writes, chat consumers,\nor channel and membership registry grants.\n\nThe hosting process keeps a separate `run-mediator` connection for effects and reads. The driver\nreceives methods and data from that host; it never receives the mediator credential or connection.\nThe host checks the journal's current activation and step identity before dispatch, and checks\npause and wait authority again at each operation. Cancellation cleanup also admits losing steps\nnamed by a settled parent whose `cancel.issued` is still false. That permission allows cleanup;\nit cannot mint or rearm a cancelled pause. Wait acknowledgements consume host-held delivery\nreceipts. A recorded match can be reread only at its bound sequence and channel. A conclave's\nrecorded ownership flag must match its step-derived channel before registry writes or cleanup.\n\nThe mediator retains endpoint-wide checkpoint rights and stream-wide leader reads as trusted\nhost authority. Record reads exposed to the driver are restricted to its own run's keys. Reads\nthat decide writes remain leader-served. A read of the journal uses the run's filtered replay\ndurable, including the diagnostic for a journal with no run record. That durable is named after\nthe takeover, and an attempt reads it many times, so reads under one takeover run one at a time in\nthe hosting process and a replay removes a durable of its own name that an interrupted earlier read\nleft behind. A durable that survives a replay's own delete belongs to a reader the process cannot\naccount for, and reading its tail is refused. A drive handles that refusal as a takeover does: the\nreads behind its steps, and the diagnostic for a journal with no run record, replay up to three\ntimes before the refusal is raised. An operator read runs under a takeover minted for that read and\nreports the refusal on its first read.\n\nA served read uses a one-shot `run-operator` credential. An answer uses a read to find the open\npause, then a second credential pinned to that token for the answer and settlement.\n`cotal run --local` uses the same driver/mediator split. On an authenticated mesh it needs the\nlocally recorded space signer to mint both credentials; a single `--creds` file is refused.\nDirect library users supplying broker clients to `MeshHandler` are constructing a trusted effect\nhost. A hosted driver receives its closed effect interface instead.\n\nThis split confines broker credentials; it is not process isolation for injected host code.\nThe runtime and its effect host share the manager process. A run's channel reach is the admitted\nceiling ([SPEC \xA714.8](../SPEC.md#148-run-admission)): the starting caller's issued channel scope,\nrecorded once in `cotal_admission_<space>` under a per-run `run-admitter` credential the driver\nnever holds, and re-read by the host before every channel effect. Spawn and turn keep their own\ndelegated checks; `notify` writes agent-addressed notices and is not channel publication. Treat\n`run` as program-execution authority bounded by that ceiling, not as sandboxing of the program.\n\nA version-1 fork can replay its settled parent history through the host. Inherited checkpoint\nidentifiers carry no authority to read, rearm or claim the parent's pauses. New child effects use\nchild-derived identifiers. A fork is a new run and takes a new admission under the caller who\nforks it; the parent's ceiling is not inherited.\n\n\n## What ships today\n\nThe language, its validator, interpreter, simulator and dry run are `@cotal-ai/lang`\n(`packages/lang`), usable in-process with your own effect handler and with no broker: `validate(src)`,\nthen `run(src, { runId, handler })`, and `resume(src, journal, { runId, pins, handler })` to pick a\nrun up from its journal (the package README has the snippet, with `SimHandler` as the handler). That\nis the in-process route, yours to drive with your own handler; a run the driver starts executes on\nthe compiled engine, as the engine paragraph below says. The wire\nsubstrate of \xA714 (the `WFJ_<space>` stream, the five record kinds, the activation barrier, the\nper-run grants) is in `@cotal-ai/core`, and the run driver, journal store, migrate and fork are\n`@cotal-ai/runtime` (`implementations/runtime`). The migrate check is reachable as\n`cotal run migrate <runId> --local --file <program>`; committing a migration it judged admissible\nis not reachable from any surface yet. On the mesh handler, `sleep`, `checkpoint`,\n`wait(message(...))`, `wait(idle(...))`, `wait(down(...))`, `wait(replied(...))`, `notify`,\n`spawn`, `conclave`, `ask`, `monitor` and `turn` are durable.\n`spawn` is\nthe manager's spawn action submitted under the step's own identity: the goal binds under the step's\nrequest id, so a resumed run re-attaches to the same seat instead of allocating a second one, a\nfailed or refused spawn is catchable as L4002 with the manager's recorded reason, and a spawn on a\nrace branch that loses is despawned by the run's own cancellation sweep. A seat belongs to the run\nthat spawned it: when the run completes, it despawns every seat it spawned, including a race\nwinner's and one whose spawn failed while its process stayed up. A spawn marked `onFork: \"adopt\"`\nis the exception: a fork can share that seat and no run can see whether another still uses it, so\nthe seat stays up until you stop it with `cotal stop` once every run sharing it is done. A seat a\nmigration handed to a later spawn follows that spawn's policy, and it stays up if any spawn that held\nit was marked `onFork: \"adopt\"`, because a fork taken before the migration may still share it. In a\nspace with several managers, a despawn counts a seat as already gone only when the manager that\nallocated it says so. A run that\nfails or is released keeps its seats until a resume completes it or you stop them with\n`cotal stop`. Start a seat with `cotal spawn` when it should outlive any run. `permits` are the budgets\nthis host meters: `turns`, how many turns the run may dispatch to the agent, and `wallClock`, a\nduration from the spawn after which no turn is admitted. The turn that would exceed one is the\ncatchable L4001 (kind `permit-turns` or `permit-wall-clock`; a deadline the remaining wall clock\ncannot hold counts as exceeding it), an adopted run counts the turns its journal recorded, and a\nbudget the host has no meter for, such as `tokens` or `spend`, is refused at the spawn rather than\naccepted and ignored. `supervise` is the restart policy this host asks the manager to enforce:\n`restarts`, how many in-window process deaths may come back under the same handle, and `window`,\nthe duration those deaths are counted in (default `10m`). The manager restarts the process in\nplace under the same name, lifecycle uid, persona, worktree and permits; `monitor` does not fire\nfor a restart, and `wait(down)` fires only when the seat is gone for good. Spending the budget\nretires the seat, and the next `turn` is the catchable L4002. The policy also covers the manager's\nown death. On a static mesh, when the manager ends without stopping the seat (a crash, or a pty seat\nthat ended with its manager), the next manager of the same instance restarts the seat the same way\nand counts that as one restart. A turn the dead manager had accepted for the seat is served to the\nrestarted seat, so the interrupted turn runs again and finished turns do not. A seat whose budget\nis spent, or whose persona file or credential changed or expired while no manager ran, is retired instead. A policy this host cannot enforce\n(an unknown key, a user-mode seat, or a runtime that cannot respawn a name in place) is refused\nat the spawn rather than accepted and ignored. `events: false` is the workflow form of\n`cotal spawn --no-events`: the seat starts without its AG-UI event plane. A connector that\npublishes none, such as Hermes, needs it, because an omitted `events` arms the plane and the\nmanager refuses that connector at the spawn. A value that is not a boolean is refused at the\nspawn. `conclave` joins its\nmembers to a real channel as durable membership rows: the channel derives from the step's own\nrequest id when the program names none (a program-named channel is borrowed, never torn down, and\na membership that predates the conclave survives its close), each member handle resolves to its\nprincipal through the seat's own presence row (an absent member is catchable as L4002), and a\nconclave cancelled on a losing branch is released by the same cancellation sweep. `ask` parks one\ncheckpoint-plane pause per attempt, answered through `cotal run answer` as a checkpoint is, and\ntells the agent through the same relay `turn` uses: one relay per attempt under the attempt's own\ntoken, carrying the schema, the attempt count, the deadline and the previous refusal, which the\nseat's connector renders as the record wanted and the hosted command that answers it. The relay\nnames the ask's absolute deadline, so the manager stops serving an attempt at the instant its\npause expires and refuses a relay already past it. When that\nliteral command is run inside the managed seat, the CLI reuses the seat's lifecycle credential and\nissued caller identity rather than minting an operator instrument. The command\ndoes not take `--by`: the manager records the authenticated caller as the answerer. A spawned seat's\nbaseline credential carries only the self-targeted `run-answer` row, and the manager accepts it only\nfor the open ask or escalation relayed to that exact incarnation. It cannot answer another seat's\nask, an unrelayed checkpoint, another run, or start and resume commands. An ask addresses\nan agent the run spawned (anything else refuses before an attempt opens), a resumed attempt tells\nthe seat nothing twice, and a seat gone at the relay is L4002. On the pause itself:\nthe shorthand of the language reference \xA76.5 is enforced (an unreadable schema is L4022), a\nnon-conforming answer costs one attempt and its refusal reason is recorded on the entry for the\nanswerer to read, exhausted attempts (default one) are the catchable L4006, and so is the one\nabsolute deadline for the whole ask passing with no conforming record (its kind is `ask-deadline`),\nincluding one that passes before an attempt's pause is armed.\n`checkpoint` binds what it asks on its own entry, so `cotal run journal` prints the question under\nthe step key an answer is addressed by while the pause is open: the address alone left whoever was\nasked reading the source to find out what \"approve\" meant. The entry also records the open\nattempt's deadline and the `onExpiry` it was armed with, which `run journal --json` reports; what\nan expiry does is still decided from the program's source. An escalation's attempt records its own\ndeadline, one `timeout` after it opens, and a resumed attempt keeps the deadline it recorded. After a checkpoint or `ask` accepts an\nanswer, the journal prints that accepted answer's recorded value and attribution under the settled\nstep. A checkpoint's comes from its frozen result, and the journal never substitutes another filed\nanswer or invents fields that result does not hold. An `ask`'s result is the value alone, so its line\nis read from the answer record its last attempt's settle named, even when that value is a record with\nfields named like a checkpoint's. Amendments print on their own lines after it.\nAn `escalate` addressed to an agent this\nrun spawned is relayed to that seat through the same turn relay an `ask` uses, carrying the prompt\nand the token to answer under, held to the pause's own deadline; a `to` naming anyone else is a\nperson, and their pause stays the one anybody can answer, with the addressee recorded and rendered\nbeside the question.\n`monitor` registers interest in an agent, and the\nregistration is the journal entry itself, carrying the handle it registered: monitoring an agent\nthat is already dead succeeds, and the death is the wait's to observe. `wait(down(...))` observes\na monitored agent, and refuses one the run never performed `monitor` on. It reads the death off presence liveness, the\nsame witness a conclave join resolves members through: the value carries the handle, the reason\n(`lapsed` when nothing live holds the name any more, `superseded` when a live row holds it under\na different incarnation) and the time of observation. Presence liveness skips any value without a\nstring `card.id` and `card.name`, so a participant that publishes a malformed row under its own key\ncannot fail another run's wait, turn or conclave join. A superseded incarnation is down at once. A\nlapsed one is down only after its presence row has stayed gone for 30 seconds, because a seat whose\nconnector stalls past the row's 6-second TTL, under host load or across a reconnect, renews it under\nthe same incarnation and is still working. The 30 seconds count only across presence reads that\neach end within 6 seconds of the previous one starting, so a slow read or a run of failed reads, which\ncould hide a renewal, starts the count over. Even then the run asks the manager first: a seat whose\nprocess the manager still runs is alive however long its row has been gone, so the count starts over.\nOnly the manager that spawned the seat can report it gone, because another manager in the space knows\nonly its own seats, so the run asks again until that one answers. When it does not answer, the presence\nrow is the only witness and the lapse stands. The asking stops at the wait's timeout or cancellation,\nand at a turn's deadline, yield or cancellation, which then decide. A wait that begins\nafter the death resolves once that holds, and a timeout resolves null on one absolute deadline a\nresumed run re-attaches to.\n`turn` wakes one seat for one host turn through the manager as a pull-shaped relay: the run\nsubmits the turn under the step's own identity, the manager holds it as a goal pinned to the\nseat's incarnation, and the seat pulls it under its own reach ahead of its next host turn, so\nnothing is pushed into a session mid-thought. The payload the seat reads names the run and the\nstep and carries the rendered run context, plus any pending notices addressed to it, which the\nturn consumes. The seat yields through `cotal_yield` (`done`, `blocked`, or `handoff` with an\naddressee), and ending its host turn yields `done` for every turn it was shown. A `handoff` names\nanother seat the same run spawned: the next `turn` in the same scope to that seat records the\nlink, a handoff to a name the run never spawned is the catchable L4005, and one to a seat bound\nto a different worktree is L4004. The deadline elapsing before any yield is the catchable L4003:\nthe acceptance names the instant, the manager's goal-bound hold denies at it, and the run arms its\nown pause on that same instant, so either side outliving the other still converges on the same\nanswer. A seat that pulled the turn earlier stops showing it to its session at that same\ninstant. A seat that dies mid-turn is read off its own presence row by the run itself, with the\nsame 30-second confirmation for a lapsed row, and is the catchable L4002, and a death the manager\nmarked on the deadline terminal reads the same way. Two\nturns on one seat, from two branches or from two runs, reach it one at a time: the language\ndispatches the second when the first settles, and the manager shows a seat the oldest unsettled\nturn alone. A turn's goal id belongs to the caller that submitted it, so two callers may each submit\na turn under the same id. A seat yields by goal id alone, so the manager refuses to hand one seat\na second caller's turn under an id it already holds. On an auth mesh the relay needs no extra grant: every spawned seat's baseline\ncredential carries its own pull, yield, and caller-bound answer rows, the run driver's operator instrument carries the\nturn request and the `inspect` read that confirms a lapse, and the manager arms the deadline hold over its own serve grant and expires it\nitself once due. An accept the manager cannot finish is unwound to a failed terminal on the goal\nit bound, and a retry of that submission is refused naming the terminal rather than accepted a\nsecond time.\n`wait(replied(...))` observes those turns from another branch: a completed turn is a reply, and\nthe wait resolves with the observation record (the handle, the yield's status and note, the\nyield's own stamp). It reads as a level, the way `wait(down)` does: a reply that already exists\nresolves the wait at once, and two replies resolve to the latest by the yield's stamp. A denied\nor cancelled turn is never a reply, so an unanswered wait rides its own mediated timeout to\n`null`, and a handle the run never spawned or turned refuses loudly, since only this run's turns\nare observable. A turn the run itself ended without an accepted yield (its deadline, a\ncancellation, a refused handoff) is never a reply, whatever the seat yields to the relay later.\nA cancelled branch also withdraws what it relayed to a seat before its cancellation completes: its\nturn, its ask attempt or its escalation ends `cancelled` through the manager's reserved `cancel`\n(SPEC \xA713.6), so the seat is not shown it after the branch's scope settles and the next turn to\nthat seat does not wait behind it. Only the manager that accepted the relay holds it, so in a\nspace with more than one manager the run sends the cancel again while another manager refuses it,\nuntil the accepting one answers. A cancel refused as `unavailable`, such as by a manager still\nreconciling at boot, or one whose reply is lost (`deadline-exceeded`) is sent again until it\nlands or the relay's deadline passes, so the branch's cancellation does not complete while the\nseat can still be shown the relay. Any other refusal fails the branch's step at once under its\ncode, since sending it again changes nothing: a manager that serves no `cancel` is refused\n`not-found` before anything is sent, and an accepting manager with no such relay pending refuses\n`failed-precondition`. The run reads the relay's deadline from its goal and retries a failed\nread, since the relay may still be served until the deadline is known. A goal record\nthat can never yield a deadline, such as one that is not JSON, fails the branch's step at once,\nand so does a refusal that lasts past the deadline.\nA `spawn` may bind its agent to a **logical worktree** (`spawn(\"builder\", { worktree: \"wt-1\" })`):\nthe handle carries the id, and the run enforces the one rule the language states about it: two\nagents never share a worktree concurrently. The validator rejects the literal case up front\n(L3022: two branches of one concurrent scope spawning into one literal worktree, named branch\nfunctions included), and the runtime guards the rest, computed ids included: a spawn claims its\ntree before it submits, so a second spawn into a tree held by a live seat or by a spawn still\nbringing one up is the catchable L4008, a spawn that ends without a handle gives the tree back,\nand the tree is reusable the moment a holder's presence row is gone, so a discharged race loser\nor a crashed seat releases its tree with no bookkeeping. A spawn the endpoint refuses at accept\nis the catchable L4000 (L4001 when the refusal is the endpoint's seat capacity), and one whose\nseat never came up is L4002. A refusal that states the command did not run is answered\nbefore it gets that far. In a space served by more than one manager the resolve and the invoke\nare separate trips through the same anycast queue, so a run's call can reach an instance it did\nnot resolve against, and that instance refuses ahead of any effect. The run drops its resolved\nhandle, re-describes and re-issues, for a bounded number of attempts; after them the refusal\nsurfaces as the effect's own failure and still states that nothing ran. A spawn that names a\n`placement` addresses one instance by name, so a refusal from it is that incarnation answering\nabout itself and is never re-issued. When the spawn also names `cwd`, that manager resolves the\nexisting absolute directory to its canonical host path before accepting the spawn. A missing,\nrelative or non-directory path refuses with no seat and never falls back to the manager workspace.\nOne hosted run may name one placement instance; a program naming several is refused instead of\nwidening one run credential across hosts. Placement is accepted only from an object-literal spawn\noption bag whose `endpoint` and `instanceId` are string literals. A computed option bag, a computed\nplacement field, or a spread in either object refuses at `run-start`. The option argument may be\nabsent, and an object-literal bag without placement is accepted.\nA turn handoff across worktrees is the L4004 described above. Recovery keeps these honest: a resumed run\nreseeds its roster, holders and handoff memos from its own journal, and the driver re-issues any\nrecorded-but-undischarged cancellation at adoption, before the engine performs a new step, so a\nloser a crash left alive does not keep its seat or its tree while the resumed run works on. The\nsame sweep withdraws a cancelled branch's relays and undelivered notices: a notice waits on the run for its\naddressee's next turn, so a decision the run cancelled would otherwise arrive at an agent with\nnothing to distinguish it from one that stood.\n\nEvery effect the language defines performs on the mesh handler; nothing is refused as\nnot-yet-durable any more. The operator surface over the driver is `cotal run`; the section above has the verbs.\n\n**Two engines, and which one runs your program.** The tree-walker is language version `1` and the\ncompiled engine is version `2`, two languages rather than two speeds of one (`spec/cotal-lang.md`\n\xA78.4 lists what differs). The driver hosts both: **every run a driver starts is stamped `2` and\nexecuted by the compiled engine**. The program runs in its own locked-down worker thread with\nnothing in its global scope, while the effects and the durable journal stay in the driver's process,\nbridged over a message port. No socket or credential enters the isolate holding the program,\nand **every version-`1` record keeps replaying on the walker**, which is the walker's job. On either\nengine the driver bounds an effect's `ok` result at the broker's `max_payload` less 4096 bytes: a\nlarger result is refused ahead of the settling append (**L5006**), the step stays pending, and the\nrun is released. The driver serves a declared set of versions, and a record whose version it does\nnot serve is refused by name (**L5023**) with the run left untouched, instead of being replayed by\nwhichever engine happens to be present. Records do not cross between versions in either\ndirection; the repair is to resume on the recorded version, or to fork.\n\n**The engine needs node 22 or newer** and refuses below it as `EngineUnavailable`, which is an\nimplementation limit and not a language error: it carries no `L` code, so there is nothing to look\nup in the catalog. It is a floor rather than a warning because the engine's frame plumbing rests on\n`AsyncLocalStorage`, and 22 is the lowest node it has been measured on. The walker has no such floor.\n"
|
|
27504
27504
|
}
|
|
27505
27505
|
],
|
|
27506
27506
|
"spec": {
|