Skip to main content

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]
CommandWhat it does
--versionWhat this binary is, as JSON. Runs no model.
validateCheck a model document and report every problem at once.
lintAsk whether the model can run at all — offered load, staffing, batching, release rate. Warns, never refuses.
runReplications, warm-up detection, KPIs with a confidence interval.
simulateOne replication, optionally checkpointing as it goes.
resumeContinue a checkpoint — recover (same run) or fork (a variant from here).
compareTwo models, one KPI, a decision with an indifference threshold.
proposeEvaluate a candidate variant against a baseline.
traceRead a Parquet flow record back, per entity or per server.
lsWhat 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 .nan and .inf and JSON cannot, so the canonical form would lose it — and a comparison against NaN is 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. propose writes the candidate it measured to candidate.json and 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.