Harness
Reference

Every path of loop.config.yaml, its type, its default and what it decides. Generated from the schema and the source.

View raw Markdown · llms.txt · For agents

The four layers that produce this configuration — the machine's global file, the project's, the team's and this machine's overlay — are described in ADR-0033. ak-harness loop validate prints the effective result, which is the only one that decides anything.

211 of 366 paths carry no description yet.

schemaVersion

Type: number · Default: 1

extends

Defaults for this kind of project (web-app, library, monorepo, data-pipeline, mobile), merged below every other layer. Anything this file states wins over the preset; the preset only fills silence.

Type: string

project

PathTypeDefaultDescription
project.namestringrequired
project.repostringrequired
project.baseBranchstring"main"
project.rootstring"."
project.stateDirstring".ak-loop"Where the loop keeps everything that is NOT the project's configuration: dispatch ledger, delivery state, contracts, events, agent memory, plans. Meant to be gitignored — the configuration lives at the repo root as loop.config.yaml, versioned; this directory is runtime state, per machine.
project.teamstringSelects the loop.config.team.<key>.yaml layer. $AK_LOOP_TEAM overrides it; a declared team whose file is missing fails loudly.
project.setupobject{}
project.setup.commandstring[]Argv (no shell — one element per arg, e.g. [pnpm, install, --frozen-lockfile]) run once in a freshly created worktree before the worker terminal opens. Unset/empty = skip.
project.setup.timeoutSecinteger600
project.setup.requiredbooleantrueWhen true, a failing/timing-out setup removes the worktree and counts as a dispatch failure instead of handing the worker a broken environment.

orca

PathTypeDefaultDescription
orca.binstring"orca"
orca.repoSelectorstringOrca repo selector for new worktrees: id:<repoId>, name:<name> or path:<abs>; default path:<project.root>.
orca.workspaceSelectorstringExisting worktree the automations run in; default: the enclosing worktree resolved by Orca.
orca.hoststring
orca.minVersionstring"1.4.200"
orca.timeoutMsinteger20000

linear

PathTypeDefaultDescription
linear.workspaceIdstringrequired
linear.teamKeystringrequired
linear.personstringrequiredLinear display name of the person whose queue this machine drains.
linear.peoplerecord<string>{}Display name → Linear user id, for assignee set and audit; the queue itself filters by display name.
linear.rotationobject{}Optional ordered handoff between owners after the current dispatchable queue drains.
linear.rotation.enabledbooleanfalse
linear.rotation.ownersstring[][]
linear.rotation.advanceWhenEmptybooleantrue
linear.queueOwnershipperson · unassigned"person"Whose queue this machine drains. person (default) keeps the historical behaviour: the issues assigned to linear.person. unassigned drains the issues with NO assignee and turns the assignee into a transient claim — written on dispatch, cleared when the item returns — so several machines can share one priority-ordered queue without colliding. Note when switching to unassigned: clearing the assignees is then REQUIRED, not cosmetic. With person and an emptied backlog the queue comes back empty and the loop looks healthy while doing nothing.
linear.statesstring[]["Todo","Ready"]
linear.excludeLabelsstring[]["blocked","needs-info"]
linear.requireLabelsstring[][]ALL of these must be on the issue (AND).
linear.anyLabelsstring[][]At least ONE of these must be on the issue (OR) — how a machine declares the slices of the board it drains, e.g. [layer:L2, layer:L3]. requireLabels cannot say this: it demands every label on the same issue, so two layers there match nothing and the queue comes back silently empty.
linear.projectsstring[][]
linear.orderpriority · updatedAt · createdAt[]["priority","updatedAt"]
linear.maxQueueinteger50
linear.inProgressStatestring"In Progress"
linear.reviewStatestring"In Review"
linear.doneStatestring"Done"
linear.blockedLabelstring"blocked"
linear.needsInfoLabelstring"needs-info"

knownFailures

Suites already red on the base branch, declared so a worker is not asked to pass a verification that nobody can pass. The harness does NOT run delivery.verifyCommand — the worker does, in its own worktree, before opening the PR. So tolerating known breakage cannot be done by parsing output the harness never sees: it has to be told to the worker, which is what this list does. Every entry carries the tracking issue on purpose. A quarantine without an owner becomes permanent, and the worker needs to know the failure is someone else's to avoid "fixing" it inside an unrelated task.

