> ## Documentation Index
> Fetch the complete documentation index at: https://iii.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Compose

> The compose daemon and its worker surface: the compose::* functions, the worker-compose.yaml schema, and the environment every container is started with.

Compose runs a group of workers as one project. The daemon reads a `worker-compose.yaml`, starts
each container in dependency order, waits for the engine to register it, and supervises it
afterwards.

The daemon is itself a worker. It registers under the name `compose` in a namespace of its own and
exposes the `compose::*` functions there, so every project operation is a normal
[trigger](./triggers), addressed to one daemon with `--namespace`.

Bare `iii compose` is the daemon command. It reads `worker-compose.yaml` when the file exists so
the namespace and engine URL can configure the daemon. The `--up` flag also loads the file as a
project and starts its containers. `iii compose logs` is a read-only client for a daemon that is
already running. `iii compose build` is a local action that prepares registry packages without
starting the daemon.

## Build registry packages

```text theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
iii compose build [-f, --file <PATH>]
```

`build` reads and validates the compose file, then downloads every `package://` container into the
same cache used by `compose::up`. The file defaults to `./worker-compose.yaml`. The command does not
connect to an engine, start a worker, or run lifecycle hooks. Local `path://` workers and built-in
engine workers need no registry download and are skipped.

```bash theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
iii compose build --file worker-compose.yaml
iii compose --up --file worker-compose.yaml
```

The cache is shared under `~/.iii/compose/packages`, or under `III_COMPOSE_STATE_DIR` when that
variable is set. A later `compose::up` still resolves package metadata, but it reuses a valid cached
artefact and does not download its bytes again. If the cache is missing, `up` keeps its existing
download fallback.

## The daemon

```text theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
iii compose [OPTIONS]
```

| Option                 | Description                                                                          |
| ---------------------- | ------------------------------------------------------------------------------------ |
| `-n, --namespace <NS>` | Namespace for the daemon and every project it loads. Overrides file namespaces.      |
| `--engine <URL>`       | Existing engine WebSocket address. Overrides the file, `III_URL`, and local default. |

A daemon holds any number of projects, and any number of daemons share one engine. What tells them
apart is the namespace: the worker name is always `compose`, so the engine leases `(namespace,
compose)` to one connection. Two daemons with different namespaces coexist; a second claiming one
that is taken is refused at registration with `DAEMON_ALREADY_SERVING`.

The daemon inherits `namespace:` from `worker-compose.yaml` when `--namespace` is absent. If the
file has no namespace, or if the daemon starts outside a project, it uses `default`. An explicit
CLI namespace also overrides `namespace:` for every project loaded by that daemon. The engine
refuses a second daemon claiming a namespace that is already served.

```text theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
$ iii compose --engine ws://127.0.0.1:49134
compose serving
  engine: ws://127.0.0.1:49134
  namespace: default
  start a project: iii trigger compose::up --namespace default file=./worker-compose.yaml
```

<Warning>
  Only one Compose daemon can serve a namespace on an engine. Set `namespace:` in the compose file
  or pass `--namespace` when several daemons must share one engine.
</Warning>

`SIGINT` and `SIGTERM` both stop the daemon and take every project down with it. `compose::stop`
does the same over the engine. When `--up` owns the engine, every project and worker stops before the
engine process is stopped.

### Starting a project with the daemon

`iii compose --up` validates the initial file, selects engine ownership, and brings the project up
without waiting for a call to name it. The daemon stays in the foreground to supervise its
containers and, in managed mode, the engine process.

```text theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
iii compose --up [-f, --file <PATH>]
```

`--file` defaults to `./worker-compose.yaml`, the same fallback a `compose::*` call gets when it
names no file. The daemon then stays in the foreground and serves, so every other operation is a
`compose::*` call as usual.

If `--namespace` is absent, this initial `--up` also uses the file's `namespace:` for the compose
daemon. An explicit CLI namespace has priority over the file.

When the file contains `engine:` and no explicit `--engine` is present, Compose materializes an engine-only config under
`~/.iii/compose/<daemon-namespace>/engine-config.yaml`, starts the engine, and removes the generated
file after a clean shutdown. An explicit `--engine` takes priority and connects to that existing
engine instead. A process-wide `III_URL` does not override a managed file engine.

