CLI reference
cwright is the single entry point. Every command reads a model document (a path, or - for stdin)
and writes its result to stdout unless --out FILE is given. A path ending .yaml or .yml
is read as YAML; anything else is read as JSON.
cwright --version
cwright validate <model> [--json]
cwright lint <model> [--json]
cwright run <model> [--replications N] [--out FILE] [--store DIR]
cwright ls [--store DIR]
cwright propose <model> <variant> --kpi NAME --indifference X
[--replications N] [--out-dir DIR]
cwright trace <flow.parquet> [--entity N] [--server RESOURCE:N] [--limit N]
cwright simulate <model> [--replication N] [--out FILE] [--cycles-out FILE]
[--checkpoint FILE --checkpoint-every EVENTS]
cwright resume <checkpoint> <model> --mode recover|fork [--out FILE]
[--checkpoint FILE --checkpoint-every EVENTS]
cwright compare <baseline> <candidate> --kpi NAME
[--replications N] [--indifference X] [--higher-is-better] [--out FILE]
| Command | What it does |
|---|---|
--version | What this binary is, as JSON. Runs no model. |
validate | Check a model document and report every problem at once. |
lint | Ask whether the model can run at all — offered load, staffing, batching, release rate. Warns, never refuses. |
run | Replications, warm-up detection, KPIs with a confidence interval. |
simulate | One replication, optionally checkpointing as it goes. |
resume | Continue a checkpoint — recover (same run) or fork (a variant from here). |
compare | Two models, one KPI, a decision with an indifference threshold. |
propose | Evaluate a candidate variant against a baseline. |
trace | Read a Parquet flow record back, per entity or per server. |
ls | What is in the run store. |
cwright --version
Prints what the binary is and runs nothing:
{
"build_isa": "custom",
"build_target": "x86_64-unknown-linux-gnu",
"engine_version": "0.0.0",
"rustc_version": "rustc 1.97.1 (8bab26f4f 2026-07-14)",
"schema_version": 1
}
⭐ The field names are the same as the manifest block of a results document, so
answering "was this result produced by this binary?" is a comparison of like with like
rather than a translation between two spellings of the same fact.
build_isa is the one field that is not in a manifest, because it is about whether a
performance number is comparable rather than whether a result is reproducible: a
binary built for a wider instruction set is not a fair benchmark against one that was not.
Why the flags look like this
--indifference is required for a comparison and has no default. A difference smaller than it is
not worth acting on, and only the domain knows what that is — guessing one would turn a judgement
into a silent default. See Experiments.
Similarly, resume requires --mode with no default: recover and fork are both correct
answers to different questions, and picking the wrong one is silent rather than an error. See
Checkpointing.
YAML or JSON
The reader dispatches on the file name: .yaml and .yml are read as YAML, and every other
extension is read as JSON. The two are the same document — the same keys, the same nesting — and
a document the reader accepts means the same model, with the same model_hash, whichever
syntax it was written in.
That is enforced rather than assumed, because the two parsers do not agree about every
text: the JSON parser is not correctly rounded on a literal carrying more significant digits
than a double holds, so it and the YAML parser can land on adjacent doubles. model_hash is a
hash of the model written as JSON, which makes JSON a model's canonical form however it
arrived, and three things are refused for breaking that:
- a document both parsers accept and read differently. A file that is valid JSON and valid YAML — which is any JSON file, so renaming one is the obvious way to reach this — must mean the same model under either name. If it does not, the refusal prints both readings and you write either; both name a real double. ⭐ A document only the YAML parser accepts (it has a comment, an unquoted key, an anchor) has a single reading and nothing to disagree with, so nothing is refused merely for being YAML.
- a non-finite number. YAML can write
.nanand.infand JSON cannot, so the canonical form would lose it — and a comparison againstNaNis false, so it passes a range check in silence rather than being caught. - a model that does not survive being written out and read back.
proposewrites the candidate it measured tocandidate.jsonand a later command reads that file, so a model which is not a fixed point of its own canonical form would be re-read as a different one. This one applies to JSON as well as YAML, and is reachable only for values far outside the range a plant model uses — below about 1e-6 or above 1e9.
YAML is otherwise the more permissive of the two: it has comments and anchors, and an unquoted
scalar becomes a string wherever a name is wanted, so id: 1 is the resource called 1 rather
than a refusal. None of that changes what the model means.
Which to use is a question about who writes it. YAML takes comments, so the reason for a
number can sit beside the number instead of in prose on another page, and that is what a model
maintained by a person wants. JSON is what a generator emits and what machines exchange, and
every document cwright writes — a results document, propose's candidate model — is JSON
whatever the input was. (A checkpoint is neither: it is a binary format with its own magic
number and checksum, described under Checkpointing.)
The same dispatch applies to the variant document propose takes, which a person also writes.
⚠️ The MCP server is not a file reader: its tools take the model as a JSON
object rather than as document text, so YAML is a thing to write in a file and hand to
cwright, not something to pass to validate_model.
Reading a model from stdin
Any command that takes a model path accepts - to read the document from standard input, which
is useful when a model is generated on the fly rather than kept as a file:
my-generator | cwright validate -
Standard input is read as JSON: a pipe carries no file name, so there is no extension to dispatch on.
Where output goes
Results are printed to stdout as JSON by default; pass --out FILE to write them to a file
instead. run --flow DIR additionally writes one Parquet flow-record file per replication —
DIR/r0.parquet, DIR/r1.parquet, and so on — described in
The results document.