> ## Documentation Index
> Fetch the complete documentation index at: https://piloop.autoresearch.inc/llms.txt
> Use this file to discover all available pages before exploring further.

# Configuration

> Every key of piloop.toml, with its type and its default.

`piloop.toml` is at the root of the project repo. `piloop init` reads it again each time you run it.

To override a key for one process, set `PILOOP_PROJECT_<KEY>`. Join nested keys with `__`, for example
`PILOOP_PROJECT_RUNTIME=subprocess` or `PILOOP_PROJECT_ROLES__WORKER__MAX_CONCURRENT=2`.

A type in the tables links to the section that describes it. A table that you leave out has the defaults of its
section.

## Top-level keys

| Key | Type | Default | Description |
| - | - | - | - |
| `name` | string | required | A short name for the project. piloop names its git refs and Herdr workspaces after it. |
| `metrics` | list\[[Metric](#metric)] | required | Every metric the evaluate command prints, each with its `role`. Each track has one `primary`, so a project with one track has exactly one. |
| `tracks` | dict\[string, [TrackConfig](#trackconfig)] | `{}` | `[tracks.<name>]`: the share and addendum of each track. Needed only with several primaries. |
| `confidence` | number | `0.95` | The confidence level of the paired bootstrap behind the verdict. For a `keep`, the lower bound of the primary's gain must be above zero at this level. A guardrail's margin is tested on the bound at this level too. |
| `evaluate` | list\[string] | required | The command (argv) that scores a commit. It runs in a clean worktree of the commit and must print `METRIC <name>=<value>` lines. |
| `runtime` | `herdr` \| `subprocess` | `"herdr"` | Where workers run. With `herdr`, every worker gets a terminal pane that you and the orchestrator can read and type into. With `subprocess`, every worker is a background process. |
| `harnesses` | list\[string] | `["pi"]` | The enabled harnesses. Runs use them in turn, unless an idea pins one. |
| `model` | dict\[string, string] | required | The model id for each harness, for example `{pi = "anthropic/claude-opus-5-5", claude = "claude-opus-5-5", codex = "gpt-6.1-sol"}`. |
| `harness` | dict\[string, [Harness](#harness)] | `{}` | Harness definitions by name: overrides of the built-in harnesses, or new ones. |
| `max_concurrent` | dict\[string, integer] | `{}` | The maximum number of agents of one harness, across all roles of the project, that are started or queued to start. Example: `{claude = 2}` when the provider of its model limits the request rate. When it is the turn of a harness at its limit, the slot goes to the next harness of the rotation. An idea or request that pins that harness waits. When unset, only the limits of the roles apply. |
| `evaluate_timeout` | number | `600` | The number of seconds before the evaluate command is stopped. The evaluation is then a crash. |
| `roles` | [Roles](#roles) | `{}` | The settings of each role (`[roles.worker]`, `[roles.watchdog]`, `[roles.research]`). See [https://piloop.autoresearch.inc/concepts/roles](https://piloop.autoresearch.inc/concepts/roles). |
| `link` | list\[string] | `[]` | Repo-relative paths, ignored by git, that piloop symlinks into every worktree, for example data or virtual environments. |
| `mcp` | dict\[string, [McpServer](#mcpserver)] | `{}` | `[mcp.<name>]`: the MCP servers that a role can list in `[roles.<kind>] mcp`. See `McpServer`. |
| `stop` | [Stop](#stop) | `{}` | `[stop]`: conditions that end the campaign. None by default. |
| `spend` | [Spend](#spend) \| null | `null` | `[spend]`: a hard budget on what the campaign spends outside piloop. None by default. |

## Metric

One metric. The evaluate command prints it as a `METRIC name=value` line.

| Key | Type | Default | Description |
| - | - | - | - |
| `name` | string | required | The name of the metric, as the evaluate command prints it. |
| `direction` | `higher` \| `lower` | required | `higher` when a larger value is better, `lower` when a smaller value is better. |
| `role` | `primary` \| `guardrail` \| `watch` | `"watch"` | How the metric affects the verdict. A `primary` metric must beat the bar with confidence for a `keep`, and each track has exactly one. A `guardrail` may get worse by at most its `margin`, or the candidate is at most `interesting`. It must never pass its `floor`, or the candidate is a `discard`. A `watch` metric is recorded and never affects the verdict. |
| `track` | string \| null | `null` | Primary metrics only. The name of the track (objective) that this metric is the primary of. When unset, the track has the metric's name, so a project with one primary needs nothing here. With several primaries, each one names its own track, and each track needs a `[tracks.<name>]` table. |
| `margin` | number \| null | `null` | The largest regression from the bar that a candidate may show, in the metric's own units (an absolute value, not a fraction). Required on a guardrail. Also required on every primary of a project with several tracks: when a candidate of another track is judged, that primary is a guardrail. When the evaluate command writes per-unit results, the margin is tested on the confidence bound of the bootstrap. Otherwise it is tested on the aggregate. |
| `floor` | number \| null | `null` | Optional. The worst value a candidate may have: at least this value for `higher`, at most this value for `lower`. It is a hard limit on the aggregate, independent of the bar, and only an edit of `piloop.toml` changes it. Allowed on a guardrail, and on a primary of a project with several tracks, where it applies to the candidates of the other tracks. |

## TrackConfig

`[tracks.<name>]`: one track of the project. A track is an objective with its own primary metric and brief.

A track has the name of its primary's `track`, or else the name of the primary. All tracks build on one trunk. A
run starts from the head, which is the latest keep on the trunk. A keep on any track must also hold the primary of
every other track within that primary's `margin` and `floor`. A project with one primary has one track and
needs no table. A project with several primaries needs one table for each track, and the shares must sum to 1.

Experimental: projects with more than one track.

| Key | Type | Default | Description |
| - | - | - | - |
| `share` | number | `1` | The fraction of `[roles.worker] max_concurrent` that the ideas of this track get. A free worker slot goes to the track that is furthest below its share and has an idea that can run, and then to the most urgent idea of that track. A track with no idea that can run gives its slot to another track. A share of 0 stops the track: its ideas wait. |
| `addendum` | string \| null | `null` | A repo-relative file that is appended to the brief of this track's runs, after `[roles.worker] addendum`. Use it to say how work on this objective is done, for example "profile first" or "keep the output identical". |

## Harness

`[harness.<name>]`: how to launch one coding agent.

piloop substitutes `{model}`, `{session_id}` and `{cwd}` (the agent's working directory) in both argument
lists. It substitutes `{prompt}` (the first prompt, which names the file that holds the brief) and `{log_dir}`
in `headless` only. An element that is exactly `{mcp}` expands to the role's MCP servers, in the form that
`mcp` names. The three built-in harnesses run without permission prompts: workers run in worktrees that the
daemon owns and throws away, and a sandbox that cannot reach the repo's `.git` cannot commit. A Claude agent
moves a foreground Bash command to the background after 5 minutes.

When the argument list in use contains `{session_id}`, piloop creates the session id. Otherwise the runtime
reports it: Herdr detects it for every harness it knows, and the subprocess runtime needs `session_id_pattern`.
When the agent ends, `entire session attach` takes this id, and `usage` uses it to find the session file and
read the tokens the agent spent.

| Key | Type | Default | Description |
| - | - | - | - |
| `executable` | string | required | The program to run. Herdr also uses it as `--kind` to detect the agent. |
| `interactive` | list\[string] | `[]` | The arguments for an interactive session (Herdr runtime). piloop types the first prompt, which names the file that holds the brief, after the agent starts. |
| `headless` | list\[string] | required | The arguments for a run without a terminal (subprocess runtime). Must include `{prompt}`. |
| `startup_dialogs` | list\[tuple\[string, list\[string]]] | `[]` | The dialogs the agent shows when it first starts in a new directory, in the order they appear. Each entry is the text to look for on the screen and the keys that accept the dialog. |
| `entire_agent` | string \| null | `null` | Entire's name for this agent: `pi`, `claude-code` or `codex` (`entire agent list` shows them). When it is set and `entire` is installed, the agent's session is attached to Entire as a checkpoint when the agent ends. When unset, the session is not captured. |
| `session_id_pattern` | string \| null | `null` | A regex with one group that finds the session id in the first lines of the process's stdout. Use it under the subprocess runtime for a harness that cannot take its id as an argument (`{session_id}`). For example, Codex `exec --json` prints `{"type":"thread.started","thread_id":"<uuid>"}` first. |
| `usage` | [UsageSpec](#usagespec) \| null | `null` | `[harness.<name>.usage]`: how to read the tokens an agent spent, when it ends, into `workers.usage`. See `UsageSpec`. When unset, token usage is not recorded. |
| `mcp` | string \| null | `null` | The renderer that expands the `{mcp}` element of the argv into the role's `[mcp.<name>]` servers: `claude`, `codex` or `pi`. Each renderer also keeps the user's own servers out, where its harness allows it. When unset, the harness takes no servers, and a role that lists servers cannot use it. |

## UsageSpec

`[harness.<name>.usage]`: how to read the tokens that an agent of this harness spent, after it ended.

`reader` names the code that reads them, and the other keys are its parameters. The built-in harnesses set
their specs as data. A project can override any key, or give a custom harness a complete spec. piloop checks the
names when the config loads.

| Key | Type | Default | Description |
| - | - | - | - |
| `reader` | string | `"jsonl"` | The reader to use. `jsonl` reads a JSONL session file under the home directory, which the keys below describe. It is the only built-in reader. |
| `file` | string \| null | `null` | For `jsonl`: a glob of the session file. piloop substitutes `{home}`, `{cwd}` (the agent's cwd, encoded as `cwd` says) and `{session_id}`. Example: `{home}/.pi/agent/sessions/{cwd}/*_{session_id}.jsonl`. When the glob matches nothing, piloop tries again with `*` for `{cwd}`. This searches one level of folders, not deeper, and the session id makes the match unique. When several files match, piloop takes the newest one. |
| `cwd` | string | `"none"` | For `jsonl`: how the harness names the folder of the agent's cwd. `dash_separators_wrapped` turns `/home/me/repo` into `--home-me-repo--`. `dash_non_alnum` turns it into `-home-me-repo`. `none` means that the folders are not named after the cwd, and matches any folder (`*`). |
| `tally` | string \| null | `null` | For `jsonl`: the file format, which says which records have token counts, how they add up, and which records hold the agent's own text. One of `pi`, `claude` or `codex`. |

## Roles

One table for each role. A role without a table uses its defaults.

| Key | Type | Default | Description |
| - | - | - | - |
| `worker` | [WorkerRole](#workerrole) | `{}` | `[roles.worker]`. |
| `watchdog` | [WatchdogRole](#watchdogrole) | `{}` | `[roles.watchdog]`. |
| `research` | [ResearchRole](#researchrole) | `{}` | `[roles.research]`. |

## RoleConfig

The keys that every `[roles.<kind>]` table accepts. Each role's table below adds keys and sets defaults.

| Key | Type | Default | Description |
| - | - | - | - |
| `harnesses` | list\[string] \| null | `null` | The harness rotation of this role. When unset, the role uses the project's `harnesses`. |
| `model` | dict\[string, string] \| null | `null` | The model for each harness in this role. It is merged over the project's `model`. |
| `timeout` | number | `600` | The number of seconds one agent may run before it is stopped. |
| `max_concurrent` | integer | `1` | The maximum number of agents of this role that run at the same time. More requests stay `requested` until a slot is free. |
| `addendum` | string \| null | `null` | A repo-relative file that is appended to `brief.md` in the briefs of this role only. Use it for instructions that only this role needs, for example how the research agent searches. |
| `mcp` | list\[string] | `[]` | The MCP servers that the agents of this role get, by the name of their `[mcp.<name>]` table. Every harness in the role's rotation must be able to take them (`[harness.<name>] mcp`). The built-in `claude`, `codex` and `pi` harnesses can. |

## WorkerRole

`[roles.worker]`: a worker does one idea in a worktree. The `queue` trigger starts it.

| Key | Type | Default | Description |
| - | - | - | - |
| `timeout` | number | `1800` | The number of seconds one worker may run before it is stopped. |
| `max_concurrent` | integer | `4` | The maximum number of workers that run at the same time. Each worker is one harness process on this machine. A run that is being scored after its worker ended does not count. |
| `explore_every` | integer | `4` | At least one run in this many starts from an alternative starting point. When none of the last `explore_every - 1` runs did, the next slot goes to the most urgent ready idea that is pinned to a base other than the head (a reference, an older keep or a note), whatever its priority. Nothing changes while no such idea is ready. 0 turns this off. |

## WatchdogRole

`[roles.watchdog]`: the watchdog investigates the anomalies that the daemon detects and reviews progress.

| Key | Type | Default | Description |
| - | - | - | - |
| `interval` | number | `1800` | The number of seconds between two progress reviews, counted from the last review. A sweep that investigates an anomaly is not a review. 0 turns the reviews off. |

## ResearchRole

`[roles.research]`: research develops seeds, keeps ideas ready for the workers and scouts for techniques.

| Key | Type | Default | Description |
| - | - | - | - |
| `timeout` | number | `900` | The number of seconds one sweep may run before it is stopped. |
| `max_concurrent` | integer | `1` | The maximum number of sweeps that run at the same time, each on the next harness of the rotation. The default is 1: sweeps that run at the same time read the same evidence and often duplicate each other, and two of them can race to develop the same seed. |
| `max_ideas` | integer | `1` | The maximum number of new ideas one sweep may insert. The seeds it develops do not count. `low_water` requests enough sweeps to fill the backlog at this many ideas each. 0 turns `low_water` off: research then only develops seeds. |
| `context` | list\[string] | `[]` | Repo-relative files or globs (for example `docs/error-analysis-*.md`) that hold the project's analysis: error analyses, comparisons with a reference, evaluation reports. The research pack lists each one with its outline, newest first, so that research reads them before it proposes ideas. |
| `buffer` | integer | `2` | The number of ready ideas that `low_water` keeps in addition to one for each worker slot (`[roles.worker] max_concurrent`). With this backlog, a free slot finds an idea at once and does not wait for a sweep. A larger value makes workers wait less for research, but the ideas they run are older. |
| `scout` | [Scout](#scout) \| null | `null` | `[roles.research.scout]`: sweeps that search for outside techniques on a schedule. Off by default. |

## Scout

`[roles.research.scout]`: the scout mode of the research role. It is off when the table is not there.

Every `interval` seconds, a research sweep runs in scout mode (`prompts/research_scout.md`). It states the open
problems of the campaign from the evidence, and searches X, GitHub and arXiv for outside techniques that address
them. It files at most `seeds` of them as seeds, signed `scout#<worker id>`, and puts the rest in the technique
library (the `techniques` table). It never files a defined idea. The search tool limits what one sweep spends on
xAI (`scripts/xai_search_mcp.py --max-usd`). Scout sweeps stop with all other launches under `[spend]`.

Experimental: scout mode.

| Key | Type | Default | Description |
| - | - | - | - |
| `interval` | number | required | The number of seconds between two scout sweeps, counted from the start of the last one. The first sweep counts from the registration of the project. Required: there is no default. |
| `seeds` | integer | `3` | The maximum number of seeds one scout sweep may file. The other techniques it finds go to the library. |

## McpServer

`[mcp.<name>]`: one MCP server, either streamable HTTP (`url`) or stdio (`command`).

Do not write API keys here. Name the environment variables that hold them. piloop passes these variables from the
daemon's environment to the agent, and the harness puts their values into the header or the server's environment.

| Key | Type | Default | Description |
| - | - | - | - |
| `url` | string \| null | `null` | The URL of a streamable HTTP server. SSE is not supported, because Codex and pi reject it. |
| `headers_env` | dict\[string, string] | `{}` | HTTP only. Maps each header name to the environment variable whose value is sent in it, for example `{ "x-api-key" = "EXA_API_KEY" }`. |
| `bearer_token_env` | string \| null | `null` | HTTP only. The environment variable whose value is sent as `Authorization: Bearer <value>`. |
| `command` | list\[string] \| null | `null` | The command (argv) of a stdio server, for example `["python3", "/path/to/server.py"]`. |
| `env` | list\[string] | `[]` | stdio only. The environment variables that the server reads, for example `["XAI_API_KEY"]`. |

## Stop

`[stop]`: conditions that end the campaign. One is enough. Without the table, the campaign has no end.

The daemon checks the conditions on every tick, before it requests anything. When one holds, the project is
`stopped` (`projects.status`, `stopped_reason`). Running workers are told to commit and stop, and are ended
ten minutes later. What they committed is scored. Pending requests and queued runs are dropped
(`campaign stopped`). One last watchdog sweep is requested, and nothing new starts.

`piloop init` resumes a stopped project when this table changed.

| Key | Type | Default | Description |
| - | - | - | - |
| `target` | number \| null | `null` | Stop when the primary metric at the head (the latest keep on the trunk) reaches this value, in its direction. Only for a project with one track. |
| `max_runs` | integer \| null | `null` | Stop after this many worker runs. A run counts while it runs, and when it ends with a candidate that is measured or waits for its measurement. A run that stopped at its own first check, with no commit or only `Piloop-Eval: skip` commits, does not count. A run that a daemon stop, a daemon crash or a campaign stop ended does not count either. `[spend]` and `max_tokens` limit what those runs cost. |
| `max_hours` | number \| null | `null` | Stop after this many hours of wall-clock time since the first run of the project started. |
| `max_tokens` | integer \| null | `null` | Stop when all the agents of the project together spent this many tokens (the `total` of `workers.usage`), summed over the agents that ended. |
| `plateau` | integer \| null | `null` | Stop after this many finished runs in a row without a new `keep` candidate. Only the runs that `max_runs` counts are counted. |

## Spend

`[spend]`: a hard budget in USD on what the campaign costs outside piloop, for example GPUs or APIs.

Every `interval` seconds, the daemon runs `command` to read the spend from the project (`projects.spend_usd`,
`spend_error`, `spend_at`). When the spend passes a `notices` threshold, the daemon posts one note to the
inbox at priority 1. At `limit`, the daemon launches no agent and starts no evaluation (no request, no
re-measure), and posts one escalation at priority 0. Work that already runs finishes and is scored. A command that
fails stops launches in the same way, with one escalation for each series of failures: an unknown spend is never
read as \$0. To resume, raise `limit` and run `piloop init`.

| Key | Type | Default | Description |
| - | - | - | - |
| `command` | list\[string] | required | The command (argv) that prints the project's spend so far in USD, as one number. It runs at the repo root with the daemon's environment, which is where its secrets come from. It must exit with code 0 within one minute. Example: `["uv", "run", "scripts/spend.py"]`. |
| `limit` | number | required | The spend in USD at which launches and evaluations stop. |
| `notices` | list\[number] | `[]` | Amounts in USD below `limit`. When the spend passes one, the daemon posts one note to the inbox. |
| `interval` | number | `300` | The number of seconds between two runs of `command`. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.