# frites

<div align="center"><img src="/files/MeN28Ym2rpN0Npxj8i5N" alt="french fries, nothing better, full stop" width="96"></div>

*frites: a coordinating ensemble proxy for Claude Code & Codex.*

Point your Claude Code or Codex at frites and go. Every prompt is answered by a **council of agents** instead of one. frites fans the prompt out to multiple models, has them work independently, then synthesizes a single vetted answer, using the subscriptions you're **already logged into** (no API keys). It decides per-prompt whether fanning out is even worth the spend. The bet is that a cross-checked council yields better output than any single agent; the cost is latency and metered spend (see [the tradeoff](/architecture/risks-and-tradeoffs)).

### Two ways to use it

* **Gateway mode (transparent proxy)**. Zero friction: run it once and *every* prompt goes through the council. It handles Q\&A, reasoning, **and** code edits by emitting the tool calls your host runs.
* **MCP worktree mode**. For when you want N **competing** full implementations run in isolated git worktrees, with your test suite picking the winner, yielding one vetted diff to apply.

### Where to start

| I want to…                    | Go to                                           |
| ----------------------------- | ----------------------------------------------- |
| Install                       | [Installation](/getting-started/installation)   |
| Use the gateway               | [Gateway mode](/product/gateway-mode)           |
| Run competing implementations | [MCP worktree mode](/product/mcp-worktree-mode) |
| Configure                     | [Configuration](/reference/configuration)       |
| Understand the design         | [Architecture overview](/architecture/overview) |
| Safety                        | [Safety model](/product/safety-model)           |
| Current status                | [Current status](/roadmap/current-status)       |

### Repository and license