PathTypeDefaultDescription
knownFailures.pathstringrequiredPath or suite name as the runner prints it.
knownFailures.issuestringrequiredTracking issue — no anonymous quarantine.
knownFailures.reasonstringrequiredWhy it is red, in one line.

reviewOverrides

Stricter review for the slices of the board that deserve it, keyed by label. The review IS the gate when there is no CI, and not every change carries the same risk: a contract that freezes evidence and a copy tweak should not be judged with the same budget. First matching entry wins, and it only overrides the fields it names — everything else falls back to delivery.review.

PathTypeDefaultDescription
reviewOverrides.anyLabelsstring[]requiredMatches when the issue carries at least ONE of these labels.
reviewOverrides.votesinteger
reviewOverrides.minSeveritynit · med · high · blocker
reviewOverrides.profilefast · fullMesmo enum de delivery.review.profile — um perfil inventado aqui só falharia no CLI.
reviewOverrides.reasonstringWhy this slice is stricter — read by whoever wonders about the cost.

models

PathTypeDefaultDescription
models.orchestratorstring[][]required
models.reviewerstring[][]required
models.builderstring[][]required
models.watcherstring[][]required
models.routingobject{}How candidates are ordered. tiers = YAML order (0.6 behaviour). hybrid = keep tiers, rank by remaining usage inside each. dynamic = flatten + usage. catalog = discover models via CLI/AA/builtin + usage.
models.routing.modetiers · hybrid · dynamic · catalog"tiers"
models.routing.usageMetricmax · session · weekly · monthly"max"Which usage window drives remaining%. max = most constrained window.
models.routing.preferKnownUsagebooleantruePrefer providers with live usage % over those with unknown usage (e.g. grok often has no %).
models.routing.excludeProvidersstring[][]
models.routing.includeProvidersstring[][]If non-empty, only these providers may be selected (still must be declared under providers).
models.routing.pinobject{}Hard pin per role (provider/model). If pinned provider is unavailable, fall through unless pinStrict.
models.routing.pin.orchestratorstring
models.routing.pin.reviewerstring
models.routing.pin.builderstring
models.routing.pin.watcherstring
models.routing.pinStrictbooleanfalse
models.routing.policyquality-first · usage-balanced · cost-first"quality-first"How to choose among the candidates a role's tiers allow. quality-first (default, today's behaviour): the best available, with failover. usage-balanced: spread across providers by remaining window. cost-first: the cheapest that the role can still use — from models.cost when declared, otherwise the last tier, which is where a config already puts its cheap last resort. Policy never widens the candidate set; it only orders it.
models.costrecord<number>{}Relative cost per provider/model, any unit you like — only the order matters. Used by policy: cost-first.
models.rolesobject{}Quality band when routing.mode: catalog (and as soft bias in hybrid).
models.roles.orchestratorobject{}
models.roles.orchestrator.qualityfrontier · balanced · fast"frontier"
models.roles.orchestrator.preferCreatorsstring[][]
models.roles.reviewerobject{}
models.roles.reviewer.qualityfrontier · balanced · fast"frontier"
models.roles.reviewer.preferCreatorsstring[][]
models.roles.builderobject{}
models.roles.builder.qualityfrontier · balanced · fast"balanced"
models.roles.builder.preferCreatorsstring[][]
models.roles.watcherobject{}
models.roles.watcher.qualityfrontier · balanced · fast"fast"
models.roles.watcher.preferCreatorsstring[][]
models.catalogobject{}
models.catalog.sourcescli · artificial-analysis · builtin[]["cli","builtin"]
models.catalog.cliCacheHoursnumber6How long a provider's CLI-discovered model list (e.g. grok models) is trusted before spawning the CLI again — it rarely changes between releases.
models.catalog.artificialAnalysisobject{}
models.catalog.artificialAnalysis.enabledbooleanfalse
models.catalog.artificialAnalysis.apiKeyEnvstring"ARTIFICIAL_ANALYSIS_API_KEY"
models.catalog.artificialAnalysis.cacheHoursnumber24
models.catalog.artificialAnalysis.endpointstring"https://artificialanalysis.ai/api/v2/data/llms/models"
models.cooldownobject{}
models.cooldown.initialMininteger30
models.cooldown.maxMininteger240
models.cooldown.probeBeforeReenablebooleantrue
models.cooldown.exhaustedPercentnumber100A usage window at or above this percent counts as exhausted.
models.providersrecord<object>required
models.providers.binstringrequired
models.providers.authsubscription · api-key · none"none"
models.providers.envKeysstring[][]
models.providers.orcaAgentstring
models.providers.orcaUsageKeystring
models.providers.tuistringrequired
models.providers.probestring[]
models.providers.headlessstring[]
models.providers.reviewProviderstring
models.providers.effortFlagstring
models.providers.subagentsbooleanfalse
models.effortobject{}Reasoning effort requested per role; only applied for providers whose effortFlag is set.
models.effort.orchestratorlow · medium · high · xhigh"high"
models.effort.reviewerlow · medium · high · xhigh"high"
models.effort.builderlow · medium · high · xhigh"medium"
models.effort.watcherlow · medium · high · xhigh"low"

