# CLI.

Eight 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] [--agents <claude,general,cursor,none>] [--json]`

### 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. |
| `--agents <list>` | all three | Comma-separated: `claude` (`CLAUDE.md` + `.claude/skills/dif-*`), `general` (`AGENTS.md`), `cursor` (`.cursorrules`), or `none`. Omit to scaffold all three. `none` can't be combined with the others. `--no-agent-files` still works as a deprecated alias for `--agents none`. |

### 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 `--agents none` to skip them entirely, or a subset like `--agents claude,cursor` to scaffold only those. `--no-agent-files` still works — it's a deprecated alias for `--agents none`.

### Exit codes

| code | meaning |
| --- | --- |
| `0` | workspace created |
| `2` | existing structural files were found without `--force` |
| `1` | an unexpected filesystem or command error |

invariant a fresh `dif init` followed by `dif validate` and `dif build` passes with zero experiments.

## $dif connect

Point an existing workspace at dif.sh Cloud. Writes the publishable key into the `events:` block of `dif/config.yaml` and forces cloud mode, preserving every other setting and comment. Doesn't build — `dif build` regenerates the client with the new key.

`dif connect --key <dif_pk_…> [--url <url>] [--json]`

### Flags

| flag | default | notes |
| --- | --- | --- |
| `--key <dif_pk_…>` | — | Required. The publishable key from your dif.sh Cloud project. Rejected if it's empty, malformed, or looks like a secret server key (`dif_live_…` / `dif_test_…`). |
| `--url <url>` | `https://cloud.dif.sh` | Only needed for a self-hosted deployment. |

### Example

```
$dif connect --key dif_pk_live_…
✓ connected dif/config.yaml to dif.sh Cloud
  next: run dif build to regenerate the client with your key
```

If the workspace was in `mode: custom`, connect switches it to cloud and warns that `dif/events/{exposure,track}.ts` are no longer used. See [events](https://dif.sh/docs/events/) for both modes.

### Exit codes

| code | meaning |
| --- | --- |
| `0` | connected |
| `1` | missing or malformed `--key`, or a filesystem error |
| `2` | no dif.sh workspace found here (run `dif init` first) |

## $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

| 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 |

invariant the 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](https://dif.sh/docs/troubleshooting/#diagnostics). `--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.

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

### 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`: 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

| code | meaning |
| --- | --- |
| `0` | compiled |
| `1` | validation, loading, or generation failed |

invariant `client.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

| 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 same `exclusion_group` won.

### 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:

1.  Update the frontmatter: `status: active` → `concluded`, set `concluded: <today>`.
2.  Replace the `## Decision` section body.
3.  Move `dif/experiments/active/<id>.md` → `dif/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

| 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 |

invariant after 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

| code | meaning |
| --- | --- |
| `0` | scaffold completed (including when existing files were kept) |
