arc: the config
The config.toml keys.
config.toml#
init writes config.toml, and it reads an existing one for the remote it names; every other subcommand reads it.
Unknown keys are refused by name, and schema_version must be 1.
A key you leave out takes its default.
| Key | Type | Default | Allowed values | When it takes effect |
|---|---|---|---|---|
schema_version |
integer | 1 |
1 |
Every run; another value is refused. |
repository.remote |
string | "origin" |
A remote name that exists in the repository | When arc reads the main branch or pushes a task branch. |
repository.main_branch |
string | "main" (init writes what it found) |
A branch name | The branch a task starts from and merges into, unless integration.target_branch names another. |
limits.concurrency |
integer | 1 |
1 or more | When radian arc run or radian arc start starts tasks: how many attempts may be in flight at once across every flow. A task also needs a slot of its own flow's flows.<name>.concurrency when the file names flows, so both limits must have room. A running gate takes no slot of either kind. |
limits.max_attempts |
integer | 3 |
1 or more | When a task fails and arc decides whether to retry: how many retries the task gets since its last lead or review rework. The count is the task's retry_count; the attempt number is never compared with the limit, and a conflict rework keeps the count. |
limits.stall_timeout_secs |
integer | 900 |
seconds | When a worker prints no new line for that long. |
limits.max_wall_time_secs |
integer | 7200 |
seconds | As an absolute limit on one attempt, on top of the stall timeout. |
limits.git_remote_timeout_secs |
integer | 300 |
seconds, 1 or more |
A git call that talks to a remote (fetch, push, ls-remote): the call, and everything it started, is killed when it runs this long. It takes effect at the next git call. A local git call (diff, log, merge-base, status, a local merge) is not bounded. In the worker wrapper the shorter of this and the attempt's remaining wall time applies. |
limits.max_cost_usd |
number | not set: no limit | 0 or more, US dollars | When an attempt ends and when a review verdict is applied: every worker attempt and every review stage adds its cost, and the task or plan budget that is reached pauses the task. A review run that reports no cost adds 0. |
limits.cancel_grace_secs |
integer | 10 |
seconds | After a cancel, before the worker (or a before_attempt command that a stop reaches) gets SIGKILL. cancel gives the wrapper one more second after the grace to reap what it stopped and write its record, so the grace itself is never shortened to make room for the record. |
limits.max_auto_rework |
integer | 2 |
0 or more | When an automatic rework would go past the limit; the plan then pauses. A review rework counts; a conflict rework that arc writes itself does not. A failed reverify does not count either, whether the loop or you started it. |
limits.max_conflict_rework |
integer | 3 |
0 or more | When a verification or integration stage fails with a conflict with main: how many conflict reworks arc writes in a row for one task. While it has written fewer, the conflict gets the brief; once it has written the limit, arc writes no brief, the task stays in review.rework_pending and waits for you. The lead's rework and an attempt that does not end in a conflict set the run back to 0. 0 writes none, so the first conflict waits for you; 1 writes exactly one. |
limits.max_auto_reverify |
integer | 2 |
0 or more; 0 turns the moved-main feature off |
When a commands or integration command failure waits for a moved main: how many automatic reverifies one task gets. A hand radian arc reverify neither checks the count nor uses it up. The review wait on an unavailable model (gate) does not use this count, so 0 leaves it running. |
limits.max_gate_deaths |
integer | 2 |
0 or more; 0 stops at the first death |
How many deaths one background gate start gets for the same attempt and inputs before the record becomes died and no gate starts for those inputs. A start is counted exactly once: a record of running, or of started with no inputs, whose gate lock is free is one death. The gate is started 1 + max_gate_deaths times at most, whether or not a pass could plan it. A gate the scheduling loop stopped itself is interrupted, not a death; a gate that left no record while its started record holds the inputs is no_record, not a death (see the gate run). |
worker.exec_args |
array of strings | [] |
any radian acp argument except --profile, --workspace, --session-id, --resume, --output, --yes, -p |
Before start, each run pass and confirm act, the line the worker would get is parsed with the radian acp parser; a rejected line exits 3. Every attempt: the arguments go to radian acp after the wrapper's fixed ones. Empty means the worker uses the host's normal model configuration. It belongs to the radian worker line only: setting it together with a non-empty worker.command is a config error that names both keys. |
worker.command |
array of strings | not set: the worker is radian acp |
The program first, then its arguments; no shell | The worker that runs an attempt, instead of radian acp (see Another agent as the worker). Unset keeps the radian worker. An empty array is a config error. A program that contains a / is a path, resolved from the repository root; any other name is looked up on PATH. A program that is not an executable file, or a bare name that is not on PATH, refuses start, run and confirm with exit 3, naming worker.command and the program. The key takes effect at the next claim. |
worker.rework_session |
string | "fresh" |
fresh, resume |
The session each worker attempt runs in: fresh starts one session per attempt, resume one per task, continued by every later attempt (see the session and the brief). A change takes effect at the next attempt. It is a [worker] key, so it applies to every flow. |
flows.<name>.exec_args |
array of strings | [] |
any radian acp argument except --profile, --workspace, --session-id, --resume, --output, --yes, -p |
The extra radian acp arguments of this flow, after the wrapper's fixed ones, on every attempt. The line is parsed and validated like worker.exec_args, and its error names flows.<name>.exec_args. A flow sets at most one of exec_args, command and acp. Takes effect at the next claim. |
flows.<name>.command |
array of strings | not set: this flow runs radian acp or its acp agent |
The program first, then its arguments; no shell | The worker of this flow instead of radian acp (see Another agent as the worker). The program is checked and resolved like worker.command's, and its errors name flows.<name>.command. A flow sets at most one of command, acp and a non-empty exec_args. Takes effect at the next claim. |
flows.<name>.acp |
array of strings | not set: this flow runs radian acp or its command |
The program first, then its arguments; no shell | The ACP agent this flow's attempts run: the wrapper is an Agent Client Protocol client and starts the array as the agent (An ACP agent as the worker). The program is checked and resolved like worker.command's, and its errors name flows.<name>.acp. A flow sets at most one of acp, command and a non-empty exec_args; acp cannot be combined with worker.rework_session = "resume". Takes effect at the next claim. |
flows.<name>.concurrency |
integer | 1 |
1 or more |
How many tasks of this flow may be in doing at once. Both this and limits.concurrency apply; a ready task whose flow is full is skipped with the queue reason waiting for a free <name> slot. The limit is on as soon as the file writes any [flows] section, the implicit default flow included (see the two concurrency limits). |
flows.<name>.allow_noop |
boolean | false |
true, false |
The value a task of this flow uses when its own allow_noop is absent; an explicit task value wins. It is read for the delivery check of each attempt, at the attempt's claim, so a change takes effect at the next attempt. The implicit default flow of a file without [flows] has false. |
flows.<name>.commit_dirty |
boolean | false |
true, false |
Whether the wrapper commits what a completed worker left uncommitted or untracked in the worktree, after its git control-file check and before the delivery check, for every worker kind (see what the wrapper checks). A failed or cancelled worker's changes are not committed. Read at the claim, so a change takes effect at the next attempt. |
flows.<name>.gate_profile |
string | "default" (the value of gate.profile) |
The name of one [gate.profiles.<name>] section |
The profile every gate run of this flow's tasks uses (the gate run); the task's extra_stages are added on top. A gate that already started keeps the profile it started with. A name with no section is a config error when the file is read. Takes effect at the next gate run (and, for before_attempt, the next attempt). |
ids.plan_prefix |
string | "PL" |
1 to 4 ASCII uppercase letters; must differ from ids.task_prefix |
When a plan id is created. |
ids.task_prefix |
string | "TK" |
1 to 4 ASCII uppercase letters; must differ from ids.plan_prefix |
When a task id is created. |
gate.profile |
string | "default" |
The name of one [gate.profiles.<name>] section |
The profile a repository without a [flows] table gates with, and the default for the implicit default flow. A flow that sets flows.<name>.gate_profile uses that profile instead. Takes effect at the next gate run of the task. |
gate.command_timeout_secs |
integer | 3600 |
seconds | Per gate command line. |
gate.review.exec_args |
array of strings | not set: [worker] exec_args |
any radian exec argument except --tools, --session-id, --workspace, --profile, --resume, --deny-read, --mcp-config, --yes, --output, --no-config-mcp, -p |
The default reviewer's legacy spelling of gate.reviewers.<name>.exec_args. It is that reviewer's value when the file has no [gate.reviewers.default] section, and a file that sets it beside that section is a config error that names both keys. |
gate.review.concurrency |
integer | 2 |
1 or more |
When the review stage starts: how many review sessions may run at once in the repository. Each session holds one of the locks gates/review-0.lock … gates/review-<n-1>.lock and its own checkout, gates/review-<n>/; 1 runs one review at a time. It belongs to no reviewer. A change takes effect at the next review. |
gate.review.first |
table | empty | exec_args and timeout_secs, each optional |
The default reviewer's legacy spelling of gate.reviewers.<name>.first: a task's first review, a review stage run when no earlier attempt of the task has a review verdict. Each key set here replaces the same [gate.review] key for that review; a rework review never reads it. A file that sets it beside a [gate.reviewers.default] section is a config error that names both keys. |
gate.review.first.exec_args |
array of strings | not set: gate.review.exec_args (and then worker.exec_args) |
any radian exec argument except --tools, --session-id, --workspace, --profile, --resume, --deny-read, --mcp-config, --yes, --output, --no-config-mcp, -p |
The default reviewer's legacy spelling of first.exec_args: the first review session's model route, after the stage's own arguments. Unset, the first review uses the reviewer's exec_args. |
gate.review.first.timeout_secs |
integer | not set: gate.review.timeout_secs |
seconds; 1 or more |
The default reviewer's legacy spelling of first.timeout_secs: the wall-time limit of one first-review session. Unset, the first review uses the reviewer's timeout_secs. |
gate.review.prompt_file |
string | not set: only the built-in prompt | a path relative to the repository root, without .. |
The default reviewer's legacy spelling of gate.reviewers.<name>.prompt_file: appended to the built-in review prompt. A file that sets it beside a [gate.reviewers.default] section is a config error that names both keys. |
gate.review.timeout_secs |
integer | 900 |
seconds; 0 waits forever |
The default reviewer's legacy spelling of gate.reviewers.<name>.timeout_secs: the wall-time limit of one review session. A file that sets it beside a [gate.reviewers.default] section is a config error that names both keys. |
gate.review.propose_followups |
boolean | true |
true, false |
When a plan has no open task: true lets arc add one todo.proposed task with the items every review round of the plan's merged tasks suggested but no earlier follow-up collected, and lets a merge record that its task is to be collected in followup_pending (see The review's follow-up); false turns it off, and a task merged while it is false is never collected later. The lead's MCP add_task on a confirmed plan is not affected. It belongs to no reviewer. |
gate.reviewers.<name> |
table | one default section |
One section per reviewer name; the default reviewer always exists |
How the review stage runs for the tasks whose profile names this reviewer. A change takes effect at the next review. |
gate.reviewers.<name>.exec_args |
array of strings | not set: [worker] exec_args |
any radian exec argument except --tools, --session-id, --workspace, --profile, --resume, --deny-read, --mcp-config, --yes, --output, --no-config-mcp, -p |
The review session's model route, after the stage's own arguments. Unset, the session uses worker.exec_args. A repeat of an argument the stage sets is refused, naming this key. A change takes effect at the next review. |
gate.reviewers.<name>.prompt_file |
string | not set: only the built-in prompt | a path relative to the repository root, without .. |
Appended to the built-in review prompt. A change takes effect at the next review. |
gate.reviewers.<name>.timeout_secs |
integer | 900 |
seconds; 0 waits forever |
The wall-time limit of one review session. A change takes effect at the next review. |
gate.reviewers.<name>.fix_level |
string | "major" |
critical, major, minor |
The level a finding must reach to fail the round: a finding at or above it and not pre_existing fails the round with review_findings, and one below it is recorded. A task's own review_fix_level replaces it for that task's reviews. The review verdict records the effective value in review_settings.fix_level. It is read when the review stage starts, so a change takes effect at the next review. |
gate.reviewers.<name>.first |
table | empty | exec_args and timeout_secs, each optional |
A task's first review, a review stage run when no earlier attempt of the task has a review verdict. Each key set here replaces the reviewer's same key for that review; a rework review never reads it. A change takes effect at the next first review. |
gate.reviewers.<name>.first.exec_args |
array of strings | not set: the reviewer's exec_args (and then worker.exec_args) |
any radian exec argument except --tools, --session-id, --workspace, --profile, --resume, --deny-read, --mcp-config, --yes, --output, --no-config-mcp, -p |
The first review session's model route, after the stage's own arguments. Unset, the first review uses the reviewer's exec_args. A repeat of an argument the stage sets is refused, naming this key. |
gate.reviewers.<name>.first.timeout_secs |
integer | not set: the reviewer's timeout_secs |
seconds; 1 or more |
The wall-time limit of one first-review session. 0 is not accepted here. Unset, the first review uses the reviewer's timeout_secs. |
gate.profiles |
table | one default section |
One section per profile name | When a gate runs. |
gate.profiles.<name>.stages |
array of tables | [{ stage = "commands", required = true }, { stage = "human", required = true }] |
stage is commands, mutation, review, ci, or human; required is true or false and defaults to true |
When a gate runs. The list's order is the round's order; when it lists both commands and review, those two start at the same time and the round is decided in the list's order (the gate run). integration is not a profile stage: it always runs at merge. |
gate.profiles.<name>.commands |
array of strings | [] |
Shell command lines | The commands stage runs them in order on its own checkout. |
gate.profiles.<name>.before_attempt |
array of strings and tables | ["merge-main"] |
"merge-main", a table { run = "<command line>", timeout_secs = <integer> }, or []; run is required and not empty, timeout_secs is 0 or more, and any other key is a config error |
The steps the wrapper runs in the worktree before each attempt, in the order listed: merge-main, or a command line with its environment, log and failure rules (the steps before an attempt). A reverify attempt runs none. Takes effect at the next attempt. |
gate.profiles.<name>.reviewer |
string | "default" |
The name of one [gate.reviewers.<name>] section |
The reviewer every review run of a task gated with this profile uses, first review and rework review alike (see the review stage). It is resolved when the review stage starts, so a change takes effect at the next review. A name with no section is a config error when the file is read, naming this key and the name. |
integration.key_commands |
array of strings | [] |
Shell command lines | The integration stage runs them in order on the integration candidate (see the key commands). Integration reads the current value, never the key_commands an older attempt recorded in its profile.json. A key_commands written in a gate profile is a config error that names [integration] key_commands. |
integration.target_branch |
string | not set: repository.main_branch |
A branch name | The branch integration fetches and builds the candidate on, the branch a first attempt starts from (merge-main merges it), and the branch the merge targets: push pushes to it, and the command check reads it. Unset, it is repository.main_branch. Takes effect at the next attempt and the next integration run. |
integration.method |
string | "push" |
push, command, pr |
How the candidate reaches the target branch. push pushes the candidate to the target branch without force. command runs integration.command in the repository root and reads its last output line (the command method); pr runs the built-in GitHub pull-request preset of command instead (the pull-request preset). A value outside the set is refused by name. Takes effect at the next integration run. |
integration.command |
string | not set | One shell command line | The command integration.method = "command" runs in the repository root to merge the candidate. It is required by that method and refused with push and with pr: each pairing is a config error that names the keys. Its environment, timeout, log and answer are in the command method. Takes effect at the next integration run. |
integration.style |
string | "merge" |
merge, squash, rebase |
How the candidate commit is built (the styles). rebase needs a linear branch: one whose range contains a merge commit is refused before anything is built. Takes effect at the next integration run. |
hooks.after_merge |
array of strings | [] (but init writes three default lines: delete the remote branch, the worktree and the local branch) |
Shell command lines | The commands run in the repository root after a task reaches done.merged, in order, under the hooks rules. They write runs/<task>/hooks/after_merge-NN.log. Takes effect when a pending hook runs. |
hooks.after_fail |
array of strings | [] |
Shell command lines | The commands run after a task enters any failed.* phase, in order, under the hooks rules. A plan cancel that ends several tasks runs each task's commands with that task's environment. They write runs/<task>/hooks/after_fail-NN.log. Takes effect when a pending hook runs. |
hooks.after_rework |
array of strings | [] |
Shell command lines | The commands run after a task enters review.rework_pending, in order, under the hooks rules. A conflict rework back to todo.ready is not this point. They write runs/<task>/hooks/after_rework-NN.log. Takes effect when a pending hook runs. |
hooks.after_awaiting_human |
array of strings | [] |
Shell command lines | The commands run after a task enters review.awaiting_human, in order, under the hooks rules. They write runs/<task>/hooks/after_awaiting_human-NN.log. Takes effect when a pending hook runs. |
hooks.after_proposed |
array of strings | [] |
Shell command lines | The commands run after a new task enters todo.proposed, in order, under the hooks rules; the MCP process records the point and runs no hook. They write runs/<task>/hooks/after_proposed-NN.log. Takes effect when a pending hook runs. |
gate.profiles.<name> above is written out as gate.profiles.default in the file init writes, and its reviewer is the gate.reviewers.default section init writes too.
A stage may be listed once per profile: a profile that lists a stage twice, or that lists integration, is refused by name.
A per-task field of the same name overrides a [limits] value for that task; a plan budget adds to the task budgets.
Flows#
[flows.<name>] is one named bundle of worker settings.
A name is 1 to 32 characters from a-z, 0-9 and -, with no leading or trailing -; any other name is refused when the file is read.
The default flow always exists:
- with no
[flows.default]section, it is built from today's keys:worker.command,worker.exec_args,gate.profile, and concurrency1; itsallow_noopandcommit_dirtyarefalse; - with
[flows.default], the flow's own keys are its worker (acporcommand, orexec_args), and a non-emptyworker.commandorworker.exec_argsbeside it is a config error that names both keys, because one setting lives in one place.
A task's allow_noop is its own value when the task file carries it, and its flow's flows.<name>.allow_noop when it does not; the delivery check of each attempt reads that value at the claim. flows.<name>.commit_dirty is read at the same claim: it decides whether the wrapper commits a completed worker's dirty worktree before the delivery check (see the wrapper's checks).
A task without a flow field uses default.
A task is claimed only when its flow and the repository both have room (the two concurrency limits); a full flow's ready task is skipped, so a task of another flow further down the claim order starts in the same pass.
worker.rework_session stays a [worker] key and applies to every flow; resume cannot be combined with a flow that sets acp.
A file with no [flows] table behaves as it did before flows existed, except that an attempt's records name the flow default.
A flow's review uses the reviewer its gate profile names: flows.<name>.gate_profile selects the profile, and the profile's reviewer selects the reviewer, so two flows can review with different models, prompts and time limits.
An attempt's worker session id, and the records that name it, are in the session and the brief: with fresh it is <task>-<attempt>, with resume the task id.
radian arc show prints it as session_id (known from the claim on, not only after the attempt ends), and, for the radian worker, radian attach <session id> opens that session's journal; a command worker or a flow's acp agent has no journal, only the attempt's result.jsonl (see the task page's conversation tab).
The same records name the worker the attempt runs: the claim writes the worker it decided on (the worker field; a flow's worker takes effect at the next claim), and the wrapper writes the worker it started when it spawns it and when it ends.
Each record's worker object carries kind (radian or command) and flow (the task's flow, default when its file has no flow field); a command worker also carries program.
A reader takes the first of those records that names one, in the same order, so a running attempt keeps its kind and flow when config.toml changes; the arc TUI's conversation tab uses that record, not the current configuration, to choose between a journal and a command worker's stdout.
Every review run of an attempt has a session of its own; its name, the fallback when a journal of that name exists, and the verdict's review_session_ids are in the review stage.