Skip to content

Config reference

The manifest is YAML. Unknown keys are ignored. Validation errors name the key and are reported together.

KeyTypeDefaultNotes
versionint1Only 1 is accepted.
projectstringrequiredPrefix for compose project names. Must match ^[a-z][a-z0-9-]{0,15}$.
stackobjectrequiredSee below.
portsobjectsee belowSee below.
exposelist[]See below.
KeyTypeDefaultNotes
fileslist of pathsrequired, at least oneRelative to the manifest. Each must exist. Merged in order.
env_fileslist of paths[]Relative to the manifest. Missing files are skipped. Used for interpolation.
profileslist of strings[]Compose profiles to enable.
KeyTypeDefaultNotes
modeauto / domains / registryautoSee Concepts.
range[lo, hi][41000, 49999]Both within 1024 to 65535, lo < hi. Only used in registry mode.
KeyTypeDefaultNotes
servicestringrequiredCompose service name.
portint0Container port. 0 means the first published port of the service.
schemestringhttpUsed to build the URL.
pathstring/Appended to the URL.

envctl writes under .envctl/<feature>/ in the worktree.

FileContents
compose.yamlThe rendered project. Regenerated on every up and render.
envKEY=VALUE lines described below.
env.jsonThe environment record: name, backend, linked branch, dataset, parent, kept, timestamps.
VariableValue
ENVCTL_ENVEnvironment name.
ENVCTL_FEATUREDeprecated alias of ENVCTL_ENV.
ENVCTL_BRANCHLinked branch, empty when unlinked.
ENVCTL_PROJECTCompose project name.
ENVCTL_BACKENDlocal.
ENVCTL_PORT_MODEdomains 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.

Every rendered service carries:

LabelValue
dev.envctl.envEnvironment name.
dev.envctl.featureDeprecated alias of dev.envctl.env.
dev.envctl.backendlocal.
dev.envctl.projectCompose project name.

~/.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.

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
TemplateStagesNotes
featureTask → Plan → Design → Code → QA → Approved changeChecks go on qa.
smallPlan → Build → Approved changeFor 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 checks before 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. default is reserved for the workflow section.

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.

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.

KeyTypeDefaultNotes
limits.parallelint1Nodes that may run at once. Above 1 requires limits.vms of at least 2.
limits.vmsint2Revision and child VMs a run may hold, including draining revisions.
limits.max_attemptsint12Attempts per node, unless the node overrides it.
limits.attempt_secondsint1800Worker, supervisor and check timeout, unless the node or check overrides it.
workflow.nodes.<id>.limits.max_attemptsintinherits1 to 100. Applies to that node only.
workflow.nodes.<id>.limits.attempt_secondsintinherits1 to 86400. Applies to that node’s worker, supervisor and checks without their own timeout_seconds.
limits.stall_secondsint600How long a running worker or supervisor may produce no output before the coordinator intervenes. 60 to 86400.
workflow.nodes.<id>.limits.stall_secondsintinherits60 to 86400. Overrides the stall window for that node’s agents.
limits.run_tokensint20000000Counted-token ceiling for the whole run, across every revision. -1 disables it. See below for what counts.
limits.cache_read_weightnumber0.1Share of each cache-read token that counts toward run_tokens, from 0 to 1. 1 counts cache reads in full.
limits.run_cost_usdnumbernoneOptional 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).

FieldTypeDefaultNotes
agents.worker.modelstringharness defaultModel the worker harness uses for every stage, unless a node overrides it.
agents.supervisor.modelstringharness defaultModel the supervisor harness uses for every stage, unless a node overrides it.
workflow.nodes.<id>.agents.worker.modelstringinheritsOverrides the worker’s model for that node only.
workflow.nodes.<id>.agents.supervisor.modelstringinheritsOverrides 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.

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: feature
    nodes:
    code: {agents: {worker: {model: sonnet}}}
    qa: {agents: {worker: {model: sonnet}}}
  • Use the small workflow 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_tokens stops a run that runs away. Raising it with a rewind that only changes limits resumes where the run stopped.

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-attention with the reason, for example run 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
KeyTypeNotes
preview.servicestringRequired. The Compose service to preview.
preview.portintRequired, 1 to 65535. The container port; envctl finds its guest-loopback binding.
preview.pathstringOptional absolute path used for the URL and the reachability check.
preview.nodestringOptional. 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 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
KeyTypeDefaultNotes
tracker.providerstringnoneOnly linear is supported today. An unknown value is rejected.
tracker.credentialstringrequired with trackerenv: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.

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"
KeyDefaultNotes
theme.nameautoauto picks light or dark from the terminal’s reported background.
theme.darkcatppuccinTheme auto uses on a dark background.
theme.lightcatppuccin-latteTheme auto uses on a light background.
theme.backgroundtruefalse stops painting the theme background, for transparent or image backgrounds.
theme.custom.<role>noneOverrides 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.