machine

PathTypeDefaultDescription
machine.floorinteger1
machine.ceilinginteger
machine.minFreeRamGbnumber4
machine.warningPercentnumber75
machine.criticalPercentnumber90
machine.agentRssMbnumber1400Per-agent RSS budget when live measurement is unavailable.
machine.wslCapinteger1

delivery

PathTypeDefaultDescription
delivery.verifyCommandstringrequired
delivery.reviewobject{}
delivery.review.clistring"agentskit-review"
delivery.review.modetrusted-local · isolated"trusted-local"agentskit-review execution mode. trusted-local reuses this user's environment (and CLI logins); the isolated default runs claude/codex with a temporary HOME and no credentials.
delivery.review.transportacp · headless · autoagentskit-review transport. headless is required for current grok-cli (ACP fails on submit_batched_findings); omit to use the CLI default.
delivery.review.profilefast · full"fast"fast = one bounded pass over the required lenses (fits a 600 s Orca stage); full = every lens, needs a long deadline or batching.
delivery.review.votesinteger1
delivery.review.concurrencyinteger4
delivery.review.minSeveritynit · med · high · blocker"med"agentskit-review severity floor that blocks auto-merge: nit < med < high < blocker.
delivery.review.deadlineMsinteger600000
delivery.review.maxCallsinteger400
delivery.review.postbooleantruePost the review to the PR (inline + summary).
delivery.review.doctorProbehelp · none"help"Doctor probe depth for the review CLI (help runs --help; none only checks PATH).
delivery.review.smallChangeLinesinteger0Cost lever: a change at or below this many lines — or touching only documentation — is reviewed by the cheapest available candidate instead of the strongest. 0 disables it and every review uses the strongest.
delivery.review.criticalPathsstring[][]Path prefixes that always get the strongest reviewer, whatever the size (contracts, security, migrations).
delivery.mergeobject{}
delivery.merge.autobooleantrue
delivery.merge.methodsquash · merge · rebase"squash"
delivery.merge.requireChecksbooleantrue
delivery.merge.requireHumanApprovalbooleanfalseExtra synchronous gate on top of a clean review + green checks: a real human must approve the PR on GitHub (reviewDecision: 'APPROVED', already fetched with every PR snapshot) before the loop merges it. False by default so existing configs keep auto-merging on a clean review, matching ADR-0027 §6.
delivery.smokeobject{}Optional bounded smoke gate before auto-merge (argv via CommandRunner; default off).
delivery.smoke.enabledbooleanfalse
delivery.smoke.kindnone · verify-argv"none"
delivery.smoke.argvstring[][]
delivery.smoke.timeoutMsinteger120000
delivery.verifyobject{}Harness-side verify runtime for smoke/doctor only; workers still see verifyCommand as a string.
delivery.verify.runtimeprocess · docker"process"
delivery.verify.argvstring[][]
delivery.verify.dockerobject{}
delivery.verify.docker.imagestring""
delivery.verify.docker.cwdstring"/work"
delivery.maxFixRoundsinteger2
delivery.workerIdleTimeoutMininteger45
delivery.maxDispatchMinutesintegerHard wall-clock ceiling on one dispatch, independent of idle detection: workerIdleTimeoutMin only catches a worker that stopped producing output, not one that is still active but has been running far longer than any real task on this project should. Unset (default) = disabled.
delivery.handoffobject{}When a worker goes idle / dies and its provider is out of usage (or otherwise unavailable), relaunch another builder on the same Orca worktree + branch with a continuation brief.
delivery.handoff.enabledbooleantrue
delivery.handoff.maxHandoffsinteger2
delivery.handoff.onlyWhenProviderUnavailablebooleantrueOnly hand off when the current provider is unavailable (exhausted/cooldown/missing).
delivery.selfEditPathsstring[]["loop.config.yaml",".github/**"]
delivery.secretFilePatternsstring[]["**/.env","**/.env.*","**/*.pem","**/*.key","**/id_rsa","**/id_rsa.*","**/credentials.json","**/*.p12","**/*.pfx"]Glob patterns (same matcher as selfEditPaths) for filenames that should never enter a PR the loop reviews or merges, regardless of the diff content — the loop cannot fetch a PR's actual diff content today, so this is a filename-shaped guardrail, not a secret-content scan. A PR touching one of these is held exactly like selfEditPaths, with a distinct reason. Defaults cover the most common accidentally-committed secret files.
delivery.ignoreChecksstring[][]Check names ignored when deciding CI is green (e.g. advisory bots).
delivery.requiredChecksstring[][]Check names that must be observed and green; empty = every reported check must pass.
delivery.cleanupWorktreebooleantrueRemove the Orca worktree after a successful merge.
delivery.returnStatestring"Todo"Linear state an abandoned (stuck/blocked) issue returns to.

