CLI.
Seven commands. Small, scriptable, friendly to coding agents. None open a browser. None ask you to log in.
Run dif from the repo root or any descendant. It walks upward until it finds dif/config.yaml. Every command takes --json as a global flag for
machine-readable output. Exit codes are documented per command; 0 means success,
non-zero means a specific kind of failure you can branch on in CI.
Run dif --help for the auto-generated overview.
$dif init
Scaffold a dif/ workspace in the current directory. Refuses to clobber existing
dif-owned files without --force.
Flags
| flag | default | notes |
|---|---|---|
--surface <name> | home | Name of the first surface to create. |
--force | off | Overwrite dif-owned structural files and generated skill files. |
--no-agent-files | off | Skip writing the agent guidance files (CLAUDE.md, AGENTS.md, …). |
Example
$dif init ✓ wrote dif/config.yaml ✓ created dif/experiments/{active,concluded}/ ✓ created dif/surfaces/home.md ✓ created dif/audiences/{locale,device_type}.ts ✓ added dif guidance to CLAUDE.md, AGENTS.md, .cursorrules
The agent files get a managed dif block; existing content around it is preserved. Pass --no-agent-files to skip them entirely.
Exit codes
| code | meaning |
|---|---|
0 | workspace created |
2 | existing structural files were found without --force |
1 | an unexpected filesystem or command error |
invarianta fresh dif init followed by dif validate and dif build passes with zero experiments.
$dif new
Draft a new experiment file. Reads up to three prior learnings from the surface and embeds them in the brief template as an HTML comment.
Flags
| flag | default | notes |
|---|---|---|
<id> | — | Kebab-case experiment id. Becomes the filename stem. |
--surface <name> | — | Required. Must exist under dif/surfaces/. |
--owner <email> | git config user.email | Falls back to git config. Errors if neither is set. |
--from <id> | — | Clone audience, variants, and exclusion_group from an
existing experiment (active or concluded). Owner, metrics, hypothesis, status, and body are
not copied. |
The draft starts with control and variant_a at 50/50 and today's date.
Example
$dif new checkout-cta-v3 --surface checkout → reading dif/surfaces/checkout.md found 3 prior learnings → drafted dif/experiments/active/checkout-cta-v3.md status: draft, owner: ada@acme.dev
Exit codes
| code | meaning |
|---|---|
0 | drafted |
1 | can't determine owner; pass --owner |
2 | id conflict (already exists) or --from source not found |
3 | surface does not exist |
invariantthe file dif new writes parses cleanly with dif validate.
$dif validate
Run every check against the workspace. Collects all errors and warnings in one pass (no fail-fast), so the report shows everything wrong at once. Writes nothing.
The validator reads:
dif/config.yaml;- every
.mdindif/experiments/active/andconcluded/; - every
.mdindif/surfaces/; - the resolver filenames in
dif/audiences/; .js/.jsx/.ts/.tsxsource fordif("<id>", ...)call sites (skipping.git,node_modules,target,dist,build, anddif/).
Checks, in order:
- Frontmatter parses; required fields present.
(E001) owneris a syntactically valid email.(E003)surfaceresolves to a loaded surface file.(E004)- Variant weights sum to 100.
(E005) - Every audience attribute is declared in
dif/config.yaml.(E006) - Every declared attribute has a resolver in
dif/audiences/.(E008) - Same-surface experiments share an
exclusion_groupor have provably-disjoint audiences.(E007) - Every
dif("<id>", ...)call site maps to an active experiment.(W001, warning)
Every diagnostic code is documented on troubleshooting. --schema-only is reserved for a future fast editor path; today it runs the same full
suite.
Example
$dif validate ✓ all checks passed
$dif validate error E003 dif/experiments/active/bad.md:1:1: `owner` is not a valid email: `not-an-email` help: Use `name@example.com` format. error E008 dif/config.yaml: attribute `plan` has no resolver help: Create `dif/audiences/plan.ts` or run `dif scaffold-audiences`. ✗ 2 error(s), 0 warning(s)
Exit codes
| code | meaning |
|---|---|
0 | no errors (warnings allowed) |
1 | at least one error, or workspace loading failed |
$dif build
Run validate, then compile the runtime artifacts and the agent context file. Aborts
before writing if validation fails.
Flags
| flag | default | notes |
|---|---|---|
--out <dir> | dif/generated | Where to write the TypeScript. Absolute or workspace-relative. Keep it inside the workspace. |
Outputs
<out>/client.ts: registers each active experiment with the SDK by side effect. Deterministic: identical inputs produce byte-identical output.<out>/audiences.ts: theattributes()helper, importing only the resolvers referenced by active experiments. Also deterministic.dif/context.json: the agent context file. Carries a generation timestamp (so it churns on every build) plus a summary of every active experiment, each surface's most recent learning, and project conventions.
Example
$dif build ✓ validated 1 active experiment ✓ client → dif/generated/client.ts ✓ audiences → dif/generated/audiences.ts ✓ context → dif/context.json
Exit codes
| code | meaning |
|---|---|
0 | compiled |
1 | validation, loading, or generation failed |
invariantclient.ts and audiences.ts compile
under tsc --strict and are byte-identical for identical inputs.
$dif qa
Trace one user's assignment chain across every active experiment, and optionally mint a preview link. No exposure events are fired; qa runs entirely against the loaded workspace.
Flags
| flag | default | notes |
|---|---|---|
--user <id> | random | The user id to trace. Omit for a time-seeded synthetic id (so repeated runs sample different buckets). |
--force <exp>=<variant> | — | Force a variant. Repeatable. Bypasses audience and exclusion; emits no exposure. |
--attr <key>=<value> | — | Set an audience attribute. Value parsed as YAML (true, 42, US all keep their type). Repeatable. |
--preview-url <url> | — | Base URL for the preview link emitted when --force is set. |
Example
$dif qa --user u_8131 trace u_8131: • checkout-cta-v2 → variant_a (bucket 7993)
$dif qa --force checkout-cta-v2=variant_a --preview-url https://staging.example.com/checkout trace (synthetic user): • checkout-cta-v2 → variant_a (forced) preview: https://staging.example.com/checkout?_dif=checkout-cta-v2%3Dvariant_a
The preview URL carries a _dif query parameter encoding the forces. The browser SDK
and framework adapters read it and persist the force in a session cookie, so you can share a
screenshot or hand the link to a teammate.
Outcomes
variant (bucket N): bucketed normally; the SDK would fire an exposure for this assignment.variant (forced): picked by--force; bypasses audience.↛ audience miss: user didn't match the audience predicate.↛ exclusion loser (winner: <id>): matched, but a higher-priority experiment in the sameexclusion_groupwon.
Exit codes
| code | meaning |
|---|---|
0 | trace completed |
1 | invalid --force / --attr syntax, malformed YAML, or a workspace
error |
$dif conclude
Conclude an active experiment. Several writes in one transaction:
- Update the frontmatter:
status: active→concluded, setconcluded: <today>. - Replace the
## Decisionsection body. - Move
dif/experiments/active/<id>.md→dif/experiments/concluded/<YYYY-MM>-<id>.md. - Prepend one learning to
dif/surfaces/<surface>.mdunder## Learnings.
Edits are computed before writing; a later filesystem failure rolls back on a best-effort basis.
Flags
| flag | default | notes |
|---|---|---|
<id> | — | Experiment id. Must exist under dif/experiments/active/. |
--decision <text> | $EDITOR | Inline decision text. If omitted, dif opens $EDITOR, then $VISUAL,
then vi. Must be non-empty. |
--skip-learning | off | Don't append to the surface log. Rare; mostly a CI escape hatch. |
Example
$dif conclude checkout-cta-v2 --decision "Shipped variant_a. +2.1% on returning visitors." → moved dif/experiments/concluded/2026-05-checkout-cta-v2.md → drafted ## Decision block → appended to dif/surfaces/home.md "Shipped variant_a. +2.1% on returning visitors."
The first line of the decision becomes the surface log summary, prefixed with the conclusion date
and experiment id. Multi-line decisions stay in the experiment file's ## Decision block; only the first line is duplicated to the surface.
Exit codes
| code | meaning |
|---|---|
0 | concluded |
1 | missing experiment or surface, empty decision, editor failure, or a filesystem error |
invariantafter a successful conclude, dif validate and dif build still pass.
$dif scaffold-audiences
Add the starter audience resolver files to an existing project. Idempotent. Existing files are
kept unless --force is supplied.
Creates dif/audiences/ if needed and writes locale.ts and device_type.ts. It never modifies dif/config.yaml; it prints the
declaration snippet to add under audience_attributes yourself.
$dif scaffold-audiences ✓ wrote dif/audiences/locale.ts ✓ wrote dif/audiences/device_type.ts → add these to dif/config.yaml under audience_attributes: - { name: locale, type: string } - { name: device_type, type: enum, values: [mobile, tablet, desktop] }
Exit codes
| code | meaning |
|---|---|
0 | scaffold completed (including when existing files were kept) |