# Configuration

## Where configuration lives

UltraPi creates its runtime configuration beneath `<PI_CODING_AGENT_DIR>/ultrapi/` on first use. This repository intentionally contains no live profile, credential, key, database, telemetry, or runtime configuration files.

Configuration is versioned rather than overwritten in place:

1. The active champion is identified by `champion.json`.
2. Each config version is an immutable JSON document in `configs/`.
3. A setting command creates the next patch version and repoints the champion.
4. Rollback only repoints the champion to an existing validated version.

Use `/ultra-config config show` to inspect the active configuration and `/ultra-config config rollback <version>` to revert it. Do not edit runtime files directly while Pi is running.

## Profiles

| Profile | Model policy | Setup |
| --- | --- | --- |
| `private` | Only the models this profile declares in `models.tiers` are allowed. | Created by the installer and selected by `pi-private`. |
| `free` | Only the discovered free models this profile declares are allowed; concurrency is capped lower. | Created by the installer and selected by `pi-free`. It fails closed when no critical-capable free model is present. |

Automatic mode requires a valid profile marker in the selected Pi directory. This prevents a profile/config mismatch from silently using a model under the wrong policy.

## Per-project overrides: `.ultra/project.json`

A repository may carry its own overlay at `.ultra/project.json`, read live at dispatch. It can only ever **tighten** what it inherits — a project cannot raise a budget, widen a scope, or re-enable a topology the user's config forbids.

```json
{
  "weeklyCreditBudget": 10,
  "dailyCreditBudget": 3,
  "acceptanceCommand": "npm test",
  "scopePaths": ["src", "tests"],
  "excludePaths": ["src/generated"],
  "forbidTopologies": ["warroom"]
}
```

| Field | Effect |
| --- | --- |
| `weeklyCreditBudget`, `dailyCreditBudget` | Applied only if lower than the configured ceiling. |
| `acceptanceCommand` | Pins the command used to verify, instead of inferring one. |
| `scopePaths` | Narrows what a writer may touch. |
| `excludePaths` | Adds to the exclusions; never removes one. |
| `forbidTopologies` | Removes topologies from automatic selection. |

These values are committed with the repository, so treat them as public. They are also the fields a weekly export redacts — an absolute path or a command with a secret in it does not belong here.

## The model roster

Each profile declares the models it may use. Nothing about the roster is built into UltraPi — any provider works once you name it.

The lists actually shipped are:

```json
{
  "models": {
    "tiers": ["openai-codex/gpt-5.6-luna", "openai-codex/gpt-5.6-terra", "openai-codex/gpt-5.6-sol"]
  }
}
```

and for the free profile, `opencode/big-pickle` plus `opencode/deepseek-v4-flash-free`, with `big-pickle` marked non-critical. Any other roster is equally valid — this one is a starting answer, not a recommendation:

```json
{
  "models": {
    "tiers": ["anthropic/claude-haiku-4-5", "anthropic/claude-sonnet-5", "anthropic/claude-opus-5"],
    "nonCritical": ["anthropic/claude-haiku-4-5"]
  }
}
```

- **`tiers` is ordered weakest to strongest, and the order is load-bearing.** A model's position is its capability rank. Root-model escalation compares ranks, and a model that stops answering is replaced by the next candidate at or above its own rank. Reordering the list changes behaviour; it is not a set.
- **`nonCritical` is optional** and lists models that may scout but may not decide — write code, repair, arbitrate, or act as the root decision model. It must be a subset of `tiers` and must not cover every tier.
- **Anything outside `tiers` is refused**, at config validation, at routing, at the provider request hook, and in the profile launcher's `enabledModels`. Adding a model means declaring its rank; there is no implicit allow.

Declare a roster at install time:

```sh
node --import tsx src/install.ts \
  --weekly-credit-budget 100 --daily-credit-budget 20 --per-task-policy balanced \
  --models anthropic/claude-haiku-4-5,anthropic/claude-sonnet-5,anthropic/claude-opus-5 \
  --non-critical-models anthropic/claude-haiku-4-5
```

The installer assigns roles from the declared ranks: the cheapest model scouts, the second-cheapest is the everyday decision model, the strongest handles deep work and arbitration, and the cheapest model *allowed to decide* does bounded writing and repair.

### What a declared model costs

Budgets are counted in credits. `price-catalog.json` holds credits per 1,000,000 tokens, and a model it does not name is priced from the USD cost the Pi model registry reports for it, at the document's `creditsPerUsd` (default **125**).

125 is the highest credits-per-USD among the three models the shipped catalogue prices — it is a capability-rank curve pinned to the cheapest model's dollar price, not a dollar scale, so per-model the rates are 125, 31.25 and 25. Taking the highest is the conservative direction: the budget bites sooner, and the USD ceiling handed to a delegated run comes out smaller. It also keeps the two inverse, so the ceiling a run is given and the charge it is billed cannot drift apart.

The practical consequence is that **an expensive roster buys fewer turns per task at the same policy**:

For the shipped roster, on the nominal call `price-catalog-coverage` measures:

| model | credits per call | calls at `balanced` |
| --- | ---: | ---: |
| `openai-codex/gpt-5.6-luna` | 0.32 | ~53 |
| `openai-codex/gpt-5.6-terra` | 0.80 | ~21 |
| `openai-codex/gpt-5.6-sol` | 1.60 | ~10 |

Raise `--per-task-policy` (or `/ultra-config policy`) if that is too few — the spawn governor scales with the policy's hard cap, so `quality` and `max` are the dial. `/ultra-config doctor` prints this table for your own roster under `price-catalog-coverage`.

