erectl evolution
Inspect and control a workspace's two self-evolution lanes from the terminal: read their state, switch each one on or off independently, queue a pass now, and roll a bad version back.
Overview
Self-evolving guidance lets a workspace refine its own guidance layer over time. Two lanes do it: one refines the workspace chat instruction, the other refines the MCP tool guidance. Each lane drafts a new version, applies it if it passes the lane's gates, and keeps every prior version so a bad one can be undone. That page explains what the feature does and what it guarantees; this one is the command reference.
The evolution command group is the terminal-side twin of the
dashboard's Evolution rail: lane state, the per-workspace switch, the active
text, the version history, rollback, the circuit breaker, and one control the
dashboard does not offer -- run a lane now instead of waiting for its next
scheduled pass.
Every subcommand hits the router's management plane at
/{project_id}/v1/management/evolution and requires an API key
carrying the management scope. A key without it gets
403 with code: "scope_insufficient". Each subcommand
resolves the active erectl context and authenticates with that
project's API key; use --context <name> to target a
different deployment, and --dry-run on any writing verb to print
what would be sent without sending it.
Reads require any membership of the project. Writes -- enable,
disable, run, breaker-reset,
rollback -- require the project owner or an operator, and return
403 with code: "access_denied" otherwise. A
workspace you may not read is reported as 404 with
code: "workspace_not_found", the same answer a workspace that
does not exist gets: the surface does not confirm a workspace to someone who
may not see it.
The key must be bound to a user: both gates above evaluate a
caller identity, so a key that carries none reads nothing and writes nothing
here. It gets 403 with code: "user_identity_required"
rather than being admitted by default. Issue a key from a user account if you
intend to script this group.
Two Independent Lanes
The --lane option takes chat_instruction or
mcp_guidance. It is required on every per-lane subcommand and it
has no default.
| Lane | What it refines |
|---|---|
chat_instruction |
The workspace chat instruction |
mcp_guidance |
The workspace's MCP tool guidance |
The two lanes are independent everywhere
(what each lane refines). Two enable flags,
two circuit breakers, two cooldowns, two version histories, two deployment
switches. Turning chat_instruction off says nothing about
mcp_guidance, and nothing in this group reads one lane's state to
decide the other's. Read both with
erectl evolution status <wid> rather than inferring one from
the other.
Each lane also has two switches stacked in front of it, and either one can
stop it: the per-workspace flag you set with enable /
disable, and a deployment-wide switch held by whoever operates the
router. A workspace flag reading enabled while the deployment
switch for that lane is off is the confusing case, so status
prints both side by side and enable tells you when it has just
turned on a lane the deployment has switched off.
Usage Pattern
erectl evolution status <wid> # Both lanes' state
erectl evolution enable <wid> --lane <lane> # Turn one lane on
erectl evolution disable <wid> --lane <lane> # Turn one lane off
erectl evolution run <wid> --lane <lane> [--wait] # Queue one pass
erectl evolution run-status <wid> --lane <lane> \ # Poll a queued pass
--run-id <id>
erectl evolution breaker-reset <wid> --lane <lane> # Close the breaker
erectl evolution show <wid> --lane <lane> # Active text
erectl evolution history <wid> --lane <lane> # Version history
erectl evolution rollback <wid> --lane <lane> \ # Restore a version
--version <n>
status is the group's default subcommand, so
erectl evolution <wid> and
erectl evolution status <wid> are the same command. Every
other verb needs its name and its --lane.
-o json switches any verb to a machine-readable dump of the whole
response. enable, disable,
breaker-reset, and rollback print a human-readable
success line before their JSON, so add -q if you intend
to parse them; run and run-status emit the run and
nothing else under -o json.
status
Prints one row per lane. This is the only verb that does not take
--lane: it always reports both, because the question it answers is
usually "which of these two is actually running".
erectl evolution status ws_1a2b3c4d
erectl evolution status ws_1a2b3c4d -o json
Columns
| Column | Meaning |
|---|---|
Lane |
chat_instruction or mcp_guidance |
Enabled |
The per-workspace flag: yes or no |
Platform |
The deployment-wide switch for that lane: on or OFF |
Breaker |
closed (the lane may write) or open (it may not) |
Active Ver |
The active evolved version number, or - when none is active |
Last Auto Apply |
When the lane last applied a version on its own, or never |
Last Manual Run |
When a run was last accepted for this lane, or never |
A lane changes nothing unless Enabled is yes,
Platform is on, and Breaker is
closed. The first two stop the lane from running at all; an open
breaker lets it run and stops it writing. OFF is printed in
capitals because it is the state that most often explains a workspace that
looks configured and is doing nothing.
enable / disable
Sets the per-workspace flag for one lane. The other lane is
untouched; confirm with erectl evolution status <wid> rather
than assuming.
erectl evolution enable ws_1a2b3c4d --lane chat_instruction
erectl evolution disable ws_1a2b3c4d --lane mcp_guidance
When you enable a lane whose deployment switch is off, the command reports the write and then says so: the flag is now on and the lane still will not run. Saying only the first half would be technically true and practically misleading.
run
run is asynchronous. It enqueues one pass for
this workspace and lane, prints a run handle, and returns; the router executes
the pass and settles the handle. It does not hold the connection open while
the pass runs, and it does not report the pass's outcome -- poll the handle
with run-status, or pass
--wait.
A queued pass runs through the same gates a scheduled pass runs: the
per-workspace flag, the circuit breaker, and the lane's rolling edit budget all
still apply when it executes. run changes when a lane
runs, never what it is allowed to do.
erectl evolution run ws_1a2b3c4d --lane chat_instruction
erectl evolution run ws_1a2b3c4d --lane chat_instruction --wait
erectl evolution run ws_1a2b3c4d --lane mcp_guidance --wait --wait-timeout 300
Options
| Option | Description |
|---|---|
--lane <lane> |
chat_instruction or mcp_guidance (required, no default) |
--wait |
Poll the handle until the run succeeds or fails, then exit accordingly |
--wait-timeout <seconds> |
Seconds to poll with --wait before giving up. Default 900; must be positive. |
Responses
| Status | Meaning |
|---|---|
202 |
A new pass was enqueued. The body carries the run handle, deduplicated: false, and cooldown_seconds. |
200 |
A pass for this workspace and lane was already queued or running. The body carries that run and deduplicated: true. |
429 |
The cooldown holds. See The Cooldown. |
503 |
The lane cannot run at all. See When a Run Is Refused. |
deduplicated: true is not an error and not a second pass. Asking
again while a pass is in flight hands back the run already in flight rather
than starting another, so a client that polls by retrying run
converges on one handle instead of a queue of duplicates. The CLI says which
of the two happened before it prints the handle.
With --wait, the command polls the handle until the run reaches
succeeded or failed, then exits 0 or
non-zero to match. If --wait-timeout elapses first, the run is
still going -- the command prints the exact run-status invocation
to resume with and exits non-zero. A timeout is a statement about the wait,
not about the run.
run-status
Reads the handle run printed. --run-id takes that
handle; a value that is not a UUID is rejected before any request is issued.
This is a read, so any project member can call it.
erectl evolution run-status ws_1a2b3c4d \
--lane chat_instruction \
--run-id 11111111-2222-3333-4444-555555555555
erectl evolution run-status ws_1a2b3c4d \
--lane chat_instruction \
--run-id 11111111-2222-3333-4444-555555555555 --wait
Run states
| Status | Meaning |
|---|---|
queued |
Accepted and waiting; no router has claimed it yet |
running |
A router claimed it and the pass is executing |
succeeded |
The pass completed. processed reports the units of work it did. |
failed |
The pass did not complete. failure_reason always says why. |
processed: 0 on a succeeded run is a real pass that
found nothing eligible, not a failure; the CLI says so in those words rather
than printing a bare zero. --wait and
--wait-timeout behave exactly as they do on run.
The command exits non-zero for a failed run whether or not
--wait was passed, so a script can branch on the exit code without
parsing output. A run that never settles is failed after an hour, so a handle
cannot sit in running forever.
A handle that does not belong to this workspace and lane returns
404 with code: "run_not_found".
When a Run Is Refused
A queue makes "accepted" the easy answer to everything, which is how a surface
stops being useful for debugging. run checks the things that can
prove a pass will never execute before it enqueues anything, and
refuses with a specific code instead of returning a handle that would sit
queued forever.
Three 503 codes, deliberately kept apart. "Nothing happened" has
three different remedies and one generic error would hide which one you need.
| Code | What it means | Cooldown | What to do |
|---|---|---|---|
evolution_lane_disabled_by_platform |
The deployment-wide switch for this lane is off, so no scheduled pass runs either. The response names the setting an operator has to change. | Not consumed | Ask whoever runs the deployment to turn the lane on. Nothing you can set on the workspace changes this. |
evolution_runner_unavailable |
This router carries no evolution wiring at all, so there is nothing to run a pass. Every other verb in this group still works. | Not consumed | You are pointed at a deployment that does not run evolution. Check --context, or use one that does. |
evolution_lane_not_running |
The deployment switch is on, but the lane's controller is not running in this router. | Consumed | The router is misconfigured or came up incomplete. This is an operator problem; retrying at speed does not help, which is why this arm alone spends the allowance. |
The first two are reported before the cooldown is claimed, so discovering a deployment setting never costs you your allowance. The third consumes it on purpose: the process is broken, and hammering it at HTTP speed helps nobody.
None of the three is reported as queued. A handle you cannot poll
to a real outcome would be worse than an error.
The Cooldown
One accepted run per lane per workspace every 900 seconds (15 minutes). The two lanes hold separate allowances, and so does every workspace; the limit is on the pair, not on you and not on the project.
The window equals the lane's own scheduled interval. A run does exactly the work one scheduled pass does for one workspace, so the most an operator can do is double that workspace's duty cycle -- enough for the "I just fixed the prompt, run it again" case, not enough to turn the control into a way of spending the project's inference budget in a loop.
The allowance is claimed at enqueue, not when the pass executes. Claiming it at execution would let a caller queue many runs inside one window and the limit would mean nothing.
A refused run returns 429 with status: "cooldown",
retry_after_seconds, and a Retry-After header; the
CLI exits non-zero. An accepted run reports the same window as
cooldown_seconds so a script knows when it may ask again.
What spends the allowance
| Outcome | Consumes the cooldown |
|---|---|
202, a new run enqueued |
Yes |
200, deduplicated: true |
No |
503 evolution_lane_disabled_by_platform |
No |
503 evolution_runner_unavailable |
No |
503 evolution_lane_not_running |
Yes |
The deduplicated case is exempt for a specific reason: a client polling by
retrying run must not spend its allowance being told its own pass
is still going.
breaker-reset
Closes one lane's circuit breaker so the lane may write again. This is
the terminal form of
Resume After a Pause: the
safety system opens the breaker itself when results regress after a change.
It runs nothing -- use run for that, and expect
to run both commands when you are recovering a tripped lane.
erectl evolution breaker-reset ws_1a2b3c4d --lane chat_instruction
show
Prints the lane's active version number and the active text itself. A lane with no active version says so instead of printing an empty body.
erectl evolution show ws_1a2b3c4d --lane chat_instruction
erectl evolution show ws_1a2b3c4d --lane mcp_guidance -o json
history
Lists the lane's versions, newest first: the version number, whether it is
active, its origin, who created it, when, and the recorded change summary. A
version with no recorded timestamp shows - rather than a
fabricated time.
erectl evolution history ws_1a2b3c4d --lane chat_instruction
erectl evolution history ws_1a2b3c4d --lane chat_instruction -o json
Origins
| Origin | Meaning |
|---|---|
auto |
The lane drafted and applied it |
owner |
A person wrote it |
reverted |
A rollback wrote it, restoring an older version's text |
rollback
Restores a historical version's text. --version takes the version
number from history and must be positive.
erectl evolution rollback ws_1a2b3c4d --lane chat_instruction --version 4
History is never deleted. The target version's text is written
forward as a new version, so the version number you end up with is
higher than the one you asked for -- restoring v4 of a lane at v9 produces
v10, with origin reverted. The command prints both numbers.
Rolling back a rollback is therefore just another rollback.
A version number that does not exist returns 404 with
code: "version_not_found". A stored version that no longer passes
content policy returns 422 with
code: "content_rejected"; pick a different version.
Not Intelligence Reconciliation
Intelligence reconciliation is a different subsystem. It has nothing to do with the evolution lanes: different commands, different switch, different cooldown, no shared state. It is documented here only because the two are easy to confuse when both are described as "the workspace improving itself in the background".
Evolution refines a workspace's prompt text. Reconciliation is a pass
over what a workspace has already recorded -- its memories, decisions,
milestones, and relations. Nothing in the evolution group affects
it, and nothing below affects the lanes. Both commands live in the
workspaces group.
Turning the pass on or off
erectl workspaces update ws_1a2b3c4d --intelligence-reconciliation true
erectl workspaces update ws_1a2b3c4d --intelligence-reconciliation false
--intelligence-reconciliation governs whether the periodic
reconciliation pass runs for this workspace. It is distinct from project
intelligence extraction, which is what produces the records in the
first place; this flag governs the pass over what extraction produced.
Running the pass now
erectl workspaces revalidate-intelligence ws_1a2b3c4d
Queues one reconciliation pass and returns as soon as the job is accepted,
printing the queued job's id. Like evolution run
it is asynchronous, and unlike it there is no status verb to poll -- the
command's job is to get the pass scheduled.
Rate limited to one accepted trigger per workspace per hour,
and the allowance is shared with the dashboard's equivalent control, so
alternating the two surfaces does not double the rate. A refused trigger returns
429 with retry_after_seconds and a
Retry-After header. That hour is a different window from the
evolution cooldown's 15 minutes, on a different subject.
A workspace whose reconciliation flag is off is skipped when the pass runs
rather than being refused at the trigger, so a queued job that appears to do
nothing is usually a workspace that opted out. Turn it back on with
--intelligence-reconciliation true.
Examples
Find Out Why a Lane Is Doing Nothing
# Both lanes, both switches, the breaker, and the last activity
erectl evolution status ws_1a2b3c4d
# Platform OFF -> a deployment setting; nothing here will fix it
# Enabled no -> erectl evolution enable ws_1a2b3c4d --lane chat_instruction
# Breaker open -> erectl evolution breaker-reset ws_1a2b3c4d --lane chat_instruction
Run a Lane and Wait for the Outcome
erectl evolution run ws_1a2b3c4d --lane chat_instruction --wait
# Or enqueue now and poll later
RUN_ID=$(erectl evolution run ws_1a2b3c4d --lane chat_instruction -o json -q | jq -r .run.id)
erectl evolution run-status ws_1a2b3c4d --lane chat_instruction --run-id "$RUN_ID"
Undo a Bad Version
erectl evolution history ws_1a2b3c4d --lane mcp_guidance
erectl evolution show ws_1a2b3c4d --lane mcp_guidance
# Restore v4. The result is a NEW version with v4's text.
erectl evolution rollback ws_1a2b3c4d --lane mcp_guidance --version 4
Turn One Lane Off Without Touching the Other
erectl evolution disable ws_1a2b3c4d --lane mcp_guidance
erectl evolution status ws_1a2b3c4d # chat_instruction is still enabled