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 concurrency 1; its allow_noop and commit_dirty are false;
  • with [flows.default], the flow's own keys are its worker (acp or command, or exec_args), and a non-empty worker.command or worker.exec_args beside 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.