frites is an Apache-2.0 licensed open-source monorepo. See the [repository structure](/development/repository-structure) for how the packages fit together, and consult the repository root [LICENSE](https://github.com/whatl3y/frites/blob/main/LICENSE/README.md) for the full license text.


# Installation

frites is a coordinating ensemble proxy for Claude Code and Codex. You point your existing agent at frites, and every prompt is answered by a council of agents instead of one, using the subscriptions you are already logged into (no API keys).

The fastest path is the always-on transparent-proxy gateway. Install it once, point your editor at it, and every prompt flows through the council from then on.

## Prerequisites

* **`claude` and/or `codex` installed and logged in.** frites drives the agents you already have. Children use the accounts you are already authenticated against: Claude keychain OAuth, Codex ChatGPT sign-in. No API keys are required. (For how auth and billing work, see [auth and billing](/product/auth-and-billing).)
* **Node.js >= 22.**
* **macOS**, or a major Linux distribution with **systemd user services**.

## Install

```bash
npm install -g @frites/cli
frites install
```

`frites install` starts the transparent-proxy gateway on `http://127.0.0.1:6767` as an always-on background service.

* On **macOS**, it writes a launchd user agent.
* On **Linux**, it writes and enables a `systemd --user` unit.

In both cases the service auto-starts on login and restarts on crash. To install on a different port, pass `--port`:

```bash
frites install --port 7000
```

For the full set of install/status/restart/stop/uninstall commands and the launchd vs systemd details, see [service management](/getting-started/service-management).

## What the gateway does

The gateway is a transparent proxy: it impersonates the model endpoint your editor talks to and intercepts every prompt with zero "use frites" friction. It handles Q\&A and reasoning turns, and it drives the host's full agentic loop by emitting the `Read` / `Edit` / `Bash` tool calls your editor executes on the real files. For each prompt it decides whether fanning the request out to a council is worth the spend, runs the agents independently, then synthesizes a single vetted answer.

## Idle costs nothing

The service is always running, but it only spends when you send prompts. While idle it sits on `127.0.0.1` waiting, so idle = $0. Cost scales with how often you fan out (see [cost telemetry](/concepts/cost-telemetry)).

## Next steps

Point your editor at the gateway, then open a new session:

* [Configure Claude Code](/getting-started/configure-claude-code): set `ANTHROPIC_BASE_URL` and `ANTHROPIC_AUTH_TOKEN`.
* [Configure Codex](/getting-started/configure-codex): add the `frites` model provider to `~/.codex/config.toml`.
* [First run](/getting-started/first-run): confirm reachability and watch the council work on your first request.
* [Service management](/getting-started/service-management): install, status, restart, stop, and uninstall.


# Configure Claude Code

To route Claude Code through the frites gateway, point its model endpoint at the local gateway URL. frites impersonates the Anthropic endpoint, so Claude Code talks to frites exactly as it would to `api.anthropic.com`.

## Settings

Add the following `env` block to `~/.claude/settings.json`:

```json
{ "env": { "ANTHROPIC_BASE_URL": "http://127.0.0.1:6767", "ANTHROPIC_AUTH_TOKEN": "frites" } }
```

* **`ANTHROPIC_BASE_URL`**: the gateway URL. Use `http://127.0.0.1:6767` for the default port. If you installed the service on a different port (`frites install --port 7000`), use that port instead.
* **`ANTHROPIC_AUTH_TOKEN`**: set to `frites`. The gateway binds to `127.0.0.1` only and does not validate this token against an upstream account; child agents authenticate using the accounts you are already logged into (see [auth and billing](/product/auth-and-billing)).

## Open a new session

Claude Code reads `~/.claude/settings.json` when a session starts, so **open a new session** after editing the file. Existing sessions keep their old endpoint until restarted. From then on, every prompt in that session flows through the frites council.

## Next steps

* [First run](/getting-started/first-run): confirm the gateway is reachable and watch the council work.
* [Configure Codex](/getting-started/configure-codex): if you also use Codex.
* [Service management](/getting-started/service-management): managing the always-on gateway.


# Configure Codex

To route Codex through the frites gateway, register frites as a model provider in `~/.codex/config.toml` and select it. frites impersonates the provider endpoint, so Codex talks to the local gateway as it would to a normal provider.

## Settings

Add the following to `~/.codex/config.toml`:

```toml
model_provider = "frites"
[model_providers.frites]
base_url = "http://127.0.0.1:6767/v1"
wire_api = "responses"
env_key = "FRITES_KEY"
```

Then export the key Codex will send:

```bash
export FRITES_KEY=frites
```

* **`base_url`**: the gateway's `/v1` base. Use `http://127.0.0.1:6767/v1` for the default port; if you installed on a different port (`frites install --port 7000`), use that port.
* **`wire_api = "responses"`**: Codex talks to the gateway over the `/v1/responses` surface.
* **`env_key = "FRITES_KEY"`**: names the environment variable Codex reads to obtain the auth token it sends.

## FRITES\_KEY is read by Codex, not by frites

`FRITES_KEY` is consumed by **Codex** because you named it in `env_key`; frites itself does not read it. It is simply the token Codex presents to the gateway. The gateway binds to `127.0.0.1` only and does not validate it against an upstream account. Child agents authenticate with the accounts you are already logged into (see [auth and billing](/product/auth-and-billing)). For the full list of variables that frites itself reads, see [environment variables](/reference/environment-variables).

## Next steps

* [First run](/getting-started/first-run): confirm the gateway is reachable and watch the council work.
* [Configure Claude Code](/getting-started/configure-claude-code): if you also use Claude Code.
* [Service management](/getting-started/service-management): managing the always-on gateway.


# First run

Once the gateway is installed and your editor is pointed at it, your next prompt is answered by the council. This page walks through confirming reachability and reading the live progress on your first request.

## Confirm the gateway is reachable

Before sending a prompt, check that the service is installed, loaded, and responding:

```bash
frites status
```

`frites status` reports three things: whether the service file is installed, whether the service manager has it loaded (launchd on macOS, systemd on Linux), and whether the gateway is reachable over HTTP. It probes `http://127.0.0.1:6767/v1/models` and prints `reachable ✓` on success. If it is not reachable, see [service management](/getting-started/service-management).

## Send the request and watch live progress

Open a new session in Claude Code or Codex (so it picks up the gateway endpoint) and send a prompt. While the turn runs, frites streams live progress on the host's *thinking* channel (Claude) or *reasoning* channel (Codex), visually separate from the answer, so it never pollutes the result or the next turn.

By default the panel shows per-agent **telemetry**: which agents frites is consulting, a live per-agent counter (tokens streamed so far plus elapsed time) that climbs as each child works, and when each one finishes (with duration, tokens, and cost), then synthesis. A **heartbeat** line (`still working — Ns elapsed`) keeps a long multi-model turn from ever looking stuck.

How the result lands depends on the turn:

* A **tool-bearing turn** (the usual Claude Code agentic loop) runs the whole council on the thinking channel, closes with a one-line **council recap**, and then emits the synthesized tool call or answer.
* A **pure answer turn** (no tools: Q\&A, the Codex/Responses surface) instead streams the final answer live, token by token, as the synthesizer produces it.

This channel is live and per-turn. It shows what is happening right now, and most editors collapse it once the turn ends. It is the "is it working?" view, not a durable record. For the full after-the-fact detail of any turn, read the gateway log (see [logging](/reference/logging)).

## What the council recap means

On a tool-bearing turn, frites closes with a one-line council recap, for example:

```
◆ council recap — N agents + synth · 18.3s · $0.072
```

It summarizes the turn just completed: how many agents were consulted plus the synthesizer (`N agents + synth`), the wall-clock duration (`18.3s`), and the total metered spend for the turn (`$0.072`). For how spend is measured and estimated per backend, see [cost telemetry](/concepts/cost-telemetry).

## Not every turn shows the whole council

Seeing a single agent on some follow-up turns is expected, not a bug. With the default `fanOutScope: first-turn`, only the substantive request turn fans out; the mechanical tool-loop steps that follow run a single agent, so you may see `single agent — tool-loop continuation` on those turns. The host's background and utility calls (titles, summaries, topic detection) also always run a single agent.

## Next steps

* [Cost telemetry](/concepts/cost-telemetry): what the per-agent costs and recap totals mean.
* [Logging](/reference/logging): the durable, after-the-fact log of every turn.
* [Service management](/getting-started/service-management): restart, stop, and uninstall the gateway.


# Service management

The frites gateway runs as an always-on background service. `frites install` sets it up; the commands below manage its lifecycle. For the complete CLI command and flag list, see [the CLI reference](/reference/cli).

## Commands

```bash
frites install             # install/start the gateway service
frites install --port 7000 # install/start on a different port
frites status              # installed? loaded? reachable?
frites restart             # restart after config changes or upgrades
frites stop                # remove the background service
frites uninstall           # same as stop
```

* **`frites install`**: installs and starts the service on `http://127.0.0.1:6767`. It auto-starts on login, restarts on crash, and idle costs nothing.
* **`frites install --port <N>`**: installs on a different port. Use the same port in your editor config and in `frites status`.
* **`frites status`**: reports whether the service file is installed, whether the service manager has it loaded, and whether the gateway is reachable over HTTP (it probes `http://127.0.0.1:<port>/v1/models`).
* **`frites restart`**: restart the service, e.g. after changing config or upgrading `@frites/cli`. Errors if the service is not installed.
* **`frites stop`** / **`frites uninstall`**: remove the background service. These are aliases for the same action.

## Platform behavior

`frites install` adapts to your OS. macOS and systemd Linux are supported; on any other OS the service commands exit with an error and direct you to run `frites gateway` in the foreground instead.

### macOS: launchd

On macOS, `frites install` writes a launchd user agent (`com.frites.gateway`) to `~/Library/LaunchAgents/com.frites.gateway.plist` and loads it. The agent runs at load and is kept alive, so it auto-starts on login and restarts on crash. `frites status` reports the plist path and the `launchctl list` line for the agent. `frites restart` unloads and reloads the agent; `frites stop` unloads and removes the plist.

### Linux: systemd --user

On Linux, `frites install` writes a `systemd --user` unit (`frites-gateway.service`) to `~/.config/systemd/user/frites-gateway.service`, then runs `systemctl --user daemon-reload` and `systemctl --user enable --now`. The unit uses `Restart=always`, so it restarts on crash, and it is wired to `default.target` so it starts on login. `frites status` reports the unit path plus its `is-active` / `is-enabled` state. `frites restart` runs `systemctl --user restart`; `frites stop` disables and removes the unit, then reloads the daemon.

In both cases the service writes its logs under `~/.frites/` (`gateway.log` for output, `gateway.err` for crashes).

## Compatible `frites service ...` form

Every management command above also has a longer, equivalent form under `frites service`, which remains supported for compatibility:

```bash
frites service install [--port N]
frites service status
frites service restart
frites service uninstall
frites service logs
```

The direct commands (`frites install`, `frites status`, and so on) are the intended UX; the `frites service <...>` form does the same thing. See [the CLI reference](/reference/cli) for the full command and flag list.


# Overview

frites is a coordinating ensemble proxy for Claude Code and Codex. Point your existing agent at frites and every prompt is answered by a **council of agents** instead of one: frites fans the prompt out to multiple models, has them work independently, then synthesizes a single vetted answer, using the subscriptions you are **already logged into** (no API keys). It decides per-prompt whether fanning out is even worth the spend.

## The problem it solves

The host CLI (Claude Code or Codex) is already a capable single agent with a tool loop over your files. A single agent, however, gives you one attempt and one perspective. frites' bet is that a cross-checked council of independent agents (different model families and prompt framings, filtered and synthesized) yields a more correct and complete result than any single agent. Diversity comes from mixing model families (`claude` × `codex`) and prompt framing, not from a temperature knob (neither CLI exposes one).

The cost of that bet is latency and metered spend: more agents run, and programmatic use draws on metered usage rather than free interactive limits. This is the core "better output, slower" tradeoff. See [risks and tradeoffs](/architecture/risks-and-tradeoffs) for the canonical discussion.

## Two ways to use it

frites ships two surfaces over one shared engine for two different needs.

| Surface                                         | What it is                                                                                                                                                   | When to use it                                                                                 |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- |
| [Gateway mode](/product/gateway-mode)           | A transparent proxy you point your agent at. It intercepts *every* prompt (Q\&A, reasoning, and code edits) with zero "use frites" friction. **Start here.** | Everyday work: the frictionless default for handling everything.                               |
| [MCP worktree mode](/product/mcp-worktree-mode) | An on-demand MCP tool that runs N **competing** full implementations in isolated git worktrees, with your test suite picking the winner.                     | Heavy code edits where you want N full implementations filtered by tests into one vetted diff. |

Gateway mode is the primary, everyday surface: run it once and every prompt goes through the council, including plain Q\&A and code edits (it emits the `Read` / `Edit` / `Bash` tool calls your host executes). MCP worktree mode is the deliberate, heavier path for when correctness matters more than latency: N implementations run to completion, your tests filter them, and you get one verified diff to apply to a fresh branch.

## The council bet

The value of frites is reconciliation quality: many independent attempts, filtered by execution and adjudication rather than by vibes. Whether to fan out at all is itself gated by policy and prompt classification, so the council runs where it earns its cost. In gateway mode, quality is grounded in an LLM synthesizer adjudicating independent proposals; in worktree mode, quality is grounded in **running your tests**: the result is verified, not just adjudicated.

For the next level of detail, continue to [Gateway mode](/product/gateway-mode) or [MCP worktree mode](/product/mcp-worktree-mode).


# Gateway mode

Gateway mode is the primary, everyday surface. frites runs as a transparent proxy that impersonates the model endpoint (`ANTHROPIC_BASE_URL` for Claude Code, the provider `base_url` for Codex) and intercepts **every** prompt with zero "use frites" friction. Run it once and every prompt goes through the council.

## A transparent proxy for everything

The gateway handles **both** everyday Q\&A / reasoning **and** code edits. It does not edit files directly: on a coding turn it has the council decide the next action, then emits the normal `Read` / `Edit` / `Bash` `tool_use` your host executes against the real files under the host's own permission model. This is verified end-to-end (a real `claude` client through the gateway has read, edited, and fixed a bug with the tests passing) and it runs with **no API key** (subscription `claude -p` children decide the action; the gateway constructs the `tool_use` envelope).

## Fan-out behavior

For each intercepted prompt, frites decides whether fanning out is worth the metered spend, then runs N child agents independently and synthesizes their work into one result. Two levers shape this:

* **Whether** a turn fans out is governed by `fanOutPolicy`. See [fan-out policy](/concepts/fan-out-policy).
* **Which** turns of a request may fan out is governed by `fanOutScope`. See [fan-out scope](/concepts/fan-out-scope). By default (`first-turn`), only the substantive request turn fans out a full council; the mechanical tool-loop continuations that follow run a single agent, and the host's background/utility traffic (haiku-tier title, summary, and topic-detection calls) always runs a single agent regardless.

Because of this scoping, **not every turn shows the whole council**. Seeing `single agent — tool-loop continuation` on follow-up turns is expected, not a bug.

## Answer turns vs tool turns

How the result lands depends on the turn:

* **Tool-bearing turns** (the usual Claude Code agentic loop) run the whole council on the host's *thinking* / *reasoning* channel, close with a one-line council recap (`◆ council recap — N agents + synth · 18.3s · $0.072`), then emit the synthesized tool call or answer when it resolves. Tool actions are **selected, not merged**: the synthesizer picks exactly one proposed tool call verbatim rather than blending inputs.
* **Pure answer turns** (no tools: Q\&A, the Codex/Responses surface) instead **stream the final answer live**, token by token, as the synthesizer produces it.

Either way, the progress channel is visually separate from the answer and never pollutes it or the next turn.

## Live progress stream

frites is deliberately verbose so you can see the council working. While a turn runs it streams live progress on the host's thinking (Claude) / reasoning (Codex) channel: which agents it is consulting, a live per-agent counter (tokens streamed and elapsed time) that climbs as each child works, when each finishes (with duration, tokens, and cost), synthesis, and a "still working — Ns elapsed" heartbeat so a long multi-model turn never looks stuck.

This channel is **live and per-turn**: it shows what is happening right now, and most editors collapse it once the turn ends, so it is the "is it working?" view, not a durable record. By default the panel shows per-agent telemetry only (state plus counters); set `progressDetail` to `interleaved` to also stream each child's actual output live, agent-prefixed (`[1] …`, `[2] …`). For the full, after-the-fact detail of any turn, read the gateway log.

## Limitations

* The progress channel is live-only; the host collapses it after the turn, so the durable per-turn record lives in the gateway log, not the editor.
* With `fanOutScope: first-turn`, tool-loop continuation turns deliberately run a single agent; only the substantive request turn gets the full council.
* Codex tool-call emission on `/v1/responses` (`function_call`) is not yet built; the Anthropic `/v1/messages` `tool_use` path is done.
* Gateway mode adjudicates an answer or action; it does not run your tests. For test-verified results, use [MCP worktree mode](/product/mcp-worktree-mode).

For the HTTP surface, endpoints, and streaming details, see the [gateway API reference](/reference/gateway-api).


# MCP worktree mode

The gateway already edits code inline by emitting the `Read` / `Edit` / `Bash` tool calls your host executes. MCP worktree mode is for heavier work: running N **competing** full implementations in parallel, filtering them with your test suite, and yielding **one vetted diff** to apply to a fresh branch. It is exposed as the MCP tools `frites_implement` and `frites_apply`.

## N competing implementations in isolated worktrees

When you ask frites to implement something, the engine resolves the base commit, decides N, and creates one isolated git worktree per agent. Each child agent runs as a full agent and edits in its own worktree concurrently, so the candidates never collide. frites streams progress notifications as the agents work, then captures each candidate's diff from git.

## Tests as the oracle

Reconciliation is not a mechanical N-way merge. That produces duplicate declarations and contradictions that still compile. Instead, frites filters the candidate diffs through your repo's **test suite as the ground-truth oracle** (configured or auto-detected build, lint, and test commands), then reconciles:

* Candidates that errored, timed out, were empty, or touched no files are ignored.
* If no candidate passes, frites surfaces the closest near-miss.
* If exactly one passes, it is recommended.
* If multiple pass, a deterministic judge breaks the tie by smallest changed-line count, then fewest files touched.

This is the strongest correctness signal in frites: candidates are actual diffs tested against real commands. See [worktree oracle](/concepts/worktree-oracle) for how the oracle and tie-break work.

## Optional cross-candidate synthesis

By default (`synthesisMode: "passing-only"`), once at least two candidates pass the oracle, frites runs one more step instead of just picking a winner. It creates a fresh worktree from the same base commit, **seeds** it with the best passing diff, and asks a synthesizer agent to fold the strongest ideas from the others into one integrated implementation. That candidate is captured from git and re-run through the **same** oracle, and it is recommended only if it passes *and* stays within a sane size ceiling (`synthesisMaxBlastFactor`); otherwise frites falls back to the best individual passing child and tells you why. It never mechanically merges diffs. Set `synthesisMode: "off"` for plain winner-take-one.

Synthesis is summarized here; for the full design, gating rationale, and reconciliation policy across both surfaces, see [synthesis and reconciliation](/concepts/synthesis-and-reconciliation).

## Diff review and apply

frites returns candidate diffs plus a per-candidate comparison for you to review. It never auto-merges or pushes. When you are satisfied, `frites_apply` lands the chosen diff on a fresh `frites/<runId>` branch. This explicit human gate is the one mandatory approval step. You can always land a specific child instead of the recommendation by passing `candidateId=<agent>` to `frites_apply` (or `--apply-candidate <id>` on the CLI).

For the full tool inputs and outputs, see the [MCP tools reference](/reference/mcp-tools).

## When this beats gateway mode

Worktree mode is the far end of the "better output, slower" curve: N full implementations run to completion, then an extra synthesizer pass plus oracle run before you get a diff, minutes, not seconds. The payoff is that, unlike the gateway's answer synthesis, the worktree result is **verified** (it actually passed your tests), not just adjudicated. Reach for it when correctness matters more than latency; use the gateway, `synthesisMode: "off"`, or fewer agents when you want speed. See [risks and tradeoffs](/architecture/risks-and-tradeoffs) for the canonical tradeoff discussion.


# Auth & billing

frites runs on the subscriptions you are **already logged into**: there are **no API keys** to configure for everyday use. Children authenticate the same way you do interactively: Claude through its keychain OAuth (`claude` login), Codex through your ChatGPT sign-in (`codex` login). What changes once frites is in the loop is *where the spend lands*, because billing is decided by the invocation **surface**, server-side, not by client identity.

## Subscription-first, no API keys

When the council runs, each child is spawned headless against your local credentials. Claude children use the keychain OAuth token; Codex children use your ChatGPT account. frites withholds API keys from children by default (`passApiKeys: false`), so the CLIs fall back to OAuth rather than per-token API billing. This is the boundary that keeps frites on your subscription instead of a metered key. See the [safety model](/product/safety-model) for how the child environment is built and why keys are withheld.

## Programmatic use is metered

The catch is that *interactive* subscription limits and *programmatic* (headless) limits are not the same bucket. When frites drives the same accounts non-interactively:

* **Claude** draws the **metered Agent-SDK credit**: $20 Pro / $100 Max5x / $200 Max20x per month, at full API rates, no rollover, hard stop. This is **not** the unlimited interactive limit; it is a separate, metered allowance tied to the Agent-SDK / `claude -p` surface.
* **Codex** draws your **ChatGPT plan's Codex usage**: `codex exec` rides the ChatGPT plan's Codex limits.

The asymmetry between providers (which auth paths work, which are sanctioned, and which are banned) is detailed under [agents and runners](/architecture/agents-and-runners). The single load-bearing fact: spending scales with how often you fan out, so the council's reach is governed by [fan-out policy](/concepts/fan-out-policy) and [fan-out scope](/concepts/fan-out-scope).

## Why interactive limits can't be reused for headless fan-out

This is the most counter-intuitive part, and it is by design. Billing is **surface-based**: the vendor decides which bucket your call hits from the endpoint it arrives on, not from who is calling.

* Replaying a raw Anthropic subscription token directly against `api.anthropic.com` is **dead and banned**. Anthropic added server-side validation on Jan 9 2026 and returns `401 "only for use with Claude Code"`. Spoofing Claude Code to borrow its unlimited interactive limit is broken, ToS-violating, and pointless.
* The sanctioned headless path (`claude -p` / Agent SDK) therefore lands on the **metered** Agent-SDK credit, not the interactive limit.

So there is **no** way for anyone to get unlimited interactive-subscription limits for *headless* fan-out. Any tool that claims otherwise is either emulating Claude Code (broken/banned) or quietly billing a key. frites does not pretend the free interactive bucket is reusable; it meters honestly and keeps spend visible.

## Optional API-key overflow

API keys are an **overflow** path, not the default. Configure a key only when you want to exceed the subscription's metered allowance, or to reach non-subscription models that your plan does not cover. Two levers opt in:

* Set `passApiKeys: true` in config, **or**
* Set the environment variable `FRITES_PASS_API_KEYS=1`.

Either one lets the allowlisted child environment carry `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` through to the children, so the CLIs bill per-token instead of drawing the subscription. frites owns the fallback router because the vendors do not auto-fall-back from a depleted subscription to a key. The `passApiKeys` posture is part of the [safety model](/product/safety-model). Leaving it off is the recommended default, and is also what keeps you subscription-first.

## Cost visibility differs by backend

How much you can *see* of what was spent depends on which backend ran the child:

| Backend                        | Reports cost?        | What you see                                       |
| ------------------------------ | -------------------- | -------------------------------------------------- |
| `claude -p` (Claude)           | Yes, authoritatively | Actual spend per child and per turn                |
| `codex` on the ChatGPT backend | No                   | Nothing: reads as unknown (and once looked "free") |

Because Codex on the ChatGPT backend self-reports no cost, frites can only **estimate** its spend. Provide a per-model `pricing` rate table and frites fills in an estimate, shown with a leading `~` to mark it as derived rather than reported. Without rates, Codex spend reads as blank. The full shape of the `pricing` key lives in [reference/configuration.md](/reference/configuration), and how per-turn cost is surfaced is covered in [concepts/cost-telemetry.md](/concepts/cost-telemetry).

## Two billing modes

Because the surface decides the bucket, frites exposes two billing modes:

1. **Interactive (cheapest, higher friction).** Your real Claude Code stays the brain on its interactive subscription limits and only calls frites's MCP worktree tool for deliberate heavy edits. Maximum free subscription usage, but it requires you at the session and to invoke the tool explicitly. This mode lives on the [MCP worktree path](/product/mcp-worktree-mode).
2. **Transparent / metered (the default, friction-first).** Your Claude Code / Codex points at the [gateway](/product/gateway-mode), so frites is the brain for *every* prompt. Children use your local subscriptions but programmatically, so spend is **metered** (Agent-SDK credit / ChatGPT plan), with optional API-key overflow.

frites defaults to mode 2 for UX (friction-over-cost) and keeps spend in check with [fan-out policy](/concepts/fan-out-policy), [fan-out scope](/concepts/fan-out-scope), and per-turn [cost telemetry](/concepts/cost-telemetry). Mode 1 stays available for cost-sensitive heavy edits.


# Safety model

frites is a high-trust local automation tool. It **deliberately** launches child agents in headless, unattended mode so the council can finish a turn without blocking on interactive approval prompts. Treat it as a power tool you point at repositories you trust, not as a permission-prompt-preserving wrapper. This page is the canonical description of frites's permission posture and the blast-radius controls that bound it.

## Headless child posture

Children run without interactive approvals so N agents can run to completion without prompting each other to a halt:

* **Claude** children launch with `--permission-mode bypassPermissions`.
* **Codex** children launch with `approval_policy="never"`.

The posture is then tightened per surface, from most permissive (worktree) to most restrictive (answer-only).

## Per-surface permission boundaries

### Gateway action mode

On a coding turn, the children **decide** the next action; they do not edit files themselves. The gateway emits a normal host `Read` / `Edit` / `Bash` `tool_use`, and the **host executes it under its own permission model**. The host is the permission boundary for the actual file mutation, but do not assume each child decision passed through your usual per-command approval UI before the gateway returns a synthesized tool call.

### Gateway answer-only mode

Answer turns should inspect and answer, never mutate, so children are constrained further:

* **Claude** disallows `Edit`, `Write`, and `NotebookEdit`.
* **Codex** runs with `-s read-only` and writes only its final-message fallback **outside** the repo.

### MCP worktree mode

`frites_implement` starts full agents inside isolated git worktrees:

* **Claude** uses bypassed permissions.
* **Codex** uses `-s workspace-write` with approvals disabled.

The safety boundary here is the **worktree plus the final human diff review**. frites returns candidate diffs; `frites_apply` lands the chosen diff on a fresh `frites/<runId>` branch. It never auto-merges and never pushes. The apply gate is the one mandatory human gate. See [product/mcp-worktree-mode.md](/product/mcp-worktree-mode) for the full flow.

## Implemented blast-radius controls

frites ships several controls that bound what a headless child can reach and do:

* **Allowlist child env.** The child environment is built by **allowlist**, never by copying `process.env`. Only essentials pass through: `HOME`, `PATH`, locale variables, terminal/user variables, and the credentials a child needs to find its own auth.
* **Base-URL scrub.** Provider base-URL variables are stripped from every child so a child cannot be pointed back at the gateway: `ANTHROPIC_BASE_URL`, `ANTHROPIC_API_URL`, `OPENAI_BASE_URL`, `OPENAI_API_BASE`, and `CODEX_BASE_URL` are scrubbed (even if reintroduced via extra env).
* **API-key withholding.** API keys are withheld by default (`passApiKeys: false`); children use subscription OAuth unless you opt in (`passApiKeys: true` or `FRITES_PASS_API_KEYS=1`). See [product/auth-and-billing.md](/product/auth-and-billing) for the overflow path.
* **Recursion-depth fuse.** Each child gets `FRITES_DEPTH` incremented; frites refuses to spawn above the configured `maxDepth`. Children are also launched with `--strict-mcp-config` / `--ignore-user-config` so they do not auto-load frites and recursively call the gateway.
* **Patch / apply gate.** MCP worktree mode lands changes only via returned diff → explicit apply → fresh `frites/<runId>` branch. Never auto-merge, never push.
* **Local bind.** The gateway listens on `127.0.0.1` only.
* **Per-child limits.** Wall-clock timeout with process-group kill, plus per-child budget caps where the backend supports them.

## Current hardening gaps

These are known and called out so you can make an informed trust decision:

* **No strong sandbox.** There is no strong OS/container sandbox wrapping Claude children yet.
* **No secret deny-read.** Deny-read rules for paths such as `~/.ssh`, `~/.aws`, and `.env` are planned but **not enforced** today; a child can read them.
* **No interactive-prompt-preserving mode.** There is no child mode that preserves normal interactive permission prompts inside the child agents.

Hardened `sandbox-runtime` / container execution with default-deny egress remains planned work.

## Guidance for security-conscious users

Until the planned hardening lands:

* Use frites only in repositories and working trees you are comfortable letting local headless agents inspect and, in action/worktree paths, modify without per-command approval.
* Keep `passApiKeys` **off** unless you explicitly need overflow.
* Always review diffs before applying.
* Avoid running the gateway against untrusted repositories.

For the engine-level enforcement behind this posture, see [agents and runners](/architecture/agents-and-runners); for how worktrees isolate child edits, see [isolation](/architecture/isolation).


# Status & limits

A concise, user-facing view of what frites does today and where the rough edges are. For the full, dated enumeration of implementation status, see [roadmap/current-status.md](/roadmap/current-status), which is canonical.

## What works

Built and tested (126/126 unit tests plus live smoke against a real `claude` client):

* **The gateway**, both surfaces (`/v1/messages` for Claude Code and `/v1/responses` for Codex), with SSE streaming, live per-agent telemetry, live answer streaming, fan-out, synthesis, the LLM fan-out judge, `fanOutScope` first-turn scoping, background-model bypass, a per-turn council recap, and cost telemetry. See [product/gateway-mode.md](/product/gateway-mode).
* **Code editing through the gateway.** On a coding turn the gateway emits the `Read` / `Edit` / `Bash` `tool_use` your host executes on the real files, verified end-to-end (a real `claude` client → gateway fixed a bug and the tests passed), with **no API key**.
* **The background service** (launchd on macOS), so the gateway runs always-on. See [getting-started/service-management.md](/getting-started/service-management).
* **MCP worktree mode.** Worktrees → tests-as-oracle → optional cross-candidate synthesis → vetted diff → apply to a fresh branch. See [product/mcp-worktree-mode.md](/product/mcp-worktree-mode).
* **The config CLI.** `frites config` init/show/get/set/unset/validate/path with global+repo layering. See [reference/cli.md](/reference/cli).

## Known gaps

* **Value gate pending.** Whether fan-out *quality* actually beats a single agent on real tickets at acceptable cost has not yet been validated. This is the headline open question. If it fails, the thin slice is the product.
* **Codex tool-call emission on `/v1/responses` pending.** The gateway drives coding turns by emitting host-executed tool calls today on the Anthropic `/v1/messages` surface; emitting `function_call` on Codex's `/v1/responses` is **not yet built**. Codex works fully for Q\&A / reasoning turns, but the inline code-editing loop is Claude-only for now.

## Realistic limitations

* **Slower than a single agent.** A council of independent agents, cross-checked and synthesized, is the core bet: better output traded for latency and metered spend. Worktree mode is the far end of that curve (minutes, not seconds). See the tradeoff note in [architecture/risks-and-tradeoffs.md](/architecture/risks-and-tradeoffs).
* **Metered, not free.** Programmatic use draws the Agent-SDK credit / ChatGPT plan; see [product/auth-and-billing.md](/product/auth-and-billing). Spend scales with fan-out, bounded by [fan-out policy](/concepts/fan-out-policy) and [fan-out scope](/concepts/fan-out-scope).
* **Headless, high-trust posture.** Children run unattended without interactive approvals, and several hardening items are still open (no strong sandbox, no secret deny-read). Review the [safety model](/product/safety-model) before pointing frites at a repository.

For the complete, dated status list, see [roadmap/current-status.md](/roadmap/current-status).


# Council of agents

frites answers a prompt with a **council** of independent child agents instead of one. The council fans the prompt out to several configured children, has each work autonomously, then asks a synthesizer to fold their outputs into a single vetted result. The bet is that many cross-checked attempts yield better output than any single agent; the cost is latency and metered spend.

## Independent children

When a turn fans out, frites runs N child agents concurrently. Each child receives the user prompt (optionally with a configured **framing**) and works on its own, with no visibility into the other children. Children are collected with `Promise.all`, so they run truly in parallel; if one fails, its failure is converted into a textual failure block rather than aborting the whole council. If every child fails, frites returns an explicit failure; if the synthesizer fails but a child produced usable output, frites falls back to that surviving child/proposal.

Known backend/account failures also feed a provider suppression policy. For example, if Claude reports a five-hour usage limit, frites suppresses `claude-cli` until the backend reset time (or a conservative fallback TTL) and routes later gateway calls to another configured provider such as `codex-cli` when one is available.

What a child produces depends on the turn:

* **Pure answer turns** (no tools): each child returns prose. The synthesizer adjudicates the children's answers into one final response.
* **Tool/action turns** (the Claude Code agentic loop): each child acts as a decision engine and proposes exactly one next action as a JSON object: a tool call or a final answer. The synthesizer **selects** one proposed tool call verbatim rather than blending proposals.

Children normally stream to the per-agent progress telemetry, not into the user-facing answer, so users see live progress plus one synthesized result, not a visible debate.

## The synthesizer is `defaultAgents[0]`

There is **no separate synthesizer model setting.** The synthesizer is `config.defaultAgents[0]`, invoked with `role: "synth"`. Children round-robin the same array by index:

```ts
return ctx.role === "synth" ? agents[0] : agents[ctx.index % agents.length];
```

The consequence is **slot 0 is both the synthesizer and child index 0**. The first agent in `defaultAgents` does double duty. Reordering `defaultAgents` changes which agent synthesizes *and* which model child 0 runs, in lockstep. To change only the synthesizer, you reorder the array, but be aware that the new slot 0 is then also the new child 0.

This synthesizer (the cheap classifier that decides *whether* to fan out under `fanOutPolicy: auto`) is a distinct concept. See [Fan-out policy](/concepts/fan-out-policy).

## Default agent ordering and model mix

The default `defaultAgents` is a two-agent mix, in this order:

| Slot | `kind`       | Framing                                                     |
| ---- | ------------ | ----------------------------------------------------------- |
| 0    | `claude-cli` | `Make the smallest correct change that satisfies the task.` |
| 1    | `codex-cli`  | `Prefer a clean, well-structured solution.`                 |

So by default the synthesizer (and child 0) is the Claude child, child 1 is the Codex child, and `defaultN` is 2. Each entry is a `{ kind: "claude-cli" | "codex-cli", model, framing }` spec; `defaultN` (1–10) controls how many children actually run, drawn round-robin from this list.

## Prompt framing

Each child carries its own **framing** string, prepended to the prompt, that steers it toward a different point in the solution space. The default pairs "smallest correct change" against "clean, well-structured solution." On substantive prompts, frites also appends a shared child directive telling every child to reason exhaustively, inspect relevant context, and verify when changing code. (That exhaustiveness directive is stripped for cheap background/utility calls so a throwaway haiku call isn't told to read the whole repo.)

## Why diversity is not temperature-based

Candidate diversity is what justifies paying N×, and frites deliberately does **not** get it from temperature. Neither the `claude` nor the `codex` CLI exposes a `--temperature` flag, so it isn't available even if it were wanted. Same-model, same-prompt fan-out produces near-duplicates that add cost without adding signal.

Instead, diversity comes from two levers:

1. **Model-mix:** running different model families (claude × codex) so the candidates reason differently.
2. **Prompt-framing:** giving each child a different framing ("minimal change" vs. "clean refactor") so even same-family children attack the problem differently.

This is why the default mix is N=2 (1 claude + 1 codex) rather than several same-model children, and why frites defaults conservatively until measured divergence justifies more children.

All council-shaping keys (`defaultN`, `defaultAgents`, `fanOutPolicy`, `fanOutScope`, per-child guardrails, and the synthesis tuning keys) are documented in [Configuration](/reference/configuration).


# Fan-out policy

`fanOutPolicy` controls **how aggressively** frites fans a turn out to the full council. Because every council call is metered (children run on your subscriptions programmatically, which bills), fanning out on every turn is the dominant cost lever. The policy decides whether a given turn is worth the spend at all.

It has four settings:

| Value       | Behavior                                                                       |
| ----------- | ------------------------------------------------------------------------------ |
| `always`    | Always fan out. Maximum cross-checking, maximum metered spend.                 |
| `auto`      | The coordinator judges per-prompt whether fanning out is worth it (see below). |
| `necessary` | Fan out only on hard or contested prompts; otherwise run a single agent.       |
| `never`     | Never fan out. Always run a single agent.                                      |

## The LLM classifier in `auto`

`auto` is the cost-aware default. It decides fan-out per request in two stages:

1. **Heuristic short-circuit.** frites first applies a cheap heuristic. On trivially simple prompts it skips fan-out outright, with no extra model call.
2. **LLM fan-out judge.** When the heuristic says fan-out *might* be worth it, frites asks a small LLM classifier to make the final call for the current request: fan out, or run a single agent.

This LLM fan-out judge is a separate role from the council synthesizer. The synthesizer (`defaultAgents[0]`) merges the children's outputs *after* fan-out; the fan-out judge is the cheap classifier that decides **whether** to fan out in the first place. See [Council of agents](/concepts/council-of-agents) for the synthesizer.

## Cost implications

Fan-out worthiness is a property of the **current request**, not the whole transcript, so the policy re-decides on each new request. Spend scales with how often the policy says "yes":

* `always` pays for the full N-child council on every allowed turn, the most expensive setting.
* `auto` spends only when the heuristic plus the LLM judge agree a turn benefits from cross-checking, avoiding council cost on simple prompts.
* `necessary` is more conservative still, reserving the council for hard or contested prompts.
* `never` collapses to a single agent: no council premium, no cross-checking.

`fanOutPolicy` decides *whether* a turn fans out; [`fanOutScope`](/concepts/fan-out-scope) decides *which* turns of a multi-turn agentic task even get asked. Together they bound a long task to a small, predictable number of councils. See [Configuration](/reference/configuration) for the full key list and defaults.


# Fan-out scope

`fanOutScope` controls **which turns** of a multi-turn request fan out to the council. Where [`fanOutPolicy`](/concepts/fan-out-policy) decides *whether* a turn is worth fanning out, `fanOutScope` decides *which* turns of an agentic task even get asked, and together they bound a long task to a small, predictable number of councils.

The host (Claude Code especially) runs a long tool loop for a single request: one turn to plan, one per tool round-trip, one to conclude. The gateway sees each of these as a separate inbound request. Fanning out a full council on *every* one of them multiplies metered spend by the loop length for near-zero added value on the mechanical steps (run this grep, read that file).

## first-turn (default) vs per-turn

| Value                  | Behavior                                                                                                                                                                                                                                                                |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `first-turn` (default) | Fan out on the substantive **request** turn (the initial reasoning/planning), then drive the mechanical tool-loop continuations with a single agent. A task that takes N tool round-trips pays for **one** council, not N. Fan-out re-engages on each new user request. |
| `per-turn`             | Restore fan-out on every allowed turn, including each tool step. Maximum cross-checking, maximum spend.                                                                                                                                                                 |

## Stateless continuation detection

frites distinguishes a fresh request from a tool-loop continuation **without any server-side session memory**. A turn is a continuation when its request carries a tool result back: an Anthropic `tool_result` in the last user message, or a Responses `function_call_output`. That signal is read from the request *shape* alone, so the decision is correct across server restarts and across concurrent sessions, with no stored state to get out of sync.

Under `first-turn`, a continuation turn runs a single agent; a fresh request turn re-engages the council.

## Background/utility traffic always bypasses the council

The host emits cheap small/fast-model calls (title generation, conversation summarization, topic classification) on a haiku-tier model. These **never** fan out, regardless of `fanOutScope`. frites detects them by model name (matching `haiku`, `small`, or `fast`) and pins them to a *single* child on the model the host actually asked for, tools or not. Fanning a throwaway housekeeping call out to N metered children would be pure waste.

> **Single agent, tool-loop continuation is expected.** With `fanOutScope: first-turn`, only the substantive request turn fans out; the mechanical tool-loop steps that follow run a single agent, and the host's background/utility calls always run a single agent. So seeing `single agent — tool-loop continuation` on follow-up turns is the design working, not a bug.

(Caveat: detection keys on the model name, so running the host itself on a haiku *main* model would read every turn as background and never fan out.)

## Why first-turn is the default

The substantive reasoning (where independent children disagree, surface different approaches, and earn the cost of a council) happens on the request turn. The continuations that follow are mostly mechanical tool execution where a council adds near-zero value but full metered cost. Scoping fan-out to the first turn captures the quality lift where it matters while keeping an entire agentic task to a single council, making spend predictable. `per-turn` is available when you want maximum cross-checking and accept the higher spend.

See [Configuration](/reference/configuration) for the `fanOutScope` key alongside `fanOutPolicy`, `defaultN`, and `defaultAgents`.


# Synthesis & reconciliation

frites turns several child-agent outputs into one user-facing result. It does **not** mechanically merge those outputs. Depending on the surface, it either asks an LLM synthesizer to write one final answer, asks an LLM synthesizer to select one next tool/action for the host to execute, or filters complete implementation candidates through build/lint/test oracles and recommends one whole diff.

The strongest verification lives in the worktree implementation path, where candidate diffs are actually tested. The transparent gateway path improves answer and action quality through independent proposals plus synthesis, then relies on the host tool loop to execute and validate the selected action.

## The three reconciliation strategies

Each surface intentionally uses a different strategy, because "best" means something different on each.

| Surface                      | Inputs                           | How one result is chosen                                                                | What "best" means                                                                                     |
| ---------------------------- | -------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Gateway answer turns         | N raw child answers              | LLM-mediated **synthesis** into one answer                                              | The synthesizer's final adjudicated response                                                          |
| Gateway tool/action turns    | N parsed `AgentAction` proposals | LLM-mediated **selection** of one next action                                           | The synthesizer's selected next action (tool calls selected verbatim; answer text synthesized freely) |
| Worktree implementation runs | N complete candidate diffs       | Build/lint/test **oracle filtering** + deterministic tie-break, with optional synthesis | The candidate that passes the executable oracle, smallest-diff tie-break when several pass            |

The deep internals of how the engine drives these stages (event model, failure modes, synthesis-engine shape) live in [../architecture/core-engine.md](/architecture/core-engine). Every `synthesis*` config key is documented in [../reference/configuration.md](/reference/configuration).

## Gateway answer synthesis (LLM-mediated)

For plain answer turns, children run independently and concurrently; their answers are sent, alongside the original question, to a synthesizer that produces one vetted answer. The synthesizer prompt tells the model to keep what the children agree on, adjudicate disagreements, drop unsupported or wrong claims, avoid mentioning that multiple responses existed, strip runtime artifacts (sandbox/working-directory complaints), and output only the final answer text.

There is no explicit voting algorithm, confidence score, source weighting, or external verifier in this path. The final answer is normally an LLM-mediated synthesis over raw child responses. A failing child is converted into a textual failure block rather than aborting the council; if every child fails, frites returns an explicit failure instead of spending a synthesizer call. If synthesis itself fails after at least one usable child response, frites falls back to a surviving child answer. This council brain lives in `packages/core/src/answer-council.ts`.

## Gateway tool/action selection (no blending)

Coding-agent turns with tools are stricter, because the output must be one concrete next action for the host. Each child is prompted as a decision engine and must return exactly one JSON object: `{"kind":"tool", ...}` to call a host tool, or `{"kind":"answer", ...}` to finish with text. The transcript is fenced as untrusted data so file contents, tool output, or prior model text cannot override the action-format instructions.

The key rule is that **tool actions are selected, not merged**:

* For a tool action, the synthesizer is instructed to select exactly one proposed tool call verbatim and must not blend tool names or inputs from different proposals.
* For an answer action, it may synthesize freely.

That no-blending rule is **prompt-enforced**, backed by a tolerant JSON parser and a tool-name allowlist, not a byte-structural membership check of the final tool input against the proposal set. The parser and validator reject malformed JSON and hallucinated tool names, but they do not prove the final input is byte-for-byte identical to one child proposal. The deeper semantic check happens when the host executes the selected action and returns the result on the next turn.

Whether the gateway fans out at all is itself gated by policy and prompt classification (see [fan-out-policy.md](/concepts/fan-out-policy)), and which turns are eligible is set by [fan-out-scope.md](/concepts/fan-out-scope). Background and utility calls (title generation, summarization, topic detection) are kept single-agent and are detected heuristically from small/fast model names such as `haiku`, `small`, or `fast`; this is a model-name heuristic, not a separate explicit request-type field.

## Worktree reconciliation (oracle-filtered candidate selection)

The MCP/CLI implementation path is candidate selection over complete implementation attempts, not answer synthesis. Each child runs in its own isolated git worktree, its diff is captured from git, and configured (or auto-detected) build/lint/test oracle commands run against each candidate. The reconciler then ignores unusable candidates, keeps only oracle-passers, and, when several pass, breaks the tie with a **deterministic heuristic, not an LLM judge**: smallest changed-line count, then fewest files touched. This is covered in detail on [worktree-oracle.md](/concepts/worktree-oracle).

### Synthesis stage (on by default)

When `synthesisMode` is `"passing-only"` (the default) and at least `synthesisMinCandidates` (default 2) candidates pass the oracle, a synthesis stage runs after oracle filtering and before final reconciliation. It only affects the worktree path, never the gateway. Set `synthesisMode: "off"` to restore pure winner-take-one.

1. A fresh worktree is created from the same base SHA and **seeded** with the best passing candidate's diff (via `git apply --3way`), so the synthesizer refines a known-good tree rather than reconstructing the agreed core. It falls back to fresh-from-base if the seed cannot apply.
2. One synthesizer agent (the first claude child by default, so `--max-budget-usd` is honored) integrates the other passing candidates' deltas, using their diffs and read-only worktrees as **source material, never as mandatory patches**. It is never a mechanical merge.
3. The synthesized result is captured from git like any candidate and run through the **same** oracle.
4. The synthesized candidate is preferred only when it passes that oracle **and** its blast radius stays within `synthesisMaxBlastFactor ×` the combined size of the passing inputs. Otherwise frites falls back to the best original passing candidate and records why.

## Design rationale

### Gate the preference; passing is not enough

Preferring synthesis is deliberately gated. The reconcile contract defines "best" as *smallest blast radius among oracle-passers*, and oracles are frequently weak or partial: `detectOracle` returns `{}` with no `package.json`, and a "pass" can be a single lint command exiting 0. Preferring a usually-larger synthesis on the sole evidence that it cleared the same bar the children already cleared would invert the smallest-blast-radius stance exactly when the oracle is least trustworthy. So synthesis is preferred only when it (a) passes the same full oracle **and** (b) its blast radius does not exceed `synthesisMaxBlastFactor ×` the combined size of the passing inputs (default factor `1.5`). This is the project's core "better output, slower" tradeoff applied to reconciliation (see [../architecture/risks-and-tradeoffs.md](/architecture/risks-and-tradeoffs)).

### Seed from the best passing candidate

Rather than building fresh-from-base and reconstructing the agreed core from capped prose, the synthesis worktree is created from the base SHA and seeded with the best passing child's diff. The synthesizer then starts from a known-good tree and only integrates deltas; the other passing trees stay alive on disk and are exposed read-only, so embedded diffs are a capped convenience, not the sole source. If the seed fails to apply, frites falls back to fresh-from-base.

### Fall back to the best original passing child

If the synthesized candidate produces no usable change, fails the oracle, or exceeds the blast-radius ceiling, frites keeps the best original passing candidate and records the fallback reason. The result reports whether synthesis was attempted, its inputs, whether it passed, and any fallback reason. A reviewer can still land a tighter passing child instead via `frites_apply … candidateId=<agent>` (or `frites "…" --apply-candidate <id>`).

## Non-goals

* No naive automatic hunk or N-way mechanical merge as the primary strategy: mechanical merges risk duplicated declarations, incompatible partial solutions, and code that compiles only accidentally.
* The synthesis stage never mutates a child candidate worktree.
* The synthesized result is never applied to the user's current branch automatically, and the `frites_apply` clean-working-tree gate is never weakened.
* Synthesizer prose is never treated as authoritative; the synthesized result must be captured from git.
* A synthesized candidate that has not passed the oracle is never recommended when at least one original candidate passed.

## Bottom line

frites synthesis is not N-way merge. The transparent gateway uses policy-gated fan-out, LLM-mediated synthesis for answers, and LLM-mediated selection for tool actions. The MCP/CLI implementation path uses test/build/lint oracles plus deterministic tie-breaking, optionally refined by a re-verified synthesis pass, to recommend one complete candidate diff. That separation is intentional: lightweight gateway synthesis keeps normal interaction friction low, while heavier worktree reconciliation provides stronger verification when the user wants competing full implementations reviewed before applying a diff.


# Worktree oracle

In the worktree implementation path, frites does not decide which candidate is best by reading the agents' prose. It runs the project's own build, lint, and test commands against each candidate diff and treats those commands as the **oracle**: the executable signal of whether an implementation actually works. This is what gives the worktree path its correctness guarantee: every recommended diff has passed real commands in a clean worktree.

This page covers candidate filtering and tie-breaking. For how the oracle plugs into reconciliation and the optional synthesis stage, see [synthesis-and-reconciliation.md](/concepts/synthesis-and-reconciliation).

## Tests, build, and lint as the oracle

Each child agent runs in its own isolated git worktree. After it exits, frites captures the actual git diff and the list of touched files. The child's text output is never trusted as the implementation result. The oracle commands then run inside each candidate's worktree.

Commands are either configured on the task/config or auto-detected from the repo's `package.json` scripts:

* If any of `build`, `test`, or `lint` is explicitly set, those explicit commands are used as-is.
* Otherwise, if auto-detection is on, frites detects the package manager (`pnpm`, `yarn`, `bun`, or `npm` from the corresponding lockfile) and maps each present script to `<pm> run <script>`.
* With no `package.json`, or with auto-detection turned off and nothing explicit, there is **no executable oracle**.

The oracle runs in a fixed order (**build → lint → test**) and short-circuits on the first failure, so a candidate that fails the build never runs lint or test. A candidate passes only if every configured command exits 0. If no command ran at all, the candidate has no executable oracle and does not count as passing.

## Candidate filtering and near-miss behavior

Reconciliation runs over the captured candidates with the oracle results:

1. Candidates that errored, timed out, were empty, or touched no files are ignored. Only `succeeded` candidates with at least one touched file are usable.
2. If no usable candidates exist, the run reports **near-miss** with no recommendation.
3. If there is no executable oracle, frites picks a best-effort winner from the usable candidates using the heuristic tie-breaker.
4. If an oracle exists, only candidates whose oracle **passed** are kept.
5. If no candidate passed, frites surfaces the closest **near-miss** using the heuristic over the usable candidates. There is a recommendation candidate to inspect, but it is reported as a near-miss, not a verified result.
6. If exactly one candidate passed, it is recommended.
7. If several candidates passed, the heuristic tie-breaker chooses one.

"Near-miss" is the honest signal that nothing cleared the bar: frites still surfaces the closest attempt for review rather than pretending a failing candidate is verified.

## Deterministic heuristic tie-breaker

When more than one candidate passes (or when there is no oracle and frites must still pick something), the winner is chosen by `heuristicJudge`, a **deterministic smallest-blast-radius tie-breaker, not an LLM judge**:

1. Smallest changed-line count (added/removed lines in the unified diff, excluding `+++`/`---` headers).
2. Then fewest files touched.

The verdict carries a rationale, e.g. *"Chosen from 3 test-passing candidates by smallest blast radius (42 changed lines across 2 file(s))"*, or *"Only candidate to pass the oracle"* when just one survives. Because it is purely deterministic, the same set of candidates always yields the same winner.

## No N-way mechanical merge

The worktree path recommends **one complete candidate diff** by default; it never mechanically merges candidates. A naive file- or hunk-level merge is unsafe as the primary mechanism because candidate changes interact through shared imports/exports, types, test fixtures, config, error-handling conventions, and cross-file invariants. A mechanical merge can produce a diff that compiles poorly, passes fewer tests, duplicates logic, or subtly changes behavior even when each source candidate passed alone.

The optional synthesis stage (on by default, `synthesisMode: "passing-only"`) does not change this: when at least two candidates pass, frites asks one synthesizer agent to produce a single integrated implementation in a seeded worktree, then captures it from git and re-runs the **same** oracle against it. It is only preferred when it both passes and stays within a configurable blast-radius ceiling; otherwise frites falls back to the best original passing candidate. See [synthesis-and-reconciliation.md](/concepts/synthesis-and-reconciliation) for the full synthesis policy.


# Cost & telemetry

frites is deliberately verbose about what the council is doing and what it costs. Because a single turn can fan out to several metered child agents plus a synthesizer, every turn surfaces live per-agent telemetry while it runs, a consolidated recap when it closes, and durable, after-the-fact spend detail in the gateway log.

This page explains the cost and telemetry model. For the rate-table format and worked examples see [../reference/pricing.md](/reference/pricing); for the durable log lines and verbosity controls see [../reference/logging.md](/reference/logging).

## Live per-agent telemetry

While a turn runs, frites streams progress on the host's *thinking* (Claude) / *reasoning* (Codex) channel: which agents it is consulting, a live **per-agent counter** (tokens streamed so far + elapsed) that climbs as each child works, when each one finishes (with duration, tokens, and cost), synthesis, and a "still working — Ns elapsed" heartbeat so a long multi-model turn never looks stuck.

By default the panel shows per-agent **telemetry** only (state + counters). Setting `progressDetail` to `interleaved` (`config set progressDetail interleaved` or `FRITES_PROGRESS_DETAIL=interleaved`) *also* streams each child's actual output live, line-buffered and agent-prefixed (`[1] …`, `[2] …`), so you can watch every agent think in parallel before the synthesized answer. Turn the whole channel off with `config set streamProgress false`.

This channel is live and per-turn: the "is it working?" view, not a durable record. Most editors collapse it once the turn ends. Not every turn shows the whole council: with `fanOutScope: first-turn` only the substantive request turn fans out, and background/utility calls always run a single agent, so `single agent — tool-loop continuation` on follow-up turns is expected.

## Per-child counters and reported metrics

Each child completion is normalized into a provider-comparable set of counters: total input tokens (with the cached/reused portion called out), output tokens, and cost. When a child finishes, frites emits a per-agent line such as `✓ <agent> responded (…)` (or `✓ synthesis complete (…)` for the synthesizer) carrying its duration, token usage, and cost. These per-agent figures roll up into the turn's total spend so the total is never blind to any one agent's contribution.

## Estimated (\~) vs authoritative spend

Cost visibility differs by backend, and frites marks the difference explicitly:

* **claude** reports cost authoritatively, `claude -p` returns actual spend, shown as a plain `$` figure.
* **codex** on the ChatGPT backend reports no cost. Without a rate table its spend reads as unknown (it previously looked free next to claude).

When a backend does not self-report cost, frites **estimates** it from the configured `pricing` table and marks the estimated figure with a leading `~` (for example `~$0.0123`). If no rate matches, the line reads `cost n/a`. The effective cost, reported or estimated, is what rolls into the turn total, so the total reflects codex's contribution rather than dropping it.

## Config-driven pricing table

Estimation is opt-in: there are no built-in rates. The `pricing` config key is a per-model rate table, in dollars per million tokens:

```json
{ "<model>": { "inputPerMtok": 0, "outputPerMtok": 0, "cachedInputPerMtok": 0, "cacheWritePerMtok": 0 } }
```

`inputPerMtok` and `outputPerMtok` are required; `cachedInputPerMtok` (cache reads) and `cacheWritePerMtok` (cache-write/creation input, claude only) are optional and default to `inputPerMtok` when omitted. A model is matched by exact key first, otherwise by prefix in either direction, so a `gpt-5.5` key covers `gpt-5.5-2026-…`, and a fully versioned key still matches a bare alias. The full schema and examples live in [../reference/pricing.md](/reference/pricing).

## The council recap line

A single consolidated recap line closes out the progress channel, so even after the client collapses the live block its summary view states at a glance what the council did this turn (agents consulted, wall time, call count, and cost):

```
◆ council recap — N agents + synth · 18.3s · 4 call(s) · $0.072
```

The head varies by turn: a fanned-out turn reads `N agents + synth`, a background turn reads `1 background agent [<model>]`, and an un-fanned turn reads `single agent`. The cost suffix is the turn's accumulated reported-or-estimated spend (omitted when it is zero). The recap is the at-a-glance summary; the full per-agent breakdown (request, fan-out decision, each child's start/finish/cost, synthesis, and total spend) is written to the durable gateway log. See [../reference/logging.md](/reference/logging) for tailing and verbosity.

## Cost levers

Spend scales with how often frites fans out. The default `fanOutScope: first-turn` keeps an agentic task to one council (the request turn) instead of one per tool round-trip, and the host's background haiku traffic (titles, summaries, topic detection) never fans out. Both are the main cost levers besides `fanOutPolicy` and `defaultN`.


# CLI

The `frites` command (`@frites/cli`) is the terminal entrypoint. It manages the gateway background service, edits config, tails logs, and runs a one-off agent council against a repository. Run `frites help` (`--help` / `-h`) for the inline usage summary.

## Command dispatch

The first argument selects the subcommand. Any invocation that doesn't match a known subcommand is treated as a run task, so `frites "implement X"` works without a leading `run`.

| Command                | Purpose                                                                        |
| ---------------------- | ------------------------------------------------------------------------------ |
| `frites install`       | Install and start the gateway as a background service (alias: `frites start`). |
| `frites status`        | Report whether the service is installed, loaded, and reachable.                |
| `frites restart`       | Restart the service (e.g. after a config change or upgrade).                   |
| `frites stop`          | Remove the background service (alias of `uninstall`).                          |
| `frites uninstall`     | Remove the background service.                                                 |
| `frites logs`          | Tail the gateway's detailed log. See [Logging](/reference/logging).            |
| `frites gateway`       | Run the gateway in the foreground.                                             |
| `frites config <sub>`  | Read or write configuration. See [Configuration](/reference/configuration).    |
| `frites run "<task>"`  | Run a one-off agent council (also the default when no subcommand matches).     |
| `frites service <sub>` | Legacy compatibility form for service management.                              |
| `frites help`          | Print the top-level usage.                                                     |

## Service management

`frites install`, `start`, `stop`, `uninstall`, `restart`, and `status` are thin front-ends over the service manager. On macOS the service is a launchd user agent (`com.frites.gateway`); on Linux it is a `systemd --user` unit (`frites-gateway.service`). Service install/uninstall/restart/status is only supported on macOS and Linux. On other platforms use `frites gateway` to run in the foreground.

| Command            | Flags      | Effect                                                                                                                      |
| ------------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------- |
| `frites install`   | `--port N` | Write and load the service (default port `6767`), auto-start on login, restart on crash.                                    |
| `frites start`     | `--port N` | Alias of `install`.                                                                                                         |
| `frites restart`   | —          | Reload (macOS) or `systemctl --user restart` (Linux) the service. Errors if not installed.                                  |
| `frites stop`      | —          | Alias of `uninstall`.                                                                                                       |
| `frites uninstall` | —          | Unload and remove the service files.                                                                                        |
| `frites status`    | `--port N` | Show the plist/unit path, launchd/systemd load state, and an HTTP health probe against `http://127.0.0.1:<port>/v1/models`. |

`--port` defaults to `6767`. The health probe in `status` uses the port you pass (also default `6767`), so pass the same `--port` you installed with.

## Run the gateway in the foreground

```bash
frites gateway [--port N] [--host addr]
```

Runs the gateway process directly (no service). `--port` sets `FRITES_GATEWAY_PORT` and `--host` sets `FRITES_GATEWAY_HOST` in the spawned gateway's environment.

## Logs

```bash
frites logs [-f|--follow] [-n N|--lines N] [--level debug|info|warn|error]
```

Tails `~/.frites/gateway.log`. See [Logging](/reference/logging) for full detail on the flags, level filtering, and follow behavior.

## Configuration

```bash
frites config <init|show|get|set|unset|validate|path> [--global] [--repo path] [--force]
```

| Subcommand | Arguments       | Effect                                                                                                      |
| ---------- | --------------- | ----------------------------------------------------------------------------------------------------------- |
| `init`     | —               | Write a starter config to the target file. Refuses to overwrite an existing file unless `--force`.          |
| `show`     | —               | Print the effective merged config (defaults < global < repo) as JSON; the source files are noted on stderr. |
| `get`      | `<key>`         | Print one resolved value by dotted path (e.g. `oracle.test`).                                               |
| `set`      | `<key> <value>` | Set a value in the target file (e.g. `set defaultN 3`); validated before writing.                           |
| `unset`    | `<key>`         | Remove a value from the target file; validated before writing.                                              |
| `validate` | —               | Validate the target config file against the schema.                                                         |
| `path`     | —               | Print the global and repo config paths, which exist, and the write target.                                  |

Targeting flags:

* `--global` targets `~/.frites/config.json`; without it, the target is `.frites/config.json` in the repo.
* `--repo path` chooses the repository directory (default: current working directory).
* `--force` allows `config init` to overwrite an existing file.

See [Configuration](/reference/configuration) for the full key list and layering rules.

## Run a one-off council

```bash
frites "<task>" [--repo path] [--n N] [--agents claude,codex] \
  [--accept "<criteria>"] [--base ref] [--apply | --apply-candidate <id>]
```

The leading `run` keyword is optional: `frites run "<task>"` and `frites "<task>"` are equivalent. The task instructions are every non-flag argument, joined with spaces.

| Flag                     | Meaning                                                                                                                                            |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--repo path`            | Target git repository (default: current working directory).                                                                                        |
| `--n N`                  | Number of child agents to consult.                                                                                                                 |
| `--agents claude,codex`  | Comma list of agent kinds. A token starting with `codex` maps to `codex-cli`, one starting with `claude` maps to `claude-cli`; others are ignored. |
| `--accept "<criteria>"`  | Acceptance criteria passed to the agents and the oracle.                                                                                           |
| `--base ref`             | Git ref to branch each worktree from (default `HEAD`).                                                                                             |
| `--apply`                | After the run, land the recommended candidate's diff onto a fresh branch.                                                                          |
| `--apply-candidate <id>` | Land a specific candidate's diff (implies `--apply`).                                                                                              |

The run streams progress events to stderr (agents starting/finishing, oracle results, synthesis, reconciliation) and prints the decision, per-candidate summary, synthesis status, and cost note to stdout. A synthesized candidate is marked with a `⚗︎` glyph.

Apply resolution mirrors the MCP `frites_apply` tool (see [MCP tools](/reference/mcp-tools)):

* `--apply-candidate <id>` wins over the recommendation. If no candidate with that id exists in the run, it fails loudly and lists the available ids.
* A requested candidate that produced no diff (errored, empty, or timed out) fails rather than silently falling back.
* When applied, the diff lands on a new `frites/<runId>` branch; it is never auto-committed or pushed.

## Legacy `frites service` form

```bash
frites service <install|uninstall|restart|status|logs> [--port N]
```

The longer `service` form remains supported for compatibility, but the direct commands (`frites install`, `frites status`, etc.) are the intended UX. `frites service logs` forwards to the same log tailer as `frites logs`.

## See also

* [Configuration](/reference/configuration): every config key and the layering rules.
* [Logging](/reference/logging): the gateway log and `frites logs` flags.
* [MCP tools](/reference/mcp-tools): the worktree `frites_implement` / `frites_apply` tools.


# Configuration

frites is configured by a JSON file managed with `frites config`; no hand-editing required. This page is the canonical reference for every key and its default. The schema lives in `packages/core/src/config.ts`.

## Layering

frites reads `.frites/config.json` in the repository, layered over `~/.frites/config.json` (global), layered over the built-in schema defaults. The effective precedence is:

```
defaults  <  global (~/.frites/config.json)  <  repo (.frites/config.json)
```

A repo value overrides the same key from global, which overrides the default. Inspect the merged result and its sources with `frites config show`; print the file paths and write target with `frites config path`. See the [CLI reference](/reference/cli) for the full `frites config` subcommand list.

## Selection and fan-out

| Key             | Type                                           | Default                        | Meaning                                                                                                                                                                                                                                                                        |
| --------------- | ---------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `fanOutPolicy`  | `"always" \| "auto" \| "necessary" \| "never"` | `"auto"`                       | How aggressively the gateway fans out to a council: `always` on every main turn; `auto` decides per-prompt; `necessary` only for clearly hard/contested prompts; `never` runs a single agent. See [Fan-out policy](/concepts/fan-out-policy).                                  |
| `fanOutScope`   | `"first-turn" \| "per-turn"`                   | `"first-turn"`                 | Which turns within one request may fan out. `first-turn` fans out only on the substantive request turn, then runs a single agent through the mechanical tool-loop continuation turns; `per-turn` fans out on every allowed turn. See [Fan-out scope](/concepts/fan-out-scope). |
| `defaultN`      | integer `1`–`10`                               | `2`                            | Default number of children when a task doesn't specify. Capped at 10 as a cost/concurrency guardrail.                                                                                                                                                                          |
| `defaultAgents` | array of `AgentSpec`                           | claude-1 + codex-1 (see below) | Which agents and models to consult. Order is load-bearing; see [the slot-0 note](#slot-0-is-the-synthesizer-and-child-0).                                                                                                                                                      |

The default `defaultAgents`:

```json
[
  { "id": "claude-1", "kind": "claude-cli",
    "framing": "Make the smallest correct change that satisfies the task." },
  { "id": "codex-1", "kind": "codex-cli",
    "framing": "Prefer a clean, well-structured solution." }
]
```

Each `AgentSpec` accepts `id`, `kind` (`"claude-cli"` or `"codex-cli"`), and the optional `model`, `framing`, `maxBudgetUsd`, `timeoutMs`, `hardTimeoutMs`, and (codex only) `reasoningEffort`.

## Per-child guardrails

| Key                     | Type             | Default           | Meaning                                                                                                                                                                                                                                 |
| ----------------------- | ---------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `perChildBudgetUsd`     | positive number  | `2`               | Per-child spend cap.                                                                                                                                                                                                                    |
| `perChildTimeoutMs`     | positive integer | `600000` (10 min) | Idle timeout: a child is reaped only after this long with no output, not this long after spawn. The countdown resets on every chunk the child streams. The oracle reuses this value as a per-command wall-clock cap on build/test/lint. |
| `perChildHardTimeoutMs` | positive integer | unset (off)       | Optional absolute wall-clock ceiling: kills a child this long after spawn regardless of activity. Off by default.                                                                                                                       |

## Synthesis

The synthesis stage applies to the worktree path (`frites_implement` / `frites run`). After children run and the oracle filters them, frites can ask one synthesizer agent to integrate the strongest passing candidates into a single diff, captured from git and verified by the same oracle. See [Synthesis and reconciliation](/concepts/synthesis-and-reconciliation) for the design rationale and reconciliation policy.

| Key                       | Type                      | Default                                   | Meaning                                                                                                                                                                                                                            |
| ------------------------- | ------------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `synthesisMode`           | `"off" \| "passing-only"` | `"passing-only"`                          | `passing-only` synthesizes when at least `synthesisMinCandidates` candidates pass the oracle, falling back to the best child on any failure. `off` is winner-take-one with no extra synthesizer spend.                             |
| `synthesisAgent`          | `AgentSpec`               | unset                                     | The agent that performs synthesis. When omitted, frites uses the first claude child among the selected agents (claude enforces `--max-budget-usd`, so `synthesisBudgetUsd` bites there), falling back to the first selected agent. |
| `synthesisMinCandidates`  | integer `≥ 2`             | `2`                                       | Minimum oracle-passing candidates required before synthesis runs.                                                                                                                                                                  |
| `synthesisMaxDiffChars`   | positive integer          | `60000`                                   | Cap (chars) on the combined non-seed candidate diffs embedded in the synthesis prompt; over the cap, a diff is replaced with its file list and the read-only worktree path.                                                        |
| `synthesisMaxBlastFactor` | positive number           | `1.5`                                     | The synthesized diff is preferred only when its changed-line count is `≤` this factor × the combined changed-line count of the passing inputs. Past that, frites keeps the best original passing child.                            |
| `synthesisTimeoutMs`      | positive integer          | unset → falls back to `perChildTimeoutMs` | Idle timeout for the synthesizer.                                                                                                                                                                                                  |
| `synthesisHardTimeoutMs`  | positive integer          | `1800000` (30 min)                        | Absolute wall-clock ceiling for the synthesizer. Unlike `perChildHardTimeoutMs`, this ships a concrete default because synthesis is a single serialized tail step where a hard bound is cheap and high-value.                      |
| `synthesisBudgetUsd`      | positive number           | unset → falls back to `perChildBudgetUsd` | Spend cap for the synthesizer. Enforced as a hard cap only for claude synthesizers (`--max-budget-usd`); advisory for codex (the hard timeout is the real ceiling there).                                                          |

## Cost telemetry

| Key       | Type                                                                                              | Default | Meaning                                                                                                                                                                                                                                                                                                                                                                   |
| --------- | ------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pricing` | record of `model id` → `{ inputPerMtok, outputPerMtok, cachedInputPerMtok?, cacheWritePerMtok? }` | unset   | Optional per-model token rates (in $ per million tokens) used to estimate a child's spend when its backend reports none, so codex on the ChatGPT backend shows a comparable cost instead of looking free. Lookup is exact-match first, then a prefix match. Omit to disable estimation. See [Pricing](/reference/pricing) and [Cost telemetry](/concepts/cost-telemetry). |

`ModelPricing` fields: `inputPerMtok` (uncached input), `outputPerMtok` (output, reasoning-inclusive), `cachedInputPerMtok` (cache reads; defaults to `inputPerMtok`), and `cacheWritePerMtok` (cache writes, claude only; defaults to `inputPerMtok`).

## Progress and logging

| Key              | Type                                     | Default       | Meaning                                                                                                                                                                                                                                                                                                          |
| ---------------- | ---------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `streamProgress` | boolean                                  | `true`        | Stream live council progress back to the prompting client during long council turns on the host's thinking/reasoning channel.                                                                                                                                                                                    |
| `progressDetail` | `"telemetry" \| "interleaved"`           | `"telemetry"` | How much per-child detail rides the progress channel. `telemetry` shows per-agent state + live token/elapsed/cost counters and completion summaries; `interleaved` also streams each child's actual output live, line-buffered and agent-prefixed. The env var `FRITES_PROGRESS_DETAIL` overrides this when set. |
| `logLevel`       | `"debug" \| "info" \| "warn" \| "error"` | `"info"`      | Gateway log verbosity. The env var `FRITES_LOG_LEVEL` overrides this when set. See [Logging](/reference/logging).                                                                                                                                                                                                |

## Other keys

These keys are part of the schema but are documented in detail elsewhere:

| Key                    | Type                                       | Default                            | Meaning                                                                                                                                                                                                                               |
| ---------------------- | ------------------------------------------ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `oracle`               | `{ build?, test?, lint?, autoDetect }`     | `{ autoDetect: true }`             | Build/test/lint commands for the worktree oracle; auto-detected from `package.json` scripts when none are given. See [Worktree oracle](/concepts/worktree-oracle).                                                                    |
| `maxDepth`             | integer `≥ 1`                              | `1`                                | Recursion fuse: refuse to spawn children when `FRITES_DEPTH` would exceed this.                                                                                                                                                       |
| `maxTurns`             | positive integer                           | `60`                               | Per-session safety cap on agentic turns the gateway drives before forcing a stop.                                                                                                                                                     |
| `passApiKeys`          | boolean                                    | `false`                            | Headless/metered mode: pass `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` through to children. Default false is subscription-first; the env var `FRITES_PASS_API_KEYS=1` also enables it. See [Auth and billing](/product/auth-and-billing). |
| `childDirective`       | string                                     | the shipped thoroughness directive | Woven into every substantive child prompt so all backends analyze and execute exhaustively. Set to `""` to disable.                                                                                                                   |
| `codexReasoningEffort` | `"minimal" \| "low" \| "medium" \| "high"` | `"high"`                           | Codex children's reasoning depth, injected as `-c model_reasoning_effort="<v>"`. A per-agent `AgentSpec.reasoningEffort` overrides it. `minimal` is not safe with the stock codex model.                                              |

## Slot-0 is the synthesizer and child-0

`defaultAgents` order is load-bearing. `defaultAgents[0]` doubles as the synthesizer that merges the council (there is no separate synthesizer setting in the default path) and children round-robin the whole list, so slot 0 is also child index 0. Reorder the list to change which agent synthesizes, keeping in mind that the same slot is then also the first child. (Setting an explicit `synthesisAgent` overrides which agent synthesizes without changing the child order.)

## See also

* [CLI](/reference/cli): the `frites config` subcommands.
* [Fan-out policy](/concepts/fan-out-policy) and [Fan-out scope](/concepts/fan-out-scope).
* [Synthesis and reconciliation](/concepts/synthesis-and-reconciliation).
* [Cost telemetry](/concepts/cost-telemetry) and [Pricing](/reference/pricing).
* [Worktree oracle](/concepts/worktree-oracle).
* [Logging](/reference/logging) and [Environment variables](/reference/environment-variables).


# Gateway API

The frites gateway is an HTTP server that speaks the **Anthropic Messages** and **OpenAI Responses** wire protocols, so Claude Code and Codex can point at it unmodified. Internally every request is run through the [council of agents](/concepts/council-of-agents); externally it looks like a normal model endpoint.

This page documents what the gateway process (`apps/gateway/src/index.ts`) actually implements.

## Bind address

The server listens on `FRITES_GATEWAY_HOST` (default `127.0.0.1`) and `FRITES_GATEWAY_PORT` (default `6767`). The default bind to loopback only is deliberate: the gateway runs child agents in headless/full-auto mode, so it is not exposed to the LAN by default. See the [safety model](/product/safety-model) for the blast-radius rationale.

On startup it logs a single line, for example:

```
listening on http://127.0.0.1:6767 — Anthropic (/v1/messages) + OpenAI (/v1/responses)
```

## Endpoints

| Method | Path                        | Purpose                                                                     |
| ------ | --------------------------- | --------------------------------------------------------------------------- |
| `POST` | `/v1/messages`              | Anthropic Messages. Q\&A, reasoning, and tool-bearing (agentic) turns.      |
| `POST` | `/v1/responses`             | OpenAI Responses (Codex). Answer synthesis only (see the limitation below). |
| `POST` | `/v1/messages/count_tokens` | Returns an estimated `input_tokens` count for an Anthropic request body.    |
| `GET`  | `/v1/models`                | Lists the configured child models plus a synthetic `frites-council` id.     |

Any other method/path returns `404` with a JSON error envelope. Handler exceptions return `500` with `{ type: "error", error: { message } }`.

### `POST /v1/messages`

Accepts a standard Anthropic Messages request. The gateway:

* extracts the system prompt + message history into a transcript;
* extracts any `tools` array into tool definitions;
* classifies the **last user message** (with injected harness scaffolding stripped) to decide fan-out, see [fan-out policy](/concepts/fan-out-policy);
* recovers the caller's working directory from the embedded env block in the system prompt (a line like `Primary working directory: /path`) when it points at an existing directory, so children run in the real repo;
* detects a **tool-loop continuation** turn (the last user message carries a `tool_result`) so `fanOutScope: first-turn` can reserve fan-out for the substantive request turn.

If the request has `stream: true`, the response is SSE (see [Streaming](#streaming-sse)); otherwise a single JSON `message` is returned. A tool-bearing turn can resolve to either a `text` answer or a `tool_use` block (the synthesized `Read`/`Edit`/`Bash` call the host then executes).

### `POST /v1/responses`

Accepts an OpenAI Responses request (`instructions` + `input`). It extracts the prompt and last user text the same way, recovers the working directory, and detects a continuation turn (last input item is a `function_call_output`). It supports both streaming (SSE) and non-streaming JSON.

> **Known limitation: Codex tool calls.** The Responses surface synthesizes an **answer only**. The turn is always run with an empty tool list, so the gateway never emits a Codex `function_call` on `/v1/responses`. Codex tool-call (`function_call`) emission is a planned follow-up; the Anthropic `/v1/messages` surface already emits `tool_use`. See [roadmap: current status](/roadmap/current-status).

### `POST /v1/messages/count_tokens`

Parses the body as an Anthropic request, extracts the prompt text, and returns `{ "input_tokens": <estimate> }`. The estimate is a length heuristic (roughly `ceil(chars / 4)`), not a tokenizer call. A body that fails to parse yields an estimate of `0`.

### `GET /v1/models`

Returns a `data` array of model objects. The ids are the distinct `model` values from `config.defaultAgents`, plus the synthetic id `frites-council`. Each entry has the shape `{ type: "model", id, display_name, created_at }`.

## Streaming (SSE)

When a request sets `stream: true`, the gateway responds with `content-type: text/event-stream` and keeps the connection alive. Both surfaces emit a keep-alive event roughly every 3 seconds (`ping` on Anthropic, `response.in_progress` on Responses).

Two distinct channels ride the stream:

* **Progress channel**: present only when the client is streaming **and** `streamProgress` is on. On Anthropic it is an ephemeral `thinking` block at index 0; on Responses it is a `reasoning` summary output item. It carries the live council narration: which agents are being consulted, a throttled per-agent token/elapsed counter, each agent's finish line (duration, usage, cost), and a `still working — Ns elapsed` heartbeat (default every 5s, `FRITES_HEARTBEAT_MS`). This block is ephemeral: the gateway strips it on the way back in, so echoing it back never pollutes the answer or the next turn.
* **Answer channel**: present whenever the client is streaming, regardless of `streamProgress`. Once the synthesizer begins producing the final answer, the progress block is closed and the answer is streamed **live**, token by token (`text_delta` / `output_text.delta`).

The per-agent telemetry cadence is controlled by `FRITES_TELEMETRY_MS` (default 2000ms), and the verbosity by `FRITES_PROGRESS_DETAIL` / `config.progressDetail` (`telemetry` vs `interleaved`). See [cost telemetry](/concepts/cost-telemetry) and [logging](/reference/logging).

### Live answer vs. tool calls

Only **pure answer turns** (no tools) stream the final answer live, because the synthesizer's text deltas equal its final result. **Tool-bearing turns** instead run the whole council on the progress channel, close with a one-line council recap, then emit the synthesized `tool_use` (or answer) when the turn resolves. A tool action is a parsed JSON action, not prose, so it is not streamed delta-by-delta. On the Anthropic surface, a tool action is emitted as a `tool_use` content block (`message_delta` stop reason `tool_use`).

## Traffic classification

The gateway classifies each request before deciding how hard to work:

* **Background / utility traffic**: when the requested `model` matches `haiku`, `small`, or `fast` (case-insensitive), the turn is treated as host housekeeping (title generation, conversation summarization, topic classification, or an explicitly cheap-tier subagent). It **never fans out**: a single child is pinned to the exact model the host asked for, and the exhaustiveness directive is stripped so it stays cheap.
* **Tool-loop continuation**: under `fanOutScope: first-turn`, a continuation turn (detected from the request shape) runs a single agent; fan-out re-engages on the next substantive request.
* **Substantive turns**: fan-out is decided per the configured `fanOutPolicy` (heuristic, or the LLM fan-out judge under `auto`).

## Sessions and the turn cap

The gateway is long-lived, so it derives a stable session key from a hash of the system prompt plus the first user message and tracks `{ turns, usd }` per session. When a session reaches `config.maxTurns`, the gateway forces a stop and returns a canned answer explaining the cap, rather than running another metered council. Cumulative spend is logged per turn.

## Authentication

Inbound auth is **off by default** to keep the quickstart frictionless. Set `FRITES_GATEWAY_TOKEN` to require a shared secret; when set, every request must present it via the `Authorization: Bearer <token>` header or the `x-api-key` header. The comparison is constant-time (`timingSafeEqual` on equal-length buffers). A missing or mismatched token returns `401` with `{ type: "error", error: { message: "unauthorized" } }`.

This is the gateway-side token (what Claude Code's `ANTHROPIC_AUTH_TOKEN` / Codex's `FRITES_KEY` present). For the full env var list, see [environment variables](/reference/environment-variables).

## Related

* [Gateway architecture](/architecture/gateway): internal request/turn flow.
* [Configuration](/reference/configuration): `fanOutPolicy`, `fanOutScope`, `maxTurns`, `streamProgress`, `progressDetail`, and the rest.
* [Environment variables](/reference/environment-variables): host, port, token, heartbeat/telemetry knobs.
* [Logging](/reference/logging): the durable per-turn log.


# MCP tools

The MCP worktree mode (`@frites/mcp`) exposes two tools over an stdio MCP transport: `frites_implement` runs a council of full agents in isolated git worktrees and returns one vetted diff plus a comparison, and `frites_apply` lands a chosen diff onto a fresh branch. The server is named `frites`. For how the mode fits together, see [MCP worktree mode](/product/mcp-worktree-mode).

## `frites_implement`

Dispatches a coding task to multiple full agents (claude/codex) in isolated git worktrees, filters them with the repo's tests, and returns one vetted diff plus a comparison. It is long-running (minutes). Review the result, then call `frites_apply`.

| Argument             | Type            | Required | Meaning                                                                                                                                             |
| -------------------- | --------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `task`               | string          | yes      | What to implement or fix.                                                                                                                           |
| `repoPath`           | string          | yes      | Absolute path to the target git repository.                                                                                                         |
| `n`                  | integer `1`–`5` | no       | Number of agents.                                                                                                                                   |
| `agents`             | string          | no       | Comma list of agent kinds, e.g. `claude,codex`. A token starting with `codex` maps to `codex-cli`, one starting with `claude` maps to `claude-cli`. |
| `acceptanceCriteria` | string          | no       | Acceptance criteria for the agents and oracle.                                                                                                      |
| `baseRef`            | string          | no       | Git ref to branch from (default `HEAD`).                                                                                                            |

The tool returns a Markdown result (`formatResultText`) containing the run id, decision, rationale, the recommended candidate, a per-agent comparison table (kind, status, files, Δlines, tokens in→out, oracle pass/fail), a synthesis status line, and the cost note. Synthesized candidates are marked with a `⚗︎` glyph. It also returns structured content (`toStructured`) and one `resource_link` per candidate diff, persisted under `.frites/runs/<runId>/<agentId>.diff` (with `result.json` alongside) in the target repo. On error it returns an `isError` result with the failure message.

## `frites_apply`

Applies a diff from a previous `frites_implement` run onto a fresh branch `frites/<runId>`. It applies the recommended candidate by default, or a specific one via `candidateId`. It requires a clean working tree and never pushes.

| Argument      | Type   | Required | Meaning                                                                                                                    |
| ------------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------- |
| `runId`       | string | yes      | The run id from a previous `frites_implement` call.                                                                        |
| `repoPath`    | string | yes      | Absolute path to the target git repository.                                                                                |
| `candidateId` | string | no       | Apply this candidate's diff instead of the recommended one (e.g. a tighter passing child instead of a synthesized result). |

Apply behavior:

* If `candidateId` is given but no candidate with that id exists in the run, the tool returns an error listing the available candidate ids.
* If the chosen candidate has no diff to apply, it returns an error.
* On success it applies the diff to a new branch `frites/<runId>` and returns the branch name plus structured `{ branch, runId, candidateId }`. The result tells you to review and commit; frites never auto-merges or pushes.

## Registration

### Claude Code

Register once for Claude Code (available in every repo):

```bash
claude mcp add --scope user frites -- pnpm --dir ~/nodejs/frites mcp
```

### Codex

Register once for Codex in `~/.codex/config.toml`. The 60-second default tool timeout **must** be raised: `tool_timeout_sec = 600` is required because `frites_implement` runs for minutes:

```toml
[mcp_servers.frites]
command = "pnpm"
args = ["--dir", "/Users/whatl3y/nodejs/frites", "mcp"]
tool_timeout_sec = 600
```

## Progress and result-size behavior

* **Progress.** When the MCP client supplies a `progressToken`, `frites_implement` streams `notifications/progress` updates as the engine emits events (agents starting/finishing, oracle results, synthesis, reconciliation), each with a human-readable message and an incrementing step count. Without a progress token, no notifications are sent.
* **Result size.** Full candidate diffs are written to disk under `.frites/runs/<runId>/` and surfaced as `resource_link` entries rather than inlined, keeping the returned text result compact. Per-candidate token counts are rendered compactly (e.g. `11.2k`) in the comparison table.
* **Lifecycle.** The server self-terminates when its MCP client disconnects (stdin EOF, termination signals, or being reparented to PID 1), so it never lingers as an orphan.

## Typical flow

In a session: *"use frites to implement X"* → review the diff → *"use frites\_apply with runId …"* (optionally `candidateId=<agent>` to land a specific candidate). The same flow is available from the terminal via `frites "implement X" --repo … --apply` / `--apply-candidate <id>`. See the [CLI](/reference/cli).

## See also

* [MCP worktree mode](/product/mcp-worktree-mode). The product overview of this mode.
* [CLI](/reference/cli). The standalone `frites run` equivalent.
* [Configuration](/reference/configuration). Synthesis and oracle keys that shape a run.


# Environment variables

frites is configured primarily through its [config file](/reference/configuration). Environment variables cover process-level knobs (host, port, auth) and a few overrides. This page lists **only** variables that are actually read in the frites source.

The variables fall into three groups:

1. [Consumed by frites](#consumed-by-frites): read by frites' own code.
2. [Set by you, read by Codex](#set-by-you-read-by-codex-frites_key): the Codex `env_key` label.
3. [Evaluation-only](#evaluation-only): used by the benchmark harness, documented elsewhere.

## Consumed by frites

These are read in frites' source. Most have a config-file equivalent; where the env var and config overlap, the env var wins for that process.

### Gateway process

| Variable                 | Default                     | Effect                                                                                                                                                                |
| ------------------------ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FRITES_GATEWAY_HOST`    | `127.0.0.1`                 | Bind address for the gateway server. Loopback-only by default.                                                                                                        |
| `FRITES_GATEWAY_PORT`    | `6767`                      | Bind port for the gateway server.                                                                                                                                     |
| `FRITES_GATEWAY_TOKEN`   | *(unset)*                   | Optional shared secret. When set, inbound requests must present it via `Authorization: Bearer …` or `x-api-key`; otherwise the gateway returns `401`. Off by default. |
| `FRITES_HEARTBEAT_MS`    | `5000`                      | How often (ms) to emit a `still working — Ns` heartbeat to the client during a long turn.                                                                             |
| `FRITES_TELEMETRY_MS`    | `2000`                      | How often (ms) to refresh the per-agent `~N tok · Ns` telemetry line while a child streams.                                                                           |
| `FRITES_PROGRESS_DETAIL` | *(config `progressDetail`)* | Per-agent progress verbosity: `telemetry` or `interleaved`. Overrides `config.progressDetail` when set.                                                               |

See the [Gateway API](/reference/gateway-api) page for how host/port/token affect the server, and [cost telemetry](/concepts/cost-telemetry) for the heartbeat/telemetry lines.

### Logging

| Variable           | Default                            | Effect                                                                                 |
| ------------------ | ---------------------------------- | -------------------------------------------------------------------------------------- |
| `FRITES_LOG_LEVEL` | *(config `logLevel`, else `info`)* | Gateway log verbosity: `debug`, `info`, `warn`, `error`. The env var wins over config. |
| `FRITES_LOG_JSON`  | *(unset)*                          | Set to `1` for newline-delimited JSON log lines instead of the human format.           |

See [logging](/reference/logging) for the full logging model.

### Auth / key passthrough

| Variable                  | Default                  | Effect                                                                                                                                                                                                                  |
| ------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FRITES_PASS_API_KEYS`    | *(config `passApiKeys`)* | Set to `1` to forward `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` through to child agents (metered API mode). Subscription-first by default: keys are withheld so CLIs use OAuth. Read by the gateway, MCP, and CLI.         |
| `ANTHROPIC_API_KEY`       | *(unset)*                | Forwarded to children **only** when `passApiKeys` is on. Otherwise withheld.                                                                                                                                            |
| `OPENAI_API_KEY`          | *(unset)*                | Forwarded to children **only** when `passApiKeys` is on. Otherwise withheld.                                                                                                                                            |
| `CLAUDE_CODE_OAUTH_TOKEN` | *(unset)*                | Headless Claude subscription token (from `claude setup-token`). On the [child allowlist](#child-environment), so it reaches children. This is how Claude auths where the macOS Keychain is unavailable (containers/CI). |

The child auth and billing model is the canonical topic of [auth and billing](/product/auth-and-billing).

### Config resolution

| Variable               | Default   | Effect                                                                          |
| ---------------------- | --------- | ------------------------------------------------------------------------------- |
| `FRITES_GLOBAL_CONFIG` | *(unset)* | Override the path to the global config file (normally `~/.frites/config.json`). |

### Child environment

These are managed by frites' env sandbox (`packages/agents/src/env-sandbox.ts`). You do not normally set them yourself, but they are part of the contract.

| Variable       | Role                                                                                                                                                                                                |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FRITES_DEPTH` | Recursion fuse. The parent reads it (default `0`); each child is launched with `depth + 1`. When it would reach `maxDepth`, frites refuses to spawn, preventing a child from invoking frites again. |
| `FRITES_CHILD` | Set to `1` in every spawned child environment, marking it as a frites-launched child.                                                                                                               |

Child environments are built by **allowlist**, never by copying `process.env`. The allowlist that is carried through (when present) is: `HOME`, `PATH`, `LANG`, `LC_ALL`, `LC_CTYPE`, `LC_MESSAGES`, `TERM`, `USER`, `LOGNAME`, `SHELL`, `TMPDIR`, `TZ`, `CODEX_HOME`, `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`, `XDG_DATA_HOME`, and `CLAUDE_CODE_OAUTH_TOKEN`. See the [isolation architecture](/architecture/isolation) and [safety model](/product/safety-model).

### Provider base-URL variables (scrubbed)

To prevent a child from pointing back at the gateway (a recursive fork-bomb), frites **scrubs** these base-URL variables out of every child environment, even if reintroduced via `extraEnv`:

* `ANTHROPIC_BASE_URL`
* `ANTHROPIC_API_URL`
* `OPENAI_BASE_URL`
* `OPENAI_API_BASE`
* `CODEX_BASE_URL`

`ANTHROPIC_BASE_URL` and `ANTHROPIC_AUTH_TOKEN` are still meaningful **on the host side**: they are what you set in `~/.claude/settings.json` to point Claude Code at the gateway (`ANTHROPIC_BASE_URL=http://127.0.0.1:6767`, `ANTHROPIC_AUTH_TOKEN=frites`). frites only scrubs them from the **child** environment it spawns. See [configure Claude Code](/getting-started/configure-claude-code).

## Set by you, read by Codex (`FRITES_KEY`)

`FRITES_KEY` is **not read by frites.** It is the variable named in Codex's `env_key` setting: you set it (`export FRITES_KEY=frites`), and Codex reads it to populate the auth token it presents to the gateway. It is documented here only because the Codex setup mentions it.

```toml
# ~/.codex/config.toml
model_provider = "frites"
[model_providers.frites]
base_url = "http://127.0.0.1:6767/v1"
wire_api = "responses"
env_key = "FRITES_KEY"
```

Whatever you export as `FRITES_KEY` is the token Codex sends; if you have set `FRITES_GATEWAY_TOKEN` on the gateway, `FRITES_KEY` must match it. See [configure Codex](/getting-started/configure-codex).

## Evaluation-only

The benchmark harness under `eval/` uses its own `FRITES_BENCH_*` and `AIDER_*` variables (e.g. `FRITES_BENCH_HARNESS`, `FRITES_BENCH_URL`, `FRITES_BENCH_GATEWAY_HOST`, `AIDER_REPO`, `AIDER_EDIT_FORMAT`). These are not part of the runtime product and are not duplicated here. See the evaluation runbook at [../../eval/README.md](https://github.com/whatl3y/frites/blob/main/eval/README.md) and [evaluation](/development/evaluation).


# Logging

frites keeps two distinct views of what the council is doing: a **live, per-turn** progress channel in your editor, and a **durable** gateway log on disk. The live channel shows what's happening right now and most editors collapse it once a turn ends. It's the "is it working?" view, not a record. The gateway log is the durable detail view: scroll back to any past turn long after the editor has moved on. This page covers the durable log and the `frites logs` tailer.

## The gateway log

The gateway writes one structured, leveled, turn-scoped record per line to stdout, which the background service captures to `~/.frites/gateway.log` (launchd `StandardOutPath` / systemd `StandardOutput`). Crashes and unformatted stderr land in `~/.frites/gateway.err`. Every turn writes detailed, timestamped, turn-correlated logs: request, the continuation/fan-out decision, each child's start/finish/cost, synthesis, and total spend.

Each text record is formatted as:

```
<iso-timestamp> LEVEL  [turn] message  key=value …
```

The `turn` id is a first-class prefix so per-request lines are easy to scan and grep; remaining fields follow the message as `key=value` pairs.

## `frites logs`

```bash
frites logs [-f|--follow] [-n N|--lines N] [--level debug|info|warn|error]
```

`frites logs` tails `~/.frites/gateway.log` (and appends recent crash lines from `~/.frites/gateway.err`).

| Flag                | Default | Meaning                                                                                     |
| ------------------- | ------- | ------------------------------------------------------------------------------------------- |
| `-f`, `--follow`    | off     | After printing the snapshot, stream new lines live until interrupted (Ctrl-C).              |
| `-n N`, `--lines N` | `60`    | Show the last `N` lines of the main log.                                                    |
| `--level <level>`   | none    | Only show lines at or above this minimum level. An unknown level is rejected with an error. |

Examples:

```bash
frites logs                         # last 60 lines
frites logs -f                      # follow live
frites logs -f --level debug        # include prompt/decision previews
frites logs -n 200 --level warn     # only warnings + errors
```

Without follow, `frites logs` prints the last `N` lines of `gateway.log`, then, if present, a `── gateway stderr (crashes) ──` section with the last 20 lines of `gateway.err`. With `--follow` it also streams new lines from both files as they arrive. If neither log file exists yet, it prints a hint to start the gateway or install the service first.

## Log levels

Levels are ordered `debug` < `info` < `warn` < `error`. The `--level` filter keeps lines at or above the chosen minimum. Lines that aren't level-formatted (raw stderr, crash output) are never hidden by the filter, so you never lose crash output the gateway didn't format.

The gateway's own verbosity is set by the `logLevel` config key (default `info`), overridden by the `FRITES_LOG_LEVEL` environment variable when set. To capture more detail, crank verbosity with `frites config set --global logLevel debug` (or `FRITES_LOG_LEVEL=debug`), then `frites restart` so the service picks it up. The `frites logs --level` flag filters what's *displayed*; `logLevel` controls what's *written*, so a level can only be shown if it was recorded.

## JSON output

Set `FRITES_LOG_JSON=1` to make the gateway emit newline-delimited JSON records (`{ ts, level, msg, …fields }`) instead of the human format. This is useful for ingestion by other tooling.

## Durable detail vs live progress

|          | Live progress channel                      | Gateway log                       |
| -------- | ------------------------------------------ | --------------------------------- |
| Where    | Editor thinking/reasoning channel          | `~/.frites/gateway.log`           |
| Scope    | One turn, right now                        | Every turn, retained              |
| Lifetime | Collapsed by the editor when the turn ends | Durable; scroll back any time     |
| Use      | "Is it working?"                           | After-the-fact detail of any turn |

The live channel is governed by the `streamProgress` and `progressDetail` config keys (see [Configuration](/reference/configuration)). Note that not every turn shows the whole council: with `fanOutScope: first-turn` only the substantive request turn fans out, and the host's background/utility calls always run a single agent, so single-agent continuation lines on follow-up turns are expected.

## See also

* [Environment variables](/reference/environment-variables): `FRITES_LOG_LEVEL`, `FRITES_LOG_JSON`, and related vars.
* [Configuration](/reference/configuration): the `logLevel`, `streamProgress`, and `progressDetail` keys.
* [CLI](/reference/cli): the `frites logs` command and service management.


# Pricing

frites can estimate per-child spend from a config-driven, per-model rate table. Pricing is **opt-in**: there are no built-in rates. When you supply rates, frites uses them to fill in spend figures that a backend doesn't self-report.

This page documents the pricing model itself. The `pricing` config key is defined in [configuration](/reference/configuration).

## Authoritative vs. estimated spend

How spend is reported depends on the backend:

| Backend                     | Spend source                      | Display                                                                                      |
| --------------------------- | --------------------------------- | -------------------------------------------------------------------------------------------- |
| **claude** (`claude -p`)    | The CLI self-reports actual cost. | Authoritative: shown as `$0.0123`.                                                           |
| **codex** (ChatGPT backend) | Reports no cost.                  | Estimated from the `pricing` table when rates exist, shown with a leading tilde, `~$0.0123`. |

Without a `pricing` table, codex spend reads as `cost n/a` (and previously made codex look "free" next to claude). Supplying rates replaces those blanks with estimates. Authoritative claude figures are never overwritten by estimates: the reported cost always wins, and the estimate is only computed when the backend reported none.

## The rate table

The `pricing` config key is a map of model → rates, in **dollars per million tokens** ($/Mtok):

```json
{
  "pricing": {
    "gpt-5.5": {
      "inputPerMtok": 1.25,
      "outputPerMtok": 10.0,
      "cachedInputPerMtok": 0.125,
      "cacheWritePerMtok": 1.5625
    }
  }
}
```

| Field                | Required | Meaning                                                                                      |
| -------------------- | -------- | -------------------------------------------------------------------------------------------- |
| `inputPerMtok`       | yes      | Rate for fresh (uncached) input tokens.                                                      |
| `outputPerMtok`      | yes      | Rate for output tokens (reasoning-inclusive).                                                |
| `cachedInputPerMtok` | no       | Rate for cached/reused input (cache reads). Defaults to `inputPerMtok` when omitted.         |
| `cacheWritePerMtok`  | no       | Rate for cache-write (creation) input, claude only. Defaults to `inputPerMtok` when omitted. |

## How a rate is selected

For a given model name, frites resolves the table entry as follows:

1. **Exact match** wins. If the table has a key equal to the model name, that entry is used.
2. Otherwise a **prefix match in either direction**: a table key is used if the model name starts with the key **or** the key starts with the model name.

The bidirectional prefix rule means a coarse key like `"gpt-5.5"` covers a fully versioned model id like `"gpt-5.5-2026-…"`, and a fully versioned key still matches a bare alias. If nothing fits, no rate is found and that child's spend is left unestimated.

## How an estimate is computed

frites normalizes usage to a provider-agnostic shape before estimating:

* `inputTokens` is the **grand total** input (all categories summed).
* `cacheReadTokens` is the cached/reused portion of that total.
* `cacheCreationTokens` is the cache-write portion (claude only).
* `outputTokens` is reasoning-inclusive on both providers.

The **fresh** (newly billed) input is `inputTokens − cacheReadTokens − cacheCreationTokens` (floored at zero). The estimate is then:

```
fresh * inputPerMtok
  + cacheReads * (cachedInputPerMtok ?? inputPerMtok)
  + cacheWrites * (cacheWritePerMtok ?? inputPerMtok)
  + output * outputPerMtok
```

divided by 1,000,000. When no rates are supplied for the model, the estimate is `undefined`; estimation is strictly opt-in.

## Where estimates surface

Estimated spend appears in the same places authoritative cost does: the live per-agent progress line, the per-turn council recap, and the gateway log, always marked with a `~` so estimates are visually distinct from claude's reported figures. The same estimator (`@frites/core`) is the single source of truth used by both the gateway's answer-council path and the worktree engine path.

See [cost telemetry](/concepts/cost-telemetry) for how these figures are displayed during a turn, and [configuration](/reference/configuration) for the `pricing` key alongside the other config keys.


# Overview

frites is a coordinator that dispatches a task to **multiple full coding agents**, has each do real work, then **diffs / tests / judges** their results into one vetted answer, driven from your normal Claude Code or Codex session. The value is *reconciliation quality*: many independent attempts filtered by execution, not vibes. The moat is the selector, not the fan-out.

This page is the entry point for the architecture cluster. It covers the repo-level shape, the high-level decisions that drove it, and the layer diagram. Each subsystem has its own page linked below.

## Repository shape

frites is a TypeScript pnpm monorepo split into thin **apps** (runnable entry points) and the heavy logic in **packages** (libraries with no entry points). The deliberate goal is to keep all reconciliation logic in `packages/core` so it stays transport-agnostic and fully unit-testable with mocked runners and oracles.

```
apps/                              # runnable tools (deployables / entry points)
  gateway/     @frites/gateway    TRANSPARENT PROXY (primary surface): impersonates /v1/messages
                                   (Claude Code) + /v1/responses (Codex); intercepts every prompt,
                                   answer/action-council fan-out per fanOutPolicy + fanOutScope,
                                   SSE streaming, per-turn cost telemetry. Stance-A: synthesizes the
                                   assistant turn — emits host-executed tool_use on coding turns.
  mcp/         @frites/mcp        on-demand MCP tool surface (Stance B): frites_implement +
                                   frites_apply — heavy multi-agent file edits in worktrees → diffs
  cli/         @frites/cli        standalone `frites run` + `frites config` — same engine
packages/                          # libraries (no entry points)
  core/        @frites/core       engine (funnel) + oracle + judge + config + answer-council
  isolation/   @frites/isolation  git worktree lifecycle, diff capture, apply-to-branch
  agents/      @frites/agents      headless claude/codex runners + completions + cost estimation
                                   + EnvSandbox (recursion guard)
```

The apps stay thin: each transport adapts a wire format and streams progress, but the funnel, oracle, judge, config, and councils all live in `packages/core`.

## Two surfaces, one engine

frites ships two transports over one shared engine, for different needs:

| Surface                         | Stance                              | Best for                                                                                      |
| ------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------- |
| **Gateway** (transparent proxy) | Stance A: answer/action synthesizer | The frictionless everyday brain: Q\&A, reasoning, and coding edits on *every* prompt, metered |
| **MCP** (worktree mode)         | Stance B: agentic broker            | Deliberate heavy multi-agent file edits in worktrees, filtered by tests                       |

The gateway is the **primary, everyday surface and is Stance A**: children are stateless completions, the host keeps its tool loop, and frites fans out per turn and synthesizes the assistant turn. On a coding turn it emits the `tool_use` the host executes (the children *decide* the action; they don't edit files). The MCP path is Stance B: children are full agents that do real file edits in isolated worktrees, and frites reconciles their work with the test suite as the ground-truth oracle.

The standalone CLI (`frites run` / `frites config`) calls the same engine for testing, CI, and power use.

## High-level decisions

* **Both stances ship.** The transparent-proxy gateway is the primary, lowest-friction surface (Stance A, verified editing real code end-to-end via host-executed `tool_use`, no API key). The MCP `frites_implement` path is Stance B, for when you want N competing full implementations filtered by tests.
* **"N-way merge" is the wrong mental model.** frites never mechanically merges N divergent edit trees. Reconciliation is LLM-mediated best-of-N selection, with the test suite as the ground-truth oracle and the judge scoped to *only* tie-break test-passing survivors. See [Synthesis & reconciliation](/concepts/synthesis-and-reconciliation).
* **Diversity comes from model-mix + prompt-framing, not temperature.** Neither `claude` nor `codex` exposes a temperature flag, so candidate diversity must come from mixing model families (claude × codex) and prompt framing. The default leans toward N=2 (1 claude + 1 codex).
* **Fan-out is scoped to the substantive turn.** The gateway does not fan out a full council on every mechanical tool-loop step or on background/utility traffic. See [Fan-out scope](/concepts/fan-out-scope).
* **Language: TypeScript, pnpm monorepo.** I/O-bound orchestration glue around finicky wire formats, where the official SDKs are TS-first.

## Engine state machine

The engine is a state machine over a funnel and holds zero CLI/MCP coupling:

```
DISPATCH → EXECUTE (N children in worktrees, concurrent)
        → ORACLE-FILTER (run repo tests/build/lint per candidate)
        → reconcile:  1 survivor → done
                      0 survivors → one grounded feedback round → re-filter;
                                    else surface best near-miss
                      ≥2 survivors → JUDGE (pairwise tie-break, prefer smaller diff)
        → optional gated SYNTHESIS (re-validated through oracle)
        → PRESENT (recommended diff + per-candidate comparison)
        → APPLY (on approval: git switch -c frites/<runId> && git apply --3way)
```

The engine internals, event model, and failure modes are documented in [Core engine](/architecture/core-engine).

## The rest of the architecture cluster

* [Gateway](/architecture/gateway): transparent proxy design, `/v1/messages` + `/v1/responses`, SSE, action synthesis, tool-call emission.
* [MCP worktree mode](/architecture/mcp-worktree-mode): MCP transport quirks, worktree execution, candidate diffs, oracle filtering, apply flow.
* [Core engine](/architecture/core-engine): engine internals, synthesis engine shape, event model, failure modes.
* [Agents & runners](/architecture/agents-and-runners): headless claude/codex runners and completions.
* [Isolation](/architecture/isolation): git worktree lifecycle, diff capture, apply-to-branch.
* [Data flow](/architecture/data-flow): end-to-end request flow for both surfaces.
* [Risks & tradeoffs](/architecture/risks-and-tradeoffs): the "better output, slower" tradeoff and the top risks.

For the safety and permission posture, see the canonical [Safety model](/product/safety-model). For child auth and billing, see [Auth & billing](/product/auth-and-billing).


# Gateway

The gateway (`apps/gateway`, `@frites/gateway`) is frites's primary, everyday surface: a **transparent proxy** that impersonates the model endpoint and intercepts every prompt with zero "use frites" friction. It is the implementation of **Stance A**: children are stateless completions, the host keeps its tool loop, and frites synthesizes the assistant turn per turn.

This page covers the proxy design. For the wire-level request/response shapes and SSE event sequence, see [Gateway API](/reference/gateway-api).

## Transparent proxy design

frites impersonates the provider endpoint so the host CLI talks to frites instead of the real backend:

* **Claude Code** points `ANTHROPIC_BASE_URL` at the gateway and posts to `/v1/messages`.
* **Codex** points its provider `base_url` at the gateway and posts to `/v1/responses`.

Because frites is the brain for every prompt, all traffic is metered (there is no free interactive top-level). The gateway binds to `127.0.0.1` only. The recursion risk (children inheriting `ANTHROPIC_BASE_URL` and recursively calling the gateway) is handled by env-scrubbing every child; see the [Safety model](/product/safety-model).

The two endpoints are:

| Endpoint                         | Host                             | Status                                                         |
| -------------------------------- | -------------------------------- | -------------------------------------------------------------- |
| `POST /v1/messages`              | Claude Code (Anthropic Messages) | Answer + action council, including `tool_use` emission         |
| `POST /v1/responses`             | Codex (OpenAI Responses)         | Answer synthesis only; `function_call` emission is a follow-up |
| `POST /v1/messages/count_tokens` | Claude Code                      | Token counting passthrough                                     |

## Per-turn flow

Each inbound request is one host turn. The gateway classifies the traffic, decides whether to fan out, runs the relevant council, and streams the result back over SSE.

* **Answer/reasoning turns** call `runAnswerCouncil`: N children independently answer, then the synthesizer adjudicates them into one final answer.
* **Coding turns with tools** call `runActionCouncil`: N children each propose exactly one next action as JSON, and the synthesizer selects one concrete next action for the host to execute.

Whether a turn fans out at all is gated by `fanOutPolicy` (`always | auto | necessary | never`) and, under `auto`, a cheap LLM fan-out judge with a heuristic short-circuit on trivially-simple prompts. **Which** turns get the question is bounded by `fanOutScope`, see [Fan-out scope](/concepts/fan-out-scope). Background/utility traffic (host haiku calls for title generation, summarization, classification) never fans out and is pinned to a single child.

The synthesis and selection rules are canonical in [Synthesis & reconciliation](/concepts/synthesis-and-reconciliation).

## Action synthesis and tool-call emission

On a coding turn the gateway drives the host's full agentic loop **with no API key**. Subscription `claude -p` children decide the next action via `runActionCouncil`, and the gateway constructs the host-executed `tool_use` envelope:

* Each child is prompted as a decision engine and must return exactly one JSON object: `{"kind":"tool", ...}` to ask the host to call a tool, or `{"kind":"answer", ...}` to finish with text.
* The synthesizer selects one proposed tool call **verbatim** (it is instructed never to blend tool names or inputs from different proposals); for an answer action it may synthesize freely.
* The gateway then encodes the selected action as an Anthropic `tool_use` content block with `stop_reason: "tool_use"`, which the host executes under its own permission model. The deeper semantic check happens when the host returns the tool result on the next turn.

This was verified end-to-end (real `claude` → gateway → Read → Edit → answer, bug fixed, `npm test` passed) with no API key. Codex `/v1/responses` `function_call` emission is the standing ceiling, not yet built; the Responses endpoint does answer synthesis only for now.

## The synthesizer is `defaultAgents[0]`

The synthesizer is **not** a separate model. It is `config.defaultAgents[0]`, invoked with `role: "synth"` (see `specFor` in `apps/gateway/src/index.ts`). Children round-robin the same `defaultAgents` array, so slot 0 is both the synthesizer and child index 0, and reordering `defaultAgents` changes both. There is no separate synthesizer model setting.

This synthesizer is distinct from the *fan-out judge*, the cheap classifier under `fanOutPolicy: auto` that decides **whether** to fan out.

## Progress streaming

The gateway streams over SSE. For answer turns, only the synthesizer streams live into the final answer block; child output normally goes to progress telemetry, not the user-facing answer. In interleaved progress mode child output can be shown in the progress channel, but it stays separate from the final answer, so users normally see progress plus one final synthesized answer, not a visible debate between children.

Each turn also carries per-turn cost telemetry (config-driven `pricing` estimation for backends that don't self-report cost, e.g. codex) and emits a closing **council recap** line. The in-editor thinking/reasoning channel is live-only and the host collapses it once the turn ends, so the durable per-turn detail lives in the gateway log.

## Related pages

* [Gateway API](/reference/gateway-api): the wire-level endpoint and SSE reference.
* [Data flow](/architecture/data-flow): the full request → council → result sequence.
* [Fan-out scope](/concepts/fan-out-scope): which turns fan out.
* [Synthesis & reconciliation](/concepts/synthesis-and-reconciliation): how answers and actions are reconciled.
* [Auth & billing](/product/auth-and-billing): why every gateway prompt is metered.


# MCP worktree mode

MCP worktree mode (`apps/mcp`, `@frites/mcp`) is frites's **Stance B** surface: an on-demand MCP tool that runs N competing full implementations as real agents in isolated git worktrees, filters them through the repo's test suite as the ground-truth oracle, and recommends one vetted diff. It exposes two tools over stdio: `frites_implement` and `frites_apply`.

Impersonation is the wrong fit here: returning N candidate diffs plus a comparison and running minutes-long worktree agents needs a tool call, not a single model turn. So the heavy multi-agent file-edit work lives on the MCP surface rather than the gateway.

## MCP transport and host quirks

The tools run over stdio. Several MCP host behaviors are load-bearing and were verified against the real hosts:

* **Progress notifications are display-only. They do NOT extend either host's deadline.** Size timeouts to worst-case wall-clock up front.
* **Claude Code:** set the per-tool `timeout` to `600000` and `alwaysLoad: true` so the tool isn't hidden behind Tool Search. It renders `notifications/progress` inline.
* **Codex:** `tool_timeout_sec` defaults to **60s and MUST be raised to 600** or every run dies.
* **Result size:** Claude warns at \~10k tokens and hard-caps at \~25k. Return compact `structuredContent` plus a `resource_link` to each diff, never inline N full diffs.
* **No MCP `sampling` for the judge:** Claude Code doesn't implement a sampling client and Codex explicitly refused to. frites calls models with its own credentials instead.

## Worktree execution

When the host calls `frites_implement {task, repoPath, n?, agents?}`, the engine:

1. Selects N agents from the task or config and resolves the base commit.
2. Creates one isolated git worktree per agent (managed by `@frites/isolation`).
3. Spawns detached headless children that edit in their own worktree concurrently, each launched with an allowlist env built by the `EnvSandbox` (auth kept, base-URLs scrubbed, `FRITES_DEPTH` incremented).
4. Streams `notifications/progress` ("agent 2 editing app.ts / running tests") as the children work.

The worktree lifecycle, diff capture, and cleanup are documented in [Isolation](/architecture/isolation).

## Candidate diffs and oracle filtering

Each child's work is captured as a candidate diff (`git diff --staged`). The engine then runs the configured or auto-detected oracle commands (build, lint, test) against each candidate and reconciles them into one recommendation:

* Candidates that errored, timed out, were empty, or touched no files are ignored.
* If oracle commands exist, only candidates whose oracle passed are kept; the closest near-miss is surfaced if none pass.
* One passing candidate is recommended directly; multiple passing candidates are tie-broken by the deterministic smallest-blast-radius `heuristicJudge` (fewest changed lines, then fewest files).

An optional, on-by-default synthesis stage can integrate the passing candidates' deltas into one re-validated candidate. The full reconciliation and synthesis algorithm is canonical in [Synthesis & reconciliation](/concepts/synthesis-and-reconciliation), and the oracle mechanics are detailed in [Worktree oracle](/concepts/worktree-oracle).

## Apply flow

The MCP path lands changes only via an explicit, gated apply. It never auto-merges or pushes:

1. `frites_implement` returns `structuredContent` plus `resource_link`s to each candidate diff. The caller persists diffs and run metadata for review.
2. The user reviews the recommended diff and the per-candidate comparison.
3. `frites_apply {runId}` lands the diff on a fresh branch: `git switch -c frites/<runId> && git apply --3way`. It accepts a `candidateId` so a reviewer can land a tighter passing child instead of the recommended candidate.

This explicit apply is the one mandatory human gate. The permission posture for worktree children (bypassed permissions for Claude, `-s workspace-write` with approvals disabled for Codex) is canonical in the [Safety model](/product/safety-model).

## Related pages

* [Isolation](/architecture/isolation): worktree lifecycle, diff capture, apply-to-branch.
* [Worktree oracle](/concepts/worktree-oracle): the test/build/lint oracle.
* [MCP tools](/reference/mcp-tools): the `frites_implement` / `frites_apply` tool reference.
* [Core engine](/architecture/core-engine): the shared engine state machine.
* [Data flow](/architecture/data-flow): the end-to-end worktree sequence.


# Core engine

The engine is the transport-agnostic heart of frites. It lives in `packages/core/src/engine.ts` (the `@frites/core` package) and holds zero CLI/MCP/git coupling, so it is fully unit-testable with mocked runners and oracle. It powers the heavy-edit worktree path behind `frites_implement` and the CLI runner; the gateway answer/action councils are a separate path (see [the gateway architecture](/architecture/gateway)).

The engine is a state machine over a funnel:

```
DISPATCH → EXECUTE (N children in worktrees, concurrent)
        → ORACLE-FILTER (run repo tests/build/lint per candidate)
        → optional gated SYNTHESIS (re-validated through oracle)
        → RECONCILE (1 survivor → done; 0 → near-miss; ≥2 → judge tie-break)
        → PRESENT (recommended diff + per-candidate comparison)
        → APPLY (on approval, on a fresh frites/apply/<runId> branch)
```

## Engine boundaries

The engine declares its dependencies as structural interfaces in `EngineDeps`, satisfied by `@frites/isolation` and `@frites/agents` at the edges:

* `WorktreeManagerLike`: `resolveBase`, `create`, `captureDiff`, `cleanup`, and optional `applyDiffToWorktree` (see [isolation](/architecture/isolation)).
* `RunAgentFn` (`runAgent`) spawns one child in a worktree and returns an `AgentRunOutput` (status, summary, cost, normalized token usage). See [agents and runners](/architecture/agents-and-runners).
* `RunOracleFn` (`runOracle`) and `oracleCommands` run build/lint/test against one candidate worktree.
* `config` (`FritesConfig`), `newRunId`, and an optional external `signal` for client-disconnect cancellation.

`runEngine(task, deps, onEvent)` drives the whole funnel and returns a `RunResult`. All worktree cleanup runs in a single `finally` over the shared `handles` map, so every worktree (children and synthesis) is reaped even on a throw.

## Dispatch and selectAgents

`selectAgents(task, config)` resolves the agent roster:

* If the task supplies `agents`, they are used as-is.
* Otherwise it takes `config.defaultAgents` and round-robins to `n` specs, where `n = max(1, min(task.n ?? config.defaultN, 10))` (capped at 10). Indices past the base array get a suffixed id (e.g. `claude-1-2`).

Each agent runs concurrently via `Promise.all` over `runOneAgent`. `runOneAgent` creates the worktree, registers its handle in `handles` immediately (so cleanup always covers it), emits `agent-started`, runs the child with a prompt from `buildPrompt`, then calls `captureDiff`. A candidate is `succeeded` only when the child exited succeeded AND touched at least one file; otherwise `empty`, `errored`, or `timed-out`.

`buildPrompt` assembles the child prompt from the task instructions, optional acceptance criteria, the agent's `framing`, a fixed "work only within this repository / keep tests green" instruction, and the shared `childDirective`.

## Oracle filter

`runOracleFor` runs the configured `oracleCommands` against each succeeded candidate's worktree. Commands run in `build → lint → test` order, and a build failure short-circuits the rest (`packages/core/src/oracle.ts`). A candidate `passed` only when every command that ran exited 0; if no command ran, `hadOracle` is false and the candidate does not pass. Oracle commands are either explicit config or auto-detected from `package.json` scripts via the detected package manager (pnpm/yarn/bun/npm). With no `package.json` and no override, the oracle is empty.

## Synthesis stage

When `synthesisMode` is `"passing-only"` (the default; `"off"` restores winner-take-one) and at least `synthesisMinCandidates` (default 2) candidates pass the oracle, `maybeRunSynthesis` runs after oracle filtering and before final reconciliation. It only affects this worktree path, never the gateway. The canonical design rationale lives in [synthesis and reconciliation](/concepts/synthesis-and-reconciliation); the engine-level shape is:

1. **Eligibility**: `evaluateSynthesisEligibility` requires synthesis enabled, an executable oracle, and `≥ synthesisMinCandidates` usable, oracle-passing candidates. If not eligible (or the run is already aborted), it emits `synthesis-skipped` with a reason and records `attempted: false`.
2. **Seed**: `heuristicJudge` picks the smallest passing diff as the seed. A fresh worktree is created from the same base SHA and seeded with the seed candidate's diff via `applyDiffToWorktree` (`git apply --3way`), so the synthesizer refines a known-good tree. If the seed cannot apply (or the manager has no `applyDiffToWorktree`), it falls back to fresh-from-base.
3. **Synthesizer**: `selectSynthesizer` picks `config.synthesisAgent`, else the first claude child (so `--max-budget-usd` / `synthesisBudgetUsd` actually bites), else the first agent. A reserved id (`synthesis-1`, …) is guaranteed not to collide with any child id. The synthesis worktree handle is registered in `handles` the instant it is created, so the engine's `finally` reaps it on any later throw.
4. **Prompt**: `buildSynthesisPrompt` gives the synthesizer the task, acceptance criteria, base ref/SHA, and the OTHER passing candidates' diffs (smallest first, embedded up to `synthesisMaxDiffChars`; past the cap a candidate is reduced to its file list plus its read-only worktree path). The instruction is to integrate the strongest ideas, never to mechanically concatenate patches.
5. **Capture + verify**: the synthesis diff is captured with the same `captureDiff` and run through the SAME oracle.

The synthesis candidate is a normal `Candidate` (tagged `synthesis: true`, with `synthesizedFrom`). It is appended to both `result.candidates` and `result.oracle`, so cost telemetry, persistence, the comparison table, and the survivor count all flow through one source of truth.

### Synthesis preference

`applySynthesisPreference` is a thin wrapper applied on top of the pure `reconcile` result over the original candidates. The synthesized candidate is preferred (yielding decision `"synthesis"`) only when it is usable, passed the same oracle, AND its blast radius (`diffSize`) is within `synthesisMaxBlastFactor ×` (default 1.5) the combined changed-line count of the passing inputs. Otherwise frites falls back to the best original passing candidate and records a `fallbackReason` on the `SynthesisInfo`. Gating the preference preserves the smallest-blast-radius safety stance: passing the oracle is the same bar the children already cleared, so an unconditional preference for a usually-larger synthesis would invert that stance when the oracle is weak.

### Synthesis event model

In addition to the per-agent events, synthesis emits a dedicated event sequence (`packages/core/src/events.ts`) so a long synthesis run is observable:

| Event                                                    | Meaning                                                                    |
| -------------------------------------------------------- | -------------------------------------------------------------------------- |
| `synthesis-skipped`                                      | synthesis not eligible (with `reason`)                                     |
| `synthesis-started`                                      | `inputAgents` and `seededFrom` (the seed candidate id, if seeding applied) |
| `synthesis-progress`                                     | streamed synthesizer output / seed-failure notice                          |
| `synthesis-finished`                                     | candidate `status` + `filesTouched`                                        |
| `synthesis-oracle-started` / `synthesis-oracle-finished` | synthesis oracle run + `passed`                                            |

### Synthesis failure modes

The stage fails safe in every case, always falling back to the best original passing candidate (recorded via `SynthesisInfo.fallbackReason`):

* **Empty diff**: the synthesizer touched no files, so its status becomes `empty`, `usable` is false, and it falls back with "produced no usable change".
* **Errored / timed-out**: the synthesizer process fails or is reaped by the idle/hard timeout, so it is not usable and falls back. The synthesizer ships a concrete `synthesisHardTimeoutMs` (default 30 min) ceiling unlike the off-by-default per-child hard timeout.
* **Oracle fail**: the synthesis runs and produces a diff but fails the SAME build/lint/test oracle, so it falls back with "failed the oracle".
* **Over-broad**: synthesis passes but exceeds the `synthesisMaxBlastFactor ×` ceiling, so it falls back to avoid an over-broad change.
* **Aborted before synthesis**: if `deps.signal.aborted` before the stage, it is skipped (the engine does not check `.aborted` between phases otherwise).

In all fallback cases the best original passing candidate is still recommended. Synthesis can only ever improve on, never lose, a verified child result.

## Reconciliation and the judge

`reconcile` is pure over the original candidates:

1. Keep only `usable` candidates (`succeeded` with ≥1 file touched). If none → decision `near-miss`, no recommendation.
2. If there is no executable oracle → `heuristicJudge` picks a best-effort winner; decision `no-oracle` with an explicit "NOT verified by tests" rationale.
3. Filter usable candidates to oracle survivors. If none → surface the closest near-miss via `heuristicJudge`; decision `near-miss`.
4. Exactly one survivor → recommend it; decision `single` (only one agent ran) or `tests`.
5. ≥2 survivors → `heuristicJudge` tie-break; decision `judge`.

`heuristicJudge` (`packages/core/src/judge.ts`) is the deterministic smallest-blast-radius tie-breaker: it ranks survivors by smallest changed-line count (`diffSize`), then fewest files touched. This is the v1 selector; an LLM pairwise judge called with frites's own credentials is later work. frites never mechanically N-way merges divergent trees.

`ReconcileDecision` is one of `single | tests | judge | synthesis | near-miss | no-oracle`. Note `decision` is not compiler-enforced at render sites (it is string-interpolated), so every surface must handle the `synthesis` value explicitly.

## Cost telemetry

`costNote` sums per-candidate spend across all candidates (including synthesis). It prefers the backend's self-reported `costUsd` (claude reports `total_cost_usd`) and falls back to a `pricing`-table estimate from captured tokens when a backend reports no cost (codex against the ChatGPT backend). Estimation is opt-in: it only runs when `pricing` rates are configured. See [cost telemetry](/concepts/cost-telemetry).

## Configuration

Every config key that controls the engine (`defaultN`, `defaultAgents`, `perChildTimeoutMs`, `perChildHardTimeoutMs`, `perChildBudgetUsd`, `oracle`, `maxDepth`, the `synthesis*` keys, and the `pricing` table) is documented canonically in [configuration](/reference/configuration).

## Related

* [Synthesis and reconciliation](/concepts/synthesis-and-reconciliation): the design rationale and reconciliation policy.
* [Configuration](/reference/configuration): all config keys.
* [Agents and runners](/architecture/agents-and-runners): how children are spawned.
* [Isolation](/architecture/isolation): worktree lifecycle and diff capture.


# Agents & runners

The `@frites/agents` package (`packages/agents`) is the child-execution layer. It turns an `AgentSpec` plus an `AgentRunContext` (cwd, prompt, abort signal, progress callback) into an `AgentRunOutput`, spawning the headless Claude or Codex CLI, streaming its NDJSON output, accumulating cost/token telemetry, and enforcing timeouts. It also owns the [environment sandbox](/architecture/isolation) recursion guard. See the package overview in [services/agents](/services/agents).

## The runner abstraction

A `CliRunnerDef` (`packages/agents/src/runner.ts`) describes one backend:

* `kind`: `claude-cli` or `codex-cli`.
* `command`: the binary (`claude` / `codex`).
* `buildArgv(spec, ctx)`: the argument vector.
* `onLine(line, emit, acc)`: parses one stdout line (typically NDJSON), emitting progress messages and accumulating results into a `RunAccumulator`.

`makeRunAgent({ runners, config, passApiKeys })` builds the `RunAgentFn` the engine calls. For each spawn it:

1. Looks up the runner by `spec.kind` (errors if none is registered).
2. Reads `currentDepth()` and calls `assertDepth(depth, config.maxDepth)`, the recursion fuse.
3. Builds the allowlist child env via `buildChildEnv`.
4. Applies config defaults to the spec so per-child budget/timeout/reasoning take effect even when the spec omits them: `maxBudgetUsd ?? perChildBudgetUsd`, `timeoutMs ?? perChildTimeoutMs`, `hardTimeoutMs ?? perChildHardTimeoutMs`, and (codex only) `reasoningEffort ?? codexReasoningEffort`.
5. Spawns and streams.

## Spawn and streaming

`spawnAndStream` (`packages/agents/src/runner.ts`) runs the child:

* **Detached process group** (`detached: true`) so the whole tree can be killed via `process.kill(-pid, signal)`.
* **Prompt over stdin, not argv.** Real transcripts exceed `ARG_MAX`, so passing the prompt as an argument would spawn `E2BIG`. The runner writes the prompt to stdin and closes it; the EOF stops the child waiting for more input (both `claude -p` and `codex -` read to EOF). `EPIPE` is swallowed if the child dies before draining.
* **Line-buffered NDJSON.** stdout is split on newlines; each non-empty line is handed to `def.onLine`, wrapped in a try/catch so schema drift never crashes the runner.
* **Logging.** All stdout/stderr is buffered and written to a per-run log file in `tmpdir()` (`frites-<id>-<ts>.log`); the path is returned as `logPath`.
* **Close handling.** On close, status is `timed-out` if a timeout fired, else `succeeded` (exit 0) or `errored`. An aborted child reports error `aborted`; a non-zero exit reports `exit code <n>`.

## Timeout behavior

Timeouts are **idle**, not wall-clock (`packages/agents/src/timeout.ts`). `startIdleTimeout` arms a countdown of `idleMs` (the spec's `timeoutMs`, default `perChildTimeoutMs` = 600000 = 10 min) that resets on every stdout/stderr chunk via `idle.touch()`. A child that keeps streaming events runs as long as it stays productive; only genuine silence (a deadlock, a stalled read, an output-less spin) reaps it. This replaced an older fixed wall-clock deadline that killed exhaustive runs mid-flight.

An optional non-resetting absolute ceiling (`hardMs` / `hardTimeoutMs` / `perChildHardTimeoutMs`) is the secondary backstop for the "spinning forever while still emitting bytes" case; it is off by default for normal children. When a timeout fires, the runner sends `SIGTERM` to the process group, then escalates to `SIGKILL` after a 3-second grace (`KILL_GRACE_MS`). The same path handles external aborts (client disconnect via `ctx.signal`).

## Claude headless runner

`claudeRunner` (`packages/agents/src/claude.ts`) invokes `claude` with:

```
-p --output-format stream-json --verbose
--permission-mode bypassPermissions
--strict-mcp-config --setting-sources project
```

Plus `--model` and `--max-budget-usd` when the spec supplies them. Headless Claude reuses the machine's subscription OAuth (keychain), so no API key is needed; for headless use this draws the metered Agent-SDK credit. `--strict-mcp-config` and `--setting-sources project` keep the child from auto-loading frites itself (recursion guard). `--permission-mode bypassPermissions` lets the worktree child edit without interactive approvals; the worktree plus the final human diff review is the boundary (see [safety model](/product/safety-model)).

`onLine` parses the stream-json events: a `system/init` emits "session started", `assistant` content emits `using <tool>` for `tool_use` blocks and accumulates text into `summary`, and the final `result` event captures `total_cost_usd` (authoritative) and usage. Claude reports fresh / cache-read / cache-write input as disjoint categories; the accumulator sums them into the total `inputTokens` and records the cache subsets. `output_tokens` already includes thinking, so no reasoning fold is needed.

## Codex headless runner

`codexRunner` (`packages/agents/src/codex.ts`) invokes `codex` with:

```
exec --ignore-user-config --json --skip-git-repo-check
-s workspace-write -C <cwd> -c approval_policy="never"
```

Plus `-c model_reasoning_effort="<effort>"` when set, `-m <model>` when set, and a trailing `-` so codex reads the prompt from stdin. Codex reuses the machine's ChatGPT sign-in (`~/.codex/auth.json`). `--ignore-user-config` prevents loading `config.toml` (which might route back to the gateway, causing recursion). Approval is disabled via `-c approval_policy="never"` (the `--ask-for-approval` flag exits 2); `-s workspace-write` lets it edit within the worktree.

**Reasoning effort.** `codexReasoningEffort` defaults to `"high"` so codex analyzes as hard as claude before acting; a per-agent `reasoningEffort` overrides it. Only `low`/`medium`/`high` are safe: `"minimal"` returns a 400 on the stock codex model because it is incompatible with the built-in web\_search/image\_gen tools. Claude has no equivalent flag; its depth comes from the model plus the shared directive.

The codex NDJSON schema drifts between versions, so `onLine` is intentionally defensive: it reads `obj.type ?? obj.msg?.type`, maps command/patch/message events to coarse progress strings ("running a command", "editing files", "thinking"), and accumulates `summary` from whichever text field is present. Codex `input_tokens` is already the inclusive total (cached is a subset), so it is passed through; hidden `reasoning_output_tokens` are folded into `outputTokens` so the total is comparable with claude. The ChatGPT backend usually omits `cost_usd`; when present (API-key path) it is authoritative.

## Child directive and completions

Every substantive child prompt (answer, action, and execute paths) has the shared thoroughness directive appended (`childDirective`, default `DEFAULT_CHILD_DIRECTIVE` in `packages/core/src/config.ts`). It tells all backends to read before answering, trace the actual execution path, consider edge cases, and verify by running the build/tests. This is the provider-agnostic half of "make every agent thorough": it lifts codex to claude-like depth. Background/utility turns (title generation, summarization, the fan-out judge) deliberately skip it; set `childDirective` to `""` to disable. The agents package also exposes answer-only completions for the gateway answer/action councils, which use the same runners with answer-only permission constraints (see [safety model](/product/safety-model)).

## Pricing hooks

Both runners normalize token usage into the same `AgentRunOutput`/`Candidate` shape (`inputTokens` total, `cacheReadTokens`/`cacheCreationTokens` subsets, reasoning-inclusive `outputTokens`). Claude reports authoritative `costUsd`; codex against the ChatGPT backend reports none, so the engine estimates its spend from the configured `pricing` table and captured tokens. See [cost telemetry](/concepts/cost-telemetry) and the [pricing reference](/reference/pricing).

## Env-sandbox integration

Before any spawn, `makeRunAgent` calls `assertDepth` and `buildChildEnv`, which build the child environment by allowlist (never copying `process.env`), withhold API keys unless opted in, scrub base-URL variables, and increment `FRITES_DEPTH`. This is the recursion guard and secret-minimization boundary, detailed in [isolation](/architecture/isolation) and the canonical [safety model](/product/safety-model).

## Related

* [Agents service](/services/agents): package overview.
* [Isolation](/architecture/isolation): worktree lifecycle and the env sandbox.
* [Core engine](/architecture/core-engine): how the engine drives runners.
* [Safety model](/product/safety-model): permission posture per surface.


# Isolation

The `@frites/isolation` package (`packages/isolation`) gives each child agent its own git worktree to edit in, captures the resulting diff, and lands an approved diff on a fresh branch. `WorktreeManager` implements the `WorktreeManagerLike` interface the [core engine](/architecture/core-engine) depends on, so the engine has zero git coupling. See the package overview in [services/isolation](/services/isolation).

The authoritative implementation result is never the child's output text; it is the git diff captured from its worktree. This is what makes the worktree path frites's strongest verification surface.

## Worktree lifecycle

### Resolve base

`resolveBase(repoPath, ref?)` first asserts the path is a git repo (`assertGitRepo` runs `git rev-parse --is-inside-work-tree`; a non-repo raises an instructive error pointing at `git init`). It then resolves the base ref (default `HEAD`) to a concrete SHA with `git rev-parse`, so every child branches from the same immutable commit.

### Create

`create(repoPath, runId, agentId, baseSha)` runs:

```
git worktree add --quiet -b frites/run/<runId>/<agentId> .frites/worktrees/<runId>/<agentId> <baseSha>
```

The worktree path lives under `.frites/worktrees/<runId>/<agentId>` inside the repo (gitignored, local to the repo). The branch is namespaced `frites/run/<runId>/<agentId>` so it can never collide at the ref level with the apply branch `frites/apply/<runId>`. Git refs are files, so a branch named `frites/<runId>` could not coexist with `frites/<runId>/<agentId>`.

The engine creates worktrees concurrently (one per child) and registers each handle in its shared `handles` map the instant `create` returns, so cleanup always covers them on any later throw.

## captureDiff

After a child exits, `captureDiff(worktreePath)` reads the actual change out of git:

1. `git add -A`: stage everything, including new files, so the diff is complete.
2. `git diff --staged --no-color -- . <excludes>`: the unified diff.
3. `git diff --staged --name-only -- . <excludes>`: the touched-file list.

The excludes (`DIFF_EXCLUDES`) drop `node_modules`, `dist`, and `.frites` so generated artifacts never pollute candidate diffs. A candidate is usable only when its status is `succeeded` and it touched at least one file; this captured diff is the candidate's `diff` and `filesTouched`.

## Seeding the synthesis worktree

`applyDiffToWorktree(worktreePath, diff)` exists to seed the [synthesis](/architecture/core-engine) worktree from a known-good tree. It runs `git apply --3way --index`, applying a captured candidate diff into a worktree created from the same base SHA (so the 3-way apply is conflict-free). `--index` stages the result, and the diff is newline-terminated if needed. This method is optional on the interface; when it is absent or throws, synthesis falls back to fresh-from-base.

## Apply to branch

`applyToBranch(repoPath, runId, diff)` is the one mandatory human gate. It is separate from implementation and lands an approved diff:

1. Assert the path is a git repo.
2. **Require a clean working tree**: `git status --porcelain` must be empty, else it throws asking the user to commit or stash first (frites is about to switch branches).
3. `git switch -c frites/apply/<runId>`: create and check out a fresh branch.
4. `git apply --3way --index` the diff. If apply fails, it throws with the branch already checked out so the user can resolve manually.

It never touches the user's current branch history, never auto-merges, and never pushes. The MCP `frites_apply` tool drives this path; with synthesis, a reviewer can pass an explicit `candidateId` to land a tighter passing child instead of the recommendation. See the [safety model](/product/safety-model) for the apply gate's place in the blast-radius controls.

## Cleanup assumptions

`cleanup(repoPath, handle)` tears down one worktree:

```
git worktree remove --force <path>
git branch -D <branch>
git worktree prune
```

`--force` and `prune` make cleanup reliable even when a worktree was left dirty or a crash interrupted a run. The engine runs cleanup for every registered handle (children and synthesis alike) inside a single `finally` via `Promise.allSettled`, so one failed removal never blocks the others. Because the other passing children's worktrees stay alive on disk until that `finally`, the synthesizer can reference them read-only during a run.

## Related

* [Isolation service](/services/isolation): package overview.
* [Core engine](/architecture/core-engine): how the engine drives the worktree manager.
* [Safety model](/product/safety-model): the apply gate and blast-radius controls.
* [Agents and runners](/architecture/agents-and-runners): how children run inside these worktrees.


# Data flow

frites has two surfaces over one shared engine, and they have distinct end-to-end flows. The gateway intercepts every prompt and synthesizes the assistant turn; the MCP worktree path runs N competing full implementations and reconciles them into one vetted diff. This page traces both.

The two flows differ at the front but share the same core shape: **request → continuation/fan-out decision → children → oracle/synthesis → result.**

## Gateway request flow

The gateway sees one inbound request per host turn (`POST /v1/messages` for Claude Code, `POST /v1/responses` for Codex). Its flow:

1. **Classify the traffic.** Background/utility calls (host haiku traffic for title generation, summarization, classification) are pinned to a single child and never fan out.
2. **Detect continuation.** A turn is a tool-loop continuation when the request carries a tool result back: an Anthropic `tool_result` in the last user message, or a Responses `function_call_output`. This is stateless: it is read from the request *shape* alone, so it is correct across restarts and concurrent sessions with no server-side session memory.
3. **Decide whether to fan out.** `fanOutScope` (default `first-turn`) bounds *which* turns even get the question: fan out on the substantive request turn, then drive the mechanical tool-loop continuations with a single agent. `fanOutPolicy` (`always | auto | necessary | never`) decides *whether* an allowed turn is worth fanning out; under `auto` a heuristic short-circuits trivial prompts and an LLM fan-out judge makes the final call. See [Fan-out scope](/concepts/fan-out-scope).
4. **Run the council.**
   * **Answer turns** call `runAnswerCouncil`: N children answer independently and concurrently (collected with `Promise.all`; a failed child becomes a textual failure block so the synthesizer still gets a complete input set), then the synthesizer adjudicates one final answer.
   * **Coding turns** call `runActionCouncil`: N children each propose exactly one next action as JSON, hallucinated tool names are rejected against the host allowlist, and the synthesizer selects one action verbatim.
5. **Stream the result.** Over SSE: for answer turns only the synthesizer streams into the final answer block; on a coding turn the gateway encodes the selected action as a host-executed `tool_use` (`stop_reason: "tool_use"`). The host executes the tool under its own permission model and returns the result, which arrives as the next continuation turn (step 2). Each turn carries per-turn cost telemetry and a closing council recap line.

The synthesizer is `config.defaultAgents[0]` invoked with `role: "synth"`; children round-robin the same array. The reconciliation rules are canonical in [Synthesis & reconciliation](/concepts/synthesis-and-reconciliation). The proxy design is in [Gateway](/architecture/gateway).

## MCP / worktree implementation flow

The worktree path is candidate selection over complete implementation attempts, not answer synthesis. Starting from the normal session:

1. **Invoke.** User in normal Claude Code says *"use frites to implement X"* → the host calls `frites_implement {task, repoPath, n?, agents?}`.
2. **Set up.** The engine resolves the base commit, decides N, and creates N isolated git worktrees. The `EnvSandbox` builds an allowlist env per child (auth kept, base-URLs scrubbed, `FRITES_DEPTH` incremented).
3. **Execute children.** `AgentRunner` spawns detached headless children that edit in isolation concurrently. The engine streams `notifications/progress` ("agent 2 editing app.ts / running tests").
4. **Capture diffs.** Each candidate's work is captured as `git diff --staged`.
5. **Oracle-filter.** Run the configured or auto-detected oracle commands (build, lint, test) per candidate. Candidates that errored, timed out, were empty, or touched no files are dropped.
6. **Reconcile.**
   * One passing candidate → recommend it.
   * Zero passing candidates → surface the closest near-miss via `heuristicJudge` (or one grounded feedback round, then re-filter).
   * Multiple passing candidates → tie-break with `heuristicJudge` (smallest changed-line count, then fewest files).
7. **Optional synthesis.** When `synthesisMode` is `"passing-only"` (default) and at least `synthesisMinCandidates` candidates pass, a synthesizer agent integrates the passing deltas in a fresh worktree seeded with the best passing candidate's diff, then the result is re-run through the **same** oracle and preferred only if it passes and stays within `synthesisMaxBlastFactor ×` the combined input size. See [Synthesis & reconciliation](/concepts/synthesis-and-reconciliation).
8. **Present.** Return `structuredContent` plus a `resource_link` to each diff; the caller persists diffs and run metadata. The user reviews the recommended diff and per-candidate comparison.
9. **Apply.** `frites_apply {runId}` lands the diff on a fresh branch (`git switch -c frites/<runId> && git apply --3way`), the one mandatory human gate. It accepts a `candidateId` to land a tighter passing child instead.

## How the two flows relate

Both flows follow **request → continuation/fan-out decision → children → oracle/synthesis → result**, but the verification depth differs:

|                  | Gateway                                                                      | MCP / worktree                                                                   |
| ---------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Children produce | One proposed answer or next action each                                      | A complete implementation (diff) each                                            |
| Verifier         | Host tool loop executes the selected action and returns the result next turn | Repo build/lint/test oracle runs against each candidate                          |
| Reconciliation   | LLM synthesis (answers) / verbatim selection (actions)                       | Oracle filter + deterministic smallest-diff tie-break + optional gated synthesis |
| Result           | Streamed assistant turn                                                      | Recommended diff + comparison, applied on approval                               |

The gateway keeps everyday interaction friction low; the worktree path provides the strongest correctness signal because candidates are actual diffs tested against real commands. See [Risks & tradeoffs](/architecture/risks-and-tradeoffs) for the "better output, slower" tradeoff.

## Related pages

* [Gateway](/architecture/gateway): the proxy surface.
* [MCP worktree mode](/architecture/mcp-worktree-mode): the worktree surface.
* [Core engine](/architecture/core-engine): the shared engine state machine and event model.
* [Fan-out scope](/concepts/fan-out-scope): which turns fan out.
* [Synthesis & reconciliation](/concepts/synthesis-and-reconciliation): how outputs are reconciled.


# Risks & tradeoffs

frites trades latency and metered spend for output quality. This page is the canonical home for that tradeoff, the top risks, the cost and latency model, hardening gaps, and the transport tradeoffs.

## The core tradeoff: better output, slower

frites's whole premise is *reconciliation quality*: many independent attempts, filtered by execution rather than vibes. The value is the selector, not the fan-out. That quality is not free:

* **The council costs latency and spend.** Fanning out to N children multiplies metered usage and runs multiple full agents instead of one. On the gateway, an LLM synthesizer then adjudicates their outputs; on the worktree path, every candidate runs the full build/lint/test oracle and an optional synthesis stage runs a further agent. Each stage adds wall-clock time.
* **The worktree result is verified, not just adjudicated.** In exchange for that cost, the MCP/worktree path returns a candidate that actually passed the repo's test suite as a ground-truth oracle. It is verified, not merely the most persuasive answer. The gateway answer/action path is lighter: it improves answer/action quality through independent proposals and synthesis, then relies on the host tool loop to execute and validate selected actions.

The two surfaces sit at different points on this curve by design: the gateway keeps everyday interaction friction low, while the worktree path spends more for stronger verification when you want competing full implementations reviewed before applying a diff. `fanOutPolicy`, `fanOutScope`, and `synthesisMode` are the levers that bound where you pay the cost.

## Top risks

1. **Reconciliation quality / verifier gap (HIGH).** Fan-out raises the ceiling; a weak selector recovers little of it. Mitigation: the tests-as-oracle spine; the LLM judge only tie-breaks survivors; honesty in the UX when no tests discriminate (a "vibes pick", surfaced as the `no-oracle` decision). Fan-out is gated behind a measured win (see the value gate below).
2. **Cost is metered, not free (HIGH).** Headless Claude burns the Agent-SDK credit then the API key; agents run roughly 4× chat tokens. Mitigation: tests-as-judge, single-survivor short-circuit, complexity-gated N, ≤1 feedback round, `--max-budget-usd`, and cost telemetry from P1.
3. **Multi-minute latency UX (HIGH).** Mitigation: size host timeouts to worst-case (Codex defaults to a 60s wall-clock and must be raised), stream rich progress, and run children truly concurrently.
4. **Full-auto safety (HIGH).** Children run headless without interactive approval. See the canonical [safety model](/product/safety-model).
5. **Context propagation (MED).** Isolation-cleaned children lack the project `CLAUDE.md`; curated context is forwarded in the prompt instead.
6. **Worktree / .git contention (MED).** The pnpm shared store amortizes installs; cleanup uses `worktree remove --force` + `prune` even on crash; `node_modules`/`dist` are excluded from diffs.

## Cost model

Spend is metered either way, because billing is decided by the invocation surface, not client identity (see [auth and billing](/product/auth-and-billing)). Controls:

* `fanOutPolicy` (`always` | `auto` | `necessary` | `never`) decides *whether* a turn fans out; `auto` uses a cheap classifier and short-circuits trivial prompts.
* `fanOutScope` (`first-turn` | `per-turn`) bounds *which* turns of a request fan out, so a task that takes N tool round-trips pays for one council, not N.
* Per-child `perChildBudgetUsd` / `--max-budget-usd` caps (claude-enforced) and `synthesisBudgetUsd` for the synthesis stage.
* Per-turn cost telemetry plus a closing council recap make spend visible; codex's footprint is estimated from the configured `pricing` table because the ChatGPT backend reports no cost. See [cost telemetry](/concepts/cost-telemetry).

## Latency model

* Children run concurrently, so a council's wall-clock is bounded by the slowest child, not their sum.
* Timeouts are idle (reset on output) so an actively-working child is not killed mid-flight, with an optional hard ceiling as a backstop; the synthesis stage ships a concrete 30-minute hard ceiling because it is a serialized tail step.
* Host deadlines do not extend on progress notifications, so MCP timeouts must be sized to worst-case wall-clock up front (Claude `timeout` `600000`; Codex `tool_timeout_sec` raised from 60 to 600).

## The value gate

Fan-out plus oracle must beat single-agent first-review-accept rate at acceptable cost, measured on roughly 10 real tickets. If it fails, the thin slice is the product. The value gate result is still open work: whether fan-out quality beats a single agent on real tickets has not been measured yet.

## Hardening gaps

frites is a high-trust local automation tool with known, documented gaps:

* No strong OS/container sandbox wraps Claude children yet.
* Secret deny-read rules for paths such as `~/.ssh`, `~/.aws`, and `.env` are planned but not enforced.
* There is no prompt-preserving child mode; child agents can inspect and (in action/worktree paths) modify the repo without per-command approval.
* Hardened `sandbox-runtime` / container execution with default-deny egress remains planned.

These belong to the safety posture detailed canonically in [the safety model](/product/safety-model).

## Transport tradeoffs

frites ships two transports over one engine, each suited to a different need:

* **Transparent proxy / gateway (primary).** Lowest friction: intercepts every prompt with no "use frites" ceremony, ideal for answer/reasoning turns and host-executed coding edits, no API key. Cost: everything is metered (frites is the brain, so there is no free interactive top-level). The recursion risk of children inheriting `ANTHROPIC_BASE_URL` is handled by env-scrubbing every child.
* **MCP server / worktree mode (heavy edits).** Returning N candidate diffs and running minutes-long worktree agents needs a tool call, not a single model turn, so impersonation is the wrong fit. Stance-B worktree work lives on MCP. This is the path that yields a verified result. Result size is constrained (Claude warns \~10k tokens, hard-caps \~25k), so frites returns compact `structuredContent` + `resource_link`s, never inline N full diffs. frites also does not depend on MCP `sampling` for the judge (neither host implements a usable sampling client); models are called with frites's own credentials.

## Related

* [Safety model](/product/safety-model): canonical permission posture and blast-radius controls.
* [Current status](/roadmap/current-status): what is built, tested, and still open.
* [Core engine](/architecture/core-engine): the reconciliation funnel.
* [Auth and billing](/product/auth-and-billing): why spend is metered.


# Gateway

The gateway is the long-lived HTTP service that fronts your existing coding agent (Claude Code or Codex). It speaks the Anthropic Messages API and the OpenAI Responses API, so the host points at it as a drop-in base URL and never knows it is talking to a council instead of a single model. The package is `@frites/gateway` (`apps/gateway`); its binary is `frites-gateway` and it builds to `apps/gateway/dist/index.js`.

For the request-handling internals and how the council turn is assembled, see [../architecture/gateway.md](/architecture/gateway). For the full endpoint + SSE event reference, see [../reference/gateway-api.md](/reference/gateway-api).

## Transparent proxy role

The gateway is a transparent shim: it accepts the same request shapes the host already sends, runs a council turn underneath, and re-encodes the result as a normal streaming or non-streaming response. No host configuration beyond the base URL changes.

* It extracts the prompt from the incoming body (Anthropic `system` + `messages`, or Responses `instructions` + `input`), extracts any declared tools, and determines whether the request is a fresh ask or a tool-loop continuation.
* It recovers the caller's working directory from the env block that Claude Code and Codex embed in the system prompt (matching `working directory:`/`cwd:` against an absolute path that exists), so children run inside the real repo rather than an empty temp dir.
* It classifies the user's actual ask, stripping injected harness scaffolding (system-reminders, IDE context) via `stripInjectedContext`, to decide fan-out, falling back to the raw text when stripping leaves nothing.
* Background/utility traffic (title generation, summarization, classification, or an explicitly cheap-tier subagent) is detected by a small/fast model name (`haiku`/`small`/`fast`) and pinned to a single child with the exhaustiveness directive stripped, so housekeeping calls never fan out into N metered children.

## Endpoints

| Method | Path                        | Purpose                                                           |
| ------ | --------------------------- | ----------------------------------------------------------------- |
| POST   | `/v1/messages`              | Anthropic Messages: answer or tool-use, streaming (SSE) or JSON.  |
| POST   | `/v1/responses`             | OpenAI Responses: answer-only synthesis, streaming (SSE) or JSON. |
| POST   | `/v1/messages/count_tokens` | Returns an estimated `input_tokens` for the prompt.               |
| GET    | `/v1/models`                | Lists configured agent models plus `frites-council`.              |

Unknown paths return `404`; handler exceptions return `500`. Token counts are estimated at roughly `ceil(length / 4)` characters per token; this is an estimate, not a tokenizer.

## Progress and live answer streaming

When the client streams, the gateway carries two channels back over SSE, both backed by the single-consumer `ProgressSink` (`apps/gateway/src/progress.ts`) which buffers early messages until the SSE writer attaches a listener, then replays and streams live:

* **Progress channel**: an ephemeral `thinking` block (Anthropic) or `reasoning` summary (Responses) at output index 0, carrying council milestone lines and a periodic heartbeat. It exists only when the client is streaming *and* `streamProgress` is on. It is signed with a placeholder signature and stripped on the way back in, so it never pollutes the answer or the next turn.
* **Answer channel**: the final answer block at index 1, streamed live as the synthesizer produces tokens. It exists whenever the client streams, independent of the progress setting. Only the synthesizer (or a lone non-fanned-out answer turn) routes text here; tool turns emit a parsed JSON action instead and do not stream a live answer.

Per-child visibility is configurable via `progressDetail` (env `FRITES_PROGRESS_DETAIL`): `telemetry` shows state plus throttled `~N tok · Ns` counters; `interleaved` additionally streams each child's output, agent-prefixed. A heartbeat (`FRITES_HEARTBEAT_MS`, default 5000ms) emits a "still working — Ns elapsed" line that names which agents the turn is waiting on; telemetry refresh is throttled by `FRITES_TELEMETRY_MS` (default 2000ms). Each turn ends with a one-line council recap (agents consulted, wall time, calls, cost) so a collapsed thinking block still summarizes what happened.

## Logging

Logging is structured, leveled, and turn-scoped (`apps/gateway/src/logger.ts`). The gateway writes one record per line to stdout, which lands in the service's `StandardOutPath` (`~/.frites/gateway.log`), the file `frites logs` tails.

* Levels are `debug | info | warn | error`. The effective level is resolved from `FRITES_LOG_LEVEL`, then `config.logLevel`, else `info`.
* Milestone lines go to the info log; high-frequency telemetry and interleaved text go to debug, so per-agent detail lives in `frites logs -f --level debug`.
* Format is human-readable by default with a `[turn]` prefix per request; set `FRITES_LOG_JSON=1` for newline-delimited JSON.

## Service behavior

The gateway is a single long-lived process bound to `FRITES_GATEWAY_HOST` (default `127.0.0.1`) and `FRITES_GATEWAY_PORT` (default `6767`). Because it is long-lived, it keeps in-memory per-session state:

* **Turn cap**: each session (keyed by a hash of the system prompt + first message) is capped at `config.maxTurns`; on hitting the cap it returns a stop answer instead of running another turn, to bound runaway cost.
* **Cumulative spend**: per-session USD is accumulated and logged each turn (`sessionUsd`), so the running cost of a conversation is visible in the log.
* **Optional auth**: an inbound shared secret is off by default; setting `FRITES_GATEWAY_TOKEN` requires a matching `Authorization: Bearer` / `x-api-key` (compared with a timing-safe check), otherwise requests get `401`.
* **API key passthrough**: `config.passApiKeys` or `FRITES_PASS_API_KEYS=1` forwards the host's provider keys down to the child runners.

On startup it logs the bind address and the effective policy (`fanOutPolicy`, `fanOutScope`, `maxTurns`, auth on/off, `streamProgress`, `progressDetail`, log level, and configured agents). It is normally run under launchd/systemd by the CLI (see [cli.md](/services/cli)) or in the foreground with `frites gateway`.


# MCP server

The MCP server is the worktree mode of frites: a Model Context Protocol server that dispatches a coding task to multiple full agents in isolated git worktrees, vets each with the repo's tests, and returns one recommended diff. The package is `@frites/mcp` (`apps/mcp`); its binary is `frites-mcp` and it talks to the MCP client over stdio.

For the full tool input/output schemas, see [../reference/mcp-tools.md](/reference/mcp-tools).

## How it runs (read this first)

`@frites/mcp` is deliberately unbuilt and unpublished:

* `apps/mcp/package.json` has **no `description`** and **no `build` script**: there is no `tsconfig.build.json` step and no compiled output.
* It is marked `"private": true`, so it is **not published to npm**.
* Both `main` and `bin` point at the **TypeScript source** (`./src/index.ts`), and the shebang is `#!/usr/bin/env -S npx tsx`, so it is executed directly through `tsx`.

There is **no `apps/mcp/dist`**, so do not configure a client to run `apps/mcp/dist/index.js`. The MCP client launches the server through a wrapper chain (pnpm → tsx → node) against the source file.

Because that wrapper chain does not forward shutdown to the grandchild server, the server self-terminates the moment its client goes away. It watches every disconnect path: stdin `end`/`close` (the client closed the pipe), `SIGTERM`/`SIGINT`, and reparenting to PID 1 (the launcher died and orphaned it, polled every 5s). This prevents stray servers from piling up across sessions. stdout is the MCP channel, so all logging goes to stderr.

## Tools

The server registers two tools (`apps/mcp/src/index.ts`):

### `frites_implement`

Dispatches a coding task to multiple full agents (claude/codex) in isolated git worktrees, filters them with the repo's tests, and returns one vetted diff plus a comparison. It is long-running (minutes).

Inputs: `task` (what to implement or fix), `repoPath` (absolute path to the target git repo), optional `n` (1–10 agents), optional `agents` (comma list of kinds, e.g. `claude,codex`), optional `acceptanceCriteria`, and optional `baseRef` (git ref to branch from, default HEAD).

It loads config from `repoPath`, builds the engine dependencies, runs the engine, and forwards engine events as MCP `notifications/progress` (when the client supplied a `progressToken`). On completion it persists the run under `<repoPath>/.frites/runs/<runId>/` (one `.diff` per candidate plus a `result.json`) and returns a formatted comparison text, a `resource_link` to each candidate diff, and structured content. On failure it returns an error result with the message.

### `frites_apply`

Applies a diff from a previous `frites_implement` run onto a **fresh** branch `frites/<runId>`. It applies the recommended candidate by default, or a specific one via `candidateId` (e.g. to land a tighter passing child instead of the synthesized result). It requires a clean working tree and **never pushes**.

Inputs: `runId`, `repoPath`, optional `candidateId`. It reads the persisted `result.json`, resolves the chosen candidate (erroring clearly if the named candidate is missing or has no diff), and applies it to a new branch via the worktree manager, returning the branch name for review and commit.

## Runtime

The runtime helpers live in `apps/mcp/src/runtime.ts`:

* `buildEngineDeps` wires the worktree manager (`@frites/isolation`), the agent runner (`makeRunAgent` over `defaultRunners` from `@frites/agents`, honoring `passApiKeys` / `FRITES_PASS_API_KEYS`), the oracle (auto-detected per repo via `detectOracle`), and a run-id generator. It threads the MCP request's `AbortSignal` into the engine so a cancelled call cancels the run.
* `parseAgents` turns a `claude,codex` string into agent specs (prefix-matched to `claude-cli` / `codex-cli`).
* `persistRun` / `readResult` own the on-disk run record under `.frites/runs/<runId>/`.
* `describeEvent` maps each engine event to a one-line progress message; `formatResultText` renders the human-facing comparison table (agent, kind, status, files, Δlines, tokens, oracle) and the synthesis/cost summary; `toStructured` produces the machine-readable result returned as `structuredContent`.

The actual council/synthesis/reconciliation logic is the shared engine in `@frites/core` (see [core.md](/services/core)), not reimplemented here.

## Dependencies

`@frites/mcp` depends on `@frites/agents`, `@frites/core`, `@frites/isolation`, the MCP SDK (`@modelcontextprotocol/sdk`), and `zod` for the tool input schemas.


# CLI

The CLI is the operator front door for frites: it installs and manages the gateway service, manages configuration, tails the gateway log, runs the gateway in the foreground, and runs a standalone coding-council task without the gateway. The package is `@frites/cli` (`apps/cli`); its binary is `frites` and it builds to `apps/cli/dist/index.js`.

For the full command + flag reference, see [../reference/cli.md](/reference/cli).

## Command surface

Dispatch happens on the first argument (`apps/cli/src/index.ts`). Anything that is not a recognized subcommand is treated as a run task.

| Command                                                       | Effect                                           |
| ------------------------------------------------------------- | ------------------------------------------------ |
| `frites config <init\|show\|get\|set\|unset\|validate\|path>` | Manage configuration (see below).                |
| `frites gateway [--port N] [--host addr]`                     | Run the gateway in the foreground.               |
| `frites install [--port N]`                                   | Install and start the gateway as a service.      |
| `frites uninstall`                                            | Remove the gateway service.                      |
| `frites start`                                                | Alias for `install`.                             |
| `frites stop`                                                 | Alias for `uninstall`.                           |
| `frites restart`                                              | Restart the installed service.                   |
| `frites status`                                               | Show service + health status.                    |
| `frites logs [-f\|--follow] [-n N] [--level …]`               | Tail the gateway log.                            |
| `frites run "<task>" …`                                       | Run a coding-council task (standalone).          |
| `frites service <install\|uninstall\|restart\|status\|logs>`  | Compatibility alias for the service subcommands. |
| `frites help` / `--help` / `-h`                               | Print top-level usage.                           |

Note that `pnpm frites -- config …` forwards the literal `--` separator as the first arg; the CLI drops it so dispatch and flags work either way.

## Config management

`frites config` reads and writes JSON config files with the precedence **defaults < global < repo**. The write target is the repo config by default, or the global config with `--global`; `--repo <path>` selects which repo. Backed by `@frites/core` helpers:

* **`path`**: print the global and repo config paths (noting which are present) and the effective precedence + write target.
* **`init`**: write a starter config to the target (refuses to overwrite without `--force`).
* **`show`**: load the effective config, print it as JSON, and report its sources on stderr.
* **`get <key>`** / **`set <key> <value>`** / **`unset <key>`**: dotted-path access (e.g. `set defaultN 3`). Values are coerced via `parseConfigValue`. Every `set`/`unset` re-validates the resulting config and **refuses to write** if it would be invalid.
* **`validate`**: validate the target config file (or report that defaults will be used when absent).

## Service install and management

The service layer (`apps/cli/src/service.ts`) installs the gateway as a per-user background service that auto-starts on login, restarts on crash, and costs nothing while idle. It supports **macOS launchd** and **Linux systemd --user** only. On any other OS it instructs the user to run `frites gateway` in the foreground.

* **macOS**: writes a launchd plist at `~/Library/LaunchAgents/com.frites.gateway.plist` (`RunAtLoad` + `KeepAlive`), then bootstraps/loads it via `launchctl`.
* **Linux**: writes a systemd user unit at `~/.config/systemd/user/frites-gateway.service` (`Restart=always`), then `daemon-reload` + `enable --now` via `systemctl --user`.

Both resolve the gateway binary by importing `@frites/gateway` (falling back to known `dist`/`src` paths), run it with `process.execPath`, carry a curated environment (`PATH`, `HOME`, and any of `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, `FRITES_PASS_API_KEYS` that are set) plus `FRITES_GATEWAY_PORT`, and write logs to `~/.frites/gateway.log` (stdout) and `~/.frites/gateway.err` (stderr). The port defaults to `6767`.

`install` prints the snippet to point Claude Code at the gateway (`ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN` in `~/.claude/settings.json`). `status` reports whether the plist/unit exists, the launchd/systemd state, and a live health check against `/v1/models`. `frites gateway` runs the gateway directly (forwarding signals and exit code), translating `--port`/`--host` into `FRITES_GATEWAY_PORT`/`FRITES_GATEWAY_HOST`.

`frites logs` snapshots the last N lines (default 60) of `gateway.log`, appends any recent crash output from `gateway.err`, and with `-f`/`--follow` streams new lines via `tail -F`. `--level debug|info|warn|error` filters by the parsed level token while always keeping unformatted crash lines.

## Standalone run

`frites run "<task>"` (also the default for unrecognized input) runs a full coding-council task **without the gateway**, using the same engine the MCP server uses. It loads config from the repo, auto-detects the oracle, builds engine deps (worktree manager, agent runner, oracle), and runs the engine, streaming a one-line description per engine event to stderr.

Flags: `--repo <path>` (default cwd), `--n <N>`, `--agents claude,codex`, `--accept <criteria>`, `--base <ref>`, and `--apply` / `--apply-candidate <id>`. On completion it prints the decision, rationale, per-candidate summary (files, Δlines, synthesis marker), synthesis status, cost note, and the recommended candidate. With `--apply` it lands the recommended diff on a fresh branch via the worktree manager; `--apply-candidate <id>` lands a specific candidate instead and fails loudly if that candidate is missing or produced no diff. Without `--apply` it prints how to re-run to land a diff.

## Dependencies

`@frites/cli` depends on `@frites/agents`, `@frites/core`, `@frites/gateway`, and `@frites/isolation`.


# Core

`@frites/core` (`packages/core`) is the shared engine, configuration, and type layer for frites. It is the brain that fans a task out to multiple child agents, filters their results through an executable oracle, optionally synthesizes a single best implementation, and reconciles everything into one recommended candidate.

The package is deliberately **I/O-free**: it has no CLI, MCP, git, or process-spawning code of its own. Everything it touches the outside world with is expressed as a structural interface that `@frites/agents` and `@frites/isolation` satisfy at runtime. The only runtime dependency is [`zod`](https://www.npmjs.com/package/zod) for config validation. This keeps the engine fully unit-testable with fakes.

For the deeper internals (the engine event model, the stage-by-stage flow, and failure modes), see [Core engine](/architecture/core-engine).

## Exports

`packages/core/src/index.ts` re-exports the public surface:

| Module              | What it provides                                                                                                                       |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `types.js`          | `AgentSpec`, `Task`, `Candidate`, `OracleResult`, `OracleCommands`, `CommandResult`, `ReconcileDecision`, `SynthesisInfo`, `RunResult` |
| `events.js`         | `EngineEventHandler`, `noopEventHandler`, and the engine event union                                                                   |
| `config.js`         | `FritesConfigSchema`, `FritesConfig`, `resolveConfig`, `DEFAULT_CHILD_DIRECTIVE`, `withChildDirective`, pricing schemas                |
| `config-io.js`      | Config loading helpers                                                                                                                 |
| `pricing.js`        | `estimateCostUsd`, `pricingFor`, `UsageTokens`                                                                                         |
| `answer-council.js` | `runAnswerCouncil`, `decideFanOut`, `llmJudgeFanOut`, `parseFanOutVerdict`, `stripInjectedContext`                                     |
| `agent-loop.js`     | The agentic turn loop                                                                                                                  |
| `oracle.js`         | `detectOracle`, `runOracle`, `runCommand`                                                                                              |
| `judge.js`          | `heuristicJudge`, `diffSize`                                                                                                           |
| `synthesis.js`      | Synthesis eligibility, synthesizer selection, prompt construction, reconcile preference                                                |
| `engine.js`         | `runEngine` and its structural dependency interfaces                                                                                   |

## The engine (`engine.ts`)

`runEngine(task, deps, onEvent)` is the worktree-mode entry point. It takes a `Task`, a set of structural `EngineDeps`, and an event handler, and returns a `RunResult`.

### Structural dependencies

The engine never imports git or a CLI directly. Instead `EngineDeps` is satisfied by injected implementations:

* `worktrees: WorktreeManagerLike`: `resolveBase`, `create`, `captureDiff`, `cleanup`, and an optional `applyDiffToWorktree` (satisfied by [`@frites/isolation`](/services/isolation)).
* `runAgent: RunAgentFn`: runs one `AgentSpec` in a worktree and returns status, summary, cost, and normalized token usage (satisfied by [`@frites/agents`](/services/agents)).
* `runOracle: RunOracleFn`: runs build/lint/test against a worktree.
* `oracleCommands: OracleCommands`, `config: FritesConfig`, `newRunId: () => string`, and an optional external-cancellation `signal`.

### Flow

1. **Select agents.** `selectAgents` uses `task.agents` if present, else clones `config.defaultAgents` up to `n` (capped 1-10), suffixing duplicate ids.
2. **Resolve base.** `worktrees.resolveBase` pins the base ref and SHA every worktree branches from.
3. **Dispatch + execute (concurrent).** Each agent gets its own worktree (created and registered before the prompt runs, so the `finally` always reaps it), runs `runAgent`, and has its diff captured into a `Candidate`. A candidate's status becomes `empty` when it succeeded but touched no files.
4. **Oracle-filter (concurrent).** Each succeeded candidate is run through `runOracle`. With no executable oracle, candidates carry `hadOracle: false`.
5. **Synthesis (optional).** See below.
6. **Reconcile.** A pure `reconcile()` picks a winner over the original candidate pool, then `applySynthesisPreference` may override it with the synthesis candidate.

The whole run is wrapped in a `try/finally` that `Promise.allSettled`s `worktrees.cleanup` over every registered handle, so worktrees are reaped even on a throw.

### Reconciliation

`reconcile()` is pure and emits a `ReconcileDecision`:

| Decision    | Meaning                                                                                       |
| ----------- | --------------------------------------------------------------------------------------------- |
| `single`    | Only one agent; it passed the oracle.                                                         |
| `tests`     | The oracle filtered many candidates down to exactly one survivor.                             |
| `judge`     | Multiple survivors; tie-broken by `heuristicJudge`.                                           |
| `synthesis` | An oracle-passing synthesized candidate was preferred over the originals.                     |
| `near-miss` | No candidate passed (or none was usable); the closest is surfaced with a warning.             |
| `no-oracle` | No executable oracle existed; a best-effort pick by smallest diff, explicitly *not* verified. |

## The oracle (`oracle.ts`)

The oracle is frites's objective filter. `detectOracle` returns explicit `build`/`test`/`lint` commands when given, otherwise (when `autoDetect` is on) reads `package.json` scripts and prefixes them with the detected package manager (`pnpm`, `yarn`, `bun`, or `npm`, chosen from lockfiles). `runOracle` runs the commands in **build → lint → test** order, short-circuiting on the first failure, and passes only when at least one discriminating command ran and every one that ran exited 0. `runCommand` spawns via a shell, keeps a `4000`-char output tail, and supports an `AbortSignal` plus a wall-clock `timeoutMs` (which the engine wires to `config.perChildTimeoutMs`).

## The judge (`judge.ts`)

`heuristicJudge` is the v1 tie-breaker among oracle-passing survivors: it prefers the **smallest diff** (smallest blast radius) by `diffSize` (counted added/removed lines, excluding headers), then the fewest files touched. An LLM pairwise judge is a later phase.

## Config (`config.ts`)

`FritesConfigSchema` is the single zod source of truth for every tunable, and `resolveConfig(partial)` parses (and defaults) any partial input. It defines child defaults (`defaultN`, `defaultAgents`), idle/hard timeouts, budgets, oracle detection, the recursion fuse (`maxDepth`), fan-out policy and scope, progress/logging knobs, optional per-model `pricing`, and the full `synthesis*` family. `DEFAULT_CHILD_DIRECTIVE` is the thoroughness instruction woven into every substantive child prompt (`withChildDirective`). The complete key-by-key reference lives in [Configuration](/reference/configuration).

## The answer council (`answer-council.ts`)

The answer council is the transparent-proxy brain for **answer/reasoning** turns (Stance-A text synthesis): no worktrees or tools; heavy file-editing lives in the engine/MCP path.

* `decideFanOut` is the heuristic gate, honoring `config.fanOutPolicy` (`never`/`always`/`necessary`/`auto`). The `auto` and `necessary` paths inspect prompt length and a `HARD_SIGNAL` keyword regex (why, compare, design, debug, prove, optimize, …).
* `llmJudgeFanOut` upgrades that to a one-word LLM verdict, parsed **strictly** and **fail-closed** by `parseFanOutVerdict` (only a reply beginning with `fan-out` fans out; anything else resolves to a single agent). It falls back to the heuristic on any error.
* `stripInjectedContext` removes known harness wrapper tags (`system-reminder`, `ide_selection`) before classification so the judge sees the real ask.
* `runAnswerCouncil` runs N children with diverse framings (drawn from `defaultAgents`), each carrying the child directive and a Markdown-formatting directive, then asks one synthesizer to merge them into a single vetted answer, keeping agreements, adjudicating disagreements, and dropping unsupported claims, without revealing that multiple responses existed.

## Synthesis (`synthesis.ts`)

The synthesis stage integrates the strongest ideas from oracle-passing candidates into one implementation, verified by the **same** oracle, never a mechanical diff merge.

* `evaluateSynthesisEligibility` requires synthesis enabled, an executable oracle, and at least `synthesisMinCandidates` usable, oracle-passing candidates.
* `selectSynthesizer` picks `config.synthesisAgent`, else the first `claude-cli` child (so `synthesisBudgetUsd` actually bites via `--max-budget-usd`), else the first agent, mapping the `synthesis*` budget/timeout overrides onto the returned spec.
* `reservedSynthesisId` allocates a collision-free `synthesis-N` id.
* `buildSynthesisPrompt` constructs the strict integration prompt, embedding non-seed candidate diffs smallest-first up to `synthesisMaxDiffChars` and falling back to a file list + read-only worktree path past the cap.
* `applySynthesisPreference` prefers the synthesized candidate only when it is usable, passed the oracle, and its blast radius is within `synthesisMaxBlastFactor ×` the combined input size; otherwise it falls back to the best original passing candidate and records the reason.

The full design rationale, reconciliation policy, and non-goals live in [Synthesis and reconciliation](/concepts/synthesis-and-reconciliation).

## Exported types (`types.ts`)

The type layer is the contract every other package speaks:

* `AgentSpec`: id, `kind` (`claude-cli` | `codex-cli`), optional model, framing, budget, idle/hard timeout overrides, and codex `reasoningEffort`.
* `Task`: instructions, `repoPath`, optional `baseRef`, acceptance criteria, `n`, or an explicit `agents` list.
* `Candidate`: a child's worktree, diff, `filesTouched`, status (`succeeded`/`empty`/`errored`/`timed-out`), summary, cost, normalized token usage, and synthesis provenance.
* `OracleResult` / `CommandResult`: per-command output and the overall pass.
* `RunResult`: `runId`, `recommended`, all candidates/oracle results, the `decision` + `rationale`, a `costNote`, and (when enabled) `synthesis: SynthesisInfo`.

These types carry no I/O coupling, which is what lets the engine stay pure.


# Agents

`@frites/agents` (`packages/agents`) is frites's adapter layer over the headless coding CLIs. It knows how to invoke each backend, stream and parse its events, normalize token usage and cost across providers, scrub the child environment for recursion safety, and reap a stalled child. Its only dependency is [`@frites/core`](/services/core), whose structural interfaces it satisfies so the engine never spawns a process itself.

For how runners fit into the overall execution model, see [Agents and runners](/architecture/agents-and-runners).

## Exports

`packages/agents/src/index.ts` re-exports:

| Module                   | What it provides                                                                                                             |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `runner.js`              | `CliRunnerDef`, `makeRunAgent`, `RunAccumulator`: the engine-path `RunAgentFn` factory                                       |
| `completion.js`          | `runCompletion`, `parseClaudeLine`, `parseCodexLine`, `ChildEvent`, `CompletionResult`, `StreamAcc`: the answer-council path |
| `backend-errors.js`      | `classifyBackendFailure`, `ModelBackendError`, `backendFailureFrom`: normalized rate/usage/auth/context failure metadata     |
| `backend-policy.js`      | `BackendSuppressionController`: provider suppression and alternate-provider selection after retryable backend failures       |
| `claude.js` / `codex.js` | `claudeRunner` and `codexRunner` `CliRunnerDef`s                                                                             |
| `env-sandbox.js`         | `buildChildEnv`, `assertDepth`, `currentDepth`: the recursion + secret boundary                                              |
| `timeout.js`             | `startIdleTimeout`: the idle/hard reaper                                                                                     |
| `pricing.js`             | Re-exports `estimateCostUsd`, `pricingFor`, `UsageTokens` from `@frites/core` (back-compat)                                  |

`defaultRunners` is the shipped list: `[claudeRunner, codexRunner]`.

## Backend suppression and retry policy

Backend failures are classified first, then the coordinator decides what to do with them. Provider/account-scoped failures (`usage-limit`, `rate-limit`, `quota-exceeded`, `auth`, and short backend overloads) suppress that provider kind (`claude-cli` or `codex-cli`) for later calls. Reset timestamps or retry-after values win when the backend provides them; otherwise frites uses conservative TTLs: five hours for usage limits, one hour for quota, ten minutes for auth, five minutes for rate limits, and one minute for overloads. Prompt-shape failures such as `context-length`, cancellations, and unknown exits are not suppressed.

The gateway can retry the same logical child or synthesizer call through another configured unsuppressed provider. It does not retry a final-answer synthesizer after answer text has already streamed to the client, because those tokens cannot be retracted. Background/utility turns (which pin a small, cheap model) are the exception: they are not failed over to the full-price default agents — a suppressed cheap provider simply fails the cheap turn rather than silently escalating it to a premium council agent. Worktree mode records the same suppressions and uses them to avoid suppressed providers on later stages such as synthesis, but it does not automatically rerun a failed child in the same worktree: a backend can fail after partial edits, and retrying a different provider on top of those edits would blur candidate ownership.

## Runners (`runner.ts`)

A `CliRunnerDef` describes one CLI backend: its `kind`, its `command`, a `buildArgv(spec, ctx)`, and an `onLine(line, emit, acc)` parser. `makeRunAgent({ runners, config, passApiKeys })` indexes the runners by kind and returns the `RunAgentFn` the engine calls.

Before spawning, `makeRunAgent` asserts the recursion depth, builds the scrubbed child env, and applies config defaults onto the spec so a per-child budget/timeout/reasoning value always takes effect even when the spec omits it:

* `maxBudgetUsd` ← `config.perChildBudgetUsd`
* `timeoutMs` ← `config.perChildTimeoutMs` (idle)
* `hardTimeoutMs` ← `config.perChildHardTimeoutMs` (absolute, off when unset)
* `reasoningEffort` ← `config.codexReasoningEffort` (codex only; claude ignores it)

`spawnAndStream` spawns the CLI **detached** (its own process group, so it can be tree-killed via `process.kill(-pid, …)`), pipes the prompt over **stdin** and closes it (the EOF is what stops the child waiting for input; a real transcript would exceed `ARG_MAX` and trip `spawn E2BIG` if passed as argv), buffers stdout into newline-delimited lines for `onLine`, writes a combined log to a temp file, and resolves an `AgentRunOutput` with a status of `succeeded`, `errored`, or `timed-out`. On nonzero backend exits it classifies common rate-limit, usage-limit, auth, context-length, quota, and overload failures into `backendFailure` metadata while preserving the raw temp log. On idle timeout or abort it sends `SIGTERM` then escalates to `SIGKILL` after a `3000`ms grace.

## Claude runner (`claude.ts`)

Headless Claude Code, invoked as `claude -p --output-format stream-json --verbose --permission-mode bypassPermissions --strict-mcp-config --setting-sources project`, with `--model` and `--max-budget-usd` appended from the spec. It reuses the machine's subscription OAuth (keychain), so no API key is needed. `--strict-mcp-config` plus `--setting-sources project` keep the child from auto-loading frites's own MCP (a recursion guard). The `onLine` parser emits progress for tool uses, captures the assistant text/`result` as the summary, reads `total_cost_usd` as the authoritative cost, and sums Anthropic's **disjoint** input categories (fresh + cache-read + cache-creation) into the normalized input total; `output_tokens` already includes thinking, so no reasoning fold is needed.

## Codex runner (`codex.ts`)

Headless Codex, invoked as `codex exec --ignore-user-config --json --skip-git-repo-check -s workspace-write -C <cwd> -c approval_policy="never"`, then `-c model_reasoning_effort="<v>"` (when set), `-m <model>` (when set), and `-` (read prompt from stdin). It reuses the machine's ChatGPT sign-in (`~/.codex/auth.json`); approval is set via `-c approval_policy="never"` because the `--ask-for-approval` flag exits 2, and the `workspace-write` sandbox lets it edit within the worktree. `--ignore-user-config` prevents loading `config.toml` (which could route to the gateway and recurse). The NDJSON schema drifts between versions, so the parser is defensive: it pattern-matches event types for progress, captures the latest message as the summary, passes `input_tokens` through (codex's value is already the inclusive total, with cached as a subset), and **folds `reasoning_output_tokens` into `output_tokens`** so the total is comparable with claude. `cost_usd` is honored when present (the API-key path); the ChatGPT backend usually omits it.

> `model_reasoning_effort="minimal"` is **not** safe on the stock codex model. It 400s because it is incompatible with the built-in `web_search`/`image_gen` tools. frites ships `high` as the default, so use `low`/`medium`/`high`.

## Completions (`completion.ts`)

`runCompletion(kind, prompt, opts)` is the **answer-only** path used by the answer council: a single agent runs read-only (no worktree, no editing) and returns its text plus normalized cost/tokens, streaming `ChildEvent`s (`start`/`text`/`reasoning`/`tool`/`usage`) live via `opts.onEvent`.

* **Claude** runs with `--output-format stream-json --verbose --include-partial-messages` for token-level deltas, `--strict-mcp-config`, `--setting-sources project` (never `user`, which could set `ANTHROPIC_BASE_URL` to the gateway and fork-bomb), and `--disallowedTools Edit Write NotebookEdit` as a read-only guard.
* **Codex** runs with `-s read-only` and `-o <file>` (a final-message fallback written outside the repo if the event stream yields no `agent_message`), matching the execute path's reasoning depth.

It runs in the caller's real repo when `opts.cwd` is a valid absolute path (so reads actually work), otherwise in a temp scratch dir; only scratch dirs frites creates are cleaned up. `parseClaudeLine` and `parseCodexLine` are pure, fixture-tested per-line parsers shared by this path, handling both codex's newer `thread/turn/item` events and the legacy `msg`-wrapped shape.

## Environment sandbox (`env-sandbox.ts`)

The child environment is built by **allowlist**, never by copying `process.env`. This is the recursion guard and secret-minimization boundary for full-auto agents.

* `buildChildEnv` copies only the `ALLOWLIST` vars (`HOME`, `PATH`, locale, `CODEX_HOME`, `CLAUDE_CODE_OAUTH_TOKEN`, the XDG dirs, …), optionally passes `ANTHROPIC_API_KEY`/`OPENAI_API_KEY` when `passApiKeys` is set, then (as defense in depth) deletes every base-URL var in `SCRUB_EXACT` (`ANTHROPIC_BASE_URL`, `ANTHROPIC_API_URL`, `OPENAI_BASE_URL`, `OPENAI_API_BASE`, `CODEX_BASE_URL`) so a child can never be pointed back at frites. It then sets `FRITES_DEPTH = depth + 1` and `FRITES_CHILD = 1`.
* `currentDepth` reads `FRITES_DEPTH` from the env; `assertDepth(depth, maxDepth)` throws the recursion-fuse error when `depth >= maxDepth`.

## Timeouts (`timeout.ts`)

`startIdleTimeout({ idleMs, hardMs, onFire })` reaps a child that has gone **silent**, not one that is merely slow. `touch()` (called on every chunk of child output) resets the idle countdown, so a child that keeps streaming runs as long as it stays productive. Only a genuine deadlock, stalled read, or output-less spin trips it. `hardMs` is an optional non-resetting absolute ceiling for the pathological "spinning forever while still dribbling bytes" case (off when undefined/0). `onFire` runs at most once, with whichever timer tripped first, and `touch()` is inert afterward. Both the runner and completion paths drive their reaping through this controller.

## Pricing (`pricing.ts`)

A thin back-compat re-export of `estimateCostUsd`, `pricingFor`, and `UsageTokens` from [`@frites/core`](/services/core), so the engine path and the answer-council path estimate child spend identically from one source of truth. See [Cost telemetry](/concepts/cost-telemetry) and [Pricing](/reference/pricing).


# Isolation

`@frites/isolation` (`packages/isolation`) is frites's git-worktree layer. It gives every child agent its own isolated working tree branched from a single pinned base SHA, captures each agent's change as a unified diff, seeds the synthesis stage from a known-good tree, and lands an approved result on a fresh branch behind the one mandatory human gate. Its only dependency is [`@frites/core`](/services/core), whose `WorktreeManagerLike` interface it implements, so the engine drives isolation without importing git directly.

For how isolation underpins the safety model and the overall worktree flow, see [Isolation](/architecture/isolation).

## Exports

`packages/isolation/src/index.ts` exports the `WorktreeManager` class, which `implements WorktreeManagerLike`. Internally it shells out to `git` through a small `git()` helper (resolving stdout/stderr/exit code) and a `gitOrThrow()` wrapper that throws with the command and stderr on a non-zero exit.

## Lifecycle

The engine calls these methods, in order, per run:

### `assertGitRepo(repoPath)`

Runs `git rev-parse --is-inside-work-tree` and throws a clear error ("frites needs a git repo to isolate agents in worktrees") when `repoPath` is not a git repository.

### `resolveBase(repoPath, ref?)`

Asserts the repo, then `git rev-parse`s the target (`ref` or `HEAD`) into a SHA, returning `{ ref, sha }`. Every worktree in the run branches from this single pinned SHA so all candidates and the synthesizer start from an identical base.

### `create(repoPath, runId, agentId, baseSha)`

Runs `git worktree add --quiet -b <branch> <path> <baseSha>` and returns the `WorktreeHandle` `{ path, branch }`.

* **Branch:** `frites/run/<runId>/<agentId>`. Child branches live under `frites/run/...` specifically so they never collide at the git-ref level with the apply branch `frites/apply/<runId>` (git refs are files, so a branch named `frites/<runId>` cannot coexist with `frites/<runId>/<agentId>`).
* **Path:** `<repoPath>/.frites/worktrees/<runId>/<agentId>`.

### `captureDiff(worktreePath)`

Stages everything with `git add -A` (so new files are included), then reads back both the unified diff (`git diff --staged --no-color`) and the file list (`git diff --staged --name-only`), returning `{ diff, filesTouched }`. Both reads apply `DIFF_EXCLUDES` pathspecs (`:(exclude)node_modules`, `:(exclude)dist`, `:(exclude).frites`), so generated artifacts never pollute a candidate's diff. `filesTouched` is the trimmed, non-empty list of changed paths.

### `cleanup(repoPath, handle)`

Best-effort teardown: `git worktree remove --force`, `git branch -D <branch>`, then `git worktree prune`. The engine runs this for every handle in a `finally`/`allSettled`, so worktrees and their branches are reaped even when a run throws.

## Seeding synthesis: `applyDiffToWorktree(worktreePath, diff)`

The optional method on `WorktreeManagerLike`. It applies a captured candidate diff into a synthesis worktree with `git apply --3way --index`. Because the worktree was created from the **same base SHA** the diff was captured against, the 3-way apply is conflict-free. `--index` stages the result; the later `captureDiff` (`git add -A`) re-stages, so the two compose cleanly. This lets the synthesizer start from the best passing candidate's known-good tree rather than re-deriving the agreed core. The diff is normalized to end with a newline before being piped to git over stdin.

## Apply-to-branch: the one human gate

`applyToBranch(repoPath, runId, diff)` lands an approved diff, and is the single mandatory human gate. It is deliberately conservative:

1. Asserts the repo is a git repo.
2. Requires a **clean working tree** (`git status --porcelain` must be empty), throwing and asking the user to commit or stash first, because applying switches branches.
3. Creates and checks out a **fresh** branch `frites/apply/<runId>` via `git switch -c`.
4. Applies the diff with `git apply --3way --index`. On failure it throws, noting that the branch is created and checked out so the user can resolve manually.

It **never** touches the user's current branch history and **never** pushes. Landing a result is always an explicit, reviewable action on an isolated branch. See [Safety model](/product/safety-model) for how this fits the broader blast-radius posture.


# Repository structure

frites is a [pnpm](https://pnpm.io) monorepo. The workspace globs (`pnpm-workspace.yaml`) pull in two groups of packages:

```yaml
packages:
  - "apps/*"
  - "packages/*"
```

* **`apps/*`** are the runnable surfaces: the gateway, the MCP server, and the CLI.
* **`packages/*`** are the libraries those surfaces are built from.

The apps are deliberately thin: nearly all logic lives in `packages/core`, so every surface shares one engine.

## Layout

```
apps/
  gateway/  @frites/gateway  transparent proxy: /v1/messages (Claude) + /v1/responses (Codex)
  mcp/      @frites/mcp      MCP worktree tool: frites_implement + frites_apply
  cli/      @frites/cli      terminal tool: frites run + config + service
packages/
  core/        @frites/core        engine, oracle, judge, config, answer-council (no I/O coupling)
  isolation/   @frites/isolation   git worktree lifecycle + apply-to-branch
  agents/      @frites/agents       headless claude/codex runners + completions + env sandbox
```

## Apps (runnable)

| Directory      | Package           | Responsibility                                                                                                                               |
| -------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `apps/gateway` | `@frites/gateway` | Transparent model-provider proxy exposing `/v1/messages` (Claude) and `/v1/responses` (Codex). See [services/gateway.md](/services/gateway). |
| `apps/mcp`     | `@frites/mcp`     | MCP worktree tool exposing `frites_implement` + `frites_apply`. See [services/mcp-server.md](/services/mcp-server).                          |
| `apps/cli`     | `@frites/cli`     | Terminal entry point: `frites run`, config management, and service install/management. See [services/cli.md](/services/cli).                 |

## Packages (libraries)

| Directory            | Package             | Responsibility                                                                                                            |
| -------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `packages/core`      | `@frites/core`      | Shared engine, oracle, judge, config, and answer-council with no I/O coupling. See [services/core.md](/services/core).    |
| `packages/isolation` | `@frites/isolation` | Git worktree lifecycle and apply-to-branch behavior. See [services/isolation.md](/services/isolation).                    |
| `packages/agents`    | `@frites/agents`    | Headless claude/codex runners, completion helpers, and the child env sandbox. See [services/agents.md](/services/agents). |

## Apps are thin; logic lives in core

Every app depends on `@frites/core` (and most depend on `@frites/agents` / `@frites/isolation`) via `workspace:*` dependencies. Because the surfaces share one engine, behavior such as fan-out, synthesis, and the test-as-oracle path is implemented once and reused everywhere. Adding a new surface means wiring the engine to a new transport, not reimplementing the council.

For the day-to-day dev loop (`pnpm typecheck` · `pnpm test` · `pnpm gateway` · `pnpm mcp` · `pnpm frites`), see [local-development.md](/development/local-development).


# Local development

This page covers working in the frites repository itself: prerequisites, the root scripts, and the build order. For the monorepo layout, see [repository-structure.md](/development/repository-structure).

## Prerequisites

* **Node.js >= 22** (`engines.node` in the root `package.json`).
* **pnpm 10.24.0**: the repo pins `packageManager: "pnpm@10.24.0"`, so use Corepack or install that version.

Install dependencies from the repo root:

```bash
pnpm install
```

Only `esbuild` is allowed to run a postinstall build (`pnpm.onlyBuiltDependencies`), keeping installs deterministic.

## Root scripts

All scripts are defined in the root `package.json` and run from the repo root.

| Script       | Command           | What it does                                                                                              |
| ------------ | ----------------- | --------------------------------------------------------------------------------------------------------- |
| `build`      | `pnpm build`      | Builds the publishable packages in dependency order (see below).                                          |
| `clean`      | `pnpm clean`      | Removes every `dist/` and `*.tsbuildinfo` under `apps/*` and `packages/*`.                                |
| `prepack`    | runs `pnpm build` | Lifecycle hook so a publish always ships fresh `dist/`.                                                   |
| `typecheck`  | `pnpm typecheck`  | `tsc --noEmit` across the whole workspace (no emit; see [testing.md](/development/testing)).              |
| `test`       | `pnpm test`       | `vitest run`: the unit suite. See [testing.md](/development/testing).                                     |
| `test:watch` | `pnpm test:watch` | `vitest` in watch mode.                                                                                   |
| `gateway`    | `pnpm gateway`    | Runs the gateway from source via `tsx apps/gateway/src/index.ts`.                                         |
| `mcp`        | `pnpm mcp`        | Runs the MCP server from source via `tsx apps/mcp/src/index.ts`.                                          |
| `frites`     | `pnpm frites`     | Runs the CLI from source via `tsx apps/cli/src/index.ts`.                                                 |
| `eval`       | `pnpm eval`       | Runs the value-gate harness (`tsx eval/value-gate.ts`). See [evaluation.md](/development/evaluation).     |
| `bench`      | `pnpm bench`      | Runs the bench-matrix harness (`tsx eval/bench-matrix.ts`). See [evaluation.md](/development/evaluation). |

The `gateway`, `mcp`, `frites`, `eval`, and `bench` scripts all run TypeScript directly with [`tsx`](https://github.com/privatenumber/tsx). No build step is required to run a surface locally.

## Build order

`pnpm build` compiles only the five publishable packages, and it does so in a fixed order so each package's dependencies are built before it:

```
core → agents → isolation → gateway → cli
```

```json
"build": "pnpm --filter @frites/core build && pnpm --filter @frites/agents build && pnpm --filter @frites/isolation build && pnpm --filter @frites/gateway build && pnpm --filter @frites/cli build"
```

Each package's own `build` script is `tsc -p tsconfig.build.json`, which emits `dist/` (declarations + source maps) from `src/`.

`@frites/mcp` is **not** in the build chain: it is private and runs straight from TypeScript via `tsx` (the `mcp` script), so it has no `dist/`. See [release-and-packaging.md](/development/release-and-packaging) for the full publish/build distinction.

## Config files used in development

| File                        | Purpose                                                                                                                                                                        |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `pnpm-workspace.yaml`       | Declares the `apps/*` and `packages/*` workspace globs.                                                                                                                        |
| `tsconfig.json`             | Root TS config (`noEmit`, strict, ES2023/ESNext, Bundler resolution) plus `paths` aliases for `@frites/core`, `@frites/isolation`, `@frites/agents`. Used by `pnpm typecheck`. |
| `<pkg>/tsconfig.build.json` | Per-package build config extending the root; flips `noEmit` off, emits declarations + source maps into `dist/`, and excludes `test/`.                                          |
| `vitest.config.ts`          | Test runner config: Node environment, include globs for `packages/*/test` + `apps/*/test`, and the same `@frites/*` source aliases.                                            |

The `@frites/*` path aliases in both `tsconfig.json` and `vitest.config.ts` point at each package's `src/index.ts`, so typecheck and tests resolve workspace packages from source, and you do not need to build before running `pnpm typecheck` or `pnpm test`.

## Runtime configuration

frites itself reads `.frites/config.json` in the repo, layered over `~/.frites/config.json` (global). That is application configuration, not build tooling. Manage it with `frites config` and see [reference/configuration.md](/reference/configuration).


# Testing

frites has two gates you run from the repo root: a typecheck and a unit-test suite. For prerequisites and the surrounding dev loop, see [local-development.md](/development/local-development).

## Typecheck

```bash
pnpm typecheck
```

This runs `tsc --noEmit` against the whole workspace using the root `tsconfig.json` (strict mode, ES2023/ESNext, Bundler resolution). It compiles `packages/*/src`, `packages/*/test`, `apps/*/src`, `apps/*/test`, and `eval/**`, resolving the `@frites/*` packages from source via the `paths` aliases, so no build is needed first.

## Unit tests

```bash
pnpm test          # vitest run (one-shot)
pnpm test:watch    # vitest (watch mode)
```

Tests run under [Vitest](https://vitest.dev) in a Node environment (`vitest.config.ts`). The include globs are:

```
packages/*/test/**/*.test.ts
apps/*/test/**/*.test.ts
```

Like the typecheck, the runner aliases `@frites/core`, `@frites/isolation`, and `@frites/agents` to each package's `src/index.ts`, so tests exercise source directly without a build step.

### Where tests live

Each package keeps its tests in a sibling `test/` directory:

| Location                  | Test files (examples)                                                                                            |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `packages/core/test`      | `engine.test.ts`, `agent-loop.test.ts`, `answer-council.test.ts`, `synthesis.test.ts`, `config.test.ts`          |
| `packages/agents/test`    | `completion-stream.test.ts`, `env-sandbox.test.ts`, `pricing.test.ts`, `runner-usage.test.ts`, `timeout.test.ts` |
| `packages/isolation/test` | `worktree.test.ts`                                                                                               |
| `apps/gateway/test`       | `logger.test.ts`, `progress.test.ts`                                                                             |
| `apps/mcp/test`           | `runtime.test.ts`                                                                                                |

### Current count

The suite currently passes **126/126 unit tests** (per the project README's status). These are fast, host-independent unit tests. They do not spawn real child agents.

## Live smoke tests

Unit tests cover the engine, config, oracle, and telemetry without hitting real models. To validate end-to-end behavior, frites is also exercised with **live smoke tests against a real `claude` client**. For example, a real `claude` client pointed at the gateway driving a fix end-to-end, and (for worktree mode) an opt-in synthesis smoke that runs two inexpensive children against a tiny fixture repo.

**When to run them.** Live smoke tests are **metered**: each turn fans out to real child CLIs that draw on your subscriptions. Run them only when you need end-to-end confidence (after changes to the gateway transport, the agent runners, the worktree/oracle path, or synthesis), not on every edit. Keep them opt-in and small (a fixture repo, the cheapest children) so a routine `pnpm typecheck` + `pnpm test` stays free and fast as the default loop.

For the larger metered harnesses (the value-gate A/B and the bench matrix), see [evaluation.md](/development/evaluation).


# Evaluation

frites ships two evaluation harnesses under `eval/`. **`pnpm eval`** runs the *value-gate* (`eval/value-gate.ts`), a frites-specific A/B that drives a real `claude` client against the gateway to ask whether fanning out actually beats a single agent on real coding fixtures, and whether the extra cost is worth it. **`pnpm bench`** runs the *bench-matrix* (`eval/bench-matrix.ts`), a standard agentic-coding harness run across many frites configs and raw-model baselines on the same tasks, tabling accuracy, cost, and latency so you can compare frites to normal models. Both are **metered** (every run fans out to live child CLIs), so smoke-test the wiring first. The full setup, Docker sandbox, invocations, and result-reading guidance live in the canonical runbook at [../../eval/README.md](https://github.com/whatl3y/frites/blob/main/eval/README.md).


# Release & packaging

frites is published as a small set of npm packages under the `@frites/*` scope. This page describes which packages are published, how they are built, and the upgrade flow for installed users. Every claim here is grounded in the package `package.json` files; for the build order itself, see [local-development.md](/development/local-development).

## Publishable packages

Five packages are publishable. Each one declares `"publishConfig": { "access": "public" }`, ships only its `dist/` directory (`"files": ["dist"]`), and builds with `tsc -p tsconfig.build.json`:

| Package             | `bin`            | Role                                                      |
| ------------------- | ---------------- | --------------------------------------------------------- |
| `@frites/cli`       | `frites`         | Terminal entry point. The package users install globally. |
| `@frites/gateway`   | `frites-gateway` | Transparent model-provider proxy.                         |
| `@frites/core`      | —                | Shared engine, config, and types.                         |
| `@frites/agents`    | —                | Agent runner adapters and completion helpers.             |
| `@frites/isolation` | —                | Git worktree isolation helpers.                           |

Each package exposes its built entry via `"main": "./dist/index.js"`, `"types": "./dist/index.d.ts"`, and `"exports": { ".": "./dist/index.js" }`. Because `files` is restricted to `dist`, the published tarballs contain compiled JavaScript and `.d.ts` declarations only, never `src/` or `test/`.

Each publishable package declares `"license": "Apache-2.0"` and carries its own `LICENSE` file so the npm tarball includes the full Apache License 2.0 text. The repository root also has the canonical `LICENSE` file for the source tree.

Inter-package dependencies use `workspace:*` (e.g. `@frites/cli` depends on `@frites/agents`, `@frites/core`, `@frites/gateway`, `@frites/isolation`); `pnpm pack` rewrites these to the exact current version inside each tarball when a release is built (see [Cutting a release](#cutting-a-release)).

## Build artifacts

Building is the prerequisite for publishing. The root `prepack` script runs `pnpm build`, which compiles the five packages in dependency order (`core → agents → isolation → gateway → cli`). Each package's `tsconfig.build.json` flips `noEmit` off and emits declarations + source maps from `src/` into `dist/`, exactly the directory listed in `files`.

## Versioning

All five publishable packages use **fixed (lockstep) versioning** — they always share one version number and are released together. This is enforced by the `fixed` group in `.changeset/config.json`. Lockstep matters because `@frites/cli` depends on the other four at runtime: a user who runs `npm install -g @frites/cli` must receive a mutually compatible set, and `workspace:*` is rewritten to the exact current version at publish time.

`@frites/mcp` and the repo root are `private: true` and are never versioned or published; `@frites/mcp` is additionally listed under `ignore` in the Changesets config.

Versioning and publishing are managed with [Changesets](https://github.com/changesets/changesets). npm versions are immutable — an existing version can never be republished — so every release needs a fresh version bump, which is exactly what the flow below produces.

## Cutting a release

There are two paths; both end with all five packages published to npm at the same version.

### How packages reach npm

Publishing is done by [`scripts/publish.mjs`](https://github.com/whatl3y/frites/blob/main/scripts/publish.mjs) (the root `release` script), not `changeset publish`. For each public package, in dependency order, it:

1. runs `pnpm pack`, which builds a tarball with `workspace:*` rewritten to the exact current version, then
2. uploads that tarball with `npm publish`.

The upload uses **npm rather than `pnpm publish`** on purpose: when 2FA is enforced on publish, `pnpm publish` can only obtain the one-time password interactively and aborts non-interactively with `ERR_PNPM_OTP_NON_INTERACTIVE`, whereas `npm publish` accepts a code via `--otp` and supports OIDC trusted publishing in CI. The script skips any version already on npm, so it is safe to re-run after a partial failure.

### Recommended: automated via CI

1. **Describe the change.** In the PR that makes a user-facing change, run `pnpm changeset`, choose the bump (patch / minor / major), and write a one-line summary. Commit the generated file under `.changeset/`.
2. **Merge to `main`.** The release workflow (`.github/workflows/release.yml`) sees the pending changeset and opens a **"Version Packages"** PR that bumps every package version and updates changelogs.
3. **Merge the "Version Packages" PR.** With no changesets left, the same workflow builds and runs `pnpm release`, packing and `npm publish`ing all five packages in dependency order; `changesets/action` then creates the GitHub releases and git tags.

CI must authenticate to npm in a way that satisfies publish-time 2FA — which an interactive OTP can't provide in a workflow. The recommended setup is **OIDC trusted publishing**: register this repo as a trusted publisher for each `@frites/*` package on npmjs and grant the publish job `id-token: write`; npm then trusts the GitHub Actions identity with no stored secret. A stored **`NPM_TOKEN`** only works if that token is exempt from publish-time 2FA — a token that isn't exempt fails in CI with `EOTP`, since no code can be entered there.

### Manual: from a clean local checkout

If you need to publish by hand, from a clean `main`:

```bash
npm login                    # once; the account must have publish rights to @frites
pnpm install
pnpm version:packages        # applies pending changesets: bumps versions + changelogs
pnpm release:dry             # optional: pack + `npm publish --dry-run`, sends nothing
NPM_OTP=123456 pnpm release  # builds, packs, npm-publishes; set NPM_OTP only if 2FA is enforced
pnpm changeset tag           # tag the published versions (e.g. @frites/core@0.0.1)
git push --follow-tags
```

`pnpm release` always builds first, so `dist/` is fresh before anything is published. `NPM_OTP` is a current authenticator code, needed only when the account or `@frites` org enforces 2FA on publish; omit it otherwise. `pnpm release:dry` runs `node scripts/publish.mjs --dry-run`, packing each package and running `npm publish --dry-run` so you can confirm the package set, versions, and rewritten `workspace:*` ranges without sending anything.

> The initial `0.0.1` release sets the lockstep baseline and its version fields were edited directly. Every release after that should go through `pnpm changeset` so versions and changelogs stay in sync.

## The MCP server is not published

`@frites/mcp` is the exception. Its `package.json` sets `"private": true`, has **no** `publishConfig`, no `files`, and no `build` script. Its `main` and `bin` point at TypeScript source (`./src/index.ts`), and it runs via `tsx` (the root `pnpm mcp` script) rather than from a compiled `dist/`. So it is not built into `dist/` and is not published to npm. It is registered to run from the repo checkout (see [services/mcp-server.md](/services/mcp-server)).

## Installing

Users install the CLI package globally, which provides the `frites` binary:

```bash
npm install -g @frites/cli
```

The CLI depends on `@frites/gateway`, `@frites/core`, `@frites/agents`, and `@frites/isolation`, so installing it pulls in the gateway and the engine. From there, `frites install` sets up the always-on gateway service. See [getting-started/installation.md](/getting-started/installation).

## Upgrade flow

After upgrading the package (`npm install -g @frites/cli` again), restart the running gateway service so it picks up the new build:

```bash
frites restart
```

`frites restart` is the same command used after config changes. It restarts the background gateway service so the upgraded code (or new configuration) takes effect. See [getting-started/service-management.md](/getting-started/service-management).


# Current status

Snapshot dated **2026-06-16**. This is the canonical, detailed status enumeration for frites. For the user-facing summary of what works and what the known limits are, see [Status and limits](/product/status-and-limits).

## What is working and tested

Working and tested against the unit suite (typecheck clean) plus a live smoke against a real `claude` client:

### Gateway (transparent proxy)

* **Both surfaces**: `/v1/messages` (Claude Code) and `/v1/responses` (Codex).
* **SSE streaming**: live answer streaming on pure answer turns; live per-agent progress telemetry on tool-bearing turns.
* **Traffic classification**: answer turns vs. action (coding) turns vs. background/utility traffic.
* **Fan-out + synthesis**: answer/action-council fan-out per `fanOutPolicy`, with the synthesizer being `defaultAgents[0]` invoked with `role: "synth"`.
* **LLM fan-out judge**: under `fanOutPolicy: auto`, a cheap classifier decides *whether* a turn is worth fanning out, with a heuristic short-circuit on trivially-simple prompts.
* **`fanOutScope` first-turn scoping**: the council runs on the substantive request turn, then a single agent drives the mechanical tool loop via stateless continuation detection. The host's background haiku-tier traffic (titles, summaries, topic classification) never fans out.
* **Council recap**: a closing per-turn one-line council recap (e.g. `◆ council recap — N agents + synth · 18.3s · $0.072`).
* **Cost telemetry**: per-turn cost telemetry with config-driven `pricing` estimation for backends that do not self-report cost (e.g. codex on the ChatGPT backend).

**Gateway code-editing works (verified end-to-end).** On a coding turn frites emits the `Read` / `Edit` / `Bash` `tool_use` the host executes on the real files, proven end-to-end (a real `claude` client through the gateway read a file, edited it, fixed a bug, and `npm test` passed), with **no API key**: subscription `claude -p` children decide the next action via `runActionCouncil` and the gateway constructs the `tool_use` envelope the host executes.

### MCP worktree path

* `frites_implement` + `frites_apply`: full agents run in isolated git worktrees, candidate diffs are filtered through the tests-as-oracle spine, a heuristic judge tie-breaks survivors, an optional cross-candidate synthesis step folds passing diffs into one verified candidate, and the vetted diff is applied to a fresh branch.
* Progress notifications stream over stdio.

### Service management

* The **launchd** user agent (macOS) is built and tested; on Linux a `systemd --user` unit is written and enabled.

### Config CLI

* `frites config` (`init` / `show` / `get` / `set` / `unset` / `validate` / `path`) with global + repo layering.

## Remaining work

* **Value gate (quality validation).** It is not yet validated that fan-out *quality* beats a single agent on real tickets at acceptable cost. This is the standing question the project must answer with data.
* **Codex `/v1/responses`** **`function_call` emission.** The gateway drives coding turns by emitting Anthropic `/v1/messages` `tool_use` (done). Emitting Codex `/v1/responses` `function_call` envelopes is not yet built.

Further hardening and feature work tracked elsewhere includes sandbox-runtime wrapping of children, an LLM (vs. heuristic) synthesis/worktree judge, and the OpenAI OAuth-replay child. See [Deferred tasks](/roadmap/deferred-tasks).


# Deferred tasks

This page is the index of implementation plans that are intentionally not part of the current committed scope but are detailed enough to pick up later, alongside notable tasks that have since been completed.

## Planned (not implemented)

| Task                                                               | Status                   | Detail                                                                                                                                                                                                                         |
| ------------------------------------------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Gemini provider support                                            | Planned, not implemented | Add Gemini children alongside Claude and Codex, API-first then a later CLI spike. See [Gemini provider](/roadmap/gemini-provider).                                                                                             |
| OpenAI-compatible provider support (xAI Grok & open-source models) | Planned, not implemented | One generic `openai-compatible` adapter (configurable base URL) covering xAI Grok plus self-hosted / open-source models, with `xai` as a thin preset. See [OpenAI-compatible providers](/roadmap/openai-compatible-providers). |

## Completed

| Task                                                                  | Status                                         | Notes                                                                                                                                                                                                                                                                                                                                            |
| --------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Restructure docs for GitBook sync                                     | Done (2026-06-16)                              | The migration that produced this GitBook docs tree: focused, cross-linked pages under `docs/`, with `docs/SUMMARY.md` owning navigation and `docs/README.md` as the landing page. This page is itself the deferred-task index the restructure plan required once the migration was performed.                                                    |
| Synthesize a winning implementation from multiple passing child diffs | Implemented and shipped to `main` (2026-06-16) | Worktree mode folds the strongest ideas from multiple oracle-passing child diffs into one synthesized, oracle-verified candidate. `synthesisMode` defaults to `passing-only` (on); set `"off"` for winner-take-one. See [Synthesis and reconciliation](/concepts/synthesis-and-reconciliation) and [Worktree oracle](/concepts/worktree-oracle). |

For the current implementation status of shipped surfaces and the remaining work that is in active scope, see [Current status](/roadmap/current-status).


# Gemini provider

> **Status: planned, not implemented.** This page records the design for adding Gemini support. None of it ships today; frites children are still only `claude-cli` and `codex-cli`.

Goal: add Gemini support alongside the existing Claude and Codex children without destabilizing the gateway or MCP worktree paths. See the [Deferred tasks](/roadmap/deferred-tasks) index for where this fits.

## Recommended approach: two stages

Ship Gemini in two stages:

1. Add `gemini-api` first as an internal council child for gateway answer/action synthesis.
2. Consider `gemini-cli` later for answer-only or worktree execution, but only **after** a real CLI behavior spike proves it can run unattended safely.

API-first is the safer initial path because Google documents the Node SDK (`@google/genai`) and its streaming / function-calling APIs. Gemini CLI currently needs local validation before it is safe as an unattended child runner: its stdin behavior, `stream-json` schema, approval prompts, sandboxing, write behavior, and timeout behavior are not enough to rely on from docs alone.

## Current architecture touchpoints

Provider support is currently centered on `ChildKind = "claude-cli" | "codex-cli"`, mirrored through config schemas, parser helpers, CLI/MCP agent parsing, and the agents package dispatch layer.

Likely files to change:

* `packages/core/src/types.ts`
* `packages/core/src/config.ts`
* `packages/core/src/config-io.ts`
* `packages/agents/src/completion.ts`
* `packages/agents/src/env-sandbox.ts`
* `packages/agents/src/index.ts`
* `apps/cli/src/index.ts`
* `apps/mcp/src/runtime.ts`
* `apps/gateway/src/index.ts`: only if gateway dispatch assumptions are hard-coded
* `README.md`
* `docs/ARCHITECTURE.md`

Likely new file:

* `packages/agents/src/gemini-api.ts`

Possible later file:

* `packages/agents/src/gemini.ts` for a Gemini CLI runner, after the spike

## Implementation plan

1. Add `"gemini-api"` to `ChildKind` and `AgentSpecSchema`.
2. Update config loading, validation, examples, CLI/MCP `parseAgents`, and docs so users can opt into Gemini manually.
3. Add `@google/genai` to the agents package.
4. Implement a Gemini API completion adapter that returns the existing `CompletionResult` shape and emits normalized `ChildEvent` events.
5. Wire `runCompletion()` to dispatch `gemini-api`.
6. Reuse the existing gateway answer/action council behavior. Gemini should initially return text or JSON action proposals; do **not** implement native Gemini host tool calling in v1.
7. Add environment handling for `GEMINI_API_KEY`, possibly `GOOGLE_API_KEY`, and optional Vertex variables while respecting `passApiKeys`.
8. Add a README config example such as:

```json
{
  "id": "gemini-1",
  "kind": "gemini-api",
  "model": "<user-selected-gemini-model>"
}
```

9. Keep Gemini out of default agents until typecheck, unit tests, and an opt-in live smoke are stable.
10. Spike Gemini CLI separately before any `gemini-cli` implementation.

## API design notes

Use the existing provider-neutral shapes rather than adding Gemini-specific gateway logic.

For streaming, choose one Google API surface deliberately and fixture-test it:

* `generateContentStream` is enough for text-only answer support if chunks expose text and usage reliably.
* Interactions streaming may be better if native Gemini function calling becomes a goal, but it has a different event model.

Do not mix both paths casually.

For tool calls, v1 should keep using the existing frites action-council protocol: children produce a JSON action proposal, the gateway parses it, and the host executes the resulting tool call. Native Gemini function declarations can come later if needed.

For cache/cost, only populate cache-read / cache-write usage fields when Gemini exposes metadata that clearly maps to frites's existing semantics. Do not guess cache behavior.

## Auth and environment

Initial API mode should support:

* `GEMINI_API_KEY`
* possibly `GOOGLE_API_KEY`
* `GOOGLE_GENAI_USE_VERTEXAI`
* `GOOGLE_CLOUD_PROJECT`
* other Vertex env only if explicitly supported by the chosen SDK path

Keep the existing secret-minimization posture: API keys are withheld unless `passApiKeys` or `FRITES_PASS_API_KEYS=1` allows them.

If Gemini SDKs support endpoint-override variables, add them to the recursion / base-URL scrub list **before** enabling them in child environments.

## Tests

Unit tests to add or update:

* Config accepts `gemini-api`.
* CLI/MCP agent parsing recognizes Gemini aliases if aliases are added.
* Env sandbox withholds Gemini credentials by default and passes them only when configured.
* Gemini stream parser handles text deltas, final text, usage metadata, unknown events, and malformed chunks.
* Pricing works with Gemini model IDs through the existing config-driven pricing table.
* Answer council works with mixed Claude, Codex, and Gemini children.
* Action council accepts a Gemini child returning JSON with surrounding prose or code fences.

Integration tests:

* Mock Gemini SDK streams for deterministic tests.
* Add an opt-in live smoke gated by `GEMINI_API_KEY`.
* Gateway smoke with `defaultAgents` containing one Gemini child and `fanOutPolicy: never` or another low-cost setting.
* Later, capture Gemini CLI `--output-format stream-json` fixtures before implementing CLI support.

Verification after implementation:

```sh
pnpm typecheck
pnpm test
```

Run the live Gemini smoke only when credentials are present.

## Gemini CLI spike checklist

Before adding `gemini-cli`, verify locally:

* `gemini -p` behavior with large prompts.
* Whether prompt input can come from stdin without argv limits.
* `--output-format json` and `--output-format stream-json` schemas.
* Whether output schemas are stable enough for fixtures.
* Non-interactive approval and sandbox flags.
* Whether answer-only mode can prevent writes.
* Whether worktree mode can edit unattended without prompting.
* Timeout and process-group termination behavior.
* Auth modes: OAuth, API key, and Vertex.

Do not add `gemini-cli` to default worktree agents until this is proven.

## Risks

* Gemini model IDs and API surfaces may change quickly; avoid hard-coded defaults and stale pricing.
* Gemini streaming APIs differ by endpoint family; parser tests need real fixtures or faithful mocks.
* Gemini CLI may prompt, hang, or mutate files unexpectedly without a proven unattended contract.
* API mode is less subscription-reuse-friendly than the existing Claude / Codex CLI paths.
* Native Gemini function calling could overcomplicate v1; the existing JSON action protocol is enough for initial support.


# OpenAI-compatible providers

> **Status: planned, not implemented.** This page records the design for adding OpenAI-compatible child support, including xAI Grok and self-hosted / open-source models. None of it ships today; frites children are still only `claude-cli` and `codex-cli`.

Goal: add support for any provider that exposes an OpenAI-compatible `/v1/chat/completions` endpoint (xAI Grok, plus open-source or self-hosted models served through vLLM, Ollama, LM Studio, OpenRouter, Together, Fireworks, and similar gateways) alongside the existing Claude and Codex children, without destabilizing the gateway or MCP worktree paths. See the [Deferred tasks](/roadmap/deferred-tasks) index for where this fits, and the [Gemini provider](/roadmap/gemini-provider) plan, which this design parallels.

## Recommended approach: one generic adapter, presets on top

The unlock here is that xAI Grok and most open-source serving stacks already speak the same wire protocol: the OpenAI chat-completions API. So unlike Gemini (which needs a provider-specific SDK and stream parser), this is **one adapter** that varies only by base URL, credential, and model ID.

Ship it in this shape:

1. Add a single generic `openai-compatible` council child backed by the official `openai` Node SDK with a configurable `baseUrl`. This one adapter covers xAI, OpenRouter, Together, Fireworks, vLLM, Ollama, LM Studio, and anything else that implements `/v1/chat/completions`.
2. Add a thin `xai` preset over it that defaults `baseUrl` to `https://api.x.ai/v1` and the credential to `XAI_API_KEY`, so Grok works with just `{ "kind": "xai", "model": "<grok-model>" }`.

API-first is the only practical initial path: there is no official, stable Grok or generic OpenAI-compatible CLI to wrap as an unattended worktree runner. Treat any CLI option as a later, separately-spiked concern (see below).

Start as gateway answer/action council children. Worktree-mode execution can follow once the council path is stable, the same staging the Gemini plan uses.

## Current architecture touchpoints

Provider support is currently centered on `ChildKind = "claude-cli" | "codex-cli"`, mirrored through config schemas, parser helpers, CLI/MCP agent parsing, and the agents package dispatch layer.

Likely files to change:

* `packages/core/src/types.ts`: extend `ChildKind` and `AgentSpec` (new optional `baseUrl` / `apiKeyEnv` fields).
* `packages/core/src/config.ts`
* `packages/core/src/config-io.ts`
* `packages/agents/src/completion.ts`: dispatch the new kinds.
* `packages/agents/src/env-sandbox.ts`: see the base-URL and credential notes below; this is the security-sensitive change.
* `packages/agents/src/pricing.ts`: config-driven pricing for arbitrary model IDs, allow zero-cost local models.
* `packages/agents/src/index.ts`
* `apps/cli/src/index.ts`
* `apps/mcp/src/runtime.ts`
* `apps/gateway/src/index.ts`: only if gateway dispatch assumptions are hard-coded.
* `README.md`
* `docs/` provider/config pages.

Likely new file:

* `packages/agents/src/openai-compatible.ts` (the shared adapter; `xai` is a preset configured through it, not a separate adapter).

## Implementation plan

1. Add `"openai-compatible"` (and the `"xai"` preset) to `ChildKind` and `AgentSpecSchema`.
2. Extend `AgentSpec` with optional, kind-gated fields:
   * `baseUrl`: required for generic `openai-compatible`, defaulted for presets like `xai`.
   * `apiKeyEnv`: name of the env var holding the credential (defaults: `OPENAI_API_KEY` for generic, `XAI_API_KEY` for `xai`). This avoids hard-coding one credential name across many providers.
3. Add the `openai` Node SDK to the agents package. It supports a per-client `baseURL`, so it doubles as the client for every OpenAI-compatible endpoint.
4. Implement the completion adapter so it returns the existing `CompletionResult` shape and emits normalized `ChildEvent` events. Construct the SDK client with `{ baseURL, apiKey }` resolved **programmatically** from config + env, never from a base-URL env var (those are scrubbed; see Auth below).
5. Wire `runCompletion()` to dispatch `openai-compatible` / `xai`.
6. Reuse the existing gateway answer/action council behavior. Children should initially return text or JSON action proposals; do **not** implement native OpenAI tool/function calling in v1.
7. Handle credentials and base URLs per the Auth section, respecting `passApiKeys` / `FRITES_PASS_API_KEYS`.
8. Add README config examples:

```json
{
  "id": "grok-1",
  "kind": "xai",
  "model": "<user-selected-grok-model>"
}
```

```json
{
  "id": "local-1",
  "kind": "openai-compatible",
  "baseUrl": "http://localhost:11434/v1",
  "apiKeyEnv": "OLLAMA_API_KEY",
  "model": "<user-selected-open-source-model>"
}
```

9. Keep these kinds out of default agents until typecheck, unit tests, and an opt-in live smoke are stable.
10. Defer any CLI runner until a stable, unattended-safe OpenAI-compatible CLI exists and is spiked separately.

## API design notes

Use the existing provider-neutral shapes rather than adding provider-specific gateway logic.

For streaming, use `chat.completions.create({ stream: true })`. It is the broadest-compatibility surface: the Responses API and provider-specific extensions are **not** uniformly implemented by third-party / self-hosted servers, so avoid them for the generic adapter. Fixture-test the delta chunk shape (`choices[].delta.content`), the final chunk, and `usage`.

For tool calls, v1 keeps using the existing frites action-council protocol: children produce a JSON action proposal, the gateway parses it, and the host executes the resulting tool call. Native OpenAI `tools` / function calling can come later if needed.

For cache/cost, only populate cache-read / cache-write usage fields when the provider exposes metadata that clearly maps to frites's existing semantics (e.g. OpenAI's `usage.prompt_tokens_details.cached_tokens`). Many OpenAI-compatible servers omit `usage` entirely or report partial counts, so degrade gracefully and do not guess.

## Auth and environment

This is the security-sensitive part and differs from the Claude/Codex CLI paths because the endpoint is user-controlled.

* **Base URL must be passed programmatically, not via env.** `env-sandbox.ts` already scrubs `OPENAI_BASE_URL` (and `ANTHROPIC_BASE_URL`, `CODEX_BASE_URL`) from child environments as a recursion / base-URL-redirection guard. The adapter must therefore set the SDK client `baseURL` from the agent's `baseUrl` config field directly, and must **not** rely on or reintroduce a base-URL env var. Add any new base-URL env names to `SCRUB_EXACT` before they could ever be honored.
* **Credential allowlist needs extending.** `passApiKeys` currently lets only `ANTHROPIC_API_KEY` and `OPENAI_API_KEY` survive into child environments. For Grok and arbitrary providers, the gate must also honor the agent's configured `apiKeyEnv` (e.g. `XAI_API_KEY`, `OPENROUTER_API_KEY`, `TOGETHER_API_KEY`). Keep the existing posture: credentials are withheld unless `passApiKeys` or `FRITES_PASS_API_KEYS=1` allows them, and only the specifically-named key is passed; do not broaden to a wildcard.
* **Self-hosted models often need no key** (local vLLM / Ollama). Allow a missing credential when the configured `apiKeyEnv` is unset, sending a placeholder if the SDK requires a non-empty `apiKey`.

## Tests

Unit tests to add or update:

* Config accepts `openai-compatible` and `xai`, including the new `baseUrl` / `apiKeyEnv` fields and their preset defaults.
* Generic `openai-compatible` requires `baseUrl`; `xai` defaults it.
* CLI/MCP agent parsing recognizes the new kinds and any aliases.
* Env sandbox: base-URL env vars stay scrubbed; the configured `apiKeyEnv` is withheld by default and passed only when `passApiKeys` allows it; a missing local key is tolerated.
* Stream parser handles text deltas, final text, `usage` metadata (including `cached_tokens` when present), unknown events, missing `usage`, and malformed chunks.
* Pricing works with arbitrary model IDs through the config-driven pricing table, including zero-cost local models.
* Answer council works with mixed Claude, Codex, and OpenAI-compatible children.
* Action council accepts an OpenAI-compatible child returning JSON with surrounding prose or code fences.

Integration tests:

* Mock the `openai` SDK stream for deterministic tests (one fixture covers xAI and self-hosted, since the wire shape is shared).
* Add an opt-in live smoke gated by `XAI_API_KEY` (Grok) and, optionally, a local-endpoint smoke gated by an env flag pointing at a running OpenAI-compatible server.
* Gateway smoke with `defaultAgents` containing one `xai` child and `fanOutPolicy: never` or another low-cost setting.

Verification after implementation:

```sh
pnpm typecheck
pnpm test
```

Run the live smokes only when the relevant credentials / endpoints are present.

## Risks

* The user-controlled `baseUrl` is a redirection surface: it must be honored only through the SDK client and never leak into scrubbed base-URL env vars, or it could undermine the recursion guard.
* OpenAI-compatibility is uneven across third-party and self-hosted servers: streaming chunk shapes, `usage` reporting, and error formats vary. Test against faithful mocks and gate live behavior behind opt-in smokes.
* Model IDs and pricing vary wildly and change quickly (Grok versions, arbitrary open-source names); avoid hard-coded defaults and stale pricing, and allow zero-cost entries.
* Broadening the credential allowlist must stay narrow (named keys only) to preserve the secret-minimization posture.
* Native OpenAI tool/function calling could overcomplicate v1; the existing JSON action protocol is enough for initial support.


