Cotal
DocsAPI Reference
Agents

Agent launch reference

spawn, models, persona, runtimes, and seats describe agent launches and local catalogs.

runtimes

cotal runtimes [--json | --plain]

Lists every agent runtime the manager can spawn through: the built-in pty, the official providers (orca, tmux, cmux, herdr), and any custom provider installed via cotal ext add. Each installed provider is probed so you can see what is actually reachable on this machine before selecting it:

RUNTIME  STATE                  PACKAGE
pty      built in
orca     installed · reachable  @cotal-ai/orca
tmux     not installed          @cotal-ai/tmux
cmux     not installed          @cotal-ai/cmux
herdr    not installed          @cotal-ai/herdr
add one that is not installed with `cotal ext add <package>`

installed · reachable / unreachable is the provider's own available() probe; not installed means it is a known runtime you can add with cotal ext add and the package shown. --json writes one document whose result.items are {name, state, package, reachable, error}, with state one of built-in, installed or not-installed and reachable null when the provider was not asked. An installed provider whose package fails to load is shown with its error and makes the read partial (exit 4). The command reads this machine only, so its target is null. Selecting an unknown or uninstalled runtime via up --runtime <name> fails loud and, for a known one, points at the exact cotal ext add package. spawn --runtime <name> refuses the same way when the launch goes to this root's own manager. A launch that may reach a manager on another host leaves the check to that manager, as spawn describes. up --runtime chooses the runtime every seat uses unless that seat names its own. spawn --runtime chooses the runtime of that one managed seat, and the manager keeps every other seat where it is. There is no silent fallback to pty.

spawn

cotal spawn [<persona>] [--file <persona-file>] [-d | --foreground] [--dry-run] [options]

A bare spawn runs the application in this terminal, on every kind of mesh, and no manager is involved unless you pick one when it asks where to run, as described below. Its exit, or the terminal's, may end the agent. A manager runs the launch only when you ask for one. --detach (-d) hands it to a manager, waits until the agent joins, and returns without attaching. --on <instance> hands it to that manager and attaches this terminal to the agent (see Managed launch view), when that manager's runtime streams a terminal. The built-in pty runtime streams one. The tmux, cmux, orca and herdr runtimes start the agent in a surface they do not own and stream none, so a launch on one of those managers is not attached: it returns once the agent joins, with a receipt that names the runtime and what reaches the agent instead. Attaching needs usable input and output terminals and refuses --json and --plain, before an identity is allocated. A spawn in this terminal writes no structured receipt, so --json and --plain need -d, except with --dry-run. --foreground names the default and is kept for scripts. A spawn in this terminal checks its persona and connector before it starts the application: a missing persona is refused with TARGET_NOT_FOUND and a missing connector with DEPENDENCY_UNAVAILABLE, both with exit 2, and the connector refusal names the cotal ext add that installs it. --file (-f) names a persona file, never a manifest: manifests deploy with cotal team deploy <file>, and a manifest passed to --file is refused with that command.

On a terminal, a bare spawn on a user-auth mesh where several of your managers are live asks where the agent runs before it does anything else. The managers listed are the ones the exchange counts for a manager selection, so the question comes up only when spawn -d would ask which manager to use. Each manager's row gives its instance id, the host it reports, the runtime it spawns through and whether a terminal attaches to its seats (pty · terminal attach, or tmux · no terminal attach), so the choice is made before anything is launched. A manager whose presence does not carry it reads as tmux · terminal attach not reported. When presence cannot be read, each manager is listed by its instance id, and a line above the question starts with names unavailable: and gives the refusal and its remedy. This terminal is the default and runs it as above. Picking a manager launches the agent there and attaches this terminal, as --on does, and a Manager chosen receipt names the --on that skips the question. No manager credential is held while you answer. --foreground, --file and --live-only keep the spawn in this terminal without asking, and so does a command that cannot ask: no terminal on stdin and stdout, --no-input, or COTAL_NO_PROMPT=1. Cancelling the question does nothing and exits 0.

A managed launch on a terminal, picked at a question or named with --on, asks which folder the agent works in when you give no -C and the manager is not known to run on this host. The host is the one that manager reports, the name cotal ps prints as its reported host, so a manager that reports this machine's host name is never asked about and one that reports no host is. The mesh and the pin are read before anything is asked, so a stopped mesh or an --on that names no running manager is refused first and no question comes up. Enter keeps Manager's workspace (default): no folder is sent, and the manager uses its workspace root, as a launch without -C does. A folder you type is sent as typed to that manager, which resolves it on its own host with resolve-cwd before anything is launched. This terminal does not read or rewrite the path, so a Windows manager takes a drive or UNC path from any client. A folder the manager cannot use is shown with its reason and asked again, with your text left to edit. A check that could not be made at all refuses the launch and names the command again with the -C that sends the folder with it and asks nothing. The question holds no manager credential: the folder is checked under a view of that manager acquired after you answer, and the launch acquires its own. A Folder chosen receipt names the folder and the host, and for a typed folder it gives the command with the --on and -C that skip the questions. Cancelling does nothing and exits 0. spawn -d with no --on, which the exchange gives to your only live manager, asks about that manager too: the CLI reads which manager the exchange picked, keeps none of that view, and pins the launch to that manager. If it has stopped by the time you answer, the launch is refused and goes to no other manager. A command that cannot ask, or one with --json, does not ask.

