Config reference
The manifest is YAML. Unknown keys are ignored. Validation errors name the key and are reported together.
Top level
Section titled “Top level”| Key | Type | Default | Notes |
|---|---|---|---|
version | int | 1 | Only 1 is accepted. |
project | string | required | Prefix for compose project names. Must match ^[a-z][a-z0-9-]{0,15}$. |
stack | object | required | See below. |
ports | object | see below | See below. |
expose | list | [] | See below. |
| Key | Type | Default | Notes |
|---|---|---|---|
files | list of paths | required, at least one | Relative to the manifest. Each must exist. Merged in order. |
env_files | list of paths | [] | Relative to the manifest. Missing files are skipped. Used for interpolation. |
profiles | list of strings | [] | Compose profiles to enable. |
| Key | Type | Default | Notes |
|---|---|---|---|
mode | auto / domains / registry | auto | See Concepts. |
range | [lo, hi] | [41000, 49999] | Both within 1024 to 65535, lo < hi. Only used in registry mode. |
expose[]
Section titled “expose[]”| Key | Type | Default | Notes |
|---|---|---|---|
service | string | required | Compose service name. |
port | int | 0 | Container port. 0 means the first published port of the service. |
scheme | string | http | Used to build the URL. |
path | string | / | Appended to the URL. |
Generated files
Section titled “Generated files”envctl writes under .envctl/<feature>/ in the worktree.
| File | Contents |
|---|---|
compose.yaml | The rendered project. Regenerated on every up and render. |
env | KEY=VALUE lines described below. |
env.json | The environment record: name, backend, linked branch, dataset, parent, kept, timestamps. |
Env file variables
Section titled “Env file variables”| Variable | Value |
|---|---|
ENVCTL_ENV | Environment name. |
ENVCTL_FEATURE | Deprecated alias of ENVCTL_ENV. |
ENVCTL_BRANCH | Linked branch, empty when unlinked. |
ENVCTL_PROJECT | Compose project name. |
ENVCTL_BACKEND | local. |
ENVCTL_PORT_MODE | domains or registry. |
ENVCTL_HOST_<SERVICE> | service.project.orb.local in domains mode, 127.0.0.1 in registry mode. Service names are uppercased with - and . replaced by _. |
ENVCTL_PORT_<SERVICE>_<TARGET> | Allocated host port for that container port. Registry mode only. |
Labels
Section titled “Labels”Every rendered service carries:
| Label | Value |
|---|---|
dev.envctl.env | Environment name. |
dev.envctl.feature | Deprecated alias of dev.envctl.env. |
dev.envctl.backend | local. |
dev.envctl.project | Compose project name. |
Port registry
Section titled “Port registry”~/.config/envctl/ports.json, or $XDG_CONFIG_HOME/envctl/ports.json:
{ "allocations": { "mg-feat-thing/postgres/5432": 41000, "mg-feat-thing/api/8787": 41001 }}Keys are project/service/containerPort. envctl down -v removes a project’s entries. Deleting the file is safe; ports are reallocated on the next render.
Workflows (version 2, experimental)
Section titled “Workflows (version 2, experimental)”workflow is the default workflow every run uses. workflows adds named workflows a run can pick instead. Each entry takes the same keys as workflow: a template, and nodes that merge into it.
workflow: template: feature nodes: qa: checks: &checks - name: unit command: [npm, test]workflows: small: template: small nodes: build: checks: *checks| Template | Stages | Notes |
|---|---|---|
feature | Task → Plan → Design → Code → QA → Approved change | Checks go on qa. |
small | Plan → Build → Approved change | For small changes. Build implements the change and runs the checks, so build needs checks. |
Rules for every workflow:
- There is exactly one Plan stage. Task is optional; without it, Plan is the first stage.
- An approved change needs a QA stage or a stage with
checksbefore it. That stage’s passing checks, for the exact commits being published, are what the pull request is opened from. A workflow without one is rejected when it is loaded. - Workflow names are lowercase identifiers.
defaultis reserved for theworkflowsection.
Pick a workflow with envctl run create --workflow small, with tab in the dashboard’s new-run input, or with the workflow argument of the MCP envctl_create tool. envctl run workflows lists them with their stages. A run stores only the workflow it selected, and a rewind with --config keeps using that workflow.
Workflow limits (version 2, experimental)
Section titled “Workflow limits (version 2, experimental)”Version 2 workflow configurations set revision-wide limits under limits. A workflow node can override the attempt budget for its own assignments under workflow.nodes.<id>.limits. See the workflow runtime design.
| Key | Type | Default | Notes |
|---|---|---|---|
limits.parallel | int | 1 | Nodes that may run at once. Above 1 requires limits.vms of at least 2. |
limits.vms | int | 2 | Revision and child VMs a run may hold, including draining revisions. |
limits.max_attempts | int | 12 | Attempts per node, unless the node overrides it. |
limits.attempt_seconds | int | 1800 | Worker, supervisor and check timeout, unless the node or check overrides it. |
workflow.nodes.<id>.limits.max_attempts | int | inherits | 1 to 100. Applies to that node only. |
workflow.nodes.<id>.limits.attempt_seconds | int | inherits | 1 to 86400. Applies to that node’s worker, supervisor and checks without their own timeout_seconds. |
limits.stall_seconds | int | 600 | How long a running worker or supervisor may produce no output before the coordinator intervenes. 60 to 86400. |
workflow.nodes.<id>.limits.stall_seconds | int | inherits | 60 to 86400. Overrides the stall window for that node’s agents. |
limits.run_tokens | int | 20000000 | Counted-token ceiling for the whole run, across every revision. -1 disables it. See below for what counts. |
limits.cache_read_weight | number | 0.1 | Share of each cache-read token that counts toward run_tokens, from 0 to 1. 1 counts cache reads in full. |
limits.run_cost_usd | number | none | Optional ceiling on the API-list-price cost the harness reports. Claude reports cost; Codex does not. |
envctl run show prints each node’s used and allowed attempts, its timeout and its stall window. envctl run readiness includes the same values under limits.
When an agent is silent for its stall window, the coordinator first resumes the same session with a nudge asking for a status report. A nudged agent that stays silent without any activity, or a third stall in one attempt, fails the attempt with the stall evidence, and the normal retry budget applies. A command or tool call still running, or a model composing its answer after a tool result, extends the window once, to twice its length. A step that legitimately runs quietly for longer than that, such as a long test suite or build, needs a larger stall_seconds on that node. A stall window at or above attempt_seconds leaves only the attempt timeout in effect.
Workflow agent models (version 2, experimental)
Section titled “Workflow agent models (version 2, experimental)”agents.worker.model and agents.supervisor.model set the model each role’s
harness uses for every stage. A workflow node can override either role for
its own stage under workflow.nodes.<id>.agents.worker.model /
workflow.nodes.<id>.agents.supervisor.model. Unset values inherit, in
order: the node override, then the run-wide agents.<role>.model, then the
harness’s own default (no --model flag).
| Field | Type | Default | Notes |
|---|---|---|---|
agents.worker.model | string | harness default | Model the worker harness uses for every stage, unless a node overrides it. |
agents.supervisor.model | string | harness default | Model the supervisor harness uses for every stage, unless a node overrides it. |
workflow.nodes.<id>.agents.worker.model | string | inherits | Overrides the worker’s model for that node only. |
workflow.nodes.<id>.agents.supervisor.model | string | inherits | Overrides the supervisor’s model for that node only. |
Readiness probes for harness.worker/harness.supervisor always use the
run-wide model, since a connection probe has no stage in scope.
envctl run show and envctl run readiness print each node’s effective
worker and supervisor model next to its attempt budget.
Spending fewer tokens
Section titled “Spending fewer tokens”Most of a run’s tokens are cache reads: every tool call re-reads the agent’s conversation so far, so long sessions in stages that read a lot of code cost the most. On envctl’s own runs, one code attempt reported 26M of a run’s 40M tokens. Ways to spend less:
-
Use a cheaper model where the work is verified anyway. Code and QA workers are checked by their stage’s checks and by an independent supervisor, so a faster model such as Sonnet is a good fit there. Keep the default model for Plan and Design, whose decisions the later stages build on, and for every supervisor:
workflow:template: featurenodes:code: {agents: {worker: {model: sonnet}}}qa: {agents: {worker: {model: sonnet}}} -
Use the
smallworkflow for small changes. Plan, Build and the approved change skip Task, Design and a separate QA stage. See Workflows. -
Keep stages focused. Prompts that name the files and commands to use save the exploring an agent otherwise does.
-
Watch the ceiling.
limits.run_tokensstops a run that runs away. Raising it with a rewind that only changes limits resumes where the run stopped.
Token usage and ceilings
Section titled “Token usage and ceilings”Every run counts the tokens its agents use: worker and supervisor jobs, their steering resumes, and readiness probes that call a model. The counts are what the harness itself reports: input, cache writes, cache reads and output. Claude also reports an API-list-price cost, shown as “API-equivalent”; on a subscription that is a measure of usage, not a bill.
Cache reads are almost all of it. On every tool call (reading a file, running a test) the agent re-reads its whole conversation from the prompt cache, so a coding session that grows to 200K tokens and makes 130 tool calls reports about 25M cache-read tokens. They cost about a tenth of other input, so the ceiling counts them at limits.cache_read_weight (10% by default) and everything else in full. Complete feature runs on envctl itself reported 30M to 100M tokens and counted 4M to 13M.
envctl run show and the dashboard show the counted total against the ceiling and the reported total, for example tokens 5.3M counted of 20.0M (27%) · 40.2M reported · cache reads 38.8M at 10% · API-equivalent $6.50. The dashboard colors it at 80% and 100%. Usage of jobs still running is estimated until each job reports its final count.
When a run reaches limits.run_tokens or limits.run_cost_usd:
- Running workers and supervisors are stopped. Their unfinished source is kept, as with any interrupted attempt.
- No new attempt or retry starts, and readiness probes that call a model stop.
- The run shows
needs-attentionwith the reason, for examplerun counted 20.0M of its 20.0M token ceiling (limits.run_tokens; 160.4M tokens reported, cache reads at 10%). - Work already waiting for approval can still be approved; publishing uses no tokens.
To continue, raise the ceiling with envctl run rewind <run> --to <stage> --config <file> using a configuration with a higher limits.run_tokens, or cancel the run. A rewind whose configuration changes only budget limits (run_tokens, run_cost_usd, max_attempts, attempt_seconds, stall_seconds) keeps Plan and every checkpoint before <stage>, so the run resumes where it stopped. Any other configuration change, including parallel and vms, reopens Plan.
For scale: dogfooding runs of envctl on itself reported 30M to 100M tokens each, 4M to 13M counted, and $6 to $26 API-equivalent, including supervisor corrections and retries. Most of the largest run was one 47M-token code attempt.
Workflow preview (version 2, experimental)
Section titled “Workflow preview (version 2, experimental)”Services in a workflow VM publish only on the guest’s loopback interface, and Lima’s automatic port forwarding stays disabled. To open one service from your machine, opt in with preview:
preview: service: web # Compose service name port: 8080 # container port; it must be published (for example `ports: ['8080']`) path: / # optional request path, default / node: code # optional; see below| Key | Type | Notes |
|---|---|---|
preview.service | string | Required. The Compose service to preview. |
preview.port | int | Required, 1 to 65535. The container port; envctl finds its guest-loopback binding. |
preview.path | string | Optional absolute path used for the URL and the reachability check. |
preview.node | string | Optional. In a parallel workflow, a branch node previews that branch’s child VM. Otherwise the revision VM is previewed. |
The coordinator binds one port on 127.0.0.1 (never another interface) and forwards each connection over ssh -W through the VM’s Lima SSH configuration to exactly that guest port. It advertises the URL in envctl run show, the Bubble Tea header and Services panel, and the run’s runtime.preview_url only while an HTTP request through it succeeds. The forward belongs to the coordinator process: it is re-established on the same port after a restart, including for completed runs kept for inspection, and removed when the VM is released, cancelled or superseded by a rewind. Anyone who can connect to your machine’s loopback can open the preview.
Workers, supervisors and checks receive the running services’ guest-loopback endpoints as ENVCTL_SERVICE_<NAME>_HOST, ENVCTL_SERVICE_<NAME>_URL (lowest published port) and ENVCTL_SERVICE_<NAME>_PORT_<CONTAINER_PORT>. These addresses work inside the VM only. A worker’s values are fixed when its attempt starts; checks read them when they are submitted. Services with fixed published ports keep stable values across restarts.
Tracker (version 2, experimental)
Section titled “Tracker (version 2, experimental)”tracker links a run to an external issue and posts a captain’s log comment to it as each workflow stage completes: what each stage decided or built, what was checked, and what is waiting on a person.
tracker: provider: linear credential: file:/absolute/path/to/linear.token # or env:LINEAR_API_KEY| Key | Type | Default | Notes |
|---|---|---|---|
tracker.provider | string | none | Only linear is supported today. An unknown value is rejected. |
tracker.credential | string | required with tracker | env:NAME or file:/absolute/path. Unlike plugin credentials, a file: reference must be an absolute path: a tracker credential is a long-lived secret independent of any one repository checkout. Resolved lazily; never printed, and never placed in agent prompts. |
A config without a tracker: key behaves exactly as before this feature existed: it produces the same configuration digest and JSON encoding.
Link a specific run to an issue with --task-ref (a Linear issue URL or an identifier like ENG-123), the dashboard’s n input (a second, optional prompt after the objective), or the task_ref argument of the MCP envctl_create tool. A run without a link posts nothing, even with a tracker configured.
When a tracker is configured, Plan’s readiness probes verify the credential, that the linked issue exists, and that the credential can comment on it, without posting anything.
Once linked, envctl posts one comment per event: each accepted stage checkpoint (with the stage, attempt number, worker summary, supervisor review summary, checks, commits, token usage so far, and a short recap of earlier stages), a stage awaiting human approval, an approval, a rewind (new revision, target stage, and whether the objective changed), and anything needing attention such as the token ceiling or a publication failure. Each event is posted once, even across coordinator restarts and retries; a tracker outage never blocks or fails the workflow, and pending or failed posts show in envctl run show and the dashboard as captain's log: N pending, N failed. Comment bodies go through the same redaction as agent activity and are capped in size.
Dashboard themes
Section titled “Dashboard themes”The dashboard (envctl ui) reads your personal settings from ~/.config/envctl/config.yaml, or $XDG_CONFIG_HOME/envctl/config.yaml when that is set. This file belongs to you, not to a repository.
theme: name: auto # or any built-in theme below dark: tokyo-night # used by auto on a dark terminal light: tokyo-night-day # used by auto on a light terminal background: true # false keeps your terminal's own background custom: accent: "#ff9e64"| Key | Default | Notes |
|---|---|---|
theme.name | auto | auto picks light or dark from the terminal’s reported background. |
theme.dark | catppuccin | Theme auto uses on a dark background. |
theme.light | catppuccin-latte | Theme auto uses on a light background. |
theme.background | true | false stops painting the theme background, for transparent or image backgrounds. |
theme.custom.<role> | none | Overrides one color of the selected theme. |
Built-in themes: terminal, catppuccin, catppuccin-latte, tokyo-night, tokyo-night-day, gruvbox, gruvbox-light, nord, dracula, rose-pine, rose-pine-dawn, kanagawa, everforest, solarized-dark, solarized-light, github-dark, github-light.
terminal uses the 16 ANSI colors of your terminal’s own theme and never paints a background, so the dashboard matches whatever Ghostty, iTerm2 or WezTerm theme you already use.
Custom roles are background, surface (selection highlight), text, muted, line (borders), accent, success, danger, warn and info. A value is #rgb, #rrggbb, an ANSI index 0 to 255, or none for the terminal default. Unknown themes, roles or colors are reported when the dashboard starts, and it falls back to the default theme.
The choice resolves in this order: envctl ui --theme <name>, then ENVCTL_THEME, then the file. Colors are reduced to what your terminal supports, and are omitted entirely when NO_COLOR is set or output is not a terminal.
Run envctl theme show (see Dashboard themes) to see the resolved value and source of every role, including theme.custom.* overrides and theme.background: false.