```text theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
$ iii compose --up -n orders
engine started
  pid: 42042
  config: /home/me/.iii/compose/orders/engine-config.yaml
  logs: /home/me/.iii/compose/orders/engine.log
  follow logs: tail -f '/home/me/.iii/compose/orders/engine.log'

compose serving
  engine: ws://127.0.0.1:49134
  namespace: orders

→ api starting
✓ api ready (250ms)
up: 1 of 1 changed in 250ms
```

The active engine log rotates at 10 MiB. Compose keeps `engine.log.1` through `engine.log.3`, so one
namespace uses at most about 40 MiB for engine logs. The printed follow command keeps reading the
active `engine.log` across rotations. Compose strips terminal control sequences before persisting
the engine output.

### Reading worker output

Compose captures each worker's stdout and stderr until the worker exits, including output written
after it becomes ready. Retained history is bounded: the active file rotates at 10 MiB and keeps
three archives, so one worker uses at most about 40 MiB. Older output can be truncated or deleted
after rotation. Terminal control sequences are removed before output is stored or returned.

Use the logs client for a recent snapshot or a live view:

```bash theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
iii compose logs                         # last 100 lines from every worker
iii compose logs queue --tail 200        # one worker
iii compose logs queue --follow          # keep waiting for new output
iii compose logs queue --stream stderr   # only stderr
iii compose logs queue --namespace dev --engine ws://127.0.0.1:49134
```

Options for the logs client come after `logs`. `--file` names a compose file on the daemon host;
when it is omitted, the daemon uses `worker-compose.yaml` in its own working directory. Each line
is prefixed with the worker name, and stderr uses a bold prefix on a terminal.

A project that does not start ends the command with `PROJECT_DID_NOT_START`. Rollback has already
stopped whatever came up, so there is nothing left to supervise.

Without `engine:`, Compose never starts or stops an engine. It uses `--engine`, then `III_URL`, then
the local default `ws://127.0.0.1:49134`.

```bash theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
iii compose --up --engine ws://127.0.0.1:49134
```

### Running it in the background

Compose never backgrounds itself. Even when `--up` runs its managed engine in the background, the
compose daemon serves in the foreground and writes its own output to stdout. This is the shape every
process supervisor already expects, and it leaves log rotation and restart-on-failure to something
that already does both.

For a quick session, a shell redirect is enough:

```bash theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
iii compose --namespace dev --engine ws://127.0.0.1:49134 >> ~/iii-compose.log 2>&1 &
```

On a server, a unit file gives restart-on-failure and hands the output to the journal. Set a
namespace when this daemon must be isolated from another daemon on the same engine.

```ini theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
[Unit]
Description=iii compose
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/iii compose --namespace prod --engine ws://127.0.0.1:49134
Restart=always

[Install]
WantedBy=multi-user.target
```

<Warning>
  Use `Type=simple`. `Type=notify` waits for an `sd_notify` readiness message, which compose does
  not send, so systemd kills it at `TimeoutStartSec`.
</Warning>

## The `compose::*` functions

All functions accept the same payload.

| Field         | Type      | Description                                                                            |
| ------------- | --------- | -------------------------------------------------------------------------------------- |
| `file`        | string    | Which project: the path to its compose file.                                           |
| `container`   | string    | Restricts the operation to one container and the containers it depends on.             |
| `namespace`   | string    | Which daemon the caller believed they were reaching. A guard, see below.               |
| `worker`      | string    | A repeatable CLI argument for `add`; one worker for `remove`, `restart`, and `update`. |
| `workers`     | string\[] | The canonical JSON list used by `add`.                                                 |
| `function_id` | string    | The function or file contract requested by `compose::schema`.                          |
| `cursors`     | object    | Last cursor returned for each worker by `compose::logs`.                               |
| `tail`        | number    | Recent lines returned by the first `compose::logs` call. Maximum 1000.                 |
| `stream`      | string    | Optional `stdout` or `stderr` filter for `compose::logs`.                              |
| `wait_ms`     | number    | Long-poll budget for `compose::logs`. Maximum 5000 milliseconds.                       |

A project is its compose file, and nothing else names one. The same file reached twice is the same
project however it was spelled, so there is no second identity to keep in sync and no way to point
one at the wrong file.

<Warning>
  `namespace` in the payload does not select a daemon. The engine resolves a call by the
  `--namespace` flag and never reads the body, so this can only catch having reached the wrong
  daemon: a mismatch fails with `WRONG_DAEMON`. Sent alone, the call still lands wherever the flag
  pointed.