On a user-auth mesh that runs elsewhere, a spawn in this terminal provisions its agent with your login, through the agent-provisioning endpoint the mesh advertises. A mesh that advertises none is refused in one line. The line names the remedies: ask the mesh operator to advertise one, run the agent where the mesh runs, or add -d to launch it through one of your managers. A one-time enrollment, described below, also stands in for the endpoint. An answer from the endpoint other than a 401, which names cotal login, or a held name, described next, says whether the agent was provisioned is unknown (OPERATION_UNKNOWN, exit 3).

A spawn whose name is already taken checks what holds it. When a live manager of yours runs that agent, or presence or the mesh reports it online, the spawn launches nothing, says where it runs, and exits 0 with the outcome no-op. In a terminal it then attaches to that agent. With -d, --json, or no terminal it prints the cotal attach <name> --on <manager> command instead. When the mesh answers that the name is held by a lifecycle that does not run (Held agent names), the spawn releases that one lifecycle by its UID with your login and asks for the name again, for a managed launch and a spawn in this terminal alike. Nothing is adopted: the agent starts under a new lifecycle. While the mesh says the name is still retiring, the spawn asks again every 5 seconds for up to 2 minutes, then refuses LIFECYCLE_BLOCKED. A release the mesh cannot confirm is OPERATION_UNKNOWN and is not repeated. A lifecycle that a live manager of yours holds without reporting it running is not released: it may still be starting there, so the spawn refuses CONFLICT and names that manager. Any other held-name refusal stands as the manager or the mesh gave it, as CONFLICT.

Foreground actor allocation

On a user-auth mesh, an ordinary spawn in this terminal without --name allocates the first free actor in the persona's series: default_agent, default_agent_2, default_agent_3, and so on. Held local material, existing grants and a name whose agent is online are occupied; local material and grants count even when no presence is visible. A name held only by a lifecycle that does not run is released and reused, as above. Allocation preserves the existing session and its credentials. An explicit --name, a signed enrollment, and a managed handoff pin an identity and never rename it. Authorization, network and malformed-provisioning failures are not reasons to try another name.

Foreground enrollment

For a foreground spawn onto a remote user-auth mesh, a one-time enrollment may stand in for a cached human login. Prefer a private file:

cotal spawn --foreground --file ./seat.md --space main \
  --enrollment-file /run/secrets/cotal-enrollment

The file contains only the enrollment URL, ending with at most one line terminator, and must be mode 0600 on POSIX. A launcher may name the same file in COTAL_ENROLLMENT_FILE instead, and run cotal spawn --config ./seat.md --space main. An orchestrator that cannot mount a file may set COTAL_ENROLLMENT_URL; that value is redeemed byte for byte, so a trailing newline in it is refused. Naming more than one source is refused, and so is --enrollment-file with --detach, --creds or --on. Enrollment input requires --space and applies only to a foreground persona spawn. If the mesh is not registered yet, the enrollment response must carry the stock user-bundle fields and the command needs a persona file (--file, or --config from a launcher) because there is no local remote-mesh persona catalog to read. The client redeems the URL once, registers the returned mesh material, exchanges the returned actor token at the pinned auth service, and removes both enrollment variables before starting any child process. A registered mesh whose record pins no exchange URL is refused before the URL is redeemed, so the enrollment stays unspent; re-register the mesh with cotal meshes add <space> --from <url> --mode user.

A cached login for the same IdP and an enrollment are conflicting proofs, so the command refuses rather than choosing one. An invalid enrollment never falls back to login provisioning. Unknown, expired, revoked, and already-used enrollments all produce one response: ask the owner for a fresh one. See Enrollment redeem for the HTTP contract.

Managed handoff

A runtime that starts a managed seat outside the manager's filesystem hands the child a managed handoff instead: one 0600 file named by COTAL_MANAGED_HANDOFF_FILE, carrying the lifecycle the manager already enrolled, alongside COTAL_MANAGER_INSTANCE naming the launching manager. The runtime builds the command with delegatedSeatCommand:

COTAL_MANAGED_HANDOFF_FILE=/run/seat/handoff.json COTAL_MANAGER_INSTANCE=<instance-id> \
  cotal spawn --config ./seat.md --space main --name <actor> --agent claude \
    --expect-owner <owner> --expect-lifecycle-uid <uid>