contract

PathTypeDefaultDescription
contract.maxIssueCharsinteger12000Max characters of issue description + comments rendered into the orchestrator prompt.
contract.timeoutMsinteger300000
contract.maxContextReferencesinteger6Doc Bridge references appended to the orchestrator prompt when .doc-bridge/index.json exists.
contract.reuseHoursnumber72Re-generate a cached contract older than this many hours (0 = always reuse).
contract.docBridgeMaxAgeHoursnumber168Warn (or fail when requireDocBridge) when the Doc Bridge index mtime is older than this many hours.
contract.requireDocBridgebooleanfalseWhen true, doctor fails if .doc-bridge/index.json is missing or unreadable.
contract.briefScopesstring[]["playbook","for-agents"]Doc Bridge scopes resolved into the worker brief (titles/paths only).
contract.maxBriefReferencesinteger4
contract.contextProvidersdoc-bridge · rag[]["doc-bridge"]Context providers consulted when freezing a contract.

memory

PathTypeDefaultDescription
memory.enabledbooleanfalseMaster switch. When false the loop never recalls or writes memory.
memory.backendfile · none"file"
memory.storePathstring"memory"Directory under stateDir for the file KV store.
memory.maxRecallinteger5
memory.maxSummaryCharsinteger240
memory.maxBlockCharsinteger1200
memory.preferOverDocBridgebooleantrueDrop Doc Bridge refs covered by memory so the context budget shrinks.
memory.minDocBridgeWhenMemoryinteger2
memory.scopesissue · project · global[]["project","global"]
memory.includeStalebooleanfalse
memory.writeOnPromotebooleantrue
memory.categoriesworked · problem · adjustment · other[]["adjustment"]
memory.shrinkIssueCharsWhenMemorybooleantrue
memory.issueCharsWithMemoryinteger4000
memory.recurrenceobject{}When a lesson stops being an anecdote and starts being a pattern. A learning proposed minSightings times is surfaced by loop retro as ready to promote, with the exact command — so the human act is one keystroke instead of an analysis, and at most maxPerRun are offered at a time. With autoPromote.enabled the retro promotes them itself as loop-auto — under the same three bounds, and never as human (ADR-0019, amendment of 2026-09-19). Off by default: turning it on is the project's decision, and every automatic promotion stays listable and revocable.
memory.recurrence.minSightingsinteger2How many sightings make a lesson a pattern. Below 2 is "it happened once".
memory.recurrence.maxPerRuninteger3
memory.autoPromoteobject{}
memory.autoPromote.enabledbooleanfalseWhen true, loop retro promotes the recurring lessons itself, attributed to loop-auto.

agents

PathTypeDefaultDescription
agents.registryPathstring"agents.registry.yaml"
agents.requireRegistrybooleanfalseWhen true, missing registry or role entry fails doctor/routing closed.
agents.autoImprovebooleanfalseLet the retro propose improvements to the instructions of the agents this project installed under agents/&lt;id&gt;/. Off by default; architect and reviewer are never auto-changed whatever this says, and publishing anything back to the registry is always a human's gesture.
agents.evalCommandstring[][]Argv that runs the agent eval. Without it nothing is adopted: a change that cannot be measured is a guess.
agents.evalTimeoutSecinteger900
agents.maxAutoLinesinteger5More lines than this in one proposal and it waits for a human.
agents.minRationumber0.5Bad outcomes per run above which a role is worth improving at all.

