If you want to change how your project is judged, this page is for you. It’s the reference for evolve.yaml: every section, every setting. You don’t need to read it top to bottom. evolve init writes a good one for you, so come here when you want to change something and need to know what it does.
Words this page uses
- Agent: an AI tool that writes code. Run: everything Evolve does for one goal. Attempt: one agent’s try at it. Candidate: an attempt with a change worth checking.
- Check: a test Evolve runs on every candidate, which agents can’t change.
- Commit: a saved snapshot of your code in Git. Branch: a named line of commits, like
main. Diff: the line-by-line list of what a change did. - Sandbox: a sealed-off space a program runs in, cut off from the rest of the machine.
- Canary: a new version given a small share of real traffic first. Promotion: it going fully live. Rollback: putting the old version back.
- p95 latency: how long the slowest 5% of requests take to answer.
- Operator config: Evolve’s own settings file, kept by whoever runs Evolve (the operator). Reviewer and planner: agents the operator can set up to judge changes and to plan runs, both described below.
- YAML: the plain-text format these settings are written in.
How it’s used
Evolve reads evolve.yaml from the commit a run starts at, and locks it for that run. Nothing an agent changes can alter the rules it’s judged by. To check a file, run evolve policy check --file evolve.yaml.
toolchain
The tools your project is built and tested with, its compiler and test runner: go (what you get if you leave the line out), node, python, rust, or none for a project that only needs what every system has, like a Makefile or a shell script. evolve init writes the line for you when the project isn’t Go.
Checks that run your project’s own commands (build, command, gui) are given that toolchain. A few things to know:
- Checks have no network. So whatever your project depends on has to be in the repository, or already in the verifier’s cache (the store of downloads Evolve’s checker keeps for itself).
- The toolchain’s caches are kept apart from every agent’s.
- If the machine doing the checking doesn’t have the toolchain, those checks come back
unknown, with the reason. - A coding agent can see every toolchain the machine has, and may run your project’s test and build commands by itself.
repository
What agents may touch.
| Field | Meaning |
|---|---|
target_ref |
the branch a promoted change lands on, by its full Git name, such as refs/heads/main for main |
protected_existing |
paths where no attempt may change or delete the files already there, but may add new ones. evolve init puts **/*_test.go here: agents can’t weaken your tests, and can still write tests for what they build |
protected_paths |
paths no attempt may change. Must include evolve.yaml. Entries are files (evolve.yaml), directories ending in / (verification/), or globs, which are patterns where * matches any name (cmd/*.go) and **/ any number of folders (**/*_test.go) |
allowed_paths |
if set, attempts may change only these paths |
max_changed_files |
an attempt changing more files is rejected |
max_diff_lines |
an attempt with a larger diff is rejected |
generated_paths |
files a tool writes, like **/*_templ.go. They still obey protected and allowed paths, but their lines don’t count toward max_diff_lines or smallest_diff |
attempts
How many agents try, and within what limits.
| Field | Meaning |
|---|---|
count |
attempts per run, 1 to 16 |
sequential |
true starts an attempt only if the ones before it didn’t produce an eligible change; false starts them all at once. Leave it out and Evolve plans it goal by goal when there’s a reviewer (see below), or starts them all at once when there isn’t |
min_confidence |
how confident the plan has to be in an approach (0 to 1, default 0.25) before an attempt is spent on it |
plan |
false turns planning off |
timeout_seconds |
time limit for one attempt, in real seconds |
max_total_cost_usd |
stop starting attempts once the run has spent this much |
interventions |
how many times an attempt that isn’t right yet gets another go before its approach is given up (default 3; 0: never; it used to be called repairs). See “Second chances” below |
max_output_bytes |
cap on what one attempt may write |
memory_mb, cpus |
sandbox limits for one attempt |
network |
none (the default) or operator: the agent may use the network if its operator config also allows it |
strategies |
optional hints, one per attempt, so attempts try different approaches |
Second chances
When an attempt says it’s done, the checks run next, and if there’s a reviewer, it reviews the change too.
If the checks fail, or the reviewer finds the change works but has faults, the agent is told exactly what was found and asked to fix it. That’s an intervention. It happens in the same folder of code and, where the agent can manage it, the same conversation with its AI, so the agent keeps what it already knows.
A change only fails for good when it can’t be improved: the reviewer calls its approach a dead end, or it runs out of interventions. Then the run goes back to where the approach was chosen and tries the next one, told why the first was dropped. If every planned approach is dropped, it plans once more with what it learned.
Two more things. An attempt that ran out of time is carried on the same way, but if two run out of time with much the same change, the run stops as blocked. That usually means the goal is too big, or the description of what’s wanted has a problem. And a rejection for touching a protected file, going out of scope or going over budget never gets a second chance.
The plan
If the operator config names a reviewer, and nobody has already said which approaches to try (with strategies, in the objective or here), a run is planned before any attempt starts.
Here’s how. The planner reads the goal and the code, and lists the approaches it can see (up to five). For each, it says how confident it is that an agent taking that approach would pass the goal’s checks within one attempt’s limits.
Attempts only go to approaches at or above min_confidence, best first, and no more than count of them. Usually that’s one at a time, with the next one tried only if the ones before weren’t enough. The exception: when the goal leaves a real design choice and the two best approaches are close, those two run side by side and the reviewer compares them.
Each attempt is told its approach and what the planner found, so it doesn’t have to explore the code all over again.
And if no approach clears the bar? The run stops as blocked before spending a single attempt, with the planner’s reasons and any way it suggests to split the goal. The planner can also ask you a question first (answer on the run’s page, or with evolve run answer). The plan is recorded with the run, and evolve run show shows it.
service
How to build and start your program, for the checks that actually run it.
| Field | Meaning |
|---|---|
build.argv |
the build command. {{output}} is where the built program file must be written |
build.env |
extra environment variables (named settings a program reads when it starts) for the build |
start.args |
arguments to start it. {{port}} is the port (the numbered door requests come in through) it must listen on |
start.health_path |
an address that answers 200 (the web’s “OK”) once the program is ready, such as /healthz |
checks
The trusted judges. Each one runs in its own sandbox against the candidate, using your own protected copies of the files.
| Kind | What it does | Needs |
|---|---|---|
build |
builds the service with service.build |
|
command |
runs a command, such as your tests; it passes if the command finishes without an error (exit code 0) | argv |
http |
starts the service and sends it a contract: web requests and the answers they must get | contract: a protected file |
benchmark |
starts the service, sends it lots of requests as a load profile says, and times the answers | profile: a protected file, metric (p50_ms, p95_ms or p99_ms: the time within which 50%, 95% or 99% of requests are answered), maximum, min_samples |
browser |
starts the service and runs scenarios (what a person does, step by step) in a headless browser, a real browser with no window | scenarios: a protected file; optionally milestone, to run only the scenarios up to that roadmap goal |
gui |
runs scenarios against a native app’s interface (an installed app, not a web one) with no window, through its driver, a protected test that works the app like a person | scenarios: a protected file; package: the folder of Go code with the interface (default ./ui); optionally milestone, as for browser, and scales, the display sizes to run every scenario at, like [1, 1.6], to catch a layout that only works at one (see display scale) |
cli |
runs the built command-line program against a transcript: ways to run it and what each should produce; it is never started, so the policy needs no service.start |
transcript: a protected file |
Every check has an id. It can also set required (a candidate that fails it is out), repeat (run it up to 10 more times, to catch results that come and go) and timeout_seconds.
A contract lists cases like this:
schema_version: 1
id: checkout-v1
cases:
- name: applies welcome coupon
method: POST
path: /checkout
body: '{"region":"us-ny","coupon":"WELCOME5","lines":[{"sku":"sku-cheese","quantity":2}]}'
expect:
status: 200
json: {discount_cents: 89, total_cents: 1777}
A load profile says how hard to push:
schema_version: 1
id: checkout-load-v1
concurrency: 25
requests: 440
warmup: 20
max_error_ratio: 0.01
mix:
- {weight: 3, method: POST, path: /checkout, body: '…', expect_status: 200}
selection
How to pick between candidates that passed every required check. rank is a list of keys, tried in order. A later key breaks the ties an earlier one leaves.
| Key | Prefers |
|---|---|
lowest_p95_ms |
the fastest candidate |
smallest_diff |
the fewest changed lines |
fewest_files |
the fewest changed files |
lowest_cost |
the cheapest attempt |
tolerance treats values that close together as equal. So with {key: lowest_p95_ms, tolerance: 5}, a 3 ms difference counts as a tie, and the next key decides.
deployment
How the chosen candidate gets to production. Leave this section out and Evolve stops at a verified candidate, which you merge yourself.
| Field | Meaning |
|---|---|
adapter |
which deploy tool: local, argo or orchy (see Where it runs) |
environment |
the name, in the operator config, of the place to put it live |
stages |
traffic percentages, strictly increasing, such as [10, 50, 100]. A stage can also be written with named values, to set its own watching time: {percent: 5, observe_seconds: 120, min_requests: 500}. 100 is promotion |
observation_seconds_per_stage |
how long each stage is watched |
min_requests_per_stage |
fewer requests than this isn’t enough to judge by |
max_error_rate |
the canary’s error rate may not exceed this |
max_p95_ms |
the canary’s p95 latency may not exceed this |
max_p95_regression_ratio |
the canary may not be this much slower than the current version, such as 1.2 for 20% slower |
missing_metrics |
rollback or pause when the numbers to judge by (the metrics) are missing |
post_promotion_seconds |
keep watching after promotion, and roll back if it turns bad |
post_promotion_min_requests |
how many requests that watch needs before it can judge (default: min_requests_per_stage). 0 suits a quiet service: errors and slow answers still roll it back, but a lack of visitors doesn’t |
allow_destructive_migrations |
meant for database changes that can’t safely be undone. It’s accepted in the file, but Evolve doesn’t act on it yet |
experiment |
run an A/B experiment (some users get the new version, some the current one, and you compare) instead of a canary; see below |
experiment
| Field | Meaning |
|---|---|
metric.name |
what you’re trying to improve, like checkout_conversion: the share of people who go from looking at their cart to placing an order |
metric.exposure |
the request that counts as seeing the feature: {method: GET, path: /cart} |
metric.conversion |
the request that counts as success: {method: POST, path: /orders} |
split |
the percentage of users who see the candidate, such as 50 |
min_detectable_lift |
the smallest improvement you’d care about, like 0.05 for 5% |
alpha, power |
how sure the result must be: alpha is the chance you’ll accept of calling a fluke a win, power the chance of catching a real improvement. 0.05 and 0.8 are usual |
baseline_seconds |
measure the current rate first, to size the experiment |
baseline_rate |
or state it, and skip the measurement |
max_seconds |
give up after this long |
inconclusive |
keep_control (keep the current version; the default) or promote when time runs out |
See experiments for how the decision is made.
spec
Tests first. With this on, each goal’s tests are written, and agreed, before anyone builds the thing. It’s off unless you set it.
spec:
tests: true
check: tests # the command check that runs the tests (default: tests)
adopt: reviewer # who adopts the tests the reviewer chose: reviewer (default) or operator
With it on, evolve run next, evolve run "…" and evolve run start begin with a spec run for the goal. Its agent writes only new test files (the ones repository.protected_existing covers) that say what the goal asks for, as behaviour you could see from outside.
Evolve accepts those tests when the build still passes and the tests fail on the current code. That’s on purpose: a test of something new should fail until the new thing exists.
Then it’s your turn. You see the tests and adopt them. Or you say what they’re missing with evolve context add, and have them written again with --new-tests. Adopted tests are merged into the branch, which makes them existing tests: every implementation has to pass them, no agent may change them, and they stay on as regression tests for every later goal.
With a reviewer. If a reviewer is set up, the tests it chooses are adopted straight away (it asks when it isn’t sure). Want to read them first? Set spec.adopt: operator, or pass --read-tests for one goal. The chosen tests are shown, and wait for your yes.
When a test is wrong. The reviewer is given what the tests printed when Evolve ran them. It checks that each one fails because of the behaviour the goal asks for (a test that trips over its own setup doesn’t count), and that any expected values worked out by hand are right. If you still spot a bad test or a wrong number, --amend-tests adopts the tests and stops before building. Fix them on the branch (you may; agents may not), commit, and start the goal again. Evolve builds against the tests as you left them.
When an old test gets in the way. The reviewer also looks for existing tests that contradict the goal: a test that pins down the very behaviour the goal changes. No agent may change an existing test, so nothing could pass while it stands. When it finds one, the goal’s own tests are still adopted, but nothing is built. Evolve names the test and stops. Either let the goal change it (unprotect: [path] in the goal), or change it yourself and commit. Then start the goal again.
One more thing: a command check can be asked to fail on purpose. expect_fail: true passes only when the command fails.
record
What evolve sync carries to other machines.
| Setting | Meaning |
|---|---|
binaries |
local (what you get if you don’t set it): built program files stay on the machine that built them, since their commit can rebuild them. lfs: they travel too, over Git LFS (Git’s add-on for big files) on your remote, the shared copy of your repository, and count toward its storage |
A complete example
The demo’s examples/checkout/evolve.yaml uses most of what’s on this page. evolve init writes a smaller one for your project (Your first project).