cotal spawn reads the file, deletes it and drops the variable before it parses flags, prints help or loads extensions, so every outcome leaves no file. Every other command drops the variable without reading the file and leaves the file in place. The variable is read under any letter case; spellings that name different files are refused after every one of them was deleted. A file spawn cannot take, such as a missing one, is refused USAGE_INVALID, naming the variable and the path, with unset COTAL_MANAGED_HANDOFF_FILE as Next.

DelegatedSeatLaunch.managerInstanceId is required. A newer CLI child under an older manager or runtime that omits it refuses every delegated launch, so managers and the CLI must be upgraded together. The spawn refuses an absent COTAL_MANAGER_INSTANCE (COTAL_MANAGED_HANDOFF_FILE requires COTAL_MANAGER_INSTANCE), a malformed value (COTAL_MANAGER_INSTANCE "<value>" is not a valid lifecycle token ([a-z0-9]{26,32})), a malformed handoff, or one whose space, owner, actor or lifecycle UID differs from --space, --expect-owner, --name and --expect-lifecycle-uid, before any broker connection or exchange request. Every refusal on this path names the field and never a value from the handoff. The registration's server, exchange and enforcement checks, the local state this machine keeps for the space (its mesh record, user-auth state and agent secret files), target resolution, the policy refresh, the broker preflight and the agent auth preflight quote the space, the server, the exchange URL, the actor or a path named for one of them in their own diagnostics and in the filesystem errors under them. For a handoff each prints one fixed sentence that names the field and the phase instead, whether its check fails or an error is thrown. When the agent auth preflight's rollback then fails to remove a secret or file, that sentence is followed by the names of the cleanup steps that failed, without their errors. The event-plane policy refusals name the handoff's space field. An actor outside [A-Za-z0-9_] and a space that cannot name local state, such as .., are refused as malformed before any plane. A handoff conflicts with the enrollment variables, --detach, -f and --creds, and needs --config <persona-file> and COTAL_MANAGER_INSTANCE. From there it runs the enrollment consumer above without redeeming anything. See Delegated seats.

Launch options

FlagDefaultMeaning
--space <s>resolved meshTarget space
--server <url>registry entryBroker URL override
--creds <path>noneRetained flag for managed launches with -d or --on. Raw credentials alone cannot provide supported off-registry Manager control authority; an endpoint-caller triple is required. Refused for a spawn in this terminal and from a managed seat's own shell
--name <n>, -npersona's name:Presence-name override (does not choose the persona)
--file <persona-file>, -fnonePersona 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
--config <persona-or-path>noneReserved for internal launchers that set COTAL_MANAGED_HANDOFF_FILE, COTAL_ENROLLMENT_FILE or COTAL_ENROLLMENT_URL; any other use is refused and names --file
--harness <connector>, -Hpersona's agent:, else the harness preference, else claudeConnector type (claude, opencode, jcode, hermes, and so on). --agent is the legacy spelling; giving both is refused
--role <r>persona's role:Role override
--model <m>, -mpersona's model:Model override
--variant <v>persona's variant:Model variant override (connector-defined; e.g. OpenCode reasoning tiers)
--cwd <dir>, -Cthis cwd; on a managed launch, the serving manager's workspace rootWorking 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. Without it, a terminal asks for the folder when the manager is not known to run on this host.
--prompt <text>, -pnoneInitial prompt auto-submitted at start
--opt <k=v>noneConnector-specific launch option, repeatable. Wins per key over the persona's launchOptions:; a connector with no option surface refuses it
--resume <id>noneFork an existing session id into the mesh; only connectors that declare resume support accept it (see the matrix). 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); carrying one needs --on
--no-eventsevent plane on where supportedOpt out of the session's structured event plane (--events only restates the default)
--share-tools <sel>noneShare named operator MCP servers with the agent
--subscribe <a,b>persona'sChannel read-set override; "" clears it
--allow-subscribe <a,b>= subscribeRead-ACL override; "" sets it to the read set
--allow-publish <a,b>denyPost-ACL override; "" clears it
--detach, -doffLaunch through a manager, wait until the agent joins, and return without attaching (reattach with cotal attach)
--foregroundon without -d or --onRun the application in this terminal, as a spawn without -d or --on does; kept for scripts. Its exit, or the terminal's, may end the agent. Refuses --detach, --creds, --on, --restarts, and --json or --plain except with --dry-run
--on <instance>noneLaunch through one manager instance and attach to the agent, or with -d return once it joins. Takes the instance id (the whole id, as ps prints it), or on a user-auth mesh a manager's name or host, as ps takes them. Refused with --foreground and when empty. On a static or open mesh an id that names no running manager is refused TARGET_NOT_FOUND before anything is sent, naming the managers that run, or TARGET_UNAVAILABLE as an unpinned launch is when none runs. A pinned manager that does not answer is refused TARGET_UNAVAILABLE, exit 2, after the reply deadline, because the launch was never sent
--runtime <name>the serving manager's runtimeThe terminal runtime for this one managed seat (pty, or an installed extension such as tmux). It needs -d or --on. A spawn in this terminal refuses it, because that spawn has no manager runtime to choose. auto is refused USAGE_INVALID before anything is sent. Which host checks any other name is described below this table. Other seats on that manager keep the runtime they already have
--restarts <n>, --restart-window <ms>noneManaged launches only, with -d or --on, and given together: the manager restarts the agent at most <n> times within <ms>. Each must be a positive integer
--live-onlyoffA spawn in this terminal only: skip the durable backstop
--enrollment-file <private-file>noneWith --space, for a spawn in this terminal: a private file holding one seat's enrollment URL (see above)
--dry-runoffPreview the launch without allocating an identity, provisioning anything or starting an application
--expect-owner <u_…>noneWith COTAL_MANAGED_HANDOFF_FILE only, and required there: the owner the handoff must carry
--expect-lifecycle-uid <uid>noneWith COTAL_MANAGED_HANDOFF_FILE only, and required there: the lifecycle UID the handoff must carry