rag

PathTypeDefaultDescription
rag.enabledbooleanfalse
rag.queryArgvstring[][]Argv that prints a ContextSnapshot (or { references, sourceHash }) JSON on stdout.
rag.timeoutMsinteger30000
rag.maxReferencesinteger4

mcp

PathTypeDefaultDescription
mcp.enabledbooleanfalsePublic API / future CLI only in 0.6.0 — not wired into tick/deliver.
mcp.allowToolsstring[][]

plugins

PathTypeDefaultDescription
plugins.modulesstring[][]Local .mjs files (relative to project.root) loaded once at the start of tick/deliver; each exports { id, apply(bus) } and gets the loop's in-process event bus to subscribe to (src/loop/event-bus.ts) — events (contract.failed, worker.dispatched, …) and lifecycle hooks (beforeDispatch, beforeMerge, … a before* hook can block the action). Same trust level as agents.registry.yaml: files already in this repo, never fetched over the network.

github

PathTypeDefaultDescription
github.intakeLabelstringnull"loop:review"
github.reviewOnlybooleantrueIntake PRs are always review + comment only; this loop never merges a PR it did not dispatch, regardless of a clean review.

resilience

PathTypeDefaultDescription
resilience.maxConsecutiveFailuresinteger3Consecutive failures on the same issue — contract generation failing on every candidate, or a worker/worktree dispatch failing — before the loop stops retrying it and escalates instead of spinning every tick. (Pilot 2026-09-11: one unclassified quota error produced 19 silent retries across 4 issues over 7h with no cap.) contract.escalated (a genuine "needs more information" decision) does not count; a successful dispatch, a clean/findings review, or a merge clears the counter.
resilience.pausedLabelstring"loop:paused"Label applied (and checked for removal, to auto-resume) when an issue is paused after maxConsecutiveFailures.
resilience.stagePauseAfterRunsinteger3Consecutive thrown loop stage runs (config/adapter crash, not a normal idle/ok/blocked report) before that stage pauses itself.
resilience.maxUsageDeltaPercentnumberCost circuit breaker: the loop cannot count a worker CLI's internal model/tool calls (it is an opaque process), so instead it watches the builder provider's remaining Orca usage from dispatch time. If that provider's remaining usage drops by at least this many percentage points while this one issue is in flight, deliver stops nudging/reviewing/merging it and escalates like a stuck worker. Unset (default) = disabled — a config typo elsewhere must not silently start blocking normal-cost dispatches.

brief

PathTypeDefaultDescription
brief.skillsstring[][]Markdown files (paths relative to project.root) pinned verbatim into every worker brief, sha256-digested for traceability. Missing file = dispatch fails closed.
brief.maxSkillCharsinteger6000Per-file cap; a file over this length is truncated with a visible note rather than blowing the brief budget.

security

PathTypeDefaultDescription
security.piiobject{}
security.pii.enabledbooleanfalseOff by default: scanning issue text/PR findings for PII-shaped patterns before they enter a prompt or a public comment.
security.pii.actionredact · warn · block"redact"redact replaces a match with [REDACTED:&lt;kind&gt;]; warn leaves the text as-is but logs a security.pii-detected event; block fails the contract instead of sending the text anywhere.

budget

Ceilings on what the loop may spend. A budget that is reached escalates; it never insists — the loop calling the same model again with less headroom is how a bad hour becomes a bad week.

PathTypeDefaultDescription
budget.perProvidernumber100Percentage of a provider's window the loop may consume, leaving the rest for the human sharing the plan. 100 (default) changes nothing; 80 means the loop stops using a provider once 80% of the window is gone.
budget.perIssueTokensinteger0Tokens one issue may consume across every model call the loop makes for it. 0 = no ceiling.

intake

Where work comes from when nobody typed it: an alert, a log error, a piece of user feedback. intake reads the declared sources, deduplicates against what it already filed, and creates the issue with its evidence. It is what closes the cycle — and the only way the incident flow starts without a human at a keyboard.

PathTypeDefaultDescription
intake.enabledbooleanfalse
intake.sourcesobject[][]Each source is argv printing a JSON array of alerts: { id?, title, body?, severity?, url? }.
intake.sources.idstringrequired
intake.sources.commandstring[]required
intake.sources.timeoutSecinteger120
intake.sources.labelsstring[][]Labels every issue from this source carries, on top of intake.labels.
intake.labelsstring[][]
intake.dedupeWindowHoursnumber168An alert with the same fingerprint inside this window is not filed again.
intake.flowBySeverityrecord<string>{}Severity (as the source reports it, lowercased) → flow:&lt;name&gt; label, so a P0 can start the incident flow.
intake.maxPerRuninteger5

