config.yml Reference
Published May 22, 2026 · Last updated June 18, 2026 · 8 min read
1. Overview
.obvious/config.yml is the per-repo policy contract for Obvious autobuild. It parameterizes repo-specific workflow values — default base branch, PR merge method, PR-review behaviour, preview-deploy URLs, QA guide locations, and CI "generated file" commands — so the agents that work in your repository never have to hard-code them.
The file is optional. When it is absent, every value falls back to a built-in default and autobuild still runs. You only add the fields you want to change.
Where it lives: .obvious/config.yml at your repository root (the directory that contains .git). A .obvious/config.yaml extension is accepted as a fallback; .yml is tried first.
How it's loaded & validated:
-
The config is parsed fail-soft: malformed YAML, a non-object root, or an invalid field never crashes autobuild — it is reported as a diagnostic and the affected value falls back to its default.
-
The
validatecommand (below) returns a list of error strings and exits non-zero when any field is invalid, so you can check a config in CI before relying on it. -
Config changes may take a short time to take effect after editing.
2. Managing the file — the obvious:config CLI
Use the obvious config CLI to create, read, edit, and validate the file rather than hand-editing YAML where possible. Run it from your repository root:
obvious config <command> [options]
| Command | Signature | Effect |
|---|---|---|
init | init [--cwd path] [--force] | Write the skeleton config (branches.defaultBase: main). Skips if the file already exists unless --force. |
get | get <dotpath> [--cwd path] | Resolve and print one value with a source annotation, e.g. main (config) or squash (fallback). |
set | set <dotpath> <value> [--cwd path] | Patch a dot-path, re-validate, and rewrite canonical YAML. Refuses to write an invalid result. |
validate | validate [--cwd path] [--check-github] | Parse + validate; print per-field status; exit non-zero on errors. --check-github also diffs your config against your live GitHub repo settings (requires the gh CLI). |
dump-env | dump-env [--cwd path] | Print every REPO_POLICY_* variable as KEY=value. |
Flags
-
--cwd/-C <path>— set the working directory the command operates on. -
--force/-f— overwrite an existing file oninit. -
--check-github— enable drift checks against live GitHub settings onvalidate.
set coercion & restrictions
-
qa.guidePathsandqa.automationPathsaccept comma-separated input, which is split into an array. -
Numeric fields (
preview.deploy.timeoutMinutes,preview.deploy.pollIntervalSeconds,reviews.botFeedback.releaseBranchMaxRounds) are coerced to numbers; non-numeric input is rejected. -
All other paths are written as strings. After coercion,
setre-runs validation and refuses to write an invalid result. -
ci.generatedFileCommandsis an array of objects and cannot be set viaset— edit the YAML directly.
3. Complete Field Reference
Fields are grouped by top-level section. Type, Default, and Allowed values come from the config schema.
Source legend — when you run
get, each value is annotated with its origin:config= value came from the file;fallback= built-in default used;not-configured= no value and no meaningful default (empty string).
3.1 branches (required)
| Key path | Type | Default | Allowed / notes | Effect |
|---|---|---|---|---|
branches.defaultBase | string | required (fallback main) | non-empty string | Default base branch for PRs. Surfaced as REPO_POLICY_BRANCH_DEFAULT_BASE. The section and this key are the only required parts of the config. |
branches.draftEarly | — | not settable | always true | Platform invariant, not a user field. If present, it is ignored with a diagnostic. Draft-early PR creation cannot be disabled by config. |
3.2 pr
| Key path | Type | Default | Allowed / notes | Effect |
|---|---|---|---|---|
pr.merge.method | string | squash | squash | merge |
3.3 reviews
| Key path | Type | Default | Allowed / notes | Effect |
|---|---|---|---|---|
reviews.automated.enabled | boolean | (absent → unset) | true | false |
reviews.botFeedback.releaseBranchMaxRounds | number | 5 | numeric | Max rounds of bot-feedback iteration the agent performs on release-branch PRs. |
reviews.humanReview.requestPolicy | string | — | reserved | Defined in the schema but not currently consumed by the runtime. Treat as reserved. |
reviews.routing | object | — | see 3.3.1 | First-match-wins routing of files → review mode. |
3.3.1 reviews.routing.rules[]
reviews.routing is { rules: [...] }. Provide at least one rule and at most 20. Each rule:
| Key path | Type | Required | Allowed / notes |
|---|---|---|---|
reviews.routing.rules[].name | string | yes | non-empty; human-readable label |
reviews.routing.rules[].match | string[] | yes | non-empty array of repo-relative glob patterns; first match wins across all rules |
reviews.routing.rules[].mode | string | yes | none |
reviews.routing.rules[].reviewers | array of { type } | only when mode: specialized | required & non-empty for specialized; forbidden for none / single |
reviews.routing.rules[].reviewers[].type | string | yes (within reviewers) | built-in types: general |
A catch-all rule (match: ['**'] as the sole pattern, placed last) is recommended — its absence triggers a non-blocking diagnostic, not an error.
3.4 ci
| Key path | Type | Default | Allowed / notes | Effect |
|---|---|---|---|---|
ci.generatedFileCommands | array of objects | [] | each entry validated below | Commands that regenerate checked-in generated files when matching files change. The first entry's command is surfaced as REPO_POLICY_SNAPSHOT_COMMAND. |
ci.generatedFileCommands[].name | string | '' | optional | Human-readable label for the command. |
ci.generatedFileCommands[].filePatterns | string[] | [] | optional | Glob patterns that trigger the command. Non-string / blank entries are filtered out. |
ci.generatedFileCommands[].command | string | required per entry | non-empty | Shell command to run. Entries without a non-empty command are skipped. |
Per-entry shape is enforced at parse time: malformed entries are dropped, not errored.
3.5 preview
| Key path | Type | Default | Allowed / notes | Effect |
|---|---|---|---|---|
preview.primaryEnvironment | string | '' | key into preview.environments | Selects which environment supplies the URL/backend templates. If omitted, the first key in environments is used. |
preview.environments.<key>.urlTemplate | string | '' | e.g. https://pr-{prNumber}.preview.example.com | Preview URL template for that environment. Backs REPO_POLICY_PREVIEW_URL. |
preview.environments.<key>.backendBaseUrl | string | '' | e.g. https://api.staging.example.com | Backend base URL for that environment. Backs REPO_POLICY_PREVIEW_BACKEND_URL. |
preview.deploy.pollIntervalSeconds | number | 30 | numeric | How often to poll for preview-deploy readiness. |
preview.deploy.timeoutMinutes | number | 10 | numeric | Max wait for a preview deploy. Surfaced as REPO_POLICY_PREVIEW_TIMEOUT_MINUTES. |
The
REPO_POLICY_PREVIEW_URLandREPO_POLICY_PREVIEW_BACKEND_URLvalues are computed fromprimaryEnvironment+environments. You do not write them as keys in the file.
3.6 qa
| Key path | Type | Default | Allowed / notes | Effect |
|---|---|---|---|---|
qa.guidePaths | string | string[] | '' | single path or list |
qa.automationPaths | string | string[] | '' | single path or list |
When given as a list, both render joined by , . Setting via the CLI accepts comma-separated input and splits it into an array.
3.7 triageProjects
| Key path | Type | Default | Allowed / notes | Effect |
|---|---|---|---|---|
triageProjects.reviewProjectId | string | (none) | Obvious project ID (prj_…) | Project used to route automated PR-review work. null when absent or blank. |
3.8 Reserved / not currently consumed
These top-level sections are accepted without error but are not read by any current runtime reader. Document them as reserved, not active config:
| Section | Status |
|---|---|
notifications | Accepted; readable via get, but not currently consumed and has no REPO_POLICY_* var. Reserved. |
release | Accepted but not currently parsed. Reserved. |
owners | Accepted but not currently parsed. Reserved. |
runbooks | Accepted but not currently parsed. Reserved. |
Unknown future fields are ignored so newer configs never break older agents.
3.9 REPO_POLICY_* environment variables
These are injected into the build shell so scripts can read policy without parsing YAML. Use ${VAR:-fallback} syntax in shell.
| Env var | Config source path | Fallback |
|---|---|---|
REPO_POLICY_BRANCH_DEFAULT_BASE | branches.defaultBase | main |
REPO_POLICY_MERGE_METHOD | pr.merge.method | squash |
REPO_POLICY_PREVIEW_URL | preview.environments.<primary>.urlTemplate | (empty) |
REPO_POLICY_PREVIEW_BACKEND_URL | preview.environments.<primary>.backendBaseUrl | (empty) |
REPO_POLICY_PREVIEW_TIMEOUT_MINUTES | preview.deploy.timeoutMinutes | 10 |
REPO_POLICY_SNAPSHOT_COMMAND | ci.generatedFileCommands[0].command | (empty) |
REPO_POLICY_RELEASE_BASE | the repo's live default branch (not from config) | (unset; use ${REPO_POLICY_RELEASE_BASE:-main}) |
4. Validation Rules & Constraints
Enforced by validate (and by set before writing). Each violation is a returned error string; a non-empty list fails validation.
Structural
-
The config root must be a YAML mapping (object) — not a scalar or list.
-
branchesis required and must be an object;branches.defaultBasemust be a non-empty string.
Type checks (only enforced when the field is present)
-
prmust be an object;pr.mergemust be an object;pr.merge.methodmust be a string. -
preview.deploy.timeoutMinutesandpreview.deploy.pollIntervalSecondsmust be numbers. -
reviews.automatedmust be an object;reviews.automated.enabledmust be a boolean. -
reviews.botFeedback.releaseBranchMaxRoundsmust be a number. -
ci.generatedFileCommandsmust be an array.
reviews.routing cross-field rules
-
routingmust be an object;routing.rulesis required, must be a non-empty array, ≤ 20 rules. -
Per rule:
namenon-empty string;matchnon-empty array of non-empty strings;mode∈{none, single, specialized}. -
reviewersis required & non-empty whenmode: specializedand must not be set fornone/single; eachreviewers[].typemust be a non-empty string.
Soft (diagnostic, non-blocking) rules — reported but never fail validation:
-
Any unknown top-level key → "Ignoring unknown .obvious config field: …".
-
draftEarly(top-level or underbranches) → "platform invariant and is always true". -
reviews.routingwithout a catch-all (match: ['**']) rule → recommendation diagnostic. -
Malformed YAML / non-object root / unreadable file → diagnostic + full fallback policy (no crash).
Safety note: repo config is runtime guidance only. Platform safety rules, permission gates, branch protection, completion gates, and tool-layer enforcement cannot be weakened by
.obvious/config.yml.
5. Example Configurations
5.1 Minimal / default
The skeleton written by init. Everything else uses built-in defaults.
branches:
defaultBase: main
5.2 Complete annotated example
Every supported, actively-consumed field with representative values:
# ── Required ────────────────────────────────────────────────
branches:
defaultBase: main # string (required) → REPO_POLICY_BRANCH_DEFAULT_BASE
# draftEarly: NOT ALLOWED — platform invariant, always true (diagnosed if set)
# ── Pull requests ───────────────────────────────────────────
pr:
merge:
method: squash # squash | merge | rebase (default squash) → REPO_POLICY_MERGE_METHOD
# ── Reviews ─────────────────────────────────────────────────
reviews:
automated:
enabled: true # boolean; absent ⇒ unset (workspace default, then fail-open)
botFeedback:
releaseBranchMaxRounds: 5 # number (default 5)
humanReview:
requestPolicy: on-request # string — reserved (not currently consumed)
routing: # first-match-wins; ≤ 20 rules
rules:
- name: API security review
match: # non-empty glob list
- src/routes/**
mode: specialized # none | single | specialized
reviewers: # required & non-empty ONLY for `specialized`
- type: security # general | security | performance (repos may add local types)
- name: Docs only
match:
- docs/**
mode: none # reviewers MUST be omitted for none/single
- name: Catch-all
match:
- '**' # recommended final catch-all rule
mode: single
# ── CI: regenerate checked-in generated files ───────────────
ci:
generatedFileCommands:
- name: codegen-snapshots # string (optional)
filePatterns: # string[] (optional)
- src/generated/**
command: npm run generate:snapshots # required; [0].command → REPO_POLICY_SNAPSHOT_COMMAND
# ── Preview deployments ─────────────────────────────────────
preview:
primaryEnvironment: staging # key into environments (else first key used)
environments:
staging:
urlTemplate: https://pr-{prNumber}.preview.example.com # → REPO_POLICY_PREVIEW_URL
backendBaseUrl: https://api.staging.example.com # → REPO_POLICY_PREVIEW_BACKEND_URL
deploy:
pollIntervalSeconds: 30 # number (default 30)
timeoutMinutes: 10 # number (default 10) → REPO_POLICY_PREVIEW_TIMEOUT_MINUTES
# ── QA ──────────────────────────────────────────────────────
qa:
guidePaths: # string | string[]
- docs/QA.md
- e2e/QA.md
automationPaths: # string | string[]
- .github/staging-automations/
# ── Triage routing ──────────────────────────────────────────
triageProjects:
reviewProjectId: prj_xxxxxxxx # Obvious project ID for automated PR-review routing
Not configurable (do not add):
branches.draftEarly(platform invariant, always true),branches.release.base(removed — the release base is the repo's live default branch viaREPO_POLICY_RELEASE_BASE), and the computedpreviewURL/backend values (derived, not literal keys).notifications,release,owners, andrunbooksare accepted without error but are not currently consumed.