spawn refuses --allow-stale with "manifest deployment options belong to cotal team deploy <file>, not spawn". --runtime is a per-seat choice on a managed launch. Omitted, the seat uses the runtime that manager was started with. auto names a manager's own runtime, so it is refused for one seat. Any other runtime has to be installed and reachable on the host the manager runs on, and the CLI can check that only when that host is this machine. When the launch can reach only this root's own manager, because --on names it or it is the only manager running, the CLI resolves the runtime through the installed-extension lookup up --runtime uses. An unknown, uninstalled or unreachable runtime is then refused DEPENDENCY_UNAVAILABLE, exit 2, before the launch is sent, and for a known runtime the remedy names its cotal ext add package. A launch that may reach a manager on another host sends the name unchanged. That manager refuses a runtime it cannot run, the CLI reports LIFECYCLE_BLOCKED, and nothing starts. No manager substitutes pty.

Event channels

Each session uses its connector's event plane by default: a stream of structured events describing what the agent did, rather than the prose it wrote, on a channel of its own. The channel is named after the agent's principal, events.<owner>.<actor>, never after its display name, because two live agents are allowed to share a display name and would then share a stream. The launch grants publish rights on that channel alone, foreground and detached alike. On an open mesh, which issues no credentials, the launch still allocates the agent an id, so the channel names a stable actor. --no-events is the explicit opt-out unless the selected registration says policy: { events: "required" }. Required policy makes the event arm and grant mandatory, so --no-events and connectors without an event plane are refused.

The launch decision and the grant are separate on purpose. Holding publish rights on a channel is not a request to publish to it, so writing an event channel into an agent file's allowPublish does not override --no-events.

The persona (--file or the positional name, else the persona preference, else default) is loaded from the target mesh's .cotal/agents/ when it is a bare name. A reference that contains a path separator or ends in .md is loaded from that file. A relative path resolves against the mesh root, except that a managed handoff, and an enrollment onto a mesh this machine has not registered yet, resolve the persona file against the working directory. A missing persona is refused in one line with the catalog directory or the file that was checked, and the line names --file <persona.md> for a persona file elsewhere. A missing default persona also names the command that writes one: cotal up in the root of a space this machine started, or cotal persona create default for any other space. --dry-run refuses a missing persona with the same text, as TARGET_NOT_FOUND (exit 2). When a managed launch is refused because the chosen manager's catalog has no such persona, the refusal names that manager, its host and the catalog file to create there, and the Next gives the spawn again with --on for each of your other live managers whose catalog lists it. The launch flags override the file. On a user-auth mesh the effective name is also the agent's actor token, so it must match the token grammar (no -); the spawn is refused with that explanation before any request is sent. A spawn runs the agent attached to your terminal; --detach and --on hand the launch to a manager. Every mode gets the durable backstop on a mesh that runs the delivery daemon; --live-only skips it for a spawn in this terminal (messages posted while it is disconnected are then not replayed). A foreground exit retires the agent's creds and broker footprint, like a manager despawn. On a static-auth mesh its creds file is <name>.<lifecycleUid>.creds, keyed by the spawn's lifecycle UID as a manager's spawns are, so the retirement removes only that spawn's credential: another spawn of the same name and a standing <name>.creds from cotal auth mint keep theirs. On a static-auth mesh, a spawn that fails after its footprint exists, for example because the creds directory cannot be written, retires that footprint the same way before it exits with the error. The creds and the footprint are removed as separate steps, so a creds file that cannot be deleted does not leave the broker durables behind, and the failed steps are reported together. On a user-auth mesh the two arms differ: a spawn against a mesh this machine provisioned revokes the actor row on exit, while a remote spawn (an enrollment or the advertised provisioning endpoint) removes only this machine's credential files; its grant stays until the mesh operator revokes it, and the launch line says which arm you are on. A spawn through the advertised provisioning endpoint against a record that pins no exchange URL is refused before the grant is requested, so no credential lands on this machine. With no sign-in the spawn is refused with AUTH_REQUIRED (exit 2) before any request is sent. Any provisioning failure after the request, an endpoint 401 or 403 included, is reported as OPERATION_UNKNOWN (exit 3): the endpoint belongs to the deployment, and nothing says what its refusal left behind. A --share-tools selection that names a server the config does not declare is refused before anything is provisioned; on a managed spawn the manager refuses it before accepting. A managed spawn is an action: the manager accepts it and allocates the identity, and the CLI waits for a real outcome (the agent joins, exits, or about 30 seconds pass) before it reports or attaches (see the control surface). On a user-auth mesh the manager enrolls the agent with the host first. When the host refuses that enrollment, for example for a role or a channel your grant does not carry, and the manager removes the secrets it staged for the agent, the spawn is refused with the host's reason and PERMISSION_DENIED (exit 2). A host that fails or does not answer leaves OPERATION_UNKNOWN. See Connect Claude Code and Agent files. (cotal start was merged into cotal spawn --detach.) A managed spawn onto a manager from another Cotal release is refused before any request is sent when the manager's contract does not declare a field this CLI sends. The refusal names the field, calls it version skew, and gives this CLI's version and the version that manager reports in its status. When the manager is older, it names cotal update --self on the manager's host and then cotal manager restart, or a CLI at the manager's version. When this CLI is older, it names cotal update --self here. A manager that reports no version is asked to run at this CLI's version. A field you leave unset is not sent, so it never causes that refusal.