maintain

Dependencies, security and licences, on a schedule — what a bot does from outside, inside the loop and under the same Definition of Done. A check that has nothing to decide files nothing.

PathTypeDefaultDescription
maintain.enabledbooleanfalse
maintain.checksobject[][]
maintain.checks.idstringrequired
maintain.checks.commandstring[]required
maintain.checks.timeoutSecinteger600
maintain.checks.fileWhenoutput · exit-code"exit-code"output files an issue when the command prints anything; exit-code when it exits non-zero.
maintain.checks.titlestringrequired
maintain.checks.labelsstring[][]
maintain.dedupeWindowHoursnumber168How often the same unresolved finding may be filed again.

connectors

Which implementation of each connector this project uses. The engine speaks only to the interfaces (TrackerConnector, ScmConnector, RunnerConnector); adding Jira, GitLab or a cloud sandbox is a new implementation and a new value here, never a change in tick, deliver or release.

PathTypeDefaultDescription
connectors.trackerlinear"linear"
connectors.scmgithub"github"
connectors.runnerorca · local"orca"orca drives Orca's worktrees and terminals; local is git worktree + tmux + the system crontab.
connectors.localobject{}
connectors.local.worktreeRootstring"../.ak-worktrees"Where local puts its worktrees. Relative paths resolve against project.root.
connectors.local.tmuxBinstring"tmux"
connectors.local.cronMarkerstring"# ak-harness"Marker comment the harness owns in the crontab; every line it manages carries it.

release

Promotion and deploy. The loop closes an issue when it merges into project.baseBranch — the integration branch; release moves that batch to releaseBranch and runs the project's deploy, and it never starts without a human approving the batch. Everything that acts on the world keeps a human in front of it.

PathTypeDefaultDescription
release.enabledbooleanfalse
release.branchstring"production"Where the approved batch is promoted to. Must differ from project.baseBranch.
release.deploystring[]Deploy argv (no shell), run in project.root after the promotion push succeeds. Unset = promotion only.
release.deployTimeoutSecinteger1800
release.smokestring[]Post-deploy smoke argv. A non-zero exit runs rollback (when declared) and escalates.
release.smokeTimeoutSecinteger300
release.notesFilestringWhen set, release notes for the batch are written here (newest first) and committed before the promotion.
release.rollbackstring[]Rollback argv, declared by the project because only the project knows what undoing its deploy means.
release.rollbackTimeoutSecinteger900

worker

The roles that run per issue, and the plan the worker starts from. The planner and the vote run in the harness, headless, before dispatch — the same shape as freezing the contract. The model writes the plan and writes the votes; the machine counts them and decides. The worker is only launched once a plan has consensus, so it starts from an approved plan instead of inventing one.

PathTypeDefaultDescription
worker.planobject{}
worker.plan.enabledbooleanfalse
worker.plan.votesinteger3How many agents vote on the plan.
worker.plan.approvalsinteger2How many of them must approve. Default 2 of 3.
worker.plan.maxCyclesinteger3Planner → vote → replan cycles before the item becomes a human's problem. Three models disagreeing three times is an ambiguous requirement.
worker.plan.timeoutMsinteger300000
worker.rolesplanner · vote · builder · verify · review · dod[]The phases that run for one issue, in order. Unset (the default) means every phase answers for itself, from its own block — worker.plan.enabled, delivery.verify.argv, delivery.review, dod.items — which in practice is ['builder', 'review'] and is exactly what a project that never opted in already has. Declaring the list makes it the answer: a phase not named here does not run, however well configured its own block is. That is the point of declaring it.

layers

The slices of the codebase, each with the one thing that decides it: a label the tracker carries, a file boundary, and the test that closes it. This is the source; a layer's description in the tracker is a reflection of it, never the other way round. The decomposer reads these to place an issue, the brief tells the worker which test closes its layer, and the cheap verifier runs that test instead of the whole suite when the issue belongs to one.

PathTypeDefaultDescription
layers.idstringrequired
layers.labelstringrequiredThe tracker label that puts an issue in this layer, e.g. layer:L2.
layers.descriptionstring""
layers.pathsstring[][]Globs the layer owns. A PR for this layer touching anything else is reported, and held when enforce.
layers.verifystring""The command that closes this layer. Used by the brief and by the pre-review verifier.
layers.enforcebooleanfalseOff by default: a boundary that blocks before a team has drawn it properly costs more than it protects.