To override a derived price, or to declare a model genuinely free, add it to `models` in `price-catalog.json`; a declared entry always wins. A model that reaches no price from either source is **refused**, naming the model and the file — it is never treated as costing nothing, because a spend cap that silently measures zero is worse than none.

### A different model for a particular lens

Scouts and writers both accept `modelByLens`, keyed on the lens the work is being done through. For a scout the lens is its analytical angle — `flow`, `tests`, `analogues`, or `gap`. For a writer it is the envelope lens, which is the topology (`swarm`, `deep`, `warroom`) or `first-proven-result`.

```json
{
  "boundedWriter": {
    "model": "anthropic/claude-sonnet-5",
    "modelByLens": { "first-proven-result": "anthropic/claude-opus-5" }
  }
}
```

Both maps are empty by default, and deliberately so. A per-lens override can only pay for itself if you can measure that it did, and this repository publishes no measured outcome numbers; collect your own with `npm run benchmark:collect` before assuming an override is cheaper. An override is validated against the declared roster like any other model, and a writer's override must additionally be a model the roster allows to decide, because a writer decides.

### Rotation when a provider goes quiet

A provider that stops answering takes its agents down silently, and a whole branch of a run can die without producing anything. When a scout or reviewer fails for a reason the provider owns — `rate-limited`, `provider-5xx`, `timeout`, or `model-unavailable` — the same task is retried once on the next declared model at or above the failed model's rank, and the run continues on it.

Rotation is deliberately narrow:

- **Only provider failures rotate.** A result-contract violation is the model's actual answer and goes to the existing correction path; an authentication failure is a configuration problem you need to see; an abort was deliberate.
- **One retry per agent**, and only to a model the roster declares. A roster with a single tier never rotates.
- **The fallback re-enters budget admission.** If it cannot be afforded, `model.rotation.blocked` is recorded and the original failure stands — a refused rotation never ends a run that is still delivering.

Both outcomes are visible in telemetry as `model.rotated` and `model.rotation.blocked`, each carrying the node, role, both models, and the error class.

## Modes

`mode` controls the default topology requested by `/ultra`:

| Value | Behaviour |
| --- | --- |
| `auto` | The root proposes the smallest bounded topology and the controller validates or overrides it. Default. |
| `direct` | Prefer root-only local work. |
| `scout` | Prefer read-only evidence gathering before root synthesis. |
| `swarm` | Prefer scout evidence plus one delegated writer. |
| `deep` | Prefer a deeper bounded writer flow. |
| `warroom` | Prefer a typed multi-perspective review before synthesis. |

Mode is a request, not an authority bypass. High-risk, high-coupling, repeated-failure, or live-dependency conditions can force a deeper route. Conversely, high context pressure can block a wide swarm.

## Policies and budget caps

Policy controls the hard spending cap and worker limits for each task. Credits are estimated from the local price catalogue and Pi usage telemetry.

| Policy | Soft cap | Internal stop target | Hard cap | Initial scouts | Max parallel | Max total workers | Repair cycles |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| `economy` | 4 | 7 | 8 | 1 | 2 | 4 | 1 |
| `balanced` | 10 | 17 | 20 | 2 | 4 | 8 | 2 |
| `quality` | 25 | 43 | 50 | 3 | 6 | 12 | 3 |
| `max` | 60 | 100 | 120 | 3 | 8 | 16 | 4 |

Weekly and daily credit caps are an additional admission gate for automatic execution. Set them with:

```text
/ultra-config budget 20 5
```

`/ultra-config budget acknowledge` is an explicit one-task acknowledgement, not a persistent replacement for normal budget controls.

## Config field groups

The active config is validated strictly at load time. The important field groups are:

| Group | Controls |
| --- | --- |
| `profile`, `mode`, `policy` | Identity and routing default. |
| `budgets` | Weekly/daily admission caps and acknowledgement. |
| `root`, `scout`, `boundedWriter`, `repair`, `deep`, `arbitration` | Model and thinking-level selection by role. |
| `warRoom` | Whether automatic War Room is allowed (`autoEnabled`), rounds (`maxRounds`), on-demand specialists (`maxSpecialists`), and per-member messages (`maxMessagesPerMember`). There is no member-count field: the three lenses are fixed, and a config carrying `maxMembers` is rejected outright. |
| `piAgentsBudgets` | Agent count, parallelism, iterations, depth, turns, and optional cost cap. |
| `context` | Envelope target/hard caps, compaction threshold, and wide-swarm context threshold. |
| `telemetry` | Raw-vault enablement plus raw and analytics retention windows. |
| `experiment` | Challenger allocation and stop-loss thresholds. |
| `compatibility` | Pi and `pi-agents` version pins. |

The default `maxDepth` is `1`: delegated workers are leaves and cannot create another agent tree.

## Controlled experiments

UltraPi can apply a reviewed recommendation to a new immutable challenger version. It can then run one challenger experiment at a time, assign a bounded fraction of eligible requests, and stop on safety incidents, success drops, rework, cost increases, corrupted samples, or provider drift.

This is intentionally a controlled loop, not self-modification:

- A recommendation must be reviewed and explicitly accepted.
- A challenger is versioned before it runs.
- Promotion is a deliberate `/ultra-config experiment promote <id>` action.
- Routing-regret metrics are candidates for review, never an automatic verdict.

See [Weekly review](./WEEKLY_REVIEW.md) and [Routing Regret](./ROUTING_REGRET.md) for the review process.

## Compatibility changes

Treat model roster, profile policy, configuration schema, and version-pin changes as safety-sensitive. After changing dependencies or upgrading Pi, run:

```sh
npm run verify
```

Then launch the target profile and run `/ultra-config doctor`. Do not assume a TypeScript-only check proves runtime compatibility with Pi or a provider.