Manager capacity

A manager has 50 seat slots, and each seat counts once. A slot is held by a managed seat (a row in that manager's cotal ps, including a seat still joining), by a reserved launch the manager accepted but has not started a process for, or by a cooling hold. A seat that ends within 10 seconds of starting leaves its slot cooling until those 10 seconds pass, unless an operator stopped it. Such a seat holds only that cooling slot, even while its launch is still reporting the failure. A spawn refused at the limit states that split and whether waiting can free a slot:

at capacity (50 of 50 slots: 49 managed, 0 reserved, 1 cooling); waiting frees a cooling slot in 7s, or despawn one

A cooling slot frees at the stated time. A launch that has not settled frees its slot only if it fails, and a managed seat frees its slot only when it stops. The refusal counts a launch as pending only while it holds a slot, so a launch whose seat already ended is not counted. The roster counts presence, which also includes peers no manager owns, so its total is a different number.

Those 50 slots bound the host. A manager started with --seat-cap <n> also bounds how many live seats it holds for ONE owner, so a manager shared by several owners cannot be filled by the first one to ask. The default is unlimited, which leaves the 50 slots as the only bound. The cap counts the seats that manager supervises for the owner the launch belongs to, plus its launches that have not started a process yet; it does not count cooling slots, or any other owner's seats. A launch over the cap is refused before a name is reserved or anything is minted, and the seats already running are untouched:

owner <owner> holds 4 of the 4 seats manager <id> admits per owner; stop one of that owner's seats, or ask the operator who runs this manager for a higher seat cap

The CLI exits 2 with CONFLICT and a remedy naming cotal stop --name <name> and --seat-cap. Under --json, error.cause.code is resource-exhausted and error.cause.detail carries owner, seats and cap, so a caller can decide without reading the sentence. cotal_spawn shows the agent the same sentence. Stopping one of that owner's seats admits the next launch.

Launches from a managed seat

Run from a managed seat's own shell on a static or open mesh, a managed cotal spawn (with -d or --on) launches as that seat when it targets the seat's own space. A bare spawn there runs in that shell, as anywhere else. Without --space it picks that target the way the operator path does, so a recorded mesh that is not running is skipped. The CLI reads the seat's launch identity (COTAL_NAME, COTAL_ID, COTAL_LIFECYCLE_UID, COTAL_SPACE, and on a static mesh the seat's own credential), so the manager records the seat as the spawner, the same as for the seat's cotal_spawn tool. On a static mesh that credential also proves the seat's space, so a launch without COTAL_SPACE still runs as the seat, and a target space holding no credential for the seat is refused. An open mesh acts as the seat only when COTAL_SPACE names its space. The seat can then stop the child with cotal_despawn, and the manager stops the child when the seat exits. On a static mesh a seat whose agent file lacks capabilities: [spawn] is refused, because its credential holds no spawn subject. --on <instance> keeps its pin: the seat's own credential has no instance route, so on a static mesh the CLI mints a one-shot manager-caller view for the seat, pinned to that instance and carrying the spawn subject only when the seat's credential holds it. On an open mesh the call keeps the TLS requirement the mesh records. A managed spawn from a seat's shell never falls back to the operator: --creds is refused there, and so is a target the seat cannot act on as itself, such as --server with an unregistered --space, a user-auth mesh, or an open mesh that COTAL_SPACE does not name.

Foreground lifecycle isolation