documents

Where the PRD, the technical design and the decisions live once a human approves them. file writes them into the repository, which is what makes them reviewable, diffable and greppable by the workers that come later. none keeps them only in the loop's state. A tracker-document backend is the seam this leaves open; Orca's CLI has no document command today, so there is nothing honest to implement against.

PathTypeDefaultDescription
documents.backendfile · none"file"
documents.prdPathstring"docs/prd"
documents.designPathstring"docs/design"

dod

The project's half of the Definition of Done: the same list for every issue, and every item provable. The issue's half is the frozen contract's outcomes. A PR merges only when both lists are proven, and the proof — a command's output, a changed file, an absent pattern — is written on the PR. There is deliberately no manual kind: what cannot be proven is not a DoD item, it is a wish.

PathTypeDefaultDescription
dod.itemsobject[][]With items declared, the project list is enforced at merge; with none, only the contract outcomes are.
dod.items.idstringrequired
dod.items.descriptionstringrequired
dod.items.kindcommand · file-changed · pattern-absentrequiredcommand — argv the worker runs, exit 0 is the proof (the harness never runs it; the worker does, in its own worktree). file-changed — the PR must touch a path matching glob. pattern-absent — no changed file may contain pattern.
dod.items.commandstring[]
dod.items.globstring
dod.items.patternstring
dod.items.pathsstring[][]Restrict pattern-absent / file-changed to these path globs; empty = every changed file.
dod.evidenceFilestring".ak-loop/dod.json"Where the worker writes its proofs, relative to the worktree root.

tuning

Knobs the retro is allowed to move by itself, each inside a declared range and justified by a declared metric. A knob with no metric is not auto-adjustable: the metric is what proves the change helped, and it is the same number that reverts it when the next cycle is worse. Never auto-adjustable, whatever this block says: models, providers, gates and branches — the things that decide who pays and what reaches production.

PathTypeDefaultDescription
tuning.enabledbooleanfalse
tuning.maxChangesPerRetrointeger1At most this many knobs move in one retro, so a bad cycle changes one thing and stays explainable.
tuning.commitbooleanfalseCommit the edited loop.config.yaml with the reason and the evidence. Off by default: committing is the project's call.
tuning.knobsobject[][]
tuning.knobs.pathstringrequiredDotted path into this config, e.g. delivery.review.minSeverity. Must resolve to a declared field.
tuning.knobs.metricreview-findings-ratio · stuck-count · fix-rounds-per-merge · escalation-countrequiredThe metric that justifies moving it, and that reverts it when the next cycle is worse.
tuning.knobs.valuesstring,number[]Ordered ladder of allowed values, cheapest first. Use this for enums.
tuning.knobs.minnumberNumeric range. step is how far one retro may move it.
tuning.knobs.maxnumber
tuning.knobs.stepnumber

flows

Named flow profiles — one motor, several kinds of demand. A profile switches on and off what the loop spends: review strictness and votes, CI babysitting, the human gates, and (as the stages land) the worker's own roles. A selection rule picks one per issue; unmatched issues get flows.default.

