# roscoe.yaml

`roscoe.yaml` is Roscoe's configuration file. It declares the AI models the editor exposes, optionally pins server ports, and sets default retry / timeout values for executors that don't override them. Reach for it when you want to add a new model to the picker, run multiple Roscoe installs side-by-side on custom ports, or change the default retry budget for AI nodes globally.

You don't write it from scratch. `roscoe init` seeds a fully-commented template, and everything below is optional; uncomment the keys you want to override.

## Where it lives

Roscoe reads two files and merges them:

| Source  | Path                         | Purpose                                        |
| ------- | ---------------------------- | ---------------------------------------------- |
| Global  | `~/.roscoe/roscoe.yaml`      | Personal defaults, applied to every project    |
| Project | `<repo>/.roscoe/roscoe.yaml` | Team-shared overrides, committed with the repo |

**Project values win over global**, key by key. Sections (`models`, `ports`, `defaults`) merge shallowly; arrays inside a section (e.g. `models.available`) replace wholesale rather than concatenating. If you set `models.available` in the project file, the global list is ignored: you're declaring the full team-allowed set.

`roscoe init` creates both files when run inside a git repo (with all keys commented out so the implicit defaults apply). Edit them by hand, or use the **Settings** page (`/settings`) in the web UI (see [/docs/running/web-ui](/docs/running/web-ui)). The file watcher picks up changes on the next reload and broadcasts an SSE event so open browser tabs refresh without a manual reload.

## Schema

The file is parsed strictly: unknown keys fail validation. Every section is optional; omit a section to fall back to defaults.

### `models`

Controls the model picker in the workflow editor and the runner's model allowlist. The runner refuses to execute a workflow whose `ai_agent`, `ai_judge`, `consensus`, or `map` node references a model not in `available`.

| Key         | Type       | Constraint                 |
| ----------- | ---------- | -------------------------- |
| `available` | `string[]` | At least one entry         |
| `default`   | `string`   | Must appear in `available` |

### `ports`

Override the ports each Roscoe surface listens on. Each is an integer in `1..65535`.

| Key     | Service                              | Env equivalent    |
| ------- | ------------------------------------ | ----------------- |
| `api`   | `apps/server` REST API (dev mode)    | `ROSCOE_API_PORT` |
| `web`   | `apps/web` Vite dev server           | `ROSCOE_WEB_PORT` |
| `serve` | `roscoe serve` combined-port default | `ROSCOE_PORT`     |

Precedence per service: **CLI flag > env var > `roscoe.yaml` > built-in fallback**. See [/docs/configuration/environment](/docs/configuration/environment).

### `defaults`

Defaults applied to executors that don't set their own value.

| Key          | Type      | Constraint | Default  | Notes                                                                                                                                                                                                                                           |
| ------------ | --------- | ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `maxRetries` | `integer` | `0..5`     | `1`      | Per-AI-node attempt budget on transient errors and validator misses. Set on a node directly to override.                                                                                                                                        |
| `timeoutMs`  | `integer` | `>= 1000`  | `300000` | Per-request ceiling for the underlying LLM call (SDK or CLI). Applies to `ai_agent`, `ai_judge`, and per-agent inside `consensus`. Override per node with `timeoutSeconds`. The Settings UI accepts this in seconds and stores as milliseconds. |

`timeoutMs` is a total per-request budget: the SDK is configured with
`maxRetries: 0` so the configured value is not stacked. The executor's
own `withTimeout` wraps each backend call with a 5 s grace over
`timeoutMs` so that a hang surfaces as `Node "X" exceeded timeoutMs of N`
(canonical executor error) rather than a backend-specific message.

For `consensus`, `map`, and `round_robin`, the per-agent (or per-branch,
per-match) deadline defaults to `min(120_000, timeoutMs)`. Each sub-call gets
at most 2 minutes by design, capped further if you've configured a lower
default. A node-level `timeoutSeconds` overrides this entirely; it's the only
way to grant a sub-call more than 120s.