Foreground user-mode launches serialize their local actor material. A duplicate refuses without replacing the first launch's files. A new local-user foreground launch also refuses an existing grant before rotating it. Departure cleanup is conditional on the launch's token and lifecycle generation, so an earlier launch cannot revoke a successor's grant. Remote provisioning remains the mesh operator's authority and can reissue material before the client observes an existing same-host consumer.

models

cotal models [--json | --plain] [-H | --harness <connector>] [--refresh] [--on <manager-instance>]
FlagDefaultMeaning
--space <s> / --server <url> / --creds <path>resolved meshWhich manager to reach
--harness <connector>, -Hall registered applicationsApplication whose catalog to list
--on <manager-instance>native selectionAsk only the named manager instance, or on a user-auth mesh the manager a name or host picks, as ps takes them. An unavailable or unauthorized pin refuses without choosing another. On a static or open mesh an id the registry lists for no manager is refused TARGET_NOT_FOUND at once, naming the managers that run, as spawn --on does.
--refreshoffAsk the connector to refresh its provider cache
--json / -joffOne JSON document: result.items holds each catalog as returned; the requested manager ID, answering ID and answering epoch are separate, and result.notAskedManagerInstanceIds lists the live managers an unpinned read did not ask, or is null when no registry was read
--plainoffThe same report without color

Asks the selected manager for each application's model catalog (model ids plus their variants) for connectors that expose one. OpenCode and Codex query harness/provider surfaces; Jcode reads providers that enable model_catalog = true in the operator Jcode config.toml. Jcode's listed effort tiers render as <tiers> (declared, not provider-verified), and launch can still refuse one. A connector without a catalog says so with supported: false, and one whose install has no catalog it can list, such as OpenCode 2.x, gives the reason in unavailable; neither is a failed read. A connector whose catalog could not be read, or that the manager could not load, makes the read partial (exit 4) with one MODEL_CATALOG_UNREADABLE warning naming its cause. The screen points to that warning without repeating it inline. The remedy is to correct the stated cause before retrying; a refresh cannot repair missing connector configuration. A manager-level refusal is reported separately. When the space's service registry lists no live manager, an unpinned operator models refuses TARGET_UNAVAILABLE (exit 2) at once, as ps does, and names bare cotal up as the next step. An unpinned operator read reads the registry first and goes to the one manager the native selection picks. With several live managers, the report names the others in a Not asked row, and its Next asks each with --on. A manager lists a catalog row for every application its install names, so an empty list says the answering manager has no agent application installed. A connector whose harness the manager did not find at boot reports the reason boot recorded, as a spawn does, so restart the manager after installing it. Pick a result with cotal spawn --model <id> --variant <v>, where <id> is the model id as the catalog printed it. OpenCode and Codex ids are the full provider/model; Jcode ids are bare (opus-5, not cliproxy/opus-5), because the provider is selected by the operator's Jcode config and a prefixed id is refused at launch with the bare form named.

persona

cotal persona [--json|--plain]
cotal persona show <name> [--json|--plain]
cotal persona create <name> --file <persona.md>
cotal persona create <name> --prompt <text> --subscribe <a,b> [--allow-subscribe <a,b>] [--allow-publish <a,b>]
cotal persona edit <name>
cotal persona remove <name> --yes

cotal personas is the earlier parser for the same catalog and is a legacy command. It still runs, and cotal help personas still prints its help, but cotal help no longer lists it and completion does not offer it.

p aliases persona; persona rm aliases persona remove. These commands manage the selected space's local .cotal/agents catalog without connecting, refreshing a remote catalog or seeding connectors. --space and --server select the same root used by launch. A prepared space has an offline catalog before it is started. Failed target resolution never falls back to the current directory: a --server that no registration names reads the project in this folder, under the space prepared there, as launch does, and outside one it refuses with TARGET_NOT_FOUND and writes nothing. With no space selected and no project here, the refusal says so. With --on <manager>, list, show and create work on that manager's catalog instead, and edit and remove refuse there, as Personas on an exact manager describes. --announce <channel> belongs to persona create --on; without --on it refuses with USAGE_INVALID and changes no file.