</Warning>

| Function            | Takes             | Returns                                                                       |
| ------------------- | ----------------- | ----------------------------------------------------------------------------- |
| `compose::up`       | `file`            | An operation result.                                                          |
| `compose::down`     | `file`            | An operation result.                                                          |
| `compose::status`   | `file`            | The project's namespace, file, state directory, daemon pid, container states. |
| `compose::logs`     | `file`, `worker`  | Bounded stdout/stderr entries and one cursor per worker.                      |
| `compose::list`     | nothing           | The daemon name, its namespace, its pid, and every project it holds.          |
| `compose::validate` | `file`            | A validation report.                                                          |
| `compose::add`      | `file`, `workers` | What the edit did, changed workers restarted, and new workers started.        |
| `compose::remove`   | `file`, `worker`  | The worker removed, its targeted stop, and the idempotent `up`.               |
| `compose::restart`  | `file`, `worker`  | The `down` and the `up`, or one container's restart.                          |
| `compose::update`   | `file`, `worker`  | Both versions, and the restart that followed.                                 |
| `compose::stop`     | nothing           | The daemon name, its pid, and the projects it is about to stop.               |
| `compose::schema`   | `function_id`     | Request/response JSON Schemas, descriptions, timeouts, and retry safety.      |

`file` is not required. Left out, it falls back to a `worker-compose.yaml` in the daemon's own
working directory. `worker` and `workers` have no fallback.

`compose::schema` is read-only. With no `function_id`, it returns every `compose::*` contract.
Pass a function id to return one contract. The pseudo-id `worker-compose.yaml` returns the file's
JSON Schema as `request` and a complete small example as `response`. The same function schemas,
descriptions, and metadata are also published through `engine::functions::info`.

```bash theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
iii trigger compose::schema --namespace dev
iii trigger compose::schema --namespace dev function_id=compose::up
iii trigger compose::schema --namespace dev function_id=worker-compose.yaml
```

### Adding workers

`compose::add` declares one or more workers in the compose file and reconciles the project once.
On the CLI, repeat `worker=` for each worker. Each value takes a registry name (`state`), a name
with a version (`state@0.21.4`), or a directory (`./workers/api`); a leading `.` or `/` is what
makes it a path, since a registry reference may carry a host of its own. The JSON function contract
uses one list: `{ "workers": ["database", "web"] }`. The old singular JSON field remains accepted
for one worker.

An unpinned name is resolved once and written out as an exact version, so a
later `up` cannot quietly get a different build. The same worker at the same
version changes nothing and says so; at a different version it is replaced,
which is how an upgrade or a rollback is asked for.

Workers whose declarations did not change remain running. Existing workers whose resolved
versions changed restart in place, and newly declared workers start through the normal dependency
plan. This lets a worker call `compose::add` for its own project without stopping the caller before
the result can return.

A worker is rarely alone. Its manifest names what it calls, and the registry
answers with that whole graph already pinned to versions that satisfy each
other, so those are declared too, as containers with their own `start_after`.
Nothing starts behind the file: what runs is still what the file says, and an
operator can read it, pin it differently, or take a container out.

Two rules shape the expansion. Requesting a root package whose registry kind is `engine` fails with
`ENGINE_WORKER_IS_BUILTIN`, because the engine already supplies it. Engine-kind dependencies of a
normal project worker are filtered from the graph; an edge to one is dropped rather than written,
since `start_after` may only name a container the file declares. And a worker two others need is
declared once, named by both.

The file is edited, not rewritten: comments, blank lines and quoting survive,
entries are appended, and the result is parsed before it is written, so a bad
edit never reaches disk. What is written is `worker`, `version` and
`start_after`. It writes no `scripts`, so a `path://` worker needs
`scripts.start` in its own `iii.worker.yaml` to start. A `path://` worker is added alone: its
dependencies are declared in a manifest on disk rather than in the registry's
answer.

```bash theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
iii trigger compose::up     --namespace dev file=./worker-compose.yaml
iii trigger compose::up     --namespace dev file=./worker-compose.yaml container=api
iii trigger compose::status --namespace dev file=./worker-compose.yaml
iii trigger compose::logs   --namespace dev file=./worker-compose.yaml worker=api tail=100
iii trigger compose::down   --namespace dev file=./worker-compose.yaml
iii trigger compose::list   --namespace dev
iii trigger compose::add    --namespace dev file=./worker-compose.yaml worker=database worker=web
iii trigger compose::remove --namespace dev file=./worker-compose.yaml worker=state
iii trigger compose::restart --namespace dev file=./worker-compose.yaml
iii trigger compose::schema --namespace dev function_id=compose::up
iii trigger compose::stop   --namespace dev
```

