# Agent terminal control

Managed seat control uses the running manager's native endpoint commands.


## Managed seats

```bash
cotal ps [--json | --plain] [--on <instance>] [--wide] [--slots] [--space <s>]
cotal stop <name> [--force] [--on <instance>] [--space <s>]
cotal stop --self [--force]
cotal attach [<name>] [--on <instance>] [--no-reconnect] [--space <s>]
```

| Flag | Default | Meaning |
|---|---|---|
| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |
| `<name>` / `--name <n>` | none | Managed agent to stop or attach, named positionally or with `--name`. `stop` needs one unless `--self` is given. `attach` without one lists your agents to choose from on a terminal, and refuses `USAGE_INVALID` where it cannot ask |
| `--self` (`stop`) | off | Stop only the verified managed seat running this command (see [Stopping an agent](#stopping-an-agent)) |
| `--force` (`stop`) | off | Request the native hard despawn (`graceful:false`); it does not bypass authority |
| `--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. On a user-auth mesh it also takes a manager's name, its host, or the `<host>/<runtime>` an unnamed manager shows as (its runtime alone when its presence names no host), when only one of your live managers matches it; otherwise it refuses as `TARGET_AMBIGUOUS` or `TARGET_NOT_FOUND`, lists your live managers and gives the command again on each. On a static or open mesh a manager's presence does not carry its instance id, so a name is refused as `USAGE_INVALID` and the Next asks for the instance id. An empty value (`--on ""`, an unset shell variable) is refused, never treated as absent. A roster principal id (`local.…`) is refused with a message naming the instance id `ps` prints |
| `--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, a Windows seat whose process ConPTY has not created yet, or a connector that reported no provider) prints nothing, never a placeholder |
| `--json` / `-j` (`ps`) | off | One JSON document (see below). Mutually exclusive with `--wide` and `--plain` |
| `--plain` (`ps`) | off | The same report without color or marks |
| `--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 |
| `--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 |


### Control credentials

On a controller stack started with `--no-manager`, `ps` refuses `TARGET_UNAVAILABLE`: no manager
runs, so there are no managed agents to list. Bare `cotal up` adds the manager. A `manager` preset
service started at the root answers instead, and `status` names it on its Manager row. Bare `up`
then refuses `LIFECYCLE_BLOCKED`, exit 2, naming that service and the
`cotal svc stop <name> --instance <id> --space <space> --yes` that stops it; once it is stopped,
bare `up` starts the stack's own manager. A stack manager whose process died stays registered, so
`ps` waits out the manager's answer deadline first, then refuses the same way and names bare
`cotal up`, which starts it again. While an `up` is still starting the
stack's manager, `ps` refuses `TARGET_UNAVAILABLE` saying the manager is starting, and its Next is
to run `cotal ps` again once `up` reports it started. That `up` holds the root's maintenance lock;
once it has exited, `ps` gives the refusal above.

On any other static or open space, such as a legacy stack started with `--no-manager`, a manager
registry that lists no live manager makes `ps` refuse `TARGET_UNAVAILABLE`, exit 2: no manager is
registered, so none was asked. It used to fail `OPERATION_FAILED`, exit 5. `cotal status <agent>`
and the `attach` picker read the same list and refuse the same way. The refusal does not show that
no manager process runs, because a manager that is still starting or cannot register is not listed.
The registry's own answer stays under `error.cause`. A registry that could not be read is reported
as it was.

When no manager runs or is registered, the refusal's Next names the root of this machine's own stack
for the space, as bare `cotal up` in that folder, because bare `up` in another project acts on that
project's stack. When this machine only registered the space, records no stack for it, or reached
it through a `--server` other than the broker its registration records, it cannot say where the
stack runs. The Next then says to start the space's manager where its stack runs and names no local
command.

A raw `--creds` file is refused by `ps`, `stop`, `attach` and the other control commands, because
that route mints no endpoint-caller triple; the project folder, or `--space` against the registry
entry, is the route that does.

`ps` shows the instance id `--on` takes for each manager that answered, with or without agents. One
manager is listed under `Manager` as its `instance`, and with several each gets a `manager <id>`
header. With no agents its Next is `cotal spawn`, or `cotal setup` when no agent application is set
up here, as `status` reports it.

When `ps` asks every manager, it then asks each manager that answered for the host name in its own
status reply. The host shows as `· reports host <host>` after the id, or as a `reported host` row
under `Manager`. It is that manager's own claim, and `ps` does not check it. Two managers on one
host report the same name, and `--on` never takes it on a static or open mesh. These asks share one
2-second limit. A manager that has not answered by then is listed without a host, and the read
stays as complete as the census found it. `ps` does not show a manager's name. On a user-auth mesh
with several of your managers live, the host is the one each manager's presence reports.


### Seat JSON envelope

The human `ps` report is presentation text and is not a stable parsing target. Scripts use `--json`,
one envelope whose `result` is:

| Field | Meaning |
|---|---|
| `kind` | `seats`, `slots` with `--slots`, or `presence` when no manager of yours answered on a user-auth mesh |
| `items` | Each seat (or slot) row as its manager returned it, unchanged |
| `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`. An `answered` manager that gave a host name in time also has `reportedHost` |

`target` is `{space, scope: "all-managers"}` for the class scatter,
`{space, scope: "your-managers"}` for a user-auth mesh with several of your managers live,
`{space, scope: "presence"}` when none of them answered, and
`{space, scope: "manager", managerInstanceId}` for `--on` or a user-auth mesh with one. A manager that is not
`answered` makes the read `partial` (exit 4), with a `MANAGER_SILENT`, `MANAGER_REFUSED` or
`MANAGER_CONTRACT_MISMATCH` warning naming it. A `silent` manager whose `liveness` is `gone` is a
registration its host never removed. On an auth space its row names `cotal endpoint deregister
manager <id> --yes`, run from the space's own root. On any other space only that manager's own clean
stop removes it, so the row names starting it again and stopping it if its host is still available,
and says no supported command removes the record if the host is gone. The row stays and the read
stays `partial`.

`--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.

A revision-32 manager also supports a bounded native `slots` query through
`endpoint invoke manager slots --on <instance> --args '{"names":["worker"],"includeRetired":true}'`.
It point-reads up to 100 distinct aliases and can return their retired slot/head and
cleanup observations. The default `ps --slots` query is unchanged and excludes retired
rows. Omitted aliases, live-map absence and stop acknowledgements are not retirement
proof. Match the returned principal and lifecycle UID before interpreting any terminal.


### Seat observations

These are operator clients over the running manager's control plane. The default row includes the
connector, model pin, optional requested variant, and runtime as operational descriptors for the
managed row. They do not make a shared display name a unique protocol identity; use `--json` when
unambiguous owner+actor attribution is required. An omitted variant means no override was requested;
Cotal does not invent an effective provider default it cannot observe. `ps` also prints two state
facts per managed agent, because they answer different questions: the process fact from the manager's
own runtime handle (`running` with its uptime, or `exited` with how long it ran), and the mesh fact
from the roster (`idle` / `working` / `waiting` / `mesh offline`, or `not in roster` when the seat has
no presence row at all: a seat that has not joined yet, or one that never did).
If the bounded launch wait elapsed without a join, `ps` prints
`join degraded (readiness deadline elapsed)` and the manager's launch diagnosis. JSON carries
`joinHealth: { state: "degraded", reason: <diagnosis> }`, separate from the unchanged process
status. An observed join matching this seat's principal and lifecycle UID clears the diagnostic.
The manager does not stop a seat because the wait elapsed.
When the seat's
connector relays a harness-reported condition, the mesh fact carries its code and how long it has
held, so a seat whose turn died on a provider rate limit reads `waiting (rate_limit for 40m)` rather
than a bare `waiting`, and `--json` carries the whole `condition` object. When the connector reports
the seat's last work event (presence `activeAt`), the mesh fact ends with its age, such as
`· active 3s ago`, and `--json` carries `activeAt`. A seat whose turn stopped advancing keeps
heartbeating, so its presence row stays fresh and this age is what shows the stall. A seat can be
`running` and `mesh offline` at once: the process is alive and its presence has lapsed. That row says
how long, as in `mesh offline for <age>` with an age such as `3.5h`, counted from the seat's last
presence heartbeat, which `--json` carries as `offlineSince` (epoch ms). The age is read only from
the seat's own presence record, matched on its principal and lifecycle uid, so a same-named peer or
an older lifecycle never dates it. The manager log names each managed seat that is offline on the
mesh while its slot is held
(`seat offline on the mesh: <name> - last heartbeat <time>; process <state>`), including one its
watch first sees offline after a reconnect, and each one that comes back
(`seat back on the mesh: <name>`), so a watchdog that only checks process liveness has a line to
act on. The manager does not reap or re-key such a seat. The mesh fact is only a verdict while the
manager's own presence watch is fresh: when that watch has been silent past the liveness window, or
has not replayed the bucket yet, every row prints `mesh unknown` with the reason instead (`--json`
carries it as `meshView: stale | unpopulated`), because `offline` and `not in roster` would then
describe the manager's watch rather than the seat. The manager rebinds a watch that goes
silent under a live connection on its own, so `mesh unknown` normally clears within a liveness window.
On a user-auth mesh `ps` also renders each managed agent's last credential-refresh outcome, fail-closed.

**Mode split (chosen up front, never try-scatter-then-degrade):**


### Manager routing

- **Static / open mesh.** Bare `ps` is a **class scatter**: it freezes the live manager class from
  the records registry, merges every registered instance's agents grouped and attributed per
  instance, and a non-answering instance is shown as `registered, no answer within the deadline`
  (never silently omitted). A refused list or a missing answer makes the census incomplete: rows
  from other instances remain visible, but `ps` prints an incomplete-census warning on stderr and
  exits 4 (`partial`), including with `--json`. Those rows are not a complete seat count. A contract
  mismatch prints one plain comparison of the requested and served input/output digest pairs and
  advises aligning manager versions. The no-answer label means only that the instance is registered
  and did not answer. It does not say the host is down, because a dead host never deregisters itself and a
  live one can be slow; if it is gone, deregister it.
  `--on <instance>` pins the read to one exact instance id instead. A wrong pin fails loud
  rather than falling through. On a static or open mesh a well-formed id that the registry lists for
  no manager is refused `TARGET_NOT_FOUND` at once, naming the managers that run, as `spawn --on`
  does; a registered one that does not answer is reported as `manager instance <id> did not answer`
  (nothing else is asked). A credential without that
  instance's rail is reported as refused by the broker, not as an unresponsive manager. A manager
  that answers with a refusal is shown with its own cause; "no manager reachable" is said only when
  nothing answered at all. If the scatter's own registry read fails (the freeze or the reconcile),
  `ps` says the manager registry could not be read rather than pronouncing on the managers, which
  may all be up. When nothing answered and this root's stack manager process still runs, as a hung
  one does, `ps` refuses `TARGET_UNAVAILABLE` naming its pid, and its Next ends that process so bare
  `cotal up` starts the manager again.


### Versioned endpoint rails

**The verdict is scoped to the endpoint rail the request rode.** An issued caller rides the
versioned `ep.v1` rail, a separate subject space from the legacy `ep` rail, and an endpoint serves
both (SPEC 13.15). A manager older than the versioned rail serves `ep` alone, so it can be running,
registered and answering while an issued caller's request reaches nobody. Silence on `ep.v1` is
reported as `no manager answered on the <rail> rail` with `ep.v1` as the rail, and names both causes
it is consistent with: no manager running, or one older than the rail. The CLI cannot tell them
apart, because the service registry records no package version, so check whether a manager is
running and, if it is, its version. The same scoping applies to `cotal run`'s hosted verbs, which
drop the `--local` suggestion there, since `--local` drives the run from the calling process and
names the caller as its answerer. A hosted verb that no manager answered refuses
`TARGET_UNAVAILABLE`, exit 2, leading with `the manager registered for this space did not answer`,
and its Next is `cotal status`, which names a stack manager that runs but does not answer, with its
pid.


### Seat locality

**`stop` and `attach` route by seat locality.** A seat can only be stopped or attached by the
manager actually running it, and the class queue does not know which one that is. So on a
static/open mesh both verbs first ask every registered instance which one hosts the named seat, then
address that instance directly. This happens by default; you do not need `--on`. When every instance
answers and none hosts the name, `stop`, `attach` and `input` refuse `TARGET_NOT_FOUND` (exit 2)
before anything is sent, as they do with `--on`, naming the managers that answered, with `cotal ps`
as Next. When an instance did not answer for itself, the seat may run there, so they refuse
`TARGET_UNAVAILABLE` (exit 2) and name that instance. When more than one instance runs the name,
they refuse `TARGET_AMBIGUOUS` (exit 2) and name each of them. `--json` carries each refusal. On a
terminal the last two ask instead, listing only the managers that hold the name or did not answer,
and the refusal's Next gives the command again with `--on` for each.

`--on <instance>` remains the override, for when you already know where the seat lives or the
lookup itself is degraded. On a static or open mesh, `stop --on`, `attach --on` and `input --on`
with an id the registry lists for no manager are refused `TARGET_NOT_FOUND` at once, naming the
managers that run, as `spawn --on` is.
On a **user-auth mesh**, the exchange selects one authorized manager
for a short-lived `manager-caller` view. `--on` requests a specific instance. Without it, a command
run from a managed seat's shell selects the manager that launched the seat (`COTAL_MANAGER_INSTANCE`),
and any other caller's selection must be unique. Discovery and the command use that instance route.
The caller gains no registry read or scatter permission. An absent, ambiguous or unauthorized
selection refuses before sending the command. With several of your managers live, the refusal lists
each one on its own row with its name, instance id, host, activity and the time its process started.
A manager started without a name is listed as `<host>/<runtime>`. A manager started by a cotal
before 0.100.0 puts no host in its presence, so its host reads
`not reported (started by an older cotal)` and it is listed by its runtime, such as `tmux`.
Restarting it with the current cotal shows its host. Its Next gives your command again with `--on`
for each, using the name, `<host>/<runtime>` or runtime when that picks the manager out, and the
instance id otherwise. The code stays
`PERMISSION_DENIED` and the exit 2. `who --all` lists the same managers at any time. By default, when every manager you registered has
exited, the refusal says to start one with [`manager start`](/api-reference/cli-maintenance/#manager); the command is not sent to the local manager.

`ps` and `status <agent>` do not refuse: they read each of your live managers, as the user-auth
mode below describes. `stop`, `attach` and `input` with an agent name read them the same way and
run on the manager that holds the name, without asking. A name held on several of them, or found on
none that answered, is settled as on a static mesh above, among those managers only, and a manager
whose read fails counts as one that did not answer. A name every manager answered without is
refused `TARGET_NOT_FOUND`, naming the managers searched. On a terminal, `spawn -d`, `models` and
`persona` ask instead of refusing. Every manager the exchange counts is one of yours in the space the command
resolved, from the folder or `--space`, so the question lists those managers with the most recently
started first, and that one is the default. When presence cannot be read, the managers are listed
by instance id under a `names unavailable:` line, as for a bare `spawn`. The command then runs on
the manager you pick, as the Next would run it. The question and a `Manager chosen` receipt, with
the manager, its instance id and the `--on` that skips the question, go to stderr. Cancelling the question does nothing and
exits 0. Without a terminal on stdin and stdout, with `--no-input`, with `--json`, which implies
`--no-input`, or with `COTAL_NO_PROMPT=1`, the command refuses as above. A `--json` caller passes
`--on`, and the result's `target.managerInstanceId` names the manager that answered. A bare `spawn` asks where the agent runs
before any of this, and a managed launch on a manager that is not known to run on this host then
asks for its folder (see [spawn](/api-reference/cli-agents/#spawn)).

A trusted hosted composition may opt into an [interactive local primary](/docs/embedding/#interactive-manager-primary).
Under that policy, unpinned human control selects the genuine local Manager up front, even when your
own remote Manager is live; an unavailable local Manager refuses without remote substitution. This
affects all interactive Manager verbs, not only `ps`. Select your remote Manager explicitly for its
spawn, run or terminal operations. Explicit pins and managed-seat launching bindings keep their rules.
There is no user flag for the policy, and stock `up` and recovery defaults do not enable it.

A connection that the broker refuses, or that does not open within 5 seconds, sends nothing. A manager
command then connects again, as do `send` and the other commands that open a one-shot connection, up
to 4 attempts 1 second apart. A view the auth service refused for a moment, or an auth service that
answered late, then just works. When every attempt fails the command says nothing was sent and is
refused, exit 2: `PERMISSION_DENIED` when the broker refused each attempt, `TARGET_UNAVAILABLE` when
none opened in time. The broker's own words stay in the native detail. On a user-auth mesh the space's
auth service logs why it refused.

A seat is reported as **not found** only when every reachable instance answered for itself. An
instance that stayed silent past the deadline, or that refused the read rather than answering, said
nothing about which seats it hosts, so the seat may be running on it. That case reports that the
location could not be established, names the instances that did not answer, and states outright
that it is not a report that the seat is gone. Read it as unknown and retry with
`--on <instance>`; a retry loop that treats it as "already gone" stops looking for a seat that is
still running. A single manager cannot tell "hosted elsewhere" from "does not exist": it answers
`not-found` for both, which is why the search asks all of them and why an incomplete search
concludes nothing.
- **User-auth mesh.** With one of your managers live, `cotal ps` reports what that manager knows
  about your agents (an instance-addressed read against its in-memory roster, owner-filtered). With
  several live, `ps` asks nothing: it reads each manager the exchange counts live through its own
  one-instance view, one after another, and lists each under its own `manager <id>` header, as the
  class scatter does. A manager that does not answer is listed as silent and makes the read
  `partial` (exit 4), and the Next names each one by instance id: a manager that ended without a
  clean stop stays registered under its id. Managers of other owners are not asked, so completeness
  across the space is not claimed. `status <agent>` and `who` read your managers the same way.
  When none of your managers answers, or none is live, `ps` lists your agents as presence shows
  them, read-only, with their state, application and host, and exits 0. It says that no manager
  answered, and its Next starts one: bare `cotal up` on the root of the space's own stack, as for a
  static stack above, and `cotal manager start` anywhere else. `--on` and `--slots` keep refusing
  `TARGET_UNAVAILABLE`, exit 2, when their manager does not answer. Your ledger row needs the `admin` scope to
  reach `ps` at all; `spawn` alone is refused by the broker (the ep tier boundary). A broker-refused
  read keeps that refusal and adds the documented scope requirement and `cotal auth grants` remedy.
  The broker does not identify whether the missing permission is for the route, command or target;
  the CLI does not substitute another caller or retry on a different route.


### Terminal attachment

`attach` streams and drives an agent's terminal on the `pty` runtime; detach with the escape key
(Ctrl-] by default; see the [`detach-key` preference](/api-reference/config/#preferences)). The key is recognised
as the legacy control byte and as the kitty keyboard protocol and xterm modifyOtherKeys encodings of
the same press, so a terminal with either protocol enabled detaches too. It does so over a one-use, holder-bound
mesh session ([SPEC](/docs/spec/) §13.6): the manager replies with a signed session grant (never a
`127.0.0.1` URL), the CLI redeems it once over the broker, and the browser console (`cotal console`)
drives the same session. `stop` and `attach` need a running manager to talk to. On a static mesh
they are cross-agent admin operations. On a user-auth mesh, your own agents (any agent under your
owner) need only the `spawn` scope; another owner's agent needs `admin` on your ledger row
([identity & auth](/docs/identity-and-auth/)). Launch detached agents with [`spawn --detach`](/api-reference/cli-agents/#spawn).

`pty` is the only runtime that streams a terminal. A seat started by the `tmux`, `cmux`, `orca` or
`herdr` runtime runs in a surface the manager does not own, so there is no stream to give and
`attach` refuses once with `UNSUPPORTED`, exit 2, carrying that runtime's own guidance for watching
the agent where it runs, and a Next that names `cotal send` and `cotal watch`. The reconnect loop
below does not retry that refusal, because asking again cannot change it.


### Attachment reconnection

**`attach` reconnects when the link dies.** A session lives on a network link, and a laptop that
sleeps, a VPN that drops or a wifi handover kills it. When that happens `attach` prints
`[cotal: connection lost, reconnecting]` on stderr and starts asking the manager for a new session:
a fresh grant, a fresh per-session credential, a fresh connection, so every attempt re-runs the same
authorization the first attach did. On success it prints `[cotal: reconnected]`, the manager repaints
the seat's current screen the way it does for any attach, and you carry on in the same terminal.
Retries wait 1s, 2s, 5s, 10s, then 30s, for as long as the seat exists. The detach key is read the
whole time the loop runs, the waits and the attempts alike, so a reconnect never traps you: press it
while a session is being established and the attach ends there, and a session that lands behind the
press is handed back to the manager rather than left holding a slot. Everything else you type while
there is no session is dropped rather than queued, so keystrokes aimed at a terminal that turned out
to be frozen, Ctrl-C included, are not delivered to the agent by a reconnect you did not know had
happened. That starts before the first session, not at the first reconnect: at a terminal, `attach`
reads and drops what you type while it is still resolving the mesh, so a key struck at a prompt that
has not come up yet does not reach the agent when it does.
The terminal is in raw mode for the whole reconnect, including when the link died before the first
session finished opening, so the detach key works there too instead of echoing as `^]`.

A **pipe** carries script input. For example, `printf 'ls\n' | cotal attach --name web` is
buffered until the session opens. Buffering continues across reconnects, so
`tail -f log | cotal attach --name web` does not lose the part of its feed written while the link was
down. Only a terminal gets the reader; `--no-reconnect` keeps the old behaviour on both.

It stops on its own when reconnecting cannot help, and says why: a manager that refuses the attach
exits non-zero with the manager's own message, and a reconnect that finds the seat no longer there
(despawned, or its agent exited while the link was down) exits cleanly with `seat <name> is gone`.
It stops the same way on a refusal the CLI raises itself, such as a broker that denies the request.
A local connect refusal that retrying cannot fix, such as a static-auth mesh whose seed is now
missing, also exits non-zero with the refusal's own sentence. A broker that is still unreachable,
or a user-auth service that does not answer, keeps the loop trying in silence.
A refusal that could still pass, such as a manager at its session ceiling, is relayed in the
manager's own words while the loop keeps trying, once per refusal rather than once per attempt.
Pressing the detach key ends the view with `Detached`. A remote terminal-session end, including
process exit or manager shutdown, prints `Attach session ended`. Neither report claims the agent
is still running or that its cleanup completed. Use `cotal ps` to check its current state.
`--no-reconnect` restores the single-session behaviour.

Each reconnect also hands the abandoned session back to the manager, over the first link that can
carry the message, so an attach that flaps does not eat the manager's session slots one outage at a
time. If that message never gets a link, the attach says so when it ends. The live-session ceiling
defaults to 64 concurrent sessions (`--max-sessions`); the browser console opens one session per
pane, so a dashboard over a large mesh should size for agents × panes. Hitting the ceiling refuses
before a credential is minted and names `--max-sessions`.


### Session grant redemption

Which mesh `attach` resolves also decides **how it redeems the grant**. On a registered open mesh
there is no local seed. The CLI connects bare, the same way other control commands already do, and
the session rail is the caller rail that a real open-mode connection already reaches. Telling the
operator to re-register the root is false: the registered root is already the contract. On a
static-auth mesh the grant is still redeemed by minting a short-lived
session-scoped credential from the seed at the root the mesh resolved to, never from a `.cotal`
found by walking up from whichever directory you happen to be standing in. The difference is not
hypothetical: `~/.cotal` exists on every install because the mesh registry lives there, so a command
run anywhere under your home directory but outside a project used to mint from your home
directory's trust and present it to a broker that trusts a different chain, which surfaced as a
bare authorization failure that named nothing. A directory that does hold another chain for the
same space is now reported on the way past, and not obeyed:

```text
! this directory resolves to /Users/you, whose .cotal/auth holds a DIFFERENT trust chain for space "team".
  attach used /Users/you/projects/app, the root this mesh resolved to. The other one is not being used, and is worth a look.
```

When a **static-auth** mesh holds no seed at the resolved root, `attach` refuses and names what it
resolved, the broker and the root, instead of describing a directory it did not use and instead of
taking the open-mode path. An authenticated registry entry with a missing seed is still
authenticated. On a USER-AUTH mesh `attach` reads no seed. It sends your login and the session grant to
the auth service, which issues a `session-caller` bearer only if your owner and actor hold that
session. The connection it opens expires with the session grant.


### Console authority

Terminal bytes stream over the mesh; the manager's own HTTP/WS face serves the console. That endpoint binds
**loopback by default**, so nothing is exposed by accident; `cotal up --host <addr>` passes its bind
address down, which is what lets you reach the browser console (`cotal console`) for an agent whose manager runs on another machine.
`attach` does not use that face: it redeems a signed mesh session grant over the broker instead (see above), so it reaches a
remote manager regardless of the bind address. A
bare `cotal supervise` and an embedded manager stay machine-local. Set it directly with
`supervise --console-host <host>`.

That address is **recorded on the mesh** and carried forward, because it is a decision rather than
something later commands can work out for themselves (a broker dial address is not a manager bind
address). Every later manager launch for the same mesh reuses it, including a same-root `cotal up` repair
and an adopted preserved or restored listener. A manager replacement
does not quietly move a reachable attach face back to loopback. Passing `--host` again overrides it,
so you can widen or narrow exposure whenever you like; a mesh that never asked stays loopback-only
and records nothing.

Because that face mints terminal read and write authority for every managed agent's browser session, it is credentialed in two
tiers. A mesh caller receives a **ticket** bound to the single agent the manager just authorized,
single-use and short-lived, so one authorized attach can never be re-pointed at someone else's
agent. The **console token** is the operator's own, reaches every agent, and is printed only to the
manager's output. The roster, the live feed, and the PTY stream all answer `401` without one; the
static console shell is served openly, since it describes no agent.


## input

```bash
cotal input --name <n> --text <text> [--no-enter] [--on <instance>] [--space <s>]
```

| Flag | Default | Meaning |
|---|---|---|
| `--space <s>` / `--server <url>` / `--creds <path>` | resolved mesh | Which manager to reach |
| `--name <n>` | | Managed agent to type into (required) |
| `--text <text>` | | The text to type, taken verbatim (required) |
| `--no-enter` | off | Type the text and stop there, without pressing Enter |
| `--on <instance>` | class anycast | Pin to one manager instance id using the same rules as [`attach`](#managed-seats) |

Types one line into a running agent's terminal, as if you had typed it there, and returns. This is
the half of [`attach`](#managed-seats) that a program wants: `attach` is a live stream that holds a
session open and expects a terminal on your side, so a script, a cron job or a web UI cannot use it
to send a single line. `input` is one authorized call.

What it is for is **harness commands**. A line beginning with `/` is not chat and not a message: it
is something the agent's own harness handles, and the only way in is the keyboard.

```bash
cotal input --name reviewer --text "/compact"          # ask the harness to compact its context
cotal input --name reviewer --text "/model opus"       # switch its model
cotal input --name reviewer --text "hold on that PR"   # ordinary typing works too
```


### Input quoting

**Quoting.** `--text` takes a value, so a payload starting with `/` survives as written. A payload
starting with a dash needs the `=` form, because the shell-style `--text --foo` is ambiguous and is
refused rather than guessed:

```bash
cotal input --name reviewer --text=--verbose          # dash-leading text: use --text=<value>
```

Enter is pressed by default, since a command typed but never submitted has not been delivered.
`--no-enter` types the text and leaves it sitting at the prompt, which is how you stage a line and
send it later.

At a Claude selection menu, including `AskUserQuestion`, text input is refused before any bytes
are written. Pasting a number or an answer there can otherwise confirm the highlighted option
instead of the requested answer. Use `cotal attach --name <n>` and the menu's arrow keys and Enter.
This refusal also applies to `--no-enter`.

Nothing comes back but a `Terminal input written` receipt. It names the agent, space and manager,
the bytes the manager reported writing (counting the trailing carriage return) and whether Enter was
requested. It confirms the write to the terminal only. Whatever the agent does next shows up where
its output already goes: the mesh, its transcript, or an `attach`.

A write the manager cannot confirm, such as a terminal write that failed partway, exits 3 with
`Outcome unconfirmed` and is not typed again. Part of the text may already be at the prompt, so check
the seat before you send it again.

With no manager running, as after `cotal service stop stack-manager`, `input` refuses
`TARGET_UNAVAILABLE` (exit 2) before it sends anything and names bare `cotal up`, as `ps` does.
`stop` and `attach` find their seat the same way and refuse the same. A manager that stays
registered but does not answer, such as one that crashed, leaves `input` and `stop` waiting out its
answer deadline. They then refuse `TARGET_UNAVAILABLE` too, since nothing was sent, with
`cotal status` as the next step.


### Input authority

**This one is operator-only, and more narrowly than `stop` or `attach`.** Those two are granted to
anything holding `spawn`, so an agent can stop and attach to seats under its own owner. `input` is
not: it is granted only to operator credentials, which on a user-auth mesh means your ledger row
needs the `admin` scope, the same scope [`ps`](#managed-seats) already needs there. The reason is
that a write into a terminal is control of whatever is running in it, and on a user-auth mesh the
own-owner rule covers every seat under you, not only the ones you launched: a `spawn`-scoped agent
could otherwise type into a sibling it never started. Seat locality is still resolved for you.

Only the `pty` runtime can be typed into. The external terminal runtimes (`tmux`, `cmux`, `orca`,
`herdr`) attach to a process they do not own, so they have no input stream for it and the command
refuses by name rather than dropping the keystroke.


## Terminal input receipts

`input <agent> <text>` writes through the selected manager's native input command.
Use `--text` when the value begins with a dash; do not combine duplicate positional and flag
sources. `--no-enter` omits the trailing Enter request. JSON and plain receipts identify the
answering manager and its reported byte count without repeating the input body. An acknowledgment
is not proof of application consumption or task completion. Unknown results are not retried.
Managed-launch failures retain native request and accepted-goal identifiers when a reply supplied
them, including when following that accepted goal failed. No identifier is invented for a silent
submission, and a missing effect outcome is not a proved pre-execution refusal.

Goal observation keeps the caller's authorized route. A class caller reads its original goal on
that class route; an explicit `--on` or credential-bound instance stays pinned. Learning the
accepting instance ID does not confer an instance route or cause another launch. The native
follower verifies the returned fact against the original caller and goal. A launch the manager
refuses before its first effect, such as a missing persona file or connector, carries
`not-executed`. The CLI reports it `refused` with exit 2 and prints the manager's reason. A missing
persona names the manager, its host and the file it looked for, which is created on that host. A
missing connector is `DEPENDENCY_UNAVAILABLE`, as it is for a spawn in this terminal, and the
refusal names the manager's instance and the host its status reports. An
unpinned launch that no manager received is refused with `TARGET_UNAVAILABLE` and exit 2. With no
manager running, the CLI says so before it sends and names bare `cotal up`. When registered managers do not answer the launch describe, the launch
was never sent and is refused the same way. The reason follows `ps` when this folder records
its manager stopped, starting or running but silent. When every live manager
reports `classSpawn: false`, the refusal is `DEPENDENCY_UNAVAILABLE`: it names each manager and its
host, says none has an agent application installed, and names the `cotal ext add` that adds one.
A registered manager whose rail the broker reports empty is skipped without waiting and named
in a `MANAGER_SILENT` warning; the reason comes from the managers that answered.
Both refusals say to add the application where the manager runs, not on your machine. A manager
reads its applications when it starts, so restart it after adding one. A failure after the manager reserved
the name or wrote a launch file keeps its native outcome, and the CLI never reclassifies it from
its message.


## Stopping an agent

`stop <agent>` requests graceful native despawn. `--force` requests the existing
`graceful:false` contract; it does not grant authority or bypass incarnation checks.
`stop --self` uses only a verified managed seat caller and its attributed manager. It accepts no
other target or credential. Missing, mismatched and unsupported user-auth seat context refuses
without switching to the operator. JSON and plain modes show the acknowledged native result,
not a separate measurement of process exit. Stop and input do not seed connector installations.

Failed name resolution refuses before a stop, terminal input or first attach is submitted. The
receipt retains the inspect error and responding manager. It records local pre-submission proof
without inventing a native effect outcome.

When the manager refuses a stop, terminal input or first attach before anything ran, for example
because the agent is not running or the caller may not control it, its reply carries
`not-executed`. The CLI reports `refused` with exit 2, names the agent and prints the manager's
reason on one line. A failure after an effect may have started, such as a terminal write the
runtime rejected, stays `unknown` with exit 3, still shows the manager's reason, and is not retried.


### Managed restart policy

Managed imperative spawn accepts `--restarts <n> --restart-window <ms>` as a pair, using the
existing native `supervise` object. Both values must be positive safe integers. Direct foreground
launch and the legacy manifest form reject these flags instead of ignoring them.