PathTypeDefaultDescription
flows.defaultstringProfile used when no rule matches. Must name a key of profiles (or null for "change nothing").
flows.profilesrecord<object>{}
flows.profiles.reviewobjectReplaces the named delivery.review fields for issues on this flow. Other fields keep the project value.
flows.profiles.review.votesinteger
flows.profiles.review.minSeveritynit · med · high · blocker
flows.profiles.review.profilefast · full
flows.profiles.review.deadlineMsinteger
flows.profiles.mergeobject
flows.profiles.merge.autoboolean
flows.profiles.merge.requireChecksbooleanCI babysitting: with checks required, a red check becomes a fix round; without, the review is the gate.
flows.profiles.merge.requireHumanApprovalboolean
flows.profiles.maxFixRoundsinteger
flows.profiles.rolesrecord<object>{}Who runs a role on this flow, and how hard it thinks. Precedence is narrow beats broad: the role inside the profile, then the project's config, then the global one. A provider/model here narrows the role's candidate list to that pin; it never widens it, so a pin nobody can serve right now falls through to the role's ordinary candidates instead of dispatching something nobody asked for.
flows.profiles.roles.providerstring
flows.profiles.roles.modelstring
flows.profiles.roles.effortlow · medium · high · xhigh
flows.profiles.roles.timeoutMsintegerCeiling for one call of this role on this flow. Unset = the role's own default.
flows.profiles.stagesrecord<boolean>{}Per-issue phases this flow switches off (or explicitly back on), overriding worker.roles. These are the phases of one issue — not the scheduled automations, which are schedule.*. builder is the work itself: listing it as false is accepted and ignored, because a flow that builds nothing is not a flow.
flows.profiles.leadbooleanThe builder leads instead of typing: it delegates one plan item at a time and integrates the results. Only worth asking for where the provider has subagents (models.providers.&lt;id&gt;.subagents). Where it does not, the brief says so plainly and the dispatch record keeps that fact — silently dropping the request would leave a human reading "lead" in the config and a worker that never led anything.
flows.profiles.reasonstringFree-form note shown wherever the flow is reported, so a costlier gate can explain itself.
flows.selectobject[][]Rules are evaluated by kind, never by position: label, then project, then priority. A label is an explicit intention and outranks a signal; within one kind the first matching rule wins.
flows.select.flowstringrequired
flows.select.anyLabelsstring[][]
flows.select.projectsstring[][]
flows.select.prioritiesstring[][]

notifications

Where the loop calls a human. The tracker comment always happens — it is the record; this is the channel on top of it. Two generic shapes only: a webhook (Slack, Discord, Telegram bots, n8n) and a local command (system notification, mail CLI). Zero vendor code, so a new destination is configuration, not a release.

PathTypeDefaultDescription
notifications.eventsstring[]["contract.escalated","contract.failed","issue.paused","stage.paused","pr.merge-refused","release.waiting"]Loop event types that reach the channel. onEscalate always does, whatever this list says.
notifications.webhookobject
notifications.webhook.urlstringLiteral URL. Only for the user's global file, which lives outside every repository; in a versioned config use urlEnv.
notifications.webhook.urlEnvstringName of the environment variable holding the URL — the shape a versioned config uses, since this file never holds secrets.
notifications.webhook.methodPOST · PUT"POST"
notifications.webhook.headersrecord<string>{}Extra headers. Values are literal; put a token in urlEnv or a proxy instead of writing it here.
notifications.webhook.timeoutMsinteger10000
notifications.commandstring[]Argv (no shell). {summary}, {event}, {issue} and {json} are substituted per element.
notifications.commandTimeoutMsinteger10000

schedule

PathTypeDefaultDescription
schedule.tickstring"*/5 * * * *"
schedule.deliverstring"*/10 * * * *"
schedule.retrostringWhen set with retroIssue, install also creates &lt;prefix&gt;-retro.
schedule.retroIssuestringLinear issue that receives the weekly retro digest comment.
schedule.observestringWhen set, install also manages &lt;prefix&gt;-observe: the health scan whose precheck exits 0 only when a human-facing anomaly is new or overdue for a reminder.
schedule.observerobject{}
schedule.observer.sincestring"24h"Event window the scan reads, as accepted by loop observe --since.
schedule.observer.reminderHoursnumber2An unresolved problem set already notified is repeated at most this often.
schedule.observer.schedulerStallMininteger20No automation run in this long means the scheduler itself stopped, not that the loop is idle.
schedule.observer.staleLockMininteger30A stage lock older than this is presumed abandoned rather than a long run.
schedule.precheckTimeoutSecinteger120
schedule.harnessCommandstring"ak-harness"How the Orca automation invokes the harness inside the workspace; -f &lt;config&gt; is appended.
schedule.providerstringOrca agent id that runs the automation prompt; default: the watcher role's first available provider, else claude.
schedule.namePrefixstring"loop"Prefix for automation names (&lt;prefix&gt;-tick, &lt;prefix&gt;-deliver, &lt;prefix&gt;-retro, &lt;prefix&gt;-observe).
schedule.runnerprecheck · agent"precheck"precheck (default): the stage runs inside Orca's --precheck command and always exits 1, so Orca records the run (skipped_precheck, stdout captured) without ever launching an agent. agent: legacy — the precheck only tests for work and an Orca-launched agent runs the harness (needs a provider that runs non-interactively).
schedule.stageTimeoutSecinteger600Time budget for one stage when runner: precheck. Orca caps prechecks at 600 s; the stage itself must fit.
schedule.timezonestring

On this page