A project-scoped call may leave `file` out when the daemon's own working directory holds a
`worker-compose.yaml`; without one, it fails with `NO_COMPOSE_FILE`. A relative `file` resolves
against the daemon's directory, not the caller's, so pass an absolute path when they differ.

`compose::restart` is a `down` followed by an `up`, with the compose file read again between them,
so an edit made by hand takes effect without restarting the daemon. It accepts `container` like the
two halves it is made of. Nothing cleverer yet: no rolling restart, and no keeping a container that
did not change.

`compose::stop` stops a compose project but returns before the daemon exits.

`compose::validate` answers for a file and holds nothing: a CI job that only ever validates leaves
the daemon owning nothing. Validation is offline, so `package://` containers are reported under
`deferred_packages` instead of being resolved.

### Removing a worker

`compose::remove worker=state` removes the named container and every surviving `start_after`
reference to it. The complete edited declaration is validated before the file or any process
changes. Compose then stops only that container and runs an idempotent `up`, so healthy surviving
workers stay running and anything already missing can start.

Removal does not resolve the registry graph or remove other workers that were added with this
worker. Those remain declared until they are removed explicitly.

### Restarting one worker

`compose::restart worker=state` stops that container and starts it again. Nothing else moves: not
what it depends on, and not what depends on it.

That is a decision, not a shortcut. A dependent holding a connection to the worker sees it drop, and
compose does not restart the dependents to hide it. Which of them tolerate a drop is the operator's
knowledge, not compose's. Left out, `worker` restarts the whole project instead: `down`, then `up`, with the file
read again between them.

### Updating a worker

`compose::update worker=state` moves a declared container to another version of the same package.

```text theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
worker=state            the version the registry calls latest
worker=state@0.21.4     that version, which is also how a downgrade is spelled
```

The answer names both ends, because an operator who asked for "latest" does not know what it is
until the call says so:

```json theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
{ "container": "state", "from": "0.21.4-alpha.4", "to": "0.22.0", "detail": "state from 0.21.4-alpha.4 to 0.22.0" }
```

Already on the version asked for, nothing is written and `changed` is `false`.

