# cli v0.1

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.

dif init [--surface <name>] [--force] [--no-agent-files] [--json]

Flags

flagdefaultnotes
--surface <name>homeName of the first surface to create.
--forceoffOverwrite dif-owned structural files and generated skill files.
--no-agent-filesoffSkip 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

codemeaning
0workspace created
2existing structural files were found without --force
1an 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.

dif new <id> --surface <name> [--owner <email>] [--from <experiment-id>] [--json]

Flags

flagdefaultnotes
<id>Kebab-case experiment id. Becomes the filename stem.
--surface <name>Required. Must exist under dif/surfaces/.
--owner <email>git config user.emailFalls 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

codemeaning
0drafted
1can't determine owner; pass --owner
2id conflict (already exists) or --from source not found
3surface 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.

dif validate [--schema-only] [--json]

The validator reads:

  • dif/config.yaml;
  • every .md in dif/experiments/active/ and concluded/;
  • every .md in dif/surfaces/;
  • the resolver filenames in dif/audiences/;
  • .js / .jsx / .ts / .tsx source for dif("<id>", ...) call sites (skipping .git, node_modules, target, dist, build, and dif/).

Checks, in order:

  1. Frontmatter parses; required fields present. (E001)
  2. owner is a syntactically valid email. (E003)
  3. surface resolves to a loaded surface file. (E004)
  4. Variant weights sum to 100. (E005)
  5. Every audience attribute is declared in dif/config.yaml. (E006)
  6. Every declared attribute has a resolver in dif/audiences/. (E008)
  7. Same-surface experiments share an exclusion_group or have provably-disjoint audiences. (E007)
  8. 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

codemeaning
0no errors (warnings allowed)
1at 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.

dif build [--out <dir>] [--json]

Flags

flagdefaultnotes
--out <dir>dif/generatedWhere 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: the attributes() 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

codemeaning
0compiled
1validation, 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.

dif qa [--user <id>] [--force <exp>=<variant>]... [--attr <key>=<value>]... [--preview-url <url>] [--json]

Flags

flagdefaultnotes
--user <id>randomThe 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 same exclusion_group won.

Exit codes

codemeaning
0trace completed
1invalid --force / --attr syntax, malformed YAML, or a workspace error

$dif conclude

Conclude an active experiment. Several writes in one transaction:

  1. Update the frontmatter: status: activeconcluded, set concluded: <today>.
  2. Replace the ## Decision section body.
  3. Move dif/experiments/active/<id>.mddif/experiments/concluded/<YYYY-MM>-<id>.md.
  4. Prepend one learning to dif/surfaces/<surface>.md under ## Learnings.

Edits are computed before writing; a later filesystem failure rolls back on a best-effort basis.

dif conclude <id> [--decision <text>] [--skip-learning] [--json]

Flags

flagdefaultnotes
<id>Experiment id. Must exist under dif/experiments/active/.
--decision <text>$EDITORInline decision text. If omitted, dif opens $EDITOR, then $VISUAL, then vi. Must be non-empty.
--skip-learningoffDon'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

codemeaning
0concluded
1missing 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.

dif scaffold-audiences [--force] [--json]

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

codemeaning
0scaffold completed (including when existing files were kept)