Any AI node (`ai_agent`, `ai_judge`, `consensus`) can override `timeoutMs`
for itself with a `timeoutSeconds` field (see the per-node docs). This is the
knob to raise for long-running steps such as tool-enabled
[`ai_agent`](/docs/nodes/ai-agent#tool-enabled-agents) edits.

### `updates`

Controls the auto-update behaviour of `roscoe serve`. Defaults are tuned for
the common case (check every 6 hours, pre-download in the background); set
`channel: off` to opt out entirely on a CI host or a developer machine
that you don't want auto-fetching binaries.

| Key                  | Type    | Default    | Constraint              | Behaviour                                                                                                                                                        |
| -------------------- | ------- | ---------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `channel`            | `enum`  | `'stable'` | `stable \| beta \| off` | Release channel. `off` disables the manifest poll entirely. `beta` is reserved for a future opt-in pre-release track.                                            |
| `autoDownload`       | `bool`  | `true`     | —                       | When a newer release is found, download + sha256-verify the binary into `~/.roscoe/updates/pending/roscoe` so install is a near-instant rename.                  |
| `checkIntervalHours` | `float` | `6`        | `0.01 .. 168`           | How often `roscoe serve` polls the release manifest. The actual interval is jittered by about ±8% (±30 min at the default 6h) to avoid release-day herd effects. |

Override globally for one-off runs with `ROSCOE_DISABLE_UPDATE_CHECK=1`
(stops the poll without editing config).

### `analytics`

Controls product telemetry through two **independent** switches, both **on by
default**. Toggling one never affects the other.

`enabled` controls **usage data**: coarse, categorical signals about how
workflows run — whether a workflow came from a repo or the global library, how a
run was started, whether it finished, and bucketed duration/cost ranges. Usage
data never sends workflow names, prompts, node names, file paths, repo or project
names, git refs, or any content, and every event suppresses your IP address.

`crashReports` controls **crash reports** — sent when Roscoe hits an error, so
bugs can be found and fixed. A crash report carries the error type and message;
file paths and other PII are stripped before sending, but an error message can
occasionally name a workflow or node.

The identifier on every event is a random per-install id stored in
`~/.roscoe/analytics-id` (not derived from anything about you), so the data is
minimized and pseudonymous — not "anonymous". Turn either off in Settings →
Telemetry, or here.

| Key            | Type   | Default | Constraint | Behaviour                                                          |
| -------------- | ------ | ------- | ---------- | ------------------------------------------------------------------ |
| `enabled`      | `bool` | `true`  | —          | Set to `false` to stop usage data (does not affect crashes).       |
| `crashReports` | `bool` | `true`  | —          | Set to `false` to stop crash reports (does not affect usage data). |

Environment overrides always win over these settings: `DO_NOT_TRACK=1` or
`ROSCOE_ANALYTICS=off` hard-disable **both** regardless of config.

### `pricing`

Per-model list-price overrides used to cost runs. Roscoe records each run's cost:
for a `claude -p` (CLI / subscription) call it uses the CLI's own cache-aware
`total_cost_usd`; for a metered API (SDK) call, and for the subscription
API-equivalent ("would've cost $X") estimate, it prices the token usage against
these rates. Built-in defaults ship for the current Claude families, so this
section is **optional**: set it only to correct a rate or price a model Roscoe
doesn't know yet. An unknown model is estimated at the Sonnet rate and logs a
one-time warning until you add it here. Edit these in the Settings → Models
table or directly in YAML.

`pricing` is an object keyed by model id (matched case-insensitively as a
substring, so `claude-haiku-4-5` covers `claude-haiku-4-5-YYYYMMDD`). Each value
is `{ input, output }` in **USD per million tokens**.

| Key      | Type     | Constraint | Notes                    |
| -------- | -------- | ---------- | ------------------------ |
| `input`  | `number` | `>= 0`     | USD per 1M input tokens  |
| `output` | `number` | `>= 0`     | USD per 1M output tokens |

```yaml
pricing:
  claude-opus-4-8:
    input: 15
    output: 75
```

Cache read/write multipliers are not user-configurable. See
[/docs/running/cli](/docs/running/cli#spending-caps) for capping a run's cost.

## Worked example

```yaml
# yaml-language-server: $schema=./roscoe-config.schema.json
#
# Roscoe project configuration. All keys are optional — uncomment to override.
# Project-level (.roscoe/roscoe.yaml) values win over global (~/.roscoe/roscoe.yaml).

models:
  available:
    - claude-haiku-4-5
    - claude-sonnet-5
    - claude-opus-4-8
  default: claude-sonnet-5

ports:
  api: 8080 # apps/server (dev)
  web: 3000 # apps/web Vite dev server
  serve: 7777 # roscoe serve combined-port default

defaults:
  maxRetries: 1
  timeoutMs: 300000

updates:
  channel: stable # 'stable' | 'beta' | 'off'
  autoDownload: true
  checkIntervalHours: 6

analytics:
  enabled: true # usage data; false stops it (crash reports unaffected)
  crashReports: true # crash reports; false stops them (usage data unaffected)

pricing:
  # USD per 1M tokens; overrides the built-in defaults for run costing.
  claude-opus-4-8:
    input: 15
    output: 75
```

The seeded template ships with every key commented out so a fresh install behaves identically to a missing file. Uncomment only the lines you actually want to pin.

## See also

- [/docs/configuration/environment](/docs/configuration/environment) — env var overrides for ports and paths.
- [/docs/configuration/llm-backends](/docs/configuration/llm-backends) — how the model field is dispatched at runtime.
- [/docs/configuration/workflow-sources](/docs/configuration/workflow-sources) — the same global / project layering applied to workflow files.
- [/docs/running/web-ui](/docs/running/web-ui) — editing `roscoe.yaml` from the Settings page.