The container has to be declared already, and has to be a `package://` one: this edits a version
line, it does not add a container, and a `path://` worker has no version to move. Use
[`compose::add`](#adding-workers) to declare something new.

Only the version line changes. A `start_after` written by hand comes through as it was, since
rewriting the graph is what `compose::add` is for.

<Note>
  Unlike `compose::restart worker=`, an update restarts the whole project. A project is held as its
  file was read, so a new version is only picked up once the project is dropped and read again.
  Dropping it while its other containers run would leave them supervised by nothing.
</Note>

### Operation results

`compose::up` and `compose::down` return the same fields.

| Field          | Type    | Description                                                             |
| -------------- | ------- | ----------------------------------------------------------------------- |
| `operation_id` | string  | Identifier for this operation.                                          |
| `status`       | string  | `ok` or `failed`.                                                       |
| `changed`      | boolean | `false` when every requested container was already in the target state. |
| `containers`   | array   | One entry per container the operation planned.                          |

Each entry in `containers` contains `container`, `state`, `changed`. If a container failed it will
also contain an `error` object with `code` and `message`.

A failed `up` tears down any running containers, in reverse startup order, and reports those
containers as `stopped` with `changed: false`. Containers that were already running before the
operation are left alone.

### Container states

| State      | Meaning                                                    |
| ---------- | ---------------------------------------------------------- |
| `starting` | Spawned. The engine has not registered it yet.             |
| `ready`    | Registered in the engine under `(namespace, container)`.   |
| `failed`   | Exited without being asked to, or one of its hooks failed. |
| `stopped`  | Stopped by this daemon.                                    |

`compose::status` reports each declared container with its `state`, its `pid`, an `owned` flag, its
rotating `log_path`, and `last_error` when there is one. `owned` is `false` for a container this
daemon can see but did not start.

### Validation reports

| Field               | Type   | Description                                                  |
| ------------------- | ------ | ------------------------------------------------------------ |
| `namespace`         | string | Namespace the project's containers register in.              |
| `start_order`       | array  | Container keys in dependency order.                          |
| `deferred_packages` | array  | `package://` containers, which need the registry to resolve. |

## `worker-compose.yaml`

<Note>
  This documentation reflects version 1 of a worker compose file. Unknown keys and duplicate keys
  are errors. Durations can specify a unit: `500ms`, `30s`, `2m`.
</Note>

```yaml theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
namespace: shop
startup_timeout: 60s
stop_timeout: 10s
engine:
  url: ws://127.0.0.1:49134
  workers:
    configuration:
      adapter:
        name: fs
        config:
          directory: ./config
    iii-worker-manager:
      host: 127.0.0.1
      port: 49134
containers:
  database:
    worker: path://./workers/database
  api:
    worker: path://./workers/api
    start_after: [database]
    config_name: shop-api
    config_override:
      log_level: debug
    env_file: [./.env]
    environment:
      RUST_LOG: info
    scripts:
      pre_run: npm run migrate
      pre_run_timeout: 2m
      run: npm start
      post_run: ./scripts/cleanup.sh
  state:
    worker: package://registry.iii.dev/state
    version: "0.21.4"
```

### Top-level fields

| Field             | Type   | Default | Description                                                                                      |
| ----------------- | ------ | ------- | ------------------------------------------------------------------------------------------------ |
| `namespace`       | string | absent  | Namespace the project's containers register in. A project that declares none lands in `default`. |
| `startup_timeout` | string | `60s`   | Readiness budget for every container. A container may override it.                               |
| `stop_timeout`    | string | `10s`   | Grace between the polite stop and the forced kill.                                               |
| `engine`          | map    | absent  | Present when this Compose invocation owns and configures the engine.                             |
| `containers`      | map    | empty   | Project workers. May be empty only when `engine:` is present.                                    |

The namespace must already be `[a-z0-9_-]`. A value outside that set is refused with
`INVALID_NAMESPACE` rather than rewritten to fit, so what the file declares is what an operator
types into `--namespace`. Nothing about the file's path enters it, so two copies of one project
collide instead of running side by side.

### Engine fields

| Field                             | Type    | Default                | Description                                                                              |
| --------------------------------- | ------- | ---------------------- | ---------------------------------------------------------------------------------------- |
| `url`                             | string  | `ws://127.0.0.1:49134` | Managed engine endpoint used by Compose and its containers.                              |
| `registration_namespace_grace_ms` | integer | engine default         | Namespace-registration grace passed to the engine.                                       |
| `workers`                         | map     | empty                  | Direct configs for engine-owned workers. Values must be mappings; use `{}` for defaults. |

Allowed worker keys are `configuration`, `iii-worker-manager`, `iii-http-functions`, `iii-stream`,
and `iii-sandbox`. Use `#instance` for another instance of an allowed type, for example
`iii-worker-manager#rbac`. Internal `iii-engine-functions`, `iii-telemetry`, and
`iii-observability` are injected and must not be declared.

The engine section belongs to the file that started the managed daemon. A runtime change returns
`ENGINE_RESTART_REQUIRED`; restart the Compose invocation to apply it. A different file cannot
merge another section into that daemon and fails with `ENGINE_ALREADY_OWNED`.

### Container fields

Each key under `containers` is the worker name the container registers under.

| Field             | Type           | Default              | Description                                                                         |
| ----------------- | -------------- | -------------------- | ----------------------------------------------------------------------------------- |
| `worker`          | string         | required             | `path://<dir>` or `package://<registry-host>/<name>`.                               |
| `version`         | string         | absent               | Version range. Required for `package://`.                                           |
| `start_after`     | array          | empty                | Container keys that start first. Self-dependencies and cycles are rejected.         |
| `config_name`     | string         | absent               | The [configuration worker](./configuration) entry this container owns.              |
| `config_override` | mapping        | absent               | Merged on top of the fetched configuration.                                         |
| `working_dir`     | path           | the worker directory | Resolved against the compose file's directory.                                      |
| `environment`     | map            | empty                | Environment variables for this container.                                           |
| `env_file`        | array of paths | empty                | Read at start time, in declaration order. A later file wins on conflicting entries. |
| `startup_timeout` | string         | the file's value     | Readiness budget for this container.                                                |
| `scripts`         | mapping        | absent               | See below.                                                                          |

`path://` directories resolve against the compose file's directory. A missing directory fails with
`MISSING_WORKER_DIRECTORY`, and a missing `env_file` fails with `MISSING_ENV_FILE` during
validation.

A path worker normally runs as a host process. A non-empty `runtime.base_image` in its
`iii.worker.yaml` selects a local VM instead. The worker's `scripts.install` and start command then
run inside that image, and compose keeps the VM state inside the project. An invalid image reference
fails the start instead of falling back to the host or to another image.

### Worker kinds

The registry answers with a kind, and it decides how the container runs. A kind compose cannot run
fails with `UNSUPPORTED_PACKAGE_KIND`.

| Kind     | How it runs                                                                           |
| -------- | ------------------------------------------------------------------------------------- |
| `binary` | A child process on the host.                                                          |
| `bundle` | A VM. The start command is the bundle's own `scripts.start`, read in the guest.       |
| `engine` | Not installable: the registry publishes no artefact, so compose has nothing to start. |
| `image`  | Not supported: it needs the OCI runtime.                                              |

A bundle is published code that compose did not build, so it is started behind the same boundary
`iii add` puts it behind, rather than as a host process. Its configuration is published into the
guest, so `III_CONFIG` names a path inside the VM; a worker reads it the same way either way.
Bundle support can be refused machine-wide with `III_BUNDLE_WORKERS_DISABLED=1`, which compose
honours.

A bundle's VM is booted by `iii-worker`, which the installer ships beside `iii` and which needs
glibc on Linux. Compose runs it as a process rather than linking it, so the engine stays portable;
a VM container on a machine without `iii-worker` fails saying so, and every host worker is
unaffected.

Bundles need a VM, and windows has none: a bundle container there fails with
`BUNDLE_NEEDS_A_VM` before anything is downloaded. Run compose under WSL, where the VM has KVM to
run on. Every other worker kind runs on windows as it always has.

### Scripts

| Field             | Type   | Default | Description                                                  |
| ----------------- | ------ | ------- | ------------------------------------------------------------ |
| `pre_run`         | string | absent  | Runs to completion before the container is spawned.          |
| `pre_run_timeout` | string | `60s`   | Budget for `pre_run`. Rejected without a `pre_run`.          |
| `run`             | string | absent  | Start command. Rejected for `package://` containers.         |
| `post_run`        | string | absent  | Runs after the container's exit is confirmed. Never awaited. |

Both hooks run with the container's environment, working directory, and their own process group.

The start command for a `path://` container is `run`, then `scripts.start` from the worker's
`iii.worker.yaml`. A container with neither fails with `MISSING_START_COMMAND`.

When the manifest declares `runtime.base_image`, the same precedence applies inside the VM: `run`
replaces `scripts.start`, but it never moves the worker back to the host. Host source changes do not
automatically restart this VM. Use `compose::restart worker=<container>` after a source change.

Where the two files describe the same thing, `worker-compose.yaml` wins and the manifest is the
default. `run` overrides `scripts.start`, and the container key overrides the manifest's `name`: the
key is what reaches the child as `III_WORKER_NAME`, so a worker honouring the reserved contract
registers under it whatever its manifest declares. A worker that hardcodes its name instead is
caught at readiness by `WORKER_NAME_MISMATCH`, which reports the name it took.

### Configuration precedence

Lowest to highest: the configuration a package ships, the entry the configuration worker holds, then
`config_override`. The merged result is written to an owner-only file and its path is passed to the
container as `III_CONFIG`. A container that declares `config_name` does not start when the fetch
fails; the error is `CONFIG_FETCH_FAILED`.

## The container environment

A container's environment is built, and the daemon's own environment is not inherited wholesale.
Three layers apply, lowest to highest.

1. A host baseline. On Unix: `PATH`, `HOME`, `USER`, `LOGNAME`, `SHELL`, `TERM`, `TMPDIR`, `TZ`,
   `LANG`, `LC_ALL`. Windows adds the variables the platform needs, such as `SystemRoot`, `COMSPEC`,
   and `PATHEXT`.
2. The container's `env_file` entries, then its `environment` map.
3. The reserved variables, which the daemon owns.

| Variable                | Value                                                                         |
| ----------------------- | ----------------------------------------------------------------------------- |
| `III_URL`               | The engine address the daemon is connected to.                                |
| `III_NAMESPACE`         | The project's namespace.                                                      |
| `III_COMPOSE_NAMESPACE` | The supervising Compose daemon's namespace for explicit `compose::*` routing. |
| `III_COMPOSE_FILE`      | Canonical path of the compose file that owns this container.                  |
| `III_COMPOSE_DIR`       | Canonical directory that contains the owning compose file.                    |
| `III_WORKER_NAME`       | The container key.                                                            |
| `III_CONFIG`            | Path to the resolved configuration file. Absent when there is none.           |
| `III_CONFIG_NAME`       | The configuration entry the container owns. Absent when it declares none.     |

Declaring a reserved variable in `environment` or an `env_file` fails with `RESERVED_ENV_OVERRIDE`,
in both cases at `compose::validate` time.

<Note>
  For why the daemon owns these eight rather than treating them as defaults a container can replace,
  see [Understanding iii / Compose](../understanding-iii/compose).
</Note>

### Reading a value from the host

The baseline is short on purpose, so nothing an operator exported reaches a worker by accident. A
file names the values it wants instead, with `${VAR}`.

```yaml theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
containers:
  queue:
    worker: path://${WORKERS_DIR}/queue
    environment:
      RUST_LOG: ${RUST_LOG:-info}
```

References expand in any value, not only in `environment`: a worker path, a version, an `env_file`
entry. The file on disk is never rewritten.

| Written        | Means                                                                                           |
| -------------- | ----------------------------------------------------------------------------------------------- |
| `${VAR}`       | The value from compose's own environment. Unset, the file is refused with `UNDEFINED_VARIABLE`. |
| `${VAR:-text}` | The value, or `text` when it is unset. `${VAR:-}` makes it optional and empty.                  |
| `$VAR`         | Nothing. A bare name is left alone, so a `scripts.run` holding `$PWD` still reaches the shell.  |
| `$${VAR}`      | A literal `${VAR}`.                                                                             |

#### What `config_override` keeps

`config_override` is never expanded, at any depth. That block is not compose's to read: it is
carried to the configuration worker, which resolves `${VAR}` references of its own at read time.
That is how a secret is stored as a reference rather than as a value.

```yaml theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
    config_override:
      # Reaches the worker as written. The value is never in the compose file,
      # and never in what compose stores.
      api_key: ${ANTHROPIC_API_KEY}
```

A name that compose's environment does not hold is therefore not an error inside this block. Nothing
in it is compose's to resolve.

## Readiness

A container is considered up when the engine reports a worker of that name in the project's
namespace. Compose polls `engine::workers::list` every 200 ms until the container's
`startup_timeout` runs out.

| Outcome                                                        | Code                               |
| -------------------------------------------------------------- | ---------------------------------- |
| The container never appeared.                                  | `STARTUP_TIMEOUT`                  |
| The process exited while compose was waiting.                  | `CHILD_EXITED_BEFORE_REGISTRATION` |
| It registered in `default` instead of the project's namespace. | `WORKER_IGNORED_NAMESPACE`         |
| It registered under a different name in the right namespace.   | `WORKER_NAME_MISMATCH`             |
| Its functions landed outside the project's namespace.          | `FUNCTIONS_IN_WRONG_NAMESPACE`     |
| A worker already held that name in the namespace.              | `CONTAINER_NAME_TAKEN`             |

After a container is ready, the daemon checks it every 250 ms. A container that exits takes its
transitive dependents down with it and is recorded as `failed`. Nothing is restarted: v1 has no
restart policy, so a container that dies stays down until the next `up`. When the engine connection
drops and comes back, every running container gets its `startup_timeout` to register again.

What a failure takes with it depends on when it happens, and v1 has no way to declare otherwise.

| When                         | What comes down                                                                                                                     |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| During `up`                  | The operation ends. Everything it started is stopped in reverse order, and containers later in the start order are never attempted. |
| After the container is ready | Its transitive dependents. Everything else keeps running.                                                                           |

<Note>
  So a container nothing depends on ends the whole `up` if it fails at start, and is contained if it
  fails a minute later. See [Understanding iii / Compose](../understanding-iii/compose).
</Note>

## Where compose keeps state

Everything sits under `~/.iii/compose`, or under `$III_COMPOSE_STATE_DIR` when that is set.

| Path                        | Contents                                                                                                               |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `<ns>/<project>/state.json` | One project's child records. Owner-only.                                                                               |
| `<ns>/<project>/config/`    | Resolved configuration files.                                                                                          |
| `<ns>/<project>/logs/`      | Rotating stdout and stderr for every project worker.                                                                   |
| `<ns>/<project>/vm/`        | VM state for bundle and local-image containers, one directory each, plus the config each one publishes into its guest. |
| `packages/`                 | Installed `package://` artefacts, shared by projects.                                                                  |

`<ns>` is the daemon's namespace. `<project>` is derived from the compose file's canonical path:
readable enough to recognise, hashed enough that two projects in directories of the same name stay
apart. Because it is derived, it cannot be guessed. `compose::status` reports it as `state_dir`
and reports the exact `log_path` for each worker:

```bash theme={"theme":{"light":"catppuccin-latte","dark":"dark-plus"}}
iii trigger compose::status --namespace dev file=./worker-compose.yaml
# ... "state_dir": "/home/you/.iii/compose/dev/shop-3f2a1b9c"
ls /home/you/.iii/compose/dev/shop-3f2a1b9c/logs/
```

<Note>
  For why this is one place per machine rather than a directory beside each compose file, see
  [Understanding iii / Compose](../understanding-iii/compose).
</Note>

Capture continues until the worker exits. Rotation bounds disk use while keeping raw `print`,
`console.log`, `println!`, stdout, and stderr output available even when the worker does not use the
structured logger. Use `engine::logs::list` when structured OpenTelemetry fields and trace
correlation are required.

A clean shutdown clears the state file. After an unclean exit, the daemon compares each record
against the live process: a match is adopted, a dead process is recorded `failed`, and a live pid it
cannot verify is left running and reported for manual cleanup.

## Error codes

Compose errors cross the wire with a stable code and a message.

| Area                  | Codes                                                                                                                                                                                                                                                                              |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Compose file          | `COMPOSE_FILE_UNREADABLE`, `INVALID_COMPOSE_FILE`, `EMPTY_CONTAINERS`, `INVALID_DURATION`, `UNKNOWN_DEPENDENCY`, `SELF_DEPENDENCY`, `DEPENDENCY_CYCLE`, `UNSUPPORTED_WORKER_SOURCE`                                                                                                |
| Container declaration | `MISSING_VERSION_FOR_PACKAGE`, `RUN_NOT_ALLOWED_FOR_PACKAGE`, `NOT_A_PACKAGE_CONTAINER`, `PRE_RUN_TIMEOUT_WITHOUT_PRE_RUN`, `RESERVED_ENV_OVERRIDE`, `MISSING_ENV_FILE`                                                                                                            |
| Worker resolution     | `MISSING_WORKER_DIRECTORY`, `MISSING_START_COMMAND`, `INVALID_MANIFEST`                                                                                                                                                                                                            |
| Packages              | `REGISTRY_UNREACHABLE`, `PACKAGE_NOT_RESOLVED`, `PACKAGE_NOT_INSTALLED`, `PACKAGE_DOWNLOAD_FAILED`, `PACKAGE_DIGEST_MISMATCH`, `PACKAGE_ARTIFACT_EMPTY`, `UNSUPPORTED_PACKAGE_KIND`, `UNSUPPORTED_PLATFORM`                                                                        |
| Start and readiness   | `SPAWN_FAILED`, `HOOK_SPAWN_FAILED`, `HOOK_FAILED`, `HOOK_TIMEOUT`, `STARTUP_TIMEOUT`, `CHILD_EXITED_BEFORE_REGISTRATION`, `WORKER_IGNORED_NAMESPACE`, `WORKER_NAME_MISMATCH`, `FUNCTIONS_IN_WRONG_NAMESPACE`, `CONTAINER_NAME_TAKEN`, `CONFIG_FETCH_FAILED`, `ENGINE_CALL_FAILED` |
| Daemon and project    | `NO_COMPOSE_FILE`, `WRONG_DAEMON`, `INVALID_NAMESPACE`, `UNKNOWN_CONTAINER`, `INVALID_STATE_FILE`, `STATE_DIR_UNAVAILABLE`, `DAEMON_ALREADY_SERVING`, `IO_ERROR`                                                                                                                   |

## Related

<Note>
  For why compose is a worker and why a project is its file, see [Understanding iii /
  Compose](../understanding-iii/compose). For the namespace dimension itself, see [Understanding iii
  / Namespaces](../understanding-iii/namespaces). For the `iii compose` entry in the command tree,
  see the [CLI reference](../cli-reference/index#iii-compose).
</Note>