A persona name uses letters, digits, _ and -, up to 128 characters, and a name refusal says which rule it broke. Creation accepts one of --file/-f (a complete agent file), --prompt, or --from (body file, or - for explicit stdin). Complete files validate all supported fields, must name the requested persona and keep their original bytes. A file that names another persona is refused with SCHEMA_INVALID, naming the file's own name and the persona create that keeps it. A file the agent-file rules refuse is SCHEMA_INVALID naming the file, the field and the reason, such as a missing frontmatter block, the renamed channels key or a subscribe channel outside allowSubscribe. A YAML error names only its line and column, not the parser's text. They cannot be combined with --role, --model, --harness, --subscribe, --allow-subscribe or --allow-publish. Body input requires an explicit --subscribe declaration, including --subscribe "" for no channels, and accepts --role, --model and --harness. --allow-subscribe sets the channels the persona may read, and an omitted or empty one reads as the --subscribe set. --allow-publish sets the channels it may post to, and without it the persona posts nowhere. Both take a comma list, as the agent file fields allowSubscribe and allowPublish do. Both refuse control characters with USAGE_INVALID. An empty --model is refused with USAGE_INVALID too, and so is a --role or --model with a control character, before any other rule reads it. A refusal that quotes a flag's value escapes its control characters. A role is an address send -r reaches, so a role that is not one token of letters, digits, _ and - is refused, USAGE_INVALID from --role and SCHEMA_INVALID from a file's role. Each entry of --subscribe, --allow-subscribe and --allow-publish must be a valid channel name, written without #, and each --subscribe channel must be within --allow-subscribe when it is given, or creation refuses with USAGE_INVALID, naming the flag and the entry. --harness must name a harness this machine knows, as config set harness requires, or creation refuses with USAGE_INVALID and writes nothing. Create and edit refuse a definition over 1 MiB with SCHEMA_INVALID, the bound a create on a manager applies to its input, and an edit keeps that draft and names it. Creation refuses an existing definition, and with CONFLICT a name that differs from an existing one only in case, since a case-insensitive file system holds the two as one file. Mutation receipts contain names and paths, not bodies. A persona file that is a symbolic link is listed with a PERMISSION_DENIED warning that names the file it links to. spawn reads that file, but persona show, edit and remove refuse the link with PERMISSION_DENIED, and the next step names the file to copy in its place. A .cotal/agents that is a file or a link that loops holds no personas, so the listings, show, create, edit and remove refuse it with CONFLICT, naming it and what it is, and the next step is to move it out of the way. Show and edit accept a definition whose name is not its catalog name. The seeded default persona is one: its file is default.md and its agents launch as default_agent. That name is reserved, so a create under it is refused with USAGE_INVALID.

Editing opens a private draft using the editor preference (VISUAL, then EDITOR, then saved preferences). A value that is the path of an existing file runs as that program, spaces and all. Any other value runs as a shell command. With no editor set, or one that cannot be found or run, editing refuses with DEPENDENCY_UNAVAILABLE (exit 2) and names the setting to fix. The draft's private folder is made in the temporary directory (TMPDIR). One this account cannot write refuses with PERMISSION_DENIED, and one that is missing or not a directory with TARGET_UNAVAILABLE, both exit 2 and naming it before anything is made. A successful editor exit and valid definition are required before saving. A draft that fails validation is refused with SCHEMA_INVALID and the reason, keeping the draft and naming its path, so the edit is not lost. An editor that exits non-zero, as vim's :cq does, cancels the edit: it exits 0 with a USER_CANCELLED warning, saves nothing, and keeps and names a draft it changed. An editor ended by a signal fails with OPERATION_FAILED and keeps the draft. A SIGINT, SIGTERM or SIGHUP that reaches the CLI while the editor is open is passed to the editor, and the CLI waits for it to exit. SIGTERM and SIGHUP then stop the edit, as does a Ctrl-C that ends the editor: nothing is saved, an unchanged draft is removed, a changed one is kept and named, and the CLI exits 130, 143 or 129. An editor such as vim that takes Ctrl-C as a key keeps the edit going. A draft left as it was saves nothing and says the persona is unchanged. A changed or removed source refuses with CONFLICT (exit 2), leaving the current definition intact. The refusal says which one happened and keeps your edited draft in its private folder, naming it, so the edit is not lost. Local CLI create, edit and remove share a per-persona lock during comparison and commit, not while an editor or confirmation is open. Manager and external writes do not participate in that lock. Create, edit and remove refuse a catalog folder you cannot write, such as one set to mode 0500, with PERMISSION_DENIED, and leave it and its files as they were. --json and --no-input refuse editing before an editor opens. That refusal, and a removal refused for want of --yes, give the fix without the command's help.

A catalog file that is not a regular file, such as a FIFO, is not opened: show, edit and remove refuse it with PERMISSION_DENIED, and the list marks it unreadable and names it in a PERMISSION_DENIED warning, exit 4.

List, show and mutation receipts support one-document --json and line-oriented --plain. Show explicitly returns the full definition and source. List does not return the body or launch options. A malformed entry produces a partial inventory (exit 4); an unreadable catalog refuses with PERMISSION_DENIED (exit 2) rather than reporting it empty. A catalog folder that lists its file names but cannot be searched, such as one set to mode 0600, is refused the same way, empty or not, and its personas are not reported as malformed. Its next step is chmod u+rx <folder> only for a catalog folder you own whose mode denies you read or search. A folder above the catalog that you cannot enter is named instead, and a catalog folder another account owns says to ask its owner. Other read errors get no next step, and the CLI never changes a mode itself. Remove asks with cancellation as the default in a terminal. Removing the seeded default persona says so in the question and the receipt, since a bare cotal spawn launches it, and names what writes a fresh copy: a local cotal up in its root, cotal space create or cotal setup. Plain confirmation uses numbered choices. Scripts and JSON removal require --yes/-y.

