Configuration
Orion reads orion.json from the project root and supplies defaults for
anything absent. Every value below is generated from the struct definitions
and comments in
internal/config,
so this page cannot describe a field that no longer exists.
0 is usually not "unlimited"For the circuit breakers, 0 restores the shipped default rather than
removing the limit β "no limit" is never a safe reading of an absent value
in a breaker. Where a field means something different by zero, its own
description says so.
versionβ
Type: int
limitsβ
max_tool_callsβ
Type: int
Not documented in the source.
max_repeat_identicalβ
Type: int
Not documented in the source.
max_consecutive_failuresβ
Type: int
Not documented in the source.
max_same_command_failuresβ
Type: int
Not documented in the source.
max_consecutive_pollsβ
Type: int
MaxConsecutivePolls bounds waiting. A poll is exempt from the repeat counter so a long command CAN be waited for, but nothing bounded how long: a QA session polled a backgrounded suite for twenty minutes in a headless run, where a background command is never announced, and never produced the verdict that would have sent its ticket back to the implementer.
max_session_minutesβ
Type: int
Not documented in the source.
max_edits_without_verifyβ
Type: int
Not documented in the source.
max_files_touchedβ
Type: int
Not documented in the source.
max_concurrent_childrenβ
Type: int
MaxConcurrentChildren caps how many subagents supervisor.Fan runs at once. Low by default: unbounded fan-out against a rate-limited API converts a queue into a stampede (OR-162 is what misreading this limit costs).
no_progress_minutesβ
Type: int
NoProgressMinutes stops the watcher when its cycles stop achieving anything.
The spend breaker cannot see this failure. A batch that re-assembles, re-tests and lands nothing spends no tokens at all -- 91 CI runs over 13.5 hours cost $0 of budget and tripped no checkpoint, because there is no LLM anywhere in that loop. What it does spend is CI minutes and the whole night.
Nor is a warning enough on its own. OR-261 already prints "the batch landed nothing" for exactly this cycle, and it printed it more than thirty times while nobody was awake to read it.
Minutes rather than a tick count: the operator's question is "how long may this get nowhere before you wake me", and that answer must not change when the poll interval does.
max_concurrent_ticketsβ
Type: int
MaxConcurrentTickets caps how many tickets a watcher works at the same time. Unlike the limits above it does not bound one agent's behaviour; it bounds how many agents exist.
Four by default. There is no maximum: a larger value is confirmed at the point it is set, not refused.
It was two first, deliberately: every hazard concurrency introduces -- concurrent git against one shared clone, a budget checkpoint sailed past by runs already in flight, N sessions hitting one rate limit, N tickets picked that all edit the same files -- is invisible at one and obvious at two, and diagnosing it at two is cheap. The rule was "prove it at two, then raise it", and two has now been proven across a full release (v0.8.0 and the v0.8.1 queue), so this is that raise rather than a change of mind about the hazards.
Four as a default rather than a maximum: there is no maximum. A value above ConcurrencyWarnAbove is confirmed rather than refused, because the hazards scale with a machine, a repository and a rate limit that Orion cannot measure from here.
Note this multiplies with MaxConcurrentChildren above rather than capping it: four tickets each fanning out to subagents is a different load than four processes. If a stampede shows up after this change, that product is the first thing to look at.
max_breaker_tripsβ
Type: int
MaxBreakerTrips and MaxStranded bound the queue manager's eviction rules for a ticket that keeps tripping the safety breaker, or whose worktree keeps failing to settle, without landing. Zero means the shipped default, via Trips()/StrandedRounds() below -- the same "zero is not unlimited" rule every other ceiling in this struct follows.
max_strandedβ
Type: int
Not documented in the source.
gatesβ
require_plan_before_editβ
Type: bool
Not documented in the source.
protect_tests_during_fixβ
Type: bool
Not documented in the source.
production_requires_authorizationβ
Type: bool
Not documented in the source.
block_direct_push_to_default_branchβ
Type: bool
Not documented in the source.
pathsβ
intentβ
Type: string
Not documented in the source.
specsβ
Type: string
Not documented in the source.
plansβ
Type: string
Not documented in the source.
evalsβ
Type: string
Not documented in the source.
stateβ
Type: string
Not documented in the source.
protectedβ
Type: []string
Not documented in the source.
test_globsβ
Type: []string
Not documented in the source.
autonomyβ
Type: map[string]string
auto_mergeβ
enabledβ
Type: bool
Not documented in the source.
environmentsβ
Type: []string
Not documented in the source.
require_checksβ
Type: []string
Not documented in the source.
require_eval_pass_rateβ
Type: float64
Not documented in the source.
min_eval_casesβ
Type: int
Not documented in the source.
forbid_pathsβ
Type: []string
Not documented in the source.
max_changed_filesβ
Type: int
Not documented in the source.
budgetβ
weekly_usdβ
Type: float64
Not documented in the source.
weekly_tokensβ
Type: int
Not documented in the source.
pause_at_percentβ
Type: []int
PauseAtPercent are the checkpoints where a run stops for confirmation.
slackβ
enabledβ
Type: bool
Not documented in the source.
create_channel_per_projectβ
Type: bool
CreateChannelPerProject makes one channel per workspace. Channels
accumulate exactly as Jira projects do, and a bot cannot delete them;
orion slack archive is the cleanup.
channel_prefixβ
Type: string
ChannelPrefix namespaces Orion's channels so they sort together and are obviously machine-made.
privateβ
Type: bool
Private channels by default: a workspace name can reveal an unreleased project, and a public channel cannot be made private afterwards.
invite_usersβ
Type: []string
InviteUsers are added to a channel Orion creates. Slack user IDs (U...) or email addresses; emails need the users:read.email scope.
Required for a PRIVATE channel to be of any use. The bot is the only member of a channel it just made, and Slack shows a private channel to nobody outside it -- not in the sidebar, not in search. Without this Orion creates a "communication medium" that no human can see, and there is no notification to tell them it happened.
merge_approversβ
Type: []string
MergeApprovers may approve a merge from Slack. A Slack user ID (U...), a username, a display name or an email address.
The approval request MENTIONS whoever it can resolve, because that is the only form Slack notifies on -- a name in the message text is styled like any other word and reaches nobody. Resolving a name needs users:read and an email needs users:read.email, the same scope InviteUsers notes above; without them the request still sends and still names the person, and the run says the mention was lost. An ID needs no scope at all, so it is the form that always works.
EMPTY MEANS NOBODY, never everybody. Being in a channel is not authority: a project room contains people with no idea what they are approving, and a gate any member can satisfy is decoration. Defaulting to open would also mean the first repository someone forgot to configure merges on a stranger's thumbs up.
require_approvalβ
Type: bool
RequireApproval gates merging on a Slack approval. With it off, Orion reports that checks pass and waits for a human to merge on GitHub -- which is the safe default and needs no extra OAuth scopes.
mentionβ
Type: []string
Mention are Slack user IDs (U...) to @-mention when a message needs somebody to act. Empty falls back to InviteUsers.
ONLY on messages that require action: blocked, failed, and approval requests. Mentioning on every routine event is how a channel gets muted, and a muted channel delivers nothing at all -- so a mention attached to good news costs the delivery of the bad.
ciβ
auto_fixβ
Type: bool
AutoFix sends a failing build back to an agent on the same branch rather than stopping for a person.
On by default. It spends money without being asked, which is what an autonomous watcher is for; a flaky suite is bounded by MaxFixAttempts and stopped at once by an identical repeated failure.
max_fix_attemptsβ
Type: int
MaxFixAttempts bounds that loop. Zero means the built-in default, applied by Attempts -- never "unlimited", which is this package's rule for every circuit breaker.
A ceiling is not the only brake and not the most important one -- an identical repeated failure stops the loop immediately, because an agent that gets back a byte-identical error has learned nothing and spending the remaining attempts proves only that it can fail the same way three times. The ceiling is the outer bound; the repeat brake is what usually stops the loop, and raising the ceiling must never reach past it. A third attempt is therefore only ever reached by a run presenting a DIFFERENT failure each round.
Deliberately NOT clamped from above, matching Limits.ConcurrentTickets:
a configured number is honoured, and the argument about whether it is
wise happens where it is SET -- orion config limits asks for
confirmation above FixRoundsWarnAbove -- rather than silently on every
read. A file saying 40 while the loop ran 5 is a config disagreeing
with behaviour, with nothing in either place explaining the gap.
require_up_to_dateβ
Type: bool
RequireUpToDate refuses to merge a branch whose base has moved since its checks ran.
ON by default, which is unusual for a gate here and deliberate: Orion is the thing performing the merge, so merging on a verdict that no longer describes the code is a correctness failure rather than a preference. Two tickets worked in parallel can each pass alone and break the trunk together, with every signal green.
GitHub's own "require branches to be up to date" does this and cannot
be relied on: it is unavailable for private repositories on the free
plan, and with protection off gh reports a stale branch as CLEAN.
One local git merge-base --is-ancestor has neither limitation.
qaβ
enabledβ
Type: *bool
Not documented in the source.
max_roundsβ
Type: int
MaxRounds bounds the findings-fix-reverify exchange. Zero means the built-in default. Past it Orion escalates to a person rather than paying two agents to argue: QA never blocks on its own authority, so an unbounded loop is the only way this stage could stop a run, and it would do it by spending.
Not clamped from above; see CI.MaxFixAttempts for why, and FixRoundsWarnAbove for where the argument about a large number happens.
verdict_minutesβ
Type: int
VerdictMinutes bounds the VERDICT RE-ASK, not the QA run.
When QA ends without a verdict and without findings, Orion resumes its session and asks once for one. That re-ask had a hardcoded five-minute cap, and on OR-248 it killed a re-ask against a session that had been running for thirty: the change reached a pull request with no QA opinion at all, which is the worst outcome available -- neither a verdict nor a fix round, just an unverified branch and a person sent to read it.
The old comment's reasoning is still right for the ordinary case: one line is being asked for, from a session that has already done the work, and the short cap is what makes the claim true that a re-ask is cheaper than the fix round it replaces. What it missed is that resuming a large session and asking it to summarise its own findings is not a one-line job.
So it SCALES: see QA.VerdictBudget. This is the floor and the setting, zero meaning the built-in default.
e2e_base_urlβ
Type: string
E2EBaseURL is the explicit non-production target an end-to-end run may point at. EMPTY MEANS NO E2E, never "guess one": nj-agents CONVENTIONS-testing Β§T3 blocks an e2e execution without an explicit non-prod URL, and a suite that quietly found production is the failure that rule exists to prevent. Without it the stage still authors and runs unit and integration tests, and says that is what it did.
author_agentsβ
Type: int
AuthorAgents is how many subagents write the derived cases at once. Zero means the built-in default.
One agent writing fifty spec files writes them one after another, in one session, and that serial cost is the whole reason this exists. The cases are independent by construction -- being independently checkable is what makes something a case -- so the split is by case group.
BOUNDS AGENTS, NOT PROCESSES. Agents contend for a rate limit; ExecProcs below contends for CPU and disk. They are separate numbers because they are separate resources, and one value reused for both would be wrong for whichever it was not chosen for.
Limits.MaxConcurrentChildren stays the hard ceiling: supervisor.Fan reads it directly, so a larger value here cannot widen the real fan. That limit exists to stop a stampede against a rate limit, and a per-stage setting must not be able to lift it.
exec_procsβ
Type: int
ExecProcs is how much concurrency the test RUN gets. Zero means the built-in default.
Passed to the runner where the runner has its own flag for it --
go test already runs packages concurrently, and -parallel governs
within a package -- rather than spending Orion-side processes to
reimplement, worse, what the toolchain does for free. Sharding across
processes is for runners that cannot do it themselves.
BOUNDS PROCESSES, NOT AGENTS. See AuthorAgents above.
dbaβ
enabledβ
Type: *bool
Not documented in the source.
max_roundsβ
Type: int
Not documented in the source.
non_prod_dsnβ
Type: string
NonProdDSN is the explicit non-production database an EXPLAIN may be run against. EMPTY MEANS STATIC REVIEW, never "find one": the agent reads the schema and the migrations as text, and says in its report that is what it did. This is qa.e2e_base_url's hazard with a sharper edge -- a suite that quietly found production reads data, and a session that quietly found production can be asked to write it -- so the same rule applies and the name says which side of the line the value has to be on.
It is a CONNECTION STRING, so it is credential-shaped. Set it to a throwaway local or staging database and nothing else; it is written into the agent's prompt, and a prompt is not a secret store.
discoveryβ
max_roundsβ
Type: int
Not documented in the source.
collectβ
auto_rebaseβ
Type: bool
AutoRebase replays a branch that is BEHIND its base and merges CLEANLY onto that base, force-pushes it with a lease, and lets the checks re-run against what would actually be merged.
ON by default, and the only automatic rewrite of a branch in Orion. It is safe to default on because it does not choose anything: git has already said the merge is clean, so the rebase has one possible result. Contrast automatic conflict resolution, which decides.
The alternative is not "a human reviews it" -- require_up_to_date makes every merge invalidate every other open pull request, so the alternative is a person typing the same three commands once per merge per open branch, which grows with the square of the queue.
Turn it OFF for a repository you do not own: however safe the rewrite, a force-push to somebody else's branch is theirs to authorise.
batch_integrationβ
Type: bool
BatchIntegration lands the ready branches as ONE set: they are merged into an ephemeral ref, that ref is tested once, and the whole batch merges. Branches are never rewritten, so with this on there is no rebase, no force-push and no landing queue.
OFF by default, and deliberately not defaulted on the way AutoRebase is. AutoRebase is safe to default because it decides nothing: git has already said the merge is clean. This decides what lands and in what order, and a mistake mis-merges or strands every branch in flight at once rather than one branch at a time. It is turned on per repository, watched for the first few batches, and only then left alone.
With it off, nothing below is reached and the per-branch path is unchanged, so enabling it is reversible by setting it back.
There is deliberately no separate batch size. A batch can only hold branches that finished, and no more can finish than limits.max_concurrent_tickets allowed to run -- so a second number could only ever disagree with the first about the same thing.
allow_local_approvalβ
Type: bool
AllowLocalApproval lets orion approve KEY (and the web page's Approve
button, which runs it) stand in for a Slack reaction on a merge request
(ADR 0028). OFF by default and deliberately not in the web's list of
writable settings: Slack knows WHO reacted and checks them against an
allowlist, while a local approval knows only that someone with a shell on
this machine ran a command, so turning it on is the operator's decision
to treat that as approval. A rejection recorded either way still beats
every approval.
vcsβ
providerβ
Type: string
Not documented in the source.
default_branchβ
Type: string
DefaultBranch is the release branch. Most protected, merged into only from WorkBranch.
work_branchβ
Type: string
WorkBranch is the integration branch and the default PR base. It must differ from DefaultBranch: see Validate.
allow_release_branch_mergesβ
Type: bool
AllowReleaseBranchMerges waives the rule that WorkBranch and DefaultBranch are different branches.
The rule exists because Orion's responsibility ends when work merges into the integration branch; promoting that to the release branch is a human decision about what constitutes a release. Collapse the two and Orion merges agent output straight into the release branch -- not as a bug, but as configured, which is worse.
A repository with genuinely one branch and no release process is a legitimate case, so this stays possible. It is a named key rather than a reachable side effect of editing one string, because giving up the human promotion step should take a sentence that says so.
protected_branchesβ
Type: []string
ProtectedBranches may not be pushed to directly. Defaults to both long-lived branches.
branch_prefixβ
Type: string
Not documented in the source.
pr_draftβ
Type: bool
Not documented in the source.
agent_author_nameβ
Type: string
AgentAuthorName is the git AUTHOR recorded for commits the agent makes. The committer stays the human, so responsibility is unchanged and only authorship is distinguished.
Without this, an agent commit is indistinguishable from a hand-written one: same name, same email, no marker. "Who wrote this" then has no answer in the history, and a bad agent commit looks like your own work during a bisect or a blame. That defeats the point of a committed, traceable artifact chain.
Empty disables the alias and commits are authored as you.
agent_author_emailβ
Type: string
AgentAuthorEmail is the address recorded with that name, and it is the
field that actually decides what GitHub displays: GitHub matches
commits to accounts by EMAIL and ignores the name entirely. Setting
only agent_author_name therefore changes git log and changes nothing
on the web, which is exactly what happened on the first real run --
the commits were authored orion_agent and GitHub still said the
account owner had made them.
The default is a noreply address, because the email is the field that actually decides attribution: GitHub matches commits to accounts by ADDRESS and ignores the name entirely. Sharing the owner's address means the web UI keeps saying the owner made the change however the name fields read, which is what happened on the first real run.
orionbot@users.noreply.github.com carries no account id, so it resolves to nobody and the commit displays as a plain "orionbot". Two consequences, both intended:
- these commits leave the owner's contribution graph, which is correct, since the owner did not write them;
- a branch rule demanding a verified or allowlisted committer email will reject them, and the fix is a real bot account.
For a genuine avatar and profile, create a GitHub account for the bot and use its own ID+name@users.noreply.github.com here.
Setting this to the owner's real address restores the old behaviour:
the alias then lives only in git log, git blame and a bisect.
merge_strategyβ
Type: string
MergeStrategy is how an approved pull request lands: "rebase", "squash" or "merge". Empty means rebase.
This decides whether the agent's authorship survives onto the trunk, which is not obvious and was got wrong here first time round.
squash collapses the branch into ONE new commit, authored by the pull request's author -- which is whoever's token opened it, i.e. you. Every orionbot commit vanishes from the trunk's history. Tidiest log, worst attribution. merge keeps every commit, author intact, plus a merge commit. Best attribution, noisiest history. rebase replays each commit onto the base, preserving its author. Linear history AND orionbot attribution, which is why it is the default.
The cost of rebase is that the branch's incremental commits and its decision records all land on the trunk rather than being collapsed. That is a real downside; it is chosen because "who wrote this" is harder to reconstruct later than "which commits belong together".
require_up_to_dateβ
Type: bool
RequireUpToDate controls GitHub's own "require branches to be up to
date before merging" (required_status_checks.strict), applied by
orion protect.
ON by default so an operator who never touches this setting keeps
today's behaviour. It is the SERVER-SIDE twin of CI.RequireUpToDate:
that gate runs one local git merge-base --is-ancestor and can only
warn a human, who may merge anyway; this refuses the merge outright.
Turning this off does not remove that gate and does not weaken it --
with strict off it becomes the only signal that a branch is stale,
which makes it matter MORE, not less.
The cost this trades away is real: with strict on, a queue of N ready
pull requests turns every merge into a rebase of the other N-1, and
each rebase re-runs the full CI matrix -- a cost that scales with
queue depth while the benefit (catching a genuine semantic conflict)
does not. That tradeoff is the operator's to make, not orion protect's to assume.
trackerβ
enabledβ
Type: bool
Enabled is opt-in, mirroring Slack. Without it, create_project_per_idea (which defaults true) was read as "a tracker is required", so a bad Jira token made Orion refuse to run at all. That field answers "if you use a tracker, how", not "must you use one".
providerβ
Type: string
Not documented in the source.
project_keyβ
Type: string
ProjectKey binds to an existing project. When empty, Orion creates a project per idea, which requires the CREATE_PROJECT global permission and accumulates projects that a non-admin cannot delete.
create_project_per_ideaβ
Type: bool
CreatePerIdea is what the empty ProjectKey means, stated explicitly so the intent is reviewable rather than inferred from an absent field.
confirm_tree_before_createβ
Type: bool
ConfirmTreeBeforeCreate keeps the whole Epic/Story/Task tree behind one human approval. This stays true even under auto_merge: a sandboxed workspace can be deleted, a shared tracker cannot.
agent_labelβ
Type: string
AgentLabel is stamped on every issue Orion's agent creates, so agent-filed work is separable from work a person filed. The tracker equivalent of VCS.AgentAuthorName, and needed for the same reason: once the two are mixed in a backlog, no query can pull them apart again.
A LABEL rather than a reporter, deliberately. Jira's reporter must be a real account; there is no way to file as a synthetic identity without paying for a licence for it, and impersonating the human would be worse than leaving it unmarked.
queue_labelβ
Type: string
QueueLabel marks an issue as work Orion should pick up.
queue_orderβ
Type: string
QueueOrder is the JQL ORDER BY clause for the queue.
Priority first, then Rank. Rank alone would mean an urgent ticket filed today waits behind everything already in the backlog; priority alone gives no way to sequence the ties, which is most of them. Together they read as "most important first, and within equal importance the order I dragged them into".
Configurable because priority is DISABLED by default on some team-managed projects, and ordering by a field the project does not expose is not a useful default to force on everyone.
delegationβ
enabledβ
Type: bool
Not documented in the source.
nj_agents_dirβ
Type: string
NJAgentsDir points at the nj-agents clone. Empty means discover it: by env var, then by resolving an installed skill's symlink back to its clone root, then by Orion's own managed clone.
nj_agents_refβ
Type: string
NJAgentsRef pins the clone to a tag or branch. Empty clones the default branch, which pins nothing and is not reproducible across machines or across time.
extra_tool_calls_for_reviewβ
Type: int
ExtraToolCallsForReview is added to the tool budget while a delegated review orchestrator is running. Generous by design: the failure mode of too small a number is a review that cannot finish.
deep_security_review_whenβ
Type: string
DeepSecurityReviewWhen decides when to spend a deep review rather than the standard pass. Cost is real, so this is risk-tiered rather than always-on: "always", "high-risk", or "never".
high_risk_pathsβ
Type: []string
HighRiskPaths mark a change as high risk regardless of size.
inherit_operator_configβ
Type: []string
InheritOperatorConfig names the stages or actors whose runs get the OPERATOR's own Claude Code configuration -- their plugins, their MCP servers, their subagents -- instead of the curated directory Orion builds (see internal/agentcfg). An entry matches either a stage name or an actor id.
EMPTY BY DEFAULT, and that is the whole point: what a run can do is Orion's decision, and an operator who genuinely wants a plugin in the loop says so here, once, where the choice is visible and recorded in the event log at run start.
Here rather than in the global agents.json, which decisions/0005 makes the default home for actor-level settings, because this is not only an actor-level setting: it names STAGES too, and which capabilities a stage needs is a property of the repository being worked -- a design-heavy frontend repo wanting a Figma MCP in its build stage says nothing about the next repository on the same machine.
toolkitβ
repoβ
Type: string
Repo is the clone URL. Empty takes toolkit.RepoURL, so a project that declares nothing keeps the toolkit Orion has always used.
refβ
Type: string
Ref pins the clone to a tag or branch. Empty falls back to the deprecated delegation.nj_agents_ref.
dirβ
Type: string
Dir points at an existing clone, overriding the derived vendor path. Empty falls back to the deprecated delegation.nj_agents_dir.
stagesβ
Type: map[string]string
Stages maps a stage to the command that stage delegates to. Keys are canonical after loading: an alias spelling resolves to the stage stagePrompt's switch dispatches on.
EMPTY IS THE DEFAULT AND MEANS "unset", never "run nothing" -- a consumer that finds no command for a stage falls back to Orion's own built-in prompt.
attributionβ
enabledβ
Type: bool
Not documented in the source.
auto_installβ
Type: bool
AutoInstall lets orion init fetch dun through the platform's package
manager. Package-managed installs put the binary on PATH under the name
dun, which matters: the git hook resolves it by name at commit time,
and a dun that is not on PATH silently stamps every commit
undetermined -- read downstream as "no AI was used".