--- title: CLI shutdown description: Stack and service stops, process identity checks, preservation, cleanup, and reset. kind: Reference audience: operators --- import { Callout } from 'fumadocs-ui/components/callout'; `down` stops a stack or saved owned services. `clean` removes selected retained state. `reset` previews and applies guarded cleanup of one stopped local store or space identity. ## down [#down] Every `down` runs to its end when the reader of its output goes away, as `| head` does. It keeps stopping, writes nothing more, and exits with the stop's own status. `down ` routes one saved owned definition through the same selection, confirmation, reservation, native manager-impact and stop controller as `service stop`. Use `--instance ` when selection needs a full instance pin. `--yes`, `--dry-run`, `--space`, `--json` and `--plain` keep that controller's meanings; structured receipts name `down` while retaining the service-stop result and actual identity. Unknown definitions do not fall back to legacy component pidfiles. A controller stack names its members `stack-broker`, `stack-auth`, `stack-delivery` and `stack-manager`. A preset name reaches the member built on that preset, as in [`service`](cli-services.mdx#service), so `cotal down manager` stops `stack-manager` and `cotal down delivery` stops `stack-delivery` through the service stop, and the receipt's target names the member and the name you gave as `requested`. Several names in one `down` resolve each the same way, so `cotal down manager delivery` stops `stack-manager` and `stack-delivery`, and each member of the target carries its own `requested`. A name that is no preset, such as `nats`, refuses `TARGET_NOT_FOUND` there and names the `stack-` member to use, and a name with no member names `cotal service`, which lists the saved definitions. Among several names such a name refuses the whole set with the same next step, so no service stops. A set never stops the broker, so `nats`, `broker` and `stack-broker` each refuse the whole set `TARGET_NOT_FOUND` before any member resolves, with the same message for each name. The next step is a bare `cotal down`, which stops the broker with the whole stack. A saved service of your own named `nats` or `broker` is that service. A set that names another saved service running a broker refuses `UNSUPPORTED` and names that service's own stop. A stop of `stack-broker` refuses `LIFECYCLE_BLOCKED` while a legacy daemon record under the root may front a process, or another service instance of the space is live or unresolved. For those instances it names bare `cotal down`, or `cotal service stop --instance --space --yes` for each one. When one of them is not a member of the stack, such as one added with `cotal service add`, bare `cotal down` refuses too, so the refusal names that service's stop first, then bare `cotal down`, or the stops of the stack's own members and the broker stop again. Its dry run warns with that refusal. Completion offers saved definitions without imports or a network request. No hidden connector repair or shared catalog preparation precedes this route. Multiple names resolve as one selected-space set. All owned targets and process roles are checked before effects. After confirmation, the controller reserves every definition in stable order and rechecks all records and native impact before the first stop. Stops follow the displayed input order, with each native guard rechecking at effect. A later failure stops the sequence; one receipt retains completed, failed/unknown and unattempted members. Reservation/preflight refusals also retain the full selected set. Release is attempted for every acquired definition hold; a reported release error makes an otherwise known outcome partial without erasing known stop results. An already 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 queries. Duplicate names and `--instance` with multiple names are usage errors. Named `--with-agents` refuses and points to `cotal service stop --instance --with-agents`; it is never dropped. ### Stack preview Bare `down --dry-run` reads local process records for one registered space at the current project root. A root with no `.cotal/` directory, or with no stack registered at it, refuses `TARGET_NOT_FOUND`, exit 2, because there is no stack there to preview. Its next step is the folder where `cotal up` started a stack, or, when this machine has no stack started or registered at all, `cotal up` in a project, as `cotal status` says. A bare `down` in such a folder refuses the same way. A root whose stack is already stopped shows the no-op described below instead. In the machine home, a stack an earlier release started there is read the same way, and the Next names `cotal down --preserve-state`, its stop, or says that none of it is running. Add `--space ` to confirm that exact space belongs to this root; `--json` and `--plain` produce a partial, finite inspection receipt. It lists pidfile liveness, readable additive deployment IDs, retained additive attempts and unreadable paths under this root, and saved service definitions, instance IDs, recorded states and hosts, supervisor receipts, and retained install, uninstall and unit-action attempts in the selected space's workstation store. Saved records are not attributed to this project root, and their recorded states do not prove live process or supervisor status. A submitted native attempt may have changed state; the preview neither reconciles nor retries it. For each saved service instance it also shows whether it blocks bare `down`: one that still runs or cannot be verified blocks it, with the reason, and one already stopped is skipped. When one blocks, the preview warns with the refusal the real stop would give. That check observes processes on this host and changes nothing. Other local registrations with the exact recorded broker address appear as possible sharing. No match does not exclude address aliases, remote tenants or live attachments. The preview does not establish process ownership, the complete service set, exclusive broker use or agent impact. `--with-agents` only marks the requested policy. It does not reserve or signal a process, and the receipt is not an authorized stop plan. A fresh `team show` summary points to this read-only command from the recorded root. On a root with a controller journal, bare `down` reads only its recorded full instance IDs. On an interactive terminal it shows the member set and the live manager's native agent impact and asks to confirm, then stops manager, delivery and broker in that order. `--yes` acknowledges that scope in scripts and prints no plan. The receipt names the mesh once, lists each member with its result in stop order, and names `cotal up` as the next step. `--with-agents` requests the manager's native retirement policy. Another live service instance in the space refuses `LIFECYCLE_BLOCKED` before the first stop; the Next names the `cotal svc stop --instance --space --yes` command for each one. So does a process record under the root that may front a process, such as a daemon an earlier release started; the message and the Next name each record's path and pid. A record whose process is gone, or whose pid now runs another start than its identity pin records because the pid was reused, fronts nothing. It refuses neither bare `down` nor a stop of `stack-broker`, `down --dry-run` names it, and a complete bare `down` removes it with its pin and names it in a `STALE_PROCESS_RECORD` warning. The process that holds a reused pid is never signalled. The broker scope is checked again after its dependants stop. A failure stops the sequence and preserves completed and unattempted members. A member stopped by signal that has not exited 15 seconds after SIGTERM gets SIGKILL. Its row then reads `stopped with SIGKILL`, and a `STOP_ESCALATED` warning names its pid. Only a complete confirmed stop removes the delivery credential and local registration. After the delivery daemon stops, `down` reads its lease. When the daemon was already gone, or the lease still names the daemon that credential gave, as after a kill or a release a full store refused, the credential stays, and the next `up` uses it to recognise the row and wait it out before it mints a new one. When the lease cannot be read, the credential stays too, and a `DELIVERY_LEASE_UNREAD` warning says so. The JSON `cleanup` field then reads `registration-removed-delivery-credential-kept`. Saved definitions and the journal remain for the next `up`, and the space stays prepared at its root. A selection that names it stays, so the next command run without `--space` refuses with `TARGET_UNAVAILABLE`, exit 2, and names the root to start it in, rather than reach another space. No descendant, lease or authority cleanup is implied. A dry run reads records and, with a live manager, how much the space's store holds. It takes no lock, does not establish native impact and exits 0; its JSON marks `completeness` as `partial`. It lists members in stop order, says when the stop would first settle an earlier operation, and names a scope check the real stop would refuse as a warning. A member whose recorded process is already gone is recorded as stopped without loading its adapter or credential, so a second `down --yes` stops nothing and is a no-op. Before it stops a live manager, `down` reads the workflow runs it hosts and records the unfinished ones, with the steps they wait on, in the stack journal, where `reset store` lists them. A read that fails is recorded with its reason and does not stop the `down`; the JSON receipt carries the record as `operation.runs`. When the manager is live but the broker is not, `down` cannot read the manager's agent impact and refuses before any stop, from a managed seat's shell too. Run `cotal up --space ` to restart the broker from its recorded inputs while the manager keeps running, then run `cotal down`. On Linux, a broker process stopped by a signal, such as SIGSTOP, is read from its process state, and `down` refuses `TARGET_UNAVAILABLE`, exit 2, at once, naming its pid, and stops nothing. When the broker's process runs but does not answer, the manager read waits out its bound and `down` refuses the same way. The broker stops last and is never force killed, so `down` does not signal it. Resume a suspended broker with `kill -CONT ` and run `cotal down` again; end one that stays unresponsive, then take the route above. The manager's stop rides a control credential whose issuance record the broker stores first, and a store over its cap refuses that write. With a live manager, `down` then refuses `DEPENDENCY_UNAVAILABLE`, exit 2, before any stop, naming the store and the `--max-file-store` cap this machine recorded. Make room with `cotal history clear --yes`, then run `cotal down`. That purges all of the space's channel history. To keep it, end the manager on the stack's host with `kill -TERM `, using the pid the refusal names. A plain SIGTERM stops every agent that manager runs by its default policy, as [supervise](cli-processes.mdx#supervise) describes, and it needs no credential. Then `cotal down` stops the other members, and `cotal up --max-file-store ` with a larger cap starts the stack again with its history. The signal reaches only a process on that host. `cotal service stop stack-manager` and `restart` refuse the same way before submitting a stop, and name `cotal history clear --space --yes` and then the same command again. A stop also names `kill -TERM ` as the way to keep that history. A dry run names that refusal as a warning when the space holds more than the recorded cap. nats-server disables JetStream when its store refuses a write for permission or for space, and only a restart brings it back. While the broker's log on this machine shows that, the manager's stop cannot record its credential either, so `down`, `service stop stack-manager` and `restart` refuse `DEPENDENCY_UNAVAILABLE`, exit 2, before any stop, and name the logged failure. `up` refuses the same way, because it cannot check the space's streams on that broker. A dry run names the refusal as a warning. Each names the way out: end the manager on the stack's host with `kill -TERM `, which stops every agent it runs by its default policy, then run `cotal down`, fix the logged cause, and run `cotal up`. Fixing the cause alone does not bring JetStream back. Controller roots refuse preservation and manifest flags rather than switching to the legacy launcher. Outside a terminal, a bare `down` that would stop a running member refuses without `--yes`, exit 1, and prints that line and its remedy without the command help. A stop records each step in the stack journal in the root's `.cotal`. When this account cannot write that folder, `down` refuses `PERMISSION_DENIED`, exit 2, before the plan, names the folder and the `chmod u+w` that fixes it, and stops nothing. Each member's stop is also recorded in the space's service instance records, under `services/` in the Cotal home, and a folder there this account cannot write refuses the same way. A dry run names either as a refusal the real stop would hit. A controller stack's members are stopped through the registration `cotal up` wrote for it. When an older `meshes rm --force` dropped it, or an older `meshes add --force` replaced it with one added by hand at the stack's own root and broker, `down` rebuilds it from the stack journal for the stop and says so with a `REGISTRATION_REBUILT` warning, as `--dry-run` does. It does that only while the stack's broker is the launch the journal records at that root and address, and otherwise refuses `CONFLICT`, exit 2, before any member stops. A registration added by hand at another address or folder is not replaced: `down` refuses `CONFLICT` and names `cotal space forget `, after which `down` stops the stack through its journal. While another registered space names this stack's exact broker address, bare `down` refuses `LIFECYCLE_BLOCKED`, exit 2, and stops nothing, because one listener answers for both registrations. It names each of those spaces with the `cotal space show ` that reads one, the `cotal space forget ` that takes it off this address once its own stack is stopped, and the `cotal down` to run again after that. A user-auth stack's registration is not rebuilt, so it refuses `UNSUPPORTED`. A stop that refuses or is cancelled before any member stops puts the registration back as it was. A bare `down` stops only the stack of the folder it runs in. In a folder whose root holds no stack, when the space named by `--space`, or else the selected space, is a stack this machine started in another folder, `down` refuses with `TARGET_NOT_FOUND`, exit 2, and stops nothing. A folder whose own `.cotal` holds no stack, such as one a failed start left, answers for itself unless `--space` names another folder's stack. In a folder with its own controller stack, a `--space` that names another space refuses with `CONFLICT`, exit 2, and names this folder's stack. Each refusal names the space and, when this machine started it elsewhere, its root, and its next step runs the same `down` from that root. `--json` and `--plain` carry that refusal as structured output, because the folder has no stack for a bare stop to print its own lines about. When the folder of a running controller stack is deleted, its stack journal and broker configuration go with it, so no member's own stop can run. A running broker writes `.cotal/nats` back into the folder, which does not bring them back. `down --space ` from any folder stops that stack from the instance records this machine keeps, and so does a bare `down` in the folder the broker wrote back, or in a folder with no `.cotal` of its own while the space is selected. Each running member gets SIGTERM at its recorded process, manager first and broker last, after `down` checks that the process is still the one its record pinned, and its record is marked stopped. The manager stops its agents by its default policy, so `--with-agents` and the other stop flags are refused. A member that does not exit within its window, 90 seconds for the manager and 30 for the others, ends the stop with `OPERATION_UNKNOWN` and later members are not stopped. Once every member is stopped, `down` removes the stack's saved definitions, their instance records and the registration that names the folder, so the space can be created and started in another folder. The same stop runs for a folder moved or renamed while its stack ran, which took its journal, store and trust with it. Its Next names `cotal space create --adopt --root ` first, which adopts those records where the folder is now and keeps its history, then `cotal space create ` for another folder. Outside a terminal it needs `--yes`, and `--dry-run` lists each member with its pid and process. `cotal status` marks such a project folder as gone, with `selected.rootGone` set to `true` in its JSON, and its Next names `cotal down --space --yes`. From another folder, its connection state is `root-gone`, rather than an authentication failure. `cotal space show ` also names the moved or deleted root and the same stop, while its broker row still reports reachability. Both recognize the missing stack records even when a running broker has recreated directories at the old path. Inside the moved folder, status keeps the specific move-back remedy. A command that connects to a static-auth space, such as `send` or `history`, is refused with `DEPENDENCY_UNAVAILABLE`, because the seed went with the folder, and its Next names the same `down`. ### Missing home records If the Cotal home is removed while a controller stack runs, the project's journal still names its members. `status` reports missing member records rather than assuming another home holds them. Run `cotal down --dry-run` in that project to inspect the recovery stop, then `cotal down --yes`. This Linux recovery route requires the journal's launch-time Cotal home, machine, boot, account and each member's process creation pin. A different home, an older journal without these pins, partial record loss, an unresolved replacement or a maintenance operation refuses before any member stops. Restore the original records in those cases. Recovery sends SIGTERM to each exact pinned member, manager first and broker last, and records each confirmed exit in the project journal. It does not use a bare PID, recreate service records or claim native manager-impact inspection. The manager uses its default agent shutdown policy. `--with-agents` is refused. The stop waits up to 90 seconds for the manager and 30 seconds for each other member. An unconfirmed exit returns `OPERATION_UNKNOWN` and leaves later members running. Run the stop again after that member exits. A reused PID is never signalled. The store and trust remain. After recovery, run `cotal space create ` in the project, then `cotal up`. If the journal already records a completed stop and every pinned member is gone, repeated `down` returns a no-op without confirmation, signals or journal writes. Retiring a stopped instance also retires its process pin; `up --no-manager` preserves the pin of an earlier stopped manager. A planned member gains its new pin only after readiness succeeds. If that start is interrupted, missing-record recovery refuses rather than treating the earlier process's exit as the new one's. An unconfirmed first launch also refuses when its home records are missing, even if no instance ID reached the project journal. Missing launch evidence does not mean that nothing started. ### Already-stopped stacks A bare `down` in a folder whose stack is already stopped stops nothing. It reports that the stack in that folder is not running and names `cotal up`, outcome `no-op`, exit 0, and writes nothing, so it needs no `--yes`. `--dry-run`, `--json` and `--plain` show the same. A controller stack counts as stopped when its last whole-stack operation was a completed `down` and no member and no other service instance of its space runs. That check reads no process record under the root; those are read only as described below. A stack that stopped any other way takes the full stop, which also finishes its cleanup. A legacy root counts as stopped when no registration names it, or only the one a space named after its folder keeps, and no `*.pid` record remains at the top of its `.cotal`, and the root still holds the broker configuration a start rendered there or a space prepared there. Without that evidence a legacy root still refuses, with one next step: `TARGET_NOT_FOUND` when nothing shows a stack there, and `CONFLICT` when several registrations name the root or a process record remains that no registration explains. Before either kind of root reports the no-op, `down` reads every record a local-process descriptor names under the root, an installed extension's included, at any path and name under `.cotal`. A descriptor whose record name holds a space is read for every space its files under the root name, as well as the stack's own. A record whose process runs, whose liveness the kernel does not answer, or that cannot be read or attributed refuses `LIFECYCLE_BLOCKED`, and so does a file whose space part decodes to no space. Nothing outside the root's `.cotal` is read: a link anywhere on a record's path refuses at the link, unread, as does a record that is not a regular file, is larger than a process record, cannot be opened, or changes while it is read. Each is named once, at the path where reading stopped. A `.cotal` that is itself a link never gives the no-op: on a stopped controller stack `down` refuses at `.cotal`, and on a legacy root it refuses `CONFLICT` before any record is read. A template whose `{space}` normalizes away names one path for every space and is read as that path. The refusal lists each record by name, path under the root and pid, and its next step names the pids and paths to check. It stops, adopts and removes nothing. `--with-agents` on a stopped stack refuses `LIFECYCLE_BLOCKED`, because no running manager can stop the agents; start the stack first. Its preview, `cotal down --dry-run --with-agents`, shows the no-op and warns with that refusal. When the stack's last completed `down` took `--with-agents`, or the root never started a stack, no agent of it can remain, and `--with-agents` reports the same no-op. ### Legacy stacks The legacy bare-stack and manifest apply paths described below are retained. They do not accept the new named-service JSON/plain apply receipt flags, except that `--json` and `--plain` show the no-op of a stack that is already stopped. A retained bare stop shows its recorded components and asks for confirmation on an interactive terminal, defaulting to No. Without an interactive terminal, or when prompts are disabled, it requires `--yes`, exit 1, before any stop or cleanup. This applies to a folder started with plain `cotal up` too. `--dry-run` stays read-only and needs no confirmation. This acknowledgement does not establish process ownership or native agent impact. After confirmation, the stop checks the root, registration, lifecycle blockers, broker sharing and process-record containment again. A changed target or process record refuses before any stop and asks you to review the current stack. Before a retained bare apply reaches its pidfile sweep, it now requires one locally started registration under a real isolated project root and refuses pending maintenance transitions, any valid or unreadable additive ledger or attempt under that root, or any other local registration at the exact recorded broker address. The additive ledger refusal names each ledger with the command that finishes it: `cotal down --run ` for a readable one, and for an unreadable one the reason it could not be read and the same command once it is repaired. It reads each record its sweep stops as the stopped-stack check above does. A record that check leaves unread, such as one behind a link, refuses `LIFECYCLE_BLOCKED` before the first stop and is named once, where reading stopped. No process behind it is read or signalled, and no record is removed. When a manager or delivery daemon has no pidfile but its lease row is still there, `down` prints ` has no pidfile; waiting up to s to see whether its lease is renewed` and watches the row until the lease TTL ends. A row that is written again refuses `LIFECYCLE_BLOCKED`, exit 2, before the broker stops. The refusal names that daemon and says to stop it, or put its pidfile back, then run `cotal down` again. A row that expires, or that nothing writes until the TTL and a five-second grace have passed, was left by a daemon that exited without releasing it, such as one whose broker stalled past its broker-gone window, and `down` goes on. A delivery daemon gets two seconds to release shard 0 of its lease after the signal and then exits anyway. A signal that lands while its connection redials first waits up to five seconds from the drop for the redial. So after it stops, the bare stop reads the lease while the broker still runs. When the row still names the credential this folder gave that daemon, or the lease cannot be read, the credential stays and `down` prints a `!` line saying so. The next `up` uses it to wait the row out, as after a crash, and then starts the new daemon. These are known-blocker checks, not a complete live broker-sharing or stop plan; aliases, remote tenants, agent impact and process identity remain unproved. These paths do not check full whole-stack target ownership, set exclusion, confirmed agent impact or schema reconciliation. The retained `down --preserve-state` route separately refuses a retained or unreadable additive record under its maintenance lock, without adopting its effects into the preserved set. A validated completed restore in `active` for the registered space does not block ordinary bare `down`. The other root, ownership, sharing and native process guards still apply, and stopping leaves the restore journal and fallback store unchanged. The machine home is not an isolated project root, so bare `down` refuses the home stack. Stop it with `cotal down --preserve-state`, which keeps its state for the next `cotal up`. The refusal names that command. `--preserve-state` takes its agent inventory from the manager that serves the space. When no manager holds the space's lease, as on a stack started with `--no-manager`, it refuses `TARGET_UNAVAILABLE`, exit 2, before it stops anything or writes a maintenance record. Start the manager with `cotal up`, then run `cotal down --preserve-state` again. The cut stops the delivery daemon before it takes the manager's inventory, so no durable join or leave can race it. A refusal after that point (the manager refuses to prepare, an endpoint the manager does not manage is live, or a seat cannot be checkpointed) starts the daemon again and leaves the mesh running and unfenced; a live unmanaged endpoint refuses `LIFECYCLE_BLOCKED`, exit 2, and names the endpoints. When the daemon cannot be started again, the refusal says so and names `cotal up`. An open mesh runs no delivery daemon, so a refusal there starts none, also when the cut stopped one that an earlier `up` left running. It also refuses `LIFECYCLE_BLOCKED`, exit 2, before it stops anything, while a saved service in the space still runs or cannot be verified, because the service depends on the broker it would stop. The refusal names each one and the `cotal service stop --instance --space --yes` that stops it. Bare `down` refuses the same way. The preserved state records its store by absolute path. If the project folder is moved after `down --preserve-state`, `up`, `backup create` and `backup restore` refuse `CONFLICT`, exit 2, name the folder it was moved from, and say to move it back there first. A store missing from its recorded path is refused `TARGET_NOT_FOUND` with that path; only `backup restore --accept-missing-source` restores over a lost store. After those checks, a retained bare apply refuses `LIFECYCLE_BLOCKED`, exit 2, before its pidfile sweep while a service instance in the selected space still runs or cannot be verified, because the service depends on the broker the sweep stops. It stops nothing. As on a controller stack, bare `down` does not stop a service itself: the refusal names each instance with the `cotal service stop --instance --space --yes` that stops it, then `cotal down` again. An instance whose process is already gone is skipped, and saved definitions stay. An unreadable service record keeps the code of its read refusal. A retained native attempt, an unsettled request, a process that cannot be observed, or a record from another host, which bare `down` does not touch, refuses too, and the remedy names the command that inspects or stops it. ```bash cotal down cotal down --with-agents cotal down --abandon-stop --yes cotal down --abandon-launch --yes cotal down --preserve-state [--store-dir ] [--session-store …] cotal down --dry-run [--space ] [--json|--plain] cotal down [ …] [--instance ] [--space ] [--yes|--dry-run] [--json|--plain] cotal down -f | --run [--dry-run] ``` | Flag | Default | Meaning | |---|---|---| | `--file `, `-f` | none | Tear down this manifest's deploy | | `--run ` | none | Tear down one additive ledger, written by the retired `spawn -f` deploy, by its run id | | `--space ` | 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 | | `--dry-run` | off | Print the manifest teardown, the named service stop or the bare stack preview, mutate nothing. `down -f --dry-run` previews this root's recorded stack when the manifest can be read and no additive ledger exists. An explicit `--run` stays on the ledger lookup, and a ledger teardown has no `--json` or `--plain` output | | `--with-agents` | off | Bare whole stack only: also stop and deprovision every managed agent | | `--abandon-stop ` | none | Controller stack, with `--yes`: abandon a member's stop that no receipt settles, then stop the stack. See the settlement rules under [up](cli-startup.mdx#up) | | `--abandon-launch ` | none | Controller stack, with `--yes`: record failed a member launch whose record names no process, then stop the stack. See the settlement rules under [up](cli-startup.mdx#up) | | `--preserve-state` | off | Bare whole stack only: fence the manager, retain principals and durable state, stop and prove the stack down, then publish `ready` | | `--store-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 | | `--session-store ` | 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 | A legacy ledger preview selects the saved ledger by the manifest's source hash. It does not reload launch-time persona files, so a missing persona does not prevent that preview. When this root records a stack without an additive ledger, file-form apply refuses `UNSUPPORTED` and names bare `cotal down` from that root. It does not require a run ID or stop the stack implicitly. For an additive ledger from the retired `spawn -f`, `down -f` keeps an authenticated channel when membership cannot be read. Its output includes the read failure, rather than calling every failure an absent feed. A user-auth teardown uses the signed-in user's control authority and an admin read view, plus the channel-writer view for deletion. Run it from the registered checkout as the owner who deployed the agents, with the admin scope needed to inspect membership and remove channels. It does not use the local static signer to stand in for that user's identity. If the issuer predates the membership snapshot read, upgrade it and obtain a new credential before retrying the retained run. See [membership snapshot reads](UPGRADING.md#membership-snapshot-reads-after-01154). ### Agent policy Bare `cotal down` stops the whole local stack in dependency order and leaves managed agents running when their runtime lets them outlive the manager. Before signalling the manager it verifies the spare capability of the exact recorded manager, which records what that manager's stop does with its seats, and it reports the agents left behind plus `cotal down --with-agents` as the explicit reap. When the manager had no managed agents, it prints no report. When they cannot be listed, for example on a user-auth mesh with no login on this machine, it prints `could not list managed agents ()` and the stop continues. When listing is denied, the warning says access was denied and shutdown checks continue without an agent inventory. It points to `cotal ps` with an authorized account before stopping. On a per-user auth mesh, listing all agents requires `admin`; a `--full` actor grant alone does not include it. The warning does not claim that any agent has stopped. Other shutdown safety checks still apply. The built-in pty runtime keeps each PTY inside the manager process, so those seats cannot outlive it: every manager stop stops and deprovisions them, and `down` reports them as stopped. A manager retires at most 8 seats and chat sessions at a time, and the rest wait their turn. Every manager stop the CLI makes runs this one path: `down`, Ctrl-C on a foreground `cotal up`, the teardown after that `up`'s broker exits, the leftover-manager stop before `cotal up -f`, and the delivery cutover. Each holds the manager's stop reservation, so a second stop while one is in flight is refused, names the process holding it, and leaves that stop's `--with-agents` policy in place. On Windows, manager and delivery stops send an authenticated local cleanup request because Windows process signals terminate without running cleanup handlers. The request requires the process's private capability, its creation identity and the live stopper's identity and reservation. Other platforms use `SIGTERM`. A `cotal supervise` that receives `SIGTERM` or `SIGINT` while it is still starting lets the start settle, then runs the same stop and exits without printing `manager up`, so its liveness lease is released and an immediate restart is not refused. When that start fails, the stop still releases the lease before the process prints the start error and exits 1. A stop that cannot reach the broker, for example while the connection is redialling, keeps trying to release the lease for up to 5s. If the broker still does not answer, the manager prints a warning and leaves the lease to lapse on its TTL, so a same-root restart before then waits for it. A process still running after 20s on the Windows cooperative path, or 15s after `SIGTERM`, receives `SIGKILL`. Ctrl-C stops the manager first; when that stop is refused or the manager's exit cannot be confirmed, Ctrl-C signals nothing else, prints the refusal with the reap route, and leaves the stack running; end it with `cotal down --with-agents`. `--with-agents` is a one-shot destructive policy bound to the exact verified manager process and the exact live `down` stop reservation; a stale, malformed, crashed, or different stop attempt cannot turn a later bare shutdown destructive. If a managed agent cannot be proven stopped within the manager's stop timeout, the manager logs which one, still closes its broker connections and console listener, and exits with code 1. It does not release its pidfile, liveness lease or service registration in that case, so no successor is handed authority while that agent may still run; the lease lapses on its TTL. A named `down` is the service stop above and never stops a legacy component by its pidfile, so `cotal down manager` or `cotal down web` on a legacy stack is refused unless a saved definition has that name. Only bare `cotal down` stops a legacy root's components, all together, under the folder you run it in. It takes no `--space`. Its stop lines follow a `Mesh: ` heading, and its Next names `cotal up` to start the stack again. A component pidfile that holds content that is not a pid names no process, so `down` says so, keeps the file, does not stop the broker, and keeps the artifacts and the registration. Its Next first asks you to confirm that no such component still runs for the folder, because cotal cannot check a record that names no process and a manager that does not answer on the mesh may still be running. It then names `rm ` and the same `cotal down`. A bare `down` or `down --dry-run` whose `--space` names a space whose stack is recorded in another folder refuses `TARGET_NOT_FOUND`, exit 2, naming that folder and the stop or preview to run there. The `-f` / `--run` forms tear down a [manifest deploy](cli-startup.mdx#manifest-deploys) without stopping the whole mesh and cannot be combined with service names or `--space`. A pinned manager with no spare-capability record is not signalled by bare `cotal down`. A current manager always publishes the record, so a missing one means an older manager: one that predates capability reporting, or one whose pty runtime reported that it cannot detach its agents. Stop each managed agent explicitly, then run `cotal down --with-agents` from the mesh root to stop the whole stack. An older manager does not understand the reap request, which is why the agents must already be stopped. After the known-blocker admission checks above, the retained bare `cotal down` inventories by pidfile. When this folder's registered broker answers and no `nats.pid` records it, the command does not say nothing is running. It names the space and the broker address, says no pidfile records that process, says it will not stop a process it did not start, and exits 1. Stop that broker with whatever started it (an init unit, a container, or the hand-run process). `cotal meshes rm ` only drops the registration. The probe runs whether or not other owned components were running: they stop and clear their artifacts first, then the broker is named. A `--dry-run` does not run this probe. `down` reads each process record once. A component that exits and removes its own record while `down` runs counts as having no record, so the stop goes on. Any other failed read is an error. An extension-removal reservation in a component's pidfile refuses the stop: it reports the removal in progress while the owner is live, calls the reservation stale only when the owner is proven gone or is not a pid, and otherwise refuses without advising deletion because the owner's liveness could not be confirmed. Whole-stack shutdown keeps delivery credentials until every required component has stopped. A refused stop preserves both the space-scoped credential and its legacy copy. Credential removal errors remain failures. `down` removes the manager's delivery-aware marker, shutdown intent and spare capability only after every component has stopped, so a refused stop keeps them too. ### Process identity **Teardown verifies pinned process identity before signalling.** PIDs are recycled by every OS, so a recorded pid alone is not a durable target identity. `up` and the dashboard's native child record each process's creation identity in a sibling `.identity` pin, which holds the pid and the process start reported by the OS. Every stop path, including `down` for the broker, web and extension components, and the manager, delivery and auth-service stops, applies the same rule. A pin that names a different start means the pid was reused, so teardown refuses and preserves it. A controller stack's bare `down` signals no such record, so it removes it, as described above. A torn or unreadable pin also refuses. On macOS, the reader calls `sysctl` through a bundled Node-API binding for x64 or arm64 and records the process start in microseconds. A macOS pin that an earlier release wrote holds the `ps -o lstart` start date instead and is verified with that same reader. That date keeps its spaces, including the padded day, in both the identity pin and the one-shot shutdown intent. A single record cannot span lines. On Windows, the reader calls `GetProcessTimes` through a bundled Node-API binding for x64 or arm64. It requests limited process-query access and preserves the creation FILETIME as a decimal string. It launches no subprocess. Access denial leaves identity unverified; an unreadable pin still refuses teardown. On either platform, a missing or damaged binding is an installation error and has no fallback. The pidfile and its pin are published by renames, and the pidfile rename is the commit point. Just before it, the pin holds two lines: the old process's and the new one's. A launcher that dies mid-publish therefore leaves the old record or the new one, each checked against its own pin line, never a pidfile without its pin. An old record with no pin is legacy, so its line holds `-` in place of the token and it stays legacy until the commit. A CLI older than this change reads a two-line pin as torn and refuses. Publishes of one pidfile are serialized by a lock file beside it, `.publish.lock`, because the launcher and the daemon it starts both publish the same record. The next publisher reclaims a lock left by a crashed one. When no start token can be read for the new process, its pin line holds `-` in place of the token, which reads as a legacy record, and the publish ends in the legacy shape: a pidfile with no pin. Teardown, and a daemon removing its own record on exit, take the same lock and remove the record only while the pidfile still names the pid they stopped, so a stop that races a publish leaves the new record whole. A directory at the pin path is refused by name and the record is kept, because no publish writes one and removing it could delete files Cotal did not write. The web dashboard service claims `web.pid` with an exclusive create, so a second dashboard for the same mesh is refused, and writes its pin right after the claim. A stop that runs between the two reads a legacy record. Any existing `web.pid` refuses the start, whether or not its process still runs, and the start neither adopts nor reclaims it. The pidfile pid and the pin pid are two coordinates. Automatic cleanup follows **proven death of the pidfile target** (ESRCH on that pid): a torn or unreadable sibling pin does not wedge a dead pidfile pid. A torn pairing where the pin names another pid, while the pidfile pid is still live or not proven dead, still refuses. Inspect both pids with `ps`. Do not delete `.identity` to force a stop; that weakens target-identity protection. Once the pidfile process is dead, rerunning teardown clears the stale record automatically. A Windows manager or delivery daemon without the cooperative capability must be stopped through its original supervisor before restarting with this version. Teardown refuses it without signalling or deleting its live record. It does not wait for a lease to expire or delete a lease by local pid. For components using signals, the first teardown after upgrading a running pre-pin stack has a narrower guarantee. A live record with no identity pin is signalled after a loud warning that it predates identity pinning. Restarting the component writes the pin, so later teardowns receive full match and mismatch protection. The same warning applies on platforms where no stable start token is available. For a legacy manager, bare `cotal down` also warns that agent sparing cannot be verified before it signals. Because the CLI cannot establish which SIGTERM handler that already-running binary carries, it never presents the pre-signal seat inventory as confirmed spared; a genuinely older destructive handler may still reap those agents. `--with-agents` publishes a one-shot reduced-guarantee handoff bound to the recorded manager pid and the live `.stopping` reservation's inode, then signals unconditionally. That handoff cannot be replayed by a later stop attempt. A pin that exists and does not match the live process still refuses before signal. `--with-agents` performs the old destructive logical teardown: managed processes stop and their credentials, ACL rows, and delivery footprints are deprovisioned. `--preserve-state` is a different maintenance transition: it stops retained processes while suppressing leave/deprovision cleanup, persists the manager's same-principal resume inventory, stops the entire stack without removing run/auth artifacts, and publishes a stable inode-bound cut only after every recorded process is proven stopped and the exact recorded NATS endpoint is unreachable. A missing or stale broker pidfile never counts as stopped. The attempt is bound durably before the manager is fenced, the resume document and attempt-bound `cut-intent` are fsynced before manager commit, and the manager's commitment itself is journaled (`cut-committed`) before any process stops. A retry after a crash at any of those boundaries reuses the exact recorded attempt and finishes the remaining stop and endpoint proofs idempotently, without needing the (by then intentionally dead) manager. A partial cut never publishes `ready`. It cannot be combined with component names, manifest teardown, or `--dry-run`. On a mesh that runs the delivery daemon, the cut stops it first so no join or leave can change membership under the manager's inventory. Before any manager-owned endpoint stops, the cut then reads the presence roster and refuses while an endpoint the manager does not own is live. A refusal leaves the delivery daemon stopped. A deleted presence key is the only row it reads as absent. Any other row that is not a card with an `id` equal to the presence key it was read under and a string `name` refuses the cut with `presence record is malformed`. A participant can write any value under its own key, so the key is the identity the cut trusts. ### Seat checkpoints **Seat checkpoints.** After the stack is proven down, the cut writes one checkpoint per retained seat under `.cotal/maintenance/v1/checkpoints///`, and prints the path, the continuity class and the generation for each. The path carries the preservation attempt because a checkpoint is immutable once sealed: a shared directory would make the second cut in a root refuse on the first cut's leftovers, and clearing it would destroy an artifact a rollback still needs. The capture happens only at that point because anything earlier races a harness that is still writing its transcript and its working tree. Each checkpoint directory is created 0700, refuses a destination that already exists, and holds: - `repo.bundle`, the seat `cwd`'s reachable history, anchored on the base commit the record names by full object id; - `repo.index.diff` and `repo.worktree.diff`, the staging state as two diffs, base to index and index to worktree. Two rather than one because a single combined diff restores a mixed tree with the right bytes and the wrong index: a source reporting `MM README` would come back as ` M README`; - `repo.untracked.tar`, the untracked files in scope; - the harness session pointer, when the seat's connector declares one, and the transcript store files the operator named with `--session-store`. Each records where the destination puts it back as an anchor (the workspace root, the account home, or the seat's `cwd`) plus a relative path, because the destination's root and home are its own and the source host's absolute spelling would either miss them or write outside them; - `checkpoint.json`, written last, after every digest is computed over the bytes that landed. The record carries the manager's resume entry unchanged as its first field, then the space, the seat name, the recovered `lifecycleUid`, the writer generation the cut was taken at, `capturedAt`, the recency horizon, the applied profile revision, the seat's `git status --porcelain` as the cut read it, and the continuity class. Every captured file is listed with its byte size and sha256, so an operator verifies the whole artifact with `sha256sum` and `git bundle verify`. No secret values, no operator keys and no source-host launch material enter it. The generation is the highest one this root has claimed for the seat in `.cotal/auth`. A generation entry there that is a symlink, a directory or anything else that is not a regular file refuses the cut, as every other auth record does. The continuity class is what the connector declares, capped by what the checkpoint carries. A connector declaring session continuation classifies as `exact`, but reopening a session takes both halves, the pointer that names it and the store that holds its transcript. A checkpoint missing either one cannot reopen that session, so it is recorded as `fresh` when the connector declares a fresh start and `drain-only` otherwise. A pointer with no store is capped the same way as a cut carrying neither, because it names a session whose bytes the artifact does not contain. A class is a promise the destination is entitled to act on, so it never describes bytes the artifact does not contain. The transcript store stays an operator input: this repository does not know where a harness keeps its transcript, so `exact` requires `--session-store` to name one. The recorded status is read under the same selection rule as the untracked set, so it describes the state the captured bytes can reproduce. The destination re-reads it in the promoted tree and refuses a difference. The untracked selection rule is recorded in the record and is `git ls-files --others --exclude-standard -z, excluding .cotal/`. It honors `.gitignore`, so an ignored file the seat needs does not travel and has to be moved separately. The `.cotal/` exclusion is a secrecy boundary rather than a size one: when a seat's `cwd` is also the mesh root, the control directory is untracked, and without the exclusion the broker trust material, the space account, the manager instance identity's private seed and the seat's own credentials would land inside the artifact. A checkpoint carries credential references only; the destination resolves that material itself. A seat whose launch options could not be resolved is refused rather than checkpointed, with the manager's own wording: `imperative launch options have no non-secret durable source ()`. The refusal arrives at prepare time, so the cut stops before any child does. A delegated seat (SPEC §13.17) is refused at prepare time too, with `a delegated seat is not resumed by a later manager; stop it before preserving`. A manager stop after a refused cut retires that seat through its retirement path. ## clean [#clean] ```bash cotal clean --force cotal clean --dry-run cotal clean restore-attempt --attempt --force cotal clean restore-fallback --attempt --force [--discard-history] ``` | Flag | Default | Meaning | |---|---|---| | `--space ` / `--server ` / `--creds ` | resolved mesh | `history`: target mesh | | `--dms` | off | `history`: also clear DM history | | `--store-dir ` | `.cotal/nats` | `store`/`all`: JetStream store directory | | `--force` | none | Required: destructive, no prompting | | `--dry-run` | off | `store`/`all`: list what would be removed and remove nothing; needs no `--force` | | `--attempt ` | none | `restore-attempt`: exact stale pre-commit attempt; `restore-fallback`: matching healthy committed restore | | `--discard-history` | off | `restore-fallback`: also delete an old store that holds the only copy of the history or workflow runs the restore left out | One configurable cleanup verb; every target requires `--force`, except a `--dry-run` of `store` or `all`. It takes one target and no extra positional arguments. A channel name after `history` refuses `USAGE_INVALID` (exit 1); it does not narrow the purge. The dry run quotes an explicit `--store-dir` value in its printed force command so it stays one shell argument. Only `history` takes `--space`, `--server` or `--creds`. Every other target acts on the root of the folder it runs in, so it refuses them `USAGE_INVALID` (exit 1) before local cleanup inspects or removes root state. - `history` purges the retained message backlog on the **running** broker (channels, plus DMs with `--dms`). The same operation as [`history clear`](cli-messaging.mdx#history), which stays as an alias. - `store` deletes the **stopped** mesh's JetStream store (`.cotal/nats`): streams, durable consumers, and messages. This is the reset for stale on-disk broker state, e.g. durables minted by an older, incompatible Cotal generation surviving a `down`/`up` cycle. It removes the store's `jetstream` directory, its `.cotal-store-id` marker and the broker's `ocsp` folder, then the empty store directory. A store directory that holds anything else refuses `CONFLICT` (exit 2), names those entries and removes nothing; move them out and run it again. - `all` is `store` plus the space identity (`.cotal/auth`), the local creds and markers tied to it, any crash residue a normal `down` would have swept (stale pidfiles, `run/`), and the mesh's registry entry; the next `cotal up` mints a fresh identity. On a root deployed with `up -f`, plain `cotal up` then refuses and names `cotal up -f ` when the deployment ran at another address, mode or space. A full backup is bound to the space trust it was taken under, so full backups taken before `clean all` no longer restore into this root. Only the registry of such a backup still restores, with `backup restore --only registry`, and chat and DM history start empty. Its dry run and its receipt say so when it removes that trust; take a new full backup after the next `cotal up`. On a user-auth space it also removes the space's IdP trust and its actor ledger, so every actor grant is lost. Its dry run and its receipt say so, and a later `up` then needs `cotal up --user-auth --idp ` and each actor granted again with `cotal actor grant`. `store` and `all` move the store aside to a `.cotal-clean-` folder beside it, then remove its entries there, as `reset store` and `reset space` do. A removal that stops partway, such as at a folder in the store this account cannot write, ends `OPERATION_PARTIAL` (exit 4). It names that folder and what is left in it, and its Next makes the folder removable and removes it. Nothing else records the folder, so a later `clean` or `reset` finds no store and reports nothing to remove. An existing store path that is a symbolic link refuses `CONFLICT` (exit 2), naming the link and its target. Replace the link with the directory it points to, then run the command again. The link and its target are left unchanged, including for `--dry-run`. ### Cleanup eligibility `history` needs the mesh up; `store` and `all` refuse while any recorded mesh process may still be running or any same-root recorded broker endpoint remains reachable (run `cotal down` first). On a controller root, a member whose process is gone counts as stopped once a completed `down` recorded it as already stopped, so `down --yes` after a crash is enough. A record counts as stopped only when it is absent, empty, or names a pid the kernel reports gone, so unattributable content, a removal reservation, or a pid whose liveness cannot be read refuses. They also refuse outright on a root that holds accounts for several spaces: the store and the broker trust record are shared by every space on the broker, so both targets would take out all of them and no `--space` can narrow that. `down`, `backup` and `up --restore` refuse there for the same reason. `cotal status` lists the tenants on such a root. Personas (`.cotal/agents`) and logs are never touched. Without `--store-dir`, `store` and `all` read the store the root's last broker served, as a start does: while the default `.cotal/nats` holds no JetStream data and that store does, such as after `backup restore --store-dir`, they remove that store, and the dry run names it. Custom cleanup targets must contain either the Cotal store-generation marker or a real `jetstream/` store directory; filesystem roots, project roots, and Cotal auth/maintenance trees are always refused. A store in the project folder itself, as `up --store-dir .` makes, refuses `CONFLICT` (exit 2), since the folder holds the project's own files; its Next names the store's own entries to remove by hand once the space is stopped. Its contents are not checked, so a directory that holds only a `jetstream/` directory is removed as a store, whatever that directory holds. The one exception is another space's data: when another space uses the store folder, as [up](cli-startup.mdx#up) checks it, `store` and `all` and their dry runs refuse `CONFLICT` (exit 2), name that space, its root and its streams, and remove nothing. Keep the folder while either space needs its history; once neither does, stop both and remove it by hand. In a folder whose `.cotal` is a link, every target but `history`, and both dry runs, refuse before anything reads through it: `CONFLICT`, exit 2, naming the link and where it leads, or `TARGET_NOT_FOUND` for a link that leads nowhere, which is not passed over to the records of a folder above it. When the link leads to this folder's own records, moved away and linked back, the Next names moving them back, as [up](cli-startup.mdx#up) does. In a copy, in a folder moved away from its recorded path, and in the folder a later `up` took its space's name over from, the store may hold that space's only history. There `store` and `all`, and both dry runs, refuse `TARGET_UNAVAILABLE`, exit 2, before the liveness checks, naming the folder the stack runs from and the step the commands that read there name, and remove nothing. `--dry-run` runs the same checks as `store` or `all` and prints `would remove ` for each store, credential, record and registration the command would remove, then removes nothing and takes no maintenance lock. A check that would refuse the command refuses its dry run the same way. Ctrl-C or `SIGTERM` during `store` or `all` releases the maintenance lock, reports what the clean left and exits 130 or 143. The outcome is `failed` when nothing was removed yet, and `unknown`, naming the paths removed, once removal began. `store` and `all` also refuse every maintenance journal state, with `LIFECYCLE_BLOCKED`, exit 2. The refusal names the attempt and, as Next, the step `cotal status` shows for it, such as bare `cotal up` to resume a preserved cut; once the root holds no maintenance state and its mesh is stopped, the clean runs. After a healthy committed restore, `restore-fallback` is the only supported way to remove the recorded unchanged old-store inode; it never deletes the active target, requires both the exact attempt id and `--force`, and retires the completed restore journal so a later `down --preserve-state` can start a new backup cycle. After a restore that left history or workflow runs out it also requires `--discard-history`, as `backup cleanup-fallback` does, and its refusal names the flag without printing the command help. After an `--accept-missing-source` restore there is no old store, so it only retires the journal. An attempt the root's maintenance record does not name refuses with `TARGET_NOT_FOUND`, exit 2, and deletes nothing. A repeat after a completed cleanup is such an attempt, and its refusal says the restore has settled and names `cotal status`. Without `--force`, `restore-attempt` and `restore-fallback` name the command to run with its `--attempt`. `restore-attempt` refuses the same way, with `TARGET_NOT_FOUND`, for an attempt the record does not name, and with `LIFECYCLE_BLOCKED` for a recorded attempt past its pre-commit point, which recovers forward. Without `--attempt` both verbs refuse with `USAGE_INVALID`. Each refusal's Next names the attempt the root records and its step. ## reset [#reset] ```bash cotal reset store [--dry-run] [-y|--yes] [--json|--plain] cotal reset space [--dry-run] [-y|--yes] [--json|--plain] ``` Both routes select a space this machine started. A space with a locally started registration is selected by it. A controller stack that a completed `down` stopped has no registration; its prepared record and stack journal at the same root name its broker address, store and mode, and a reset keeps both, so a later `up` starts it again in the same mode. No reset changes a stack's mode; [up](cli-startup.mdx#up) names the way to run another one. After `reset space` of a user-auth space, that `up` needs `--user-auth --idp `, as described below. A stack none of whose members ever started has no store to reset, so both refuse `TARGET_NOT_FOUND` and name `cotal up`. So does a prepared space no stack has run for, which also names `cotal space forget`. A stack whose last operation is not a completed `down` refuses `LIFECYCLE_BLOCKED` and names `cotal down --yes`. Each refusal names one reason and the command that clears it. A space that may still run names `cotal down` from its root, and lists each process that may run once. A stack member whose process also holds its component's record, such as the manager's pidfile, is listed as that member with the record's pid. A member that exited without a recorded stop, as after a crash, is settled by `cotal down --yes`: it finds the member gone and records it as already stopped, and the reset then accepts it. Only a member that no completed `down` recorded that way needs `cotal up` and then `cotal down --yes`. A legacy root has no stack journal, and its `down` drops the registration a reset selects, so no reset reaches it once stopped. A space named after its folder keeps that registration, so a reset reaches it stopped. For any other legacy root, the step after `cotal down` is `cotal clean store --force` from its root for `reset store`, or `cotal clean all --force` for `reset space`, with `--store-dir ` when the registration records a custom store. Once stopped, such a space has no record here, like a mistyped name, so a reset of either refuses `TARGET_NOT_FOUND`, says no registered or prepared space has that exact name, and names `cotal space` and then that step. Removing the store removes what the broker kept in it: workflow runs and their journals, channel history, stored messages and every other record. A run still waiting for an answer goes with it and cannot be answered or resumed afterwards. Only the broker reads the store, and it is stopped, so `cotal down` reads the stack's runs from its manager before stopping it and records them in the stack journal with the steps they wait on. The preview, the confirmation and the receipt list those runs as of the last down, and `--json` carries them as `storeContents.runsAtLastDown` and `removedRuns`. When that down could not read them, or no down recorded them, the preview says so. To see the runs then, run `cotal up` and `cotal run`, then `cotal down --yes`. ### Store preview The store dry-run is a local-impact preview that exits 0. It requires a space selected that way, a real project `.cotal` and a root readable as one local tenant. It opens with a verdict: the apply is offered, with `cotal reset store --yes` as Next, or it would refuse, with the apply's own reason and remedy. The JSON result carries it as `applyVerdict`, and `completeness` stays `partial`. Completion suggests locally started names, without proving eligibility. The preview inspects the store path with the existing guarded cleanup path checker, reads the maintenance journal, legacy process records, service-controller instances, supervisor receipts, retained native attempts and root-local additive ledgers and attempts plus fresh boot attempts, and makes a credless broker probe. Readable deployment IDs, unsettled attempts and unreadable record paths remain visible; these records do not prove live ownership or a complete broker tenant inventory. An orphan receipt or attempt remains visible without an instance record; these space-scoped records are not proven owned by this root. The preview also lists other local registrations at the exact recorded broker address. A match blocks store apply, while no match does not exclude aliases, remote tenants or live attachments. A negative credless probe cannot prove absence for TLS-first listeners. A custom store path must be absolute and one real directory. A recorded store path that is a link refuses `CONFLICT` in the preview and the apply of both resets, naming the path and the directory it points to, and nothing is removed. Replace the link with that directory to reset it. A selected root whose `.cotal` is a link refuses both resets and their previews `CONFLICT`, exit 2, before anything reads through it, naming the link, and nothing is removed; when the records were moved away and linked back, the Next names moving them back. A store in the project folder itself refuses both resets `CONFLICT` as it refuses `clean store`, naming the store's own entries to remove by hand. The preview takes no maintenance lock and cannot establish exclusive custody or a complete identity deletion set. ### Store reset Bare `reset store ` checks those conditions, displays a scoped terminal confirmation or requires `--yes` in a script, and acquires the maintenance lock. While another command holds it, such as an `up` started at the same time, `reset store` and `reset space` refuse `LIFECYCLE_BLOCKED`, exit 2, name that command's pid and remove nothing. Under the lock it rechecks the exact registration or stopped-stack records, one-tenant root, absent maintenance journal, legacy process records, service instances, supervisor receipts and retained native attempts for that space, additive ledgers and attempts plus unsettled fresh boot attempts under the root, credless broker nonresponse and pinned filesystem identity before calling the existing store-only cleanup. A readable or unreadable additive ledger or attempt blocks reset before confirmation and under the lock. It refuses another local registration at the exact broker address before confirmation and rechecks under the lock. This does not prove exclusivity against aliases or remote tenants. An orphan receipt or attempt blocks reset even when no instance record remains; a submitted native step may have changed state. A TLS-required or TLS-first target refuses because a credless probe cannot prove it is down. The route preserves trust, credentials and registration. A cleanup with an unconfirmed outcome is reported as unknown and is not retried. Credless nonresponse and the recorded process checks are not proof that no unrecorded process exists. A store directory that holds anything besides the store refuses both routes and their dry runs as it refuses `clean store`, naming those entries. A store folder another space uses refuses both routes and their dry runs `CONFLICT` the same way, naming that space, its root and its streams; the store dry-run shows it as its verdict. ### Space identity reset `reset space ` adds local identity cleanup through the existing native clean-all operation. This route currently requires Linux, a space selected as above and a plain-NATS loopback listener. It removes a locally started registration; a stopped controller stack keeps its prepared record and stack journal. On a user-auth space it also removes the IdP trust and the actor ledger, so every actor grant is lost. The preview and the receipt say so on their own line: a later `up` needs `cotal up --user-auth --idp `, and each actor needs `cotal actor grant` again. TLS-first, remote, shared-root and non-Linux targets refuse. It applies the same maintenance, deployment and process-custody guards as store reset. It does not stop a process to become eligible. When the reset removes a static-auth signing account, its preview, confirmation and receipt warn that credentials exported with `auth mint --out` stop working on this space after the next `up`. The warning is also in the JSON envelope as `EXPORTED_CREDENTIALS_INVALIDATED`. Exported files stay where they are. Mint replacements after `up`; no remote credential is revoked by this reset. Its `--dry-run` validates the existing local trust chain and inventories the native identity paths, including provider-declared secret keys, without running deprovisioning hooks. The inventory is bounded to 10,000 paths. Foreign space segments, symlinks, hard-linked files, foreign ownership, group/world-writable paths and providers without bounded cleanup metadata refuse. The refusal names the path and its reason; for a group- or world-writable path it names `chmod go-w `. Values and content fingerprints are not printed. No maintenance lock is taken by the preview. Like the store dry-run, it has a Store contents section when a store is present, naming what removing it removes and the runs the last down recorded, and `--json` carries them as `storeContents`. The apply's confirmation shows the same section. A preview that passes every check opens with a verdict, as the store dry-run does: the apply is offered, with `cotal reset space --yes` as Next. Apply shows the path inventory and defaults to cancellation, or requires `--yes` in a script. Under the maintenance lock it repeats the eligibility checks and compares the reviewed file identities. Native secret deletion is restricted to the planned keys; each filesystem removal checks the remaining reviewed tree. Added paths and changed files do not enlarge the confirmed scope. The native operation deprovisions secrets before raw identity removal and removes trust last. After checking the planned targets absent, it removes only the unchanged eligible local registration and releases its matching selection. A replacement registration is kept and reported as partial. ### Receipt limits The receipt retains acknowledged deletion paths even if later cleanup fails, and lists each path once. Ctrl-C or `SIGTERM` during either apply releases the maintenance lock, reports what the reset left and exits 130 or 143. Before the removal was submitted the outcome is `failed` and the store and identity are as they were. After it, the outcome is `unknown`, `reset space` lists the paths removed so far, and Next is the route's `--dry-run`. Unconfirmed effects are unknown and are not retried. Personas, saved service definitions and stopped-instance records, installed packages, login sessions and remote data remain. Local cleanup does not revoke a remote credential or clear Cloud data. A saved service definition whose `credentialRef` names a credential this space's account issued, such as one `cotal auth mint` wrote, would hand its child a credential the next identity rejects. Reset reads the issuing account from each such file and clears that `credentialRef`, and both the preview and the receipt name the service and the file. A file it cannot read as a credential is left as it is. To give the service a credential again, mint one after `cotal up`, then remove the service and add it again with that `credentialRef`. ### Named services [#named-services] See the [current command contract](cli-shutdown.mdx#down) for this behavior. ### Controller stacks [#controller-stacks] See the [current command contract](cli-shutdown.mdx#down) for this behavior. ### Delivery shutdown [#delivery-shutdown] See the [current command contract](cli-shutdown.mdx#down) for this behavior. ### Preserved cuts [#preserved-cuts] See the [current command contract](cli-shutdown.mdx#down) for this behavior. ### Maintenance attempts [#maintenance-attempts] See the [current command contract](cli-shutdown.mdx#clean) for this behavior.