The legacy personas parser remains available during migration: list [-v] [--running], show, edit, new and rm --force. Its new requires --subscribe, reads --from - from stdin, and overwrites an existing persona only with --force. Its local writes share the same draft validation and commit fence. Its list refuses a catalog folder it cannot read with PERMISSION_DENIED (exit 2), naming the space and giving the next step persona gives, rather than reporting it empty, and still lists a persona file that does not parse as unparseable. It escapes control characters in the names, fields and bodies it prints, as persona show does, and shows each persona's description or first prompt line whole, wrapped to the terminal's width. Its show does the same on a terminal, under the file's path, and writes only the file, byte for byte, when its output is piped or redirected, so personas show <name> > <file> copies it. It refuses a missing persona, an invalid definition and a name no persona can have with the codes persona show uses. Only its explicit --running overlay connects to the mesh. That overlay heads the list with the mesh it read and marks each persona running or not running.

Personas on an exact manager

persona --on <full-manager-instance> lists that manager's authorized catalog. persona show <name> --on <id> reads a card only when its native ownership policy allows it. A name-only row does not grant access to its instructions or permission to redefine it. These operations never substitute the local catalog or another manager.

persona create <name> --on <id> accepts one of --file, --prompt or --from, with --from - as explicit piped input. Leading frontmatter follows the native persona merge: the supplied name wins, explicit model/role/application and channel fields override the file, and the manager preserves existing ownership and redefinition policy. Inputs are bounded to 1 MiB and must be valid UTF-8. A malformed leading block refuses before the remote request.

The remote wire accepts name, body, model, role, application, initial read channels and read/post ACLs. --harness, --subscribe, --allow-subscribe and --allow-publish name those fields. Other frontmatter, including kind, tags, description, attention settings, variant, launch options, capabilities and owner, refuses as unsupported instead of being silently discarded. Remote edit and removal are not native wire operations and refuse; no local file is changed instead.

Creation is silent unless --announce <concrete-channel> is supplied. Announcement preparation must resolve to the same space, root and account context before the write. A confirmed save and a later denied or unconfirmed announcement produce a partial receipt retaining the save. Do not repeat the definition to recover an announcement; read the channel first. A stored announcement is not delivery, a spawned agent or task completion. Finite JSON/plain receipts keep the requested and answering manager, epoch and original request, without printing the creation input body. Rich results offer follow-up show or spawn commands with the original exact manager pin; a suggested launch still requires the caller's native spawn authority. When the manager writes the file into this root's catalog, the receipt's Local files line names it and JSON localFilesChanged is true. On a static space the manager records an owner only for one of its own seats. The CLI mints a new caller for each call, so its card is operator-owned, as a local persona create writes it, and the manager does not show it over --on. The receipt then offers cotal p show <name> for a file in this root, as the --on listing's Next does, and a show --on that the manager answers not-found names that command when this root's catalog holds the file. A peer joined with its own credentials is not one of the manager's seats either, so on a static space it cannot redefine or read over --on what it defines.

seats

cotal manager seats [--drain [--yes|--dry-run]] [--json|--plain]
FlagDefaultMeaning
--drainoffRetire every seat whose agent has exited. A seat whose agent still runs is kept

cotal seats is the older spelling, hidden from cotal help, and runs the same command. A drain asks first on a terminal and needs --yes in a script, as Manager maintenance describes.

The pty runtime used to start a detached custodian process for every Linux seat. It now spawns in-process, but custodians that an earlier manager started keep running, and one whose agent has exited stays resident while a manager still holds its connection. This command lists the custody records under COTAL_SEAT_ROOT (default ~/.cotal/seats), one line per seat:

StateMeaning
live-childThe agent process still runs. The seat is never signalled, and a manager can still adopt it
childlessThe agent has exited, or the record comes from an earlier boot. --drain retires the seat
drained--drain proved the custodian and the agent gone and removed the record
refusedThe 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

A drain signals only a custodian whose recorded start identity still matches the live process, so a reused pid is never touched. No process outlives a reboot, so a record from an earlier boot is reported childless and --drain removes it without signalling anything. A record with no start or boot identity is refused with or without --drain, and is never reported as running or exited. On a host that publishes no boot identity (/proc/sys/kernel/random/boot_id) every record is refused the same way, because no record can be tied to this boot. That refusal and an unreadable record signal nothing. A refusal from the reap itself can come after the drain already sent SIGKILL to the custodian. Its detail names the pid or process group the reap could not prove gone, so check those processes before you retry. The command exits non-zero when any record is refused. It is Linux-only, and on other platforms it refuses UNSUPPORTED.

Persona resolution

See the current command contract for this behavior.

On this page