Skip to content

Config file: .yggdrasil/yg-config.yaml

yg init creates this file. A bare, non-interactive yg init (no --provider) writes it with no reviewer: section at all — a keyless, script-only start. A reviewer is configured separately, interactively or via --provider [--model] [--endpoint], whenever the graph gains its first reviewer rule. yg init --upgrade lifts the graph's config version to the current one and refreshes the agent-rules files.


Schema ​

Required ​

  • version — Schema version managed by the CLI, as a quoted three-part string (version: "6.0.0"). Do not edit manually. Run yg init --upgrade to upgrade. A missing field, or an unquoted version: 5.1 (which YAML reads as a number), stops every command that loads the graph with an error saying so.

Conditionally required ​

  • reviewer — Reviewer configuration; when present, must contain tiers with at least one entry. Required only once a reviewer rule is actually effective in the graph — a script-only / keyless project (script rules only, or none) needs no reviewer: section, and yg check does not ask for one until a reviewer rule exists. Configured via yg init (interactively, or non-interactively with --provider); see Reviewer tiers below.
  • reviewer.default — Tier name aspects fall back to when they don't declare one. Required when reviewer.tiers has more than one entry; optional with exactly one tier.

Optional ​

  • coverage — Controls which files must be mapped to a node (see Coverage config below).
  • quality — Quality thresholds (see Quality config below).
  • parallel — How many reviewer-rule pairs are reviewed concurrently (positive integer, default 1). yg init writes 4 when it sets up a CLI reviewer (claude-code, codex, gemini-cli, copilot-cli) and the file has no parallel yet: each call is its own local process under your subscription, and four keep a first fill of a few hundred pairs to minutes while staying inside its rate limit. For an API reviewer, set it to what the key's rate limit allows. A tier with consensus: N runs its N passes at once, so up to parallel × N calls are in flight. Governs only the reviewer fill phase, where the cost is network latency. Script rules ignore it — they are CPU-bound and run across a worker-thread pool sized automatically from your machine's cores (no configuration; never affects verdicts, only speed). Each script-rule check a fill runs (yg check --approve, yg adopt) is under a wall-clock budget, 120 seconds by default (YG_DET_TASK_TIMEOUT_MS in the environment changes it, in milliseconds; 0 switches it off): a check still running past it is stopped and reported as unverified with cause check-failed-to-run, naming the rule and the unit, and the rest of the run carries on. The diagnostic runs — yg aspect-test, yg drill, yg simulate — run a check with no budget.
  • debug — Set true to append the output of every command that reads the graph (all but init, knowledge, schemas, prime, marketplace and --help) to .yggdrasil/.debug.log, with a failed reviewer call's full provider output.
  • auto_approve — Auto-fill mode for bare yg check (default false; see Auto-approve config below).
  • signals — Attention-layer switches (optional). Its only key today is attention (default true): the advisory "structurally unusual" note in yg context --file. Set false to silence it. See Signals below and Structural attention.
  • events — Committed-events opt-in (optional). Its only key today is committed_llm (default false): opt into a committed, team-shared record of reviewer verification events. See Events below.
  • progressive — Names the branch your changes are measured against (optional; absent means off). Its only key today is reference. With it set, a plain yg check blocks only on what your change reaches, and everything inherited from that branch is listed as a non-blocking warning. See Progressive mode below and the Progressive mode page.
  • rules_artifacts — Which of the three agent-rules artifacts (agents_md, claude_md, clinerules) this repository wants written and kept in sync (optional; each defaults to true, so an absent block — or an absent key inside one — means all three are on). The block only ever turns an artifact off. See Rules artifacts below.

Those eleven are the whole of it — version, reviewer, coverage, quality, parallel, debug, auto_approve, signals, events, progressive, rules_artifacts.

A typo at the top level is an error

Any other top-level key — in yg-config.yaml or in the yg-secrets.yaml overlay — is a config-unknown-key error that names the file and, when the key is close to a real one, the key you probably meant (auto_aprove → did you mean auto_approve?). The nested places are guarded the same way: a misspelled key directly under reviewer: or inside a tier is a hard config-reviewer-unknown-key / config-tier-unknown-key error, and signals:, events:, coverage:, progressive: and rules_artifacts: all reject unknown keys too (rules_artifacts also refuses a non-boolean value, and refuses claude_md: true alongside agents_md: false — config-rules-artifacts-unknown-key / config-rules-artifacts-orphan-import), and so do quality: (config-quality-unknown-key) and a tier's config: (config-tier-unknown-key; a value of the wrong type there is config-tier-config-invalid). A key an earlier release read and this one does not is named as retired rather than guessed at.

An unknown top-level key is ignored and everything else in both files stays in effect, so the error is about the key alone: coverage, the reviewer and every other setting still apply. It still blocks yg check, because whatever the key was meant to set is not set. A key in yg-secrets.yaml fails only where that file exists — your machine, or a CI job that writes one; yg init --upgrade lists the unknown keys of both files.

Node types are defined in the separate architecture file (.yggdrasil/yg-architecture.yaml), not in yg-config.yaml.


Full annotated example ​

yaml
version: "6.0.0"

reviewer:
  default: standard                 # Required when more than one tier; optional with one
  tiers:
    standard:                       # Tier name — referenced by aspect reviewer.tier
      provider: ollama              # LLM provider
      consensus: 1                  # Votes per aspect (odd integer >= 1)
      max_prompt_chars: 50000       # Cap on the assembled prompt (optional; absent defaults to 50000)
      config:
        model: qwen3
        endpoint: http://localhost:11434
        temperature: 0

coverage:                             # Optional — controls which files must be mapped
  required:                           # Unmapped files under these roots are a blocking error
    - "/"                             # Default: whole repo
  excluded: []                        # Files under these roots are silently ignored
  type_level: true                    # Type-level coverage; `yg init` writes true (see below)

quality:
  max_direct_relations: 10

parallel: 10                          # Concurrent reviewer-rule reviews (reviewer phase only)
debug: false
auto_approve: false   # false (default) | deterministic | full

signals:                              # Optional — attention-layer switches
  attention: true                     # The "structurally unusual" note in yg context --file (default true)

events:                               # Optional — committed-events opt-in (default off)
  committed_llm: true                 # Commit + share reviewer verification events (default false)

# progressive:                        # Optional — absent means off (every run answers for the whole project)
#   reference: origin/main            # Branch your changes are measured against

# rules_artifacts:                    # Optional — absent means all three agent-rules files are carried
#   agents_md: true                   # The AGENTS.md digest block
#   claude_md: true                   # The @AGENTS.md import line in CLAUDE.md (needs agents_md)
#   clinerules: true                  # The standalone .clinerules/yggdrasil.md copy

A fresh yg init writes a subset of this: version, quality, coverage (with required: [], type_level: true, and excluded listing the agent-rules files it wrote plus .gitattributes), debug and auto_approve, plus the reviewer: block once you configure a reviewer. It writes a rules_artifacts: block only when you switch an artifact off — the absent block already means all three are on.

Configuring a reviewer keeps the file's comments

yg init --provider … and the interactive "Configure reviewer" menu entry edit yg-config.yaml in place: they replace the reviewer: section (all of it — every tier and reviewer.default), add parallel: 4 for a CLI provider when the file sets no parallel, and leave everything else, comments included, as it was. yg-secrets.yaml is re-serialized whenever init writes or removes a key in it, so comments there are lost.


Reviewer tiers ​

Reviewer configuration uses named tiers. Each tier is an independent LLM configuration. Aspects target a tier via reviewer.tier: <name> in yg-aspect.yaml. If no tier: is declared on an aspect, the aspect uses reviewer.default from the config.

reviewer.default ​

The tier name aspects fall back to when they don't declare reviewer.tier:.

  • Required when reviewer.tiers has more than one entry — the validator emits config-default-tier-missing otherwise.
  • Optional when reviewer.tiers has exactly one entry; the single tier is the implicit default.
  • Must reference a key under reviewer.tiers.

reviewer.tiers.<name> ​

Tier name regex: ^[a-zA-Z][a-zA-Z0-9_-]{0,62}$. The literal name default is reserved (it would clash with reviewer.default visually). Convention: standard for the primary tier. Add a second tier (e.g. deep) for aspects that need a higher-capability model.

yaml
reviewer:
  default: deep
  tiers:
    standard:
      provider: anthropic
      consensus: 3
      config:
        model: claude-opus-4-7
        temperature: 0
    deep:
      provider: ollama
      consensus: 1
      config:
        model: qwen3
        endpoint: http://localhost:11434

An aspect targeting the standard tier (overriding the default):

yaml
reviewer:
  type: llm
  tier: standard

An aspect with no explicit tier uses reviewer.default (deep in the above example):

yaml
reviewer:
  type: llm

Fields per tier ​

FieldRequiredDescription
provideryesOne of the supported providers (see below)
consensusyesPositive odd integer. 1 = single call. 3 = majority vote.
max_prompt_charsnoPositive integer. Caps the assembled-prompt length for reviewer pairs on this tier (see Prompt-size gate). Absent defaults to 50000. yg init writes 50000.
config.modelrequired for ollama / openai / anthropic / google / openai-compatible / copilot-cli; optional for the other CLI providersProvider-specific model identifier. yg init always writes a model into the file: sonnet for claude-code when --model is omitted, and whatever --model names for every other provider (init requires it). When a tier in the file has no config.model, the run-time fallback applies: claude-code → sonnet (the same model init writes), codex → o4-mini, gemini-cli → gemini-2.5-flash. copilot-cli has no default and must name one (auto lets Copilot pick).
config.temperaturenoSampling temperature. Defaults to 0.
config.endpointrequired for openai-compatible (ollama defaults to http://localhost:11434)API endpoint URL
config.timeoutnoPer-call timeout in seconds, honored by every provider. Defaults to 300 for the CLI providers and ollama, and to 60 for the hosted APIs (anthropic, openai, google, openai-compatible). A call that runs past it is reported as timed out, naming this setting, and is not retried (the request may already be billed). A hosted-API request that got no answer at all — connection refused, unknown host — is retried once, and a 429 once after two seconds.
config.api_keynoProvider API key. Takes precedence over the provider's environment variable. Do not put it in yg-config.yaml — supply it through the gitignored yg-secrets.yaml overlay (see the Secrets section below).

Any other config.* key is refused (config-tier-unknown-key, with the key it is probably a typo of), and a value of the wrong type — temperature: hot, timeout: abc — is config-tier-config-invalid rather than a silent fall back to the default. The CLI providers (claude-code, codex, gemini-cli, copilot-cli) read only model and timeout; temperature and endpoint are accepted there and have no effect.

The config: mapping itself is required on every tier, even for a CLI provider whose model has a built-in default: write config: {} there (a tier without it is config-tier-config-missing).

Supported providers ​

ProviderTypeNotes
ollamalocalNo API cost; requires local install
anthropicAPIRequires ANTHROPIC_API_KEY or yg-secrets.yaml
openaiAPIRequires OPENAI_API_KEY
googleAPIRequires GOOGLE_API_KEY
openai-compatibleAPIAny OpenAI-compatible endpoint. The key is optional: config.api_key, else OPENAI_COMPATIBLE_API_KEY; with neither, requests go out with no Authorization header, which suits a keyless local server (vLLM, LM Studio, llama.cpp). It never reads OPENAI_API_KEY: the endpoint can be any server, and an OpenAI key must not travel to it.
claude-codeCLIDelegates to the installed claude CLI
codexCLIDelegates to the installed codex CLI
gemini-cliCLIDelegates to the installed gemini CLI
copilot-cliCLIDelegates to the installed GitHub Copilot CLI (copilot), billed to the Copilot plan it is signed in with

CLI providers (claude-code, codex, gemini-cli, copilot-cli) require no API key — they delegate to the installed CLI tool.

copilot-cli has no default model: the organisation's Copilot policy decides which models a seat may use, and the CLI refuses a model outside it instead of substituting another, so config.model must name one the plan allows (auto lets Copilot pick). The provider runs the real CLI, never the copilot stub the VS Code Copilot extension puts on PATH (an installer prompt): it takes YG_COPILOT_BIN when set, otherwise the first copilot on PATH outside the extension's storage. Each review runs with the user's configuration and MCP servers out of reach (an empty COPILOT_HOME), with no repository instructions, no built-in MCP servers and no shell, write, network or memory tools; the sign-in is kept.

codex (verified against codex-cli 0.156.1) runs codex exec with --skip-git-repo-check --sandbox read-only --ephemeral --ignore-user-config --ignore-rules --color never, the shell, image, sub-agent and goal tools disabled (--disable shell_tool, view_image, multi_agent, goals) and web search off (-c web_search=disabled), in an empty directory of its own (-C), with the verdict schema written to a file there for --output-schema. The login in CODEX_HOME is used; ~/.codex/config.toml is not — so neither are its MCP servers, profiles or a model_provider set there, and the default OpenAI provider answers. A codex older than these flags refuses them by name ("Unknown feature flag"), which is the reason a failed review reports.

gemini-cli (verified against @google/gemini-cli 0.61.0) runs headless with -o text --skip-trust --approval-mode plan -e none, an MCP allow-list that names no server, and --policy pointing at a file that denies every tool, in an empty directory of its own (trusted for that one run). The login and settings in ~/.gemini are used.

For both, a user-level ~/.codex/AGENTS.md or ~/.gemini/GEMINI.md still reaches the model; nothing in the reviewed repository can write either.

When the reviewer cannot run ​

yg check --approve names the cause in the provider's own terms and writes no verdict for the pairs it could not review. A CLI provider whose binary is missing says so and names the package that installs it (for copilot-cli, also YG_COPILOT_BIN and the VS Code stub). A CLI that fails reports its exit code, or the timeout it ran past, with the last few hundred characters it printed, credentials masked — which is where "please run /login" or an unknown-model error shows up. A hosted API with no key says which variable to set, and a refused request gives its HTTP status with what it usually means: 401/403 the key or its permissions, 404 or 400 the model name or endpoint path, 429 the rate limit (after one retry), 5xx the provider's own servers. The full output behind that line — a CLI's whole stderr, the raw unparseable reply — goes to .yggdrasil/.debug.log when debug: true is set in yg-config.yaml. yg init --provider runs the same binary check for a CLI provider and warns when it fails; for codex and gemini-cli it also asks for one probe verdict through the exact command a review uses (one tiny prompt), so a missing login, a rejected flag or an untrusted directory shows at setup rather than on the first yg check --approve. The configuration is written either way.


Secrets and local overrides ​

.yggdrasil/yg-secrets.yaml is a deep-merge overlay over yg-config.yaml (gitignored by default). It mirrors the same shape, and any field in it wins — use it for a tier's API key, or to point a named tier at a different provider/model/endpoint on your machine:

yaml
# .yggdrasil/yg-secrets.yaml — gitignored, never commit
reviewer:
  tiers:
    standard:
      config:
        api_key: sk-ant-...

Three keys are the exception to "any field wins". coverage.type_level (see Coverage config), progressive (see Progressive mode) and rules_artifacts (see Rules artifacts) are read off the committed yg-config.yaml before the overlay is merged, so an overlay can never introduce one, re-point one, or switch one off. Each decides something the whole branch has to agree on — what counts as covered, how much of the project a run answers for, and which files Yggdrasil maintains — so a gitignored local file must not be able to move it for one developer alone.

Because only the tier name is folded into a verdict's hash, overriding a tier's config or provider (its model, endpoint, key or timeout) never invalidates recorded baselines: the committed config names a canonical reviewer, and each machine points the same named tier at its own provider, model, or key. An overlay that changes which tier a rule resolves to — a different reviewer.default, a renamed or added tier — or changes coverage.required / coverage.excluded does change what is recorded or expected, and re-opens the pairs it moves.

API providers also check environment variables: ANTHROPIC_API_KEY, OPENAI_API_KEY (openai only), OPENAI_COMPATIBLE_API_KEY (openai-compatible only), GOOGLE_API_KEY. If the env var is set, the key is not needed in yg-secrets.yaml.

Precedence: yg-secrets.yaml over the environment. A tier's config.api_key in yg-secrets.yaml wins over the provider's environment variable, whatever the variable holds. That is why yg init never writes a key it read from the environment into yg-secrets.yaml, and why, whenever it points the tier at another provider or endpoint (--provider, or Configure reviewer in the menu), it removes the key stored for that tier and says so: left there, a key given for one provider would be sent to the next. It also removes it when the variable is exported or the key prompt is answered, so the key you chose is the one sent. Only a re-run for the same provider and endpoint, with nothing else chosen, keeps a stored key, and init then says the reviewer will send it (for a reviewer that sends no key, such as claude-code or ollama, it says the key is not used). A key typed in the menu is stored even when yg-secrets.yaml itself overrides the tier's provider or endpoint; it then goes to that override rather than to the reviewer it was typed for, and init warns about it. Nothing else in the file is touched.

Keep credentials out of yg-config.yaml: it is committed, so a key there reaches every clone and fork. Where the key sits and where it goes is your call, though — yg check never refuses a configuration over it and never withholds a key. It warns:

  • a config.api_key in yg-config.yaml is a config-committed-api-key warning (the key is never repeated in the message; it tells a key only in the working copy apart from one already in the history). The reviewer uses the key;
  • a yg-secrets.yaml tracked by git (force-added past its .gitignore entry) is a secrets-file-tracked warning. Its keys still work;
  • a tier that sends your key to an endpoint named only in the committed yg-config.yaml gets a reviewer-endpoint-committed warning: an anthropic, openai or google tier at any endpoint other than the provider's own (plain http included), whatever the key's source, and an openai-compatible tier whose key is a config.api_key (typically one stored in yg-secrets.yaml for an earlier reviewer). Whoever last changed the shared file decides where that key goes; the key is sent, and the warning shows where on every check. An endpoint you name for the tier in yg-secrets.yaml (config.endpoint, the same URL is fine) is your own choice and is not flagged.

Coverage config ​

yaml
coverage:
  required:
    - src/                  # files under src/ must be mapped — unmapped is a blocking error
  excluded:
    - vendor/               # files under vendor/ are silently ignored
    - "**/*.generated.ts"   # glob: generated files anywhere are ignored

Controls which coverage-visible files must be mapped to a node in yg check.

  • required — List of roots. Files under a required root that are not mapped to any node produce an unmapped-files error (blocks CI). Default: ["/"] (the whole repo — reproduces the previous always-map-everything behavior). An explicit empty list [] means require nothing — every uncovered file (outside excluded/nested) becomes a non-blocking uncovered-advisory warning and nothing blocks (the code's -advisory suffix means non-blocking; it is unrelated to the rule status advisory) (pure-advisory adoption: you still see the full uncovered surface, but CI stays green on coverage). The empty list only takes effect when written explicitly; omitting the whole coverage block keeps the ["/"] default. yg init writes an explicit required: [] into a fresh yg-config.yaml, so newly-initialized projects start in require-nothing mode (green from the first check) — add a root to required for each area whose unmapped files should block.

  • excluded — List of roots. Files under an excluded root are silently ignored regardless of other rules — this holds even when a MORE SPECIFIC required root also matches the same file; exclusion always wins once it matches at all. Exclusion is a global filter, not just a coverage-level rule: it cuts a matched path everywhere Yggdrasil looks, including a node's own mapping: entry naming that exact path. See below for the full reach of excluded.

  • type_level — Boolean, default false. Turns on type-level coverage: a file matched by exactly one classifying type's when predicate counts as covered by that type even though it has no node of its own. Committed config only — the gitignored yg-secrets.yaml overlay can never change it, since the flag changes what counts as covered/uncovered for yg check, and that must be the same for everyone working on the repo rather than something a local override can silently flip. Omitting the key (or the whole coverage block) leaves today's file-only coverage exactly as it is.

    With it on, every uncovered file is classified against every type that has a when predicate, and lands in exactly one outcome — one issue per file, the most-binding outcome wins, and none of the first four ever also shows up in the plain unmapped-files/uncovered-advisory listing (each already has its own, more specific verdict):

    • Exactly one type matches — covered. No issue at all.
    • Two or more types match, none of them enforce: strict — a new blocking error, ambiguous-node-type. It names every matching type and gives two ways to resolve it: create an explicit node declaring the intended type (its verdicts re-key under that node — nothing is lost), or narrow one type's when in yg-architecture.yaml so only one still matches (existing verdicts revalidate for free). This fires independent of the coverage roots — an ambiguous file blocks even if it sits outside every required root. (A file under an excluded root is exempt entirely — see below.) It is not independent of progressive mode, which is the one setting that moves it: with a reference branch named, an ambiguous file your change never touched is listed as a warning, and yg check --full blocks on it again. With no reference named — the default — it blocks unconditionally, as described.
    • A matching type has enforce: strict — left entirely to that type's own backward-scan error (type-strict-orphan or type-strict-misplaced; see Node types), never also reported as ambiguous. If the file also matches another, non-strict type, the strict error's message names it too.
    • A matching type's when could not be evaluated at all (for example, a content: predicate on a file over the 5MB scan limit) — a blocking file-unreadable error, the same code the strict backward scan uses for the same situation.
    • No type matches at all — the one outcome still reported through the ordinary unmapped-files/uncovered-advisory path, same as today, except the message now says plainly that the architecture has no type for the file — distinct from a file that matches a type but simply has no node.

    A file under an excluded root is skipped before any of this — never classified against a type, and never counted as node-owned or type-covered. With type_level on, it is still counted (into its own excluded bucket) and reported in yg check's summary line, exactly like a file the plain required/excluded roots already exclude — see the summary-line format below. This surfaced count stays behind the type_level flag; a project that has not turned it on never sees an excluded count anywhere yg check's header reports.

    A fresh yg init writes type_level: true into the new project's config (with a comment explaining it does nothing until a type declares when:) — existing projects keep the schema default of false until they opt in. With it on, yg check's summary line splits the covered files in two once a type covers any: N/M files covered (A node-owned · B type-covered) · C excluded. "node-owned" counts only files an actual node mapping covers. A file under an excluded root is neither covered nor left to cover — it is outside M and counted in its own segment, on or off — since being exempt from enforcement is not the same fact as a node or a type answering for the file. If it's on but no type declares when: anywhere in yg-architecture.yaml — true of every fresh project until its first classifying type is added — type-level coverage can never match a single file, so yg check prints a one-line notice on every run saying so rather than silently doing nothing.

    Being type-covered satisfies coverage; it does not by itself mean a rule actually runs. A rule attached to the matched type can be a per: node rule (scoped to the whole component, so there is nothing to run it against on a type-covered file, which has no component of its own), filtered off by its own when:, still draft, unreadable, excluded by its own scope.files filter, an id the architecture attaches with no matching rule definition, or — for a reviewer rule — matched to a binary file it can never review as prose. yg check --coverage reports both halves per matched type: which rules actually enforce, and which are attached but do not, each with the reason and how many files it affects (plain yg check leaves that listing out — it is the answer to a question asked when the type map changes, not on every run). A rule can be enforced (a real pair exists) and still never produce a verdict on a type-covered file, when its check.mjs itself needs ctx.node/ctx.graph or reads beyond what the architecture lets the type reach — that failure is only ever observable by actually running the check, so only yg check --approve can ever name the SPECIFIC reason, in its own post-fill report ("cannot run — …" beside the enforced count, in place of the plain "unverified" every other not-yet-filled pair gets); none of plain yg check, yg context --file, yg owner --file, yg tree, or the portal ever run the check themselves, so none of them can ever say why — but each still says whether, from a real re-verification of the lock, not merely whether an entry happens to exist, and all five agree exactly: none reads a stale entry as clean just because the lock holds some entry for it. Plain yg check already recomputes every pair's input hash, so its own qualified "unverified" count already covers both a pair the lock has never recorded and one whose recorded verdict has gone stale since a source edit (the file, the aspect, or the rule itself changed since the verdict was written). yg owner --file and yg context --file perform that identical re-verification too, scoped to just the one file asked about — cheap on top of the whole-project pair walk both already run to classify that file, never a second one — so a stale entry reads there exactly the way it does in yg check's own count: "(N of M rules unverified — no valid verdict is currently on record for it)" for yg owner --file, [enforced, unverified] per rule for yg context --file. yg tree and the portal answer for every type-covered file in the project rather than one, so the identical re-verification is a different cost each absorbs its own way: the portal already runs a full lock verification for its other counts and reads the result for type-covered files' pairs straight out of it, at no added cost; yg tree pays a bounded, dedicated re-verification of the pairs of the project's type-covered files (comfortably under yg check's own full-project pass) rather than settle for a cheaper presence check that could call a stale pair clean. Both append the identical "with no recorded verdict" wording for the same fact the two file-scoped commands report — on both the chip and the per-file row for the portal, and on yg tree's own summary line — whether that pair's verdict is missing entirely or present but stale. A rule whose effective status is advisory (it runs, but only warns) is named under its own heading, never folded in with the rules that actually block — a heading that says "enforced" never covers one that merely warns. When a rule is grouped (a bundle whose implied rules split across a per: file and a per: node half), the block names the split explicitly rather than letting the per: node half look silently absent. It also names, once, where a type's implicit parent chain stops (a fork between two parents, a cycle, or nothing above it — an omitted parents: and an explicit parents: [] are indistinguishable by the time a file reaches this report, so the wording never claims to know which one the author wrote). Because none of this stops a file from satisfying coverage, a repo can have files that are green with genuinely nothing checking them; yg check --coverage names every one of them plainly, with samples, rather than leaving that discoverable only by accident. yg context --file <path> and yg owner --file <path> give the same honest answer for one such file directly, in place of a plain "not covered" — the matched type, the chain, both the rules that apply (with their real status) and the ones that do not, and — when literally nothing applies — say so plainly rather than staying silent or claiming enforcement that isn't there. All five surfaces — yg check, yg context --file, yg owner --file, yg tree, and the portal — also agree on the one case "no rule applies" cannot cover: when the matched type's rules could not be worked out at all because an aspect implies cycle reaches it, each says so and names the cycle, instead of reporting the file as satisfying coverage with zero enforcement. yg context --file and yg owner --file report it as an error for the one file asked about; yg check --coverage's per-type block and its repo-wide zero-enforcement line both keep such a file out of "nothing applies" and name the cycle in their own section instead — the same structural fault yg check also reports, and blocks on, separately, as its usual aspect-implies-cycle error. The portal keeps such a file out of both its "satisfied" and "no rule applies" residue lines and lists it under its own "could not be worked out" line, naming the cycle, in the Coverage & Audit ledger and the Overview residue.

  • The navigation surfaces know about type-covered files too, not only yg check. yg tree prints a summary line, after the node listing, splitting the total of type-covered files the same three ways the portal's own residue lines do: how many files are checked by at least one applicable rule, how many matched a type with nothing that applies, and — only when it occurs — how many had their matched type's rules blocked from ever being worked out by an aspect implies cycle (never a synthetic tree entry — the node listing above it stays nodes-only); the count is repo-wide even under --root, since a type-covered file has no place in the hierarchy for that flag to scope it to. yg find indexes a type-covered file with its matched type's own description as searchable text; when that file is the top-ranked result, yg find's one terminal next: line points at yg context --file, never a phantom --node target — when a node or an aspect outranks it instead, that higher-ranked entry's own next step is shown there instead. yg structure widens its dependency universe with every statically-resolved import touching a type-covered file (named by the file's own path) and says "component or type-covered file" rather than misnaming a file a component. The portal's Overview and Coverage & audit views never call a type-covered file "unmapped" — one with an applicable rule is accounted for on its own line, distinct from the residue of files nothing checks at all; one whose matched type enforces nothing is not folded into that same line either — it gets its own, honestly-labeled line naming the file by path and type, using the same treatment as any other file nothing checks; and one whose matched type's rules an aspect implies cycle stopped from ever being resolved gets a third line of its own, reported as unknown rather than folded into either. The portal's Dependency-structure panel widens the same way yg structure does. Three of these stay exactly as they are today when the flag is off: yg tree's summary line disappears entirely, and yg find, yg structure, and the portal's type-covered accounting lines behave as they do for any file type-level coverage never touches. The one exception is the portal's excluded-file handling: coverage.excluded was never conditioned on type_level, so a file under an excluded root is listed under its own "deliberately excluded from coverage, never enforced" block whether or not type-level coverage is on — but that file was already outside the "source files unmapped (unguarded)" count before this change, so a project that sets coverage.excluded without ever turning type-level coverage on does not see that count move. What it does see: the residue CSV export's row for that file changes kind from uncovered-file to excluded-file, and the JSON export moves the file's path out of uncoveredFiles and into its own excludedFiles list.

  • Roots accept the same forms as a node mapping: entry — an exact file, a directory prefix (e.g. src/), or a minimatch glob (* within a path segment, ** across segments). As in a mapping entry, a root is a glob only when it contains a *; without one, ?, [ ] and { } are literal. So excluded: ["**/*.generated.ts"] ignores generated files anywhere, and required: ["services/*/api/**"] scopes the blocking (required) coverage level to a pattern. / still means the whole repo.

  • The files Yggdrasil maintains at your repo root count like any other. yg init writes and keeps up to date whichever of AGENTS.md, CLAUDE.md and .clinerules/yggdrasil.md your rules_artifacts block leaves switched on — all three by default — plus a .gitattributes entry. Under a whole-repo required (including the absent-block default) they are unmapped files like any other, so they become blocking errors the moment yg init --upgrade adds them to an existing project. A fresh yg init avoids this by writing them into coverage.excluded itself. They are repository plumbing rather than project source — the usual answer is to exclude them (yg init --upgrade prints this stanza whenever it applies to your project, built from the artifacts your project actually carries, and never edits the file itself), though mapping them to a node works equally well:

    yaml
    coverage:
      excluded:
        - AGENTS.md
        - CLAUDE.md
        - .clinerules/yggdrasil.md
        - .gitattributes

    A project that has switched an artifact off (rules_artifacts: { clinerules: false }, say) never gets that file written, so leave its line out — the stanza should name only the artifacts your project actually has.

  • Files that match neither a required nor an excluded root produce a non-blocking uncovered-advisory warning.

  • excluded is a supreme, global filter — one rule, no seam between how a mapping reaches a file. A path it matches is gone everywhere Yggdrasil looks: the repo-walking coverage checks, expected review pairs, a node's source fingerprint, the dependency-conformance pass (both its per-node file enumeration AND its resolution of an import's target file back to an owner — an import reaching into an excluded subtree is as silent as one reaching out of it), type-when classification (including the strict backward scan a type's enforce: strict runs), the mapping-overlap check, the suppression/audit universe (yg suppressions, yg advise), the portal, and the files/allowances a rule's review actually runs against — ctx.fs, the parsers, and companion resolution all refuse a path there too, including one reached through a symlink that resolves into an excluded location. yg context --file, yg owner --file, yg impact --file, yg aspect-test --file, and yg type-suggest --file agree: an excluded file is never reported as owned by, or bound to satisfy the rules of, any node, and editing it never carries a re-verification cost. This applies identically whether a directory or glob mapping entry merely swept the file in, or a node's own mapping: entry names that exact path — an explicit claim does not outrank an exclusion. It is unaffected by whether .gitignore hides anything: exclusion is read off the adopter's coverage.excluded config and the filesystem, not off any particular mapping's own file listing.

    The excluded set has two sources. The adopter's own coverage.excluded config is one. The other is default membership: a subtree that is its own separate project is a member of the excluded set whether or not any coverage.excluded line mentions it. "Its own separate project" means a subtree that carries its own nested .yggdrasil/ graph, or its own .git — a fully independent checkout, a submodule, or a linked git worktree; membership is read off the real filesystem, not guessed from a name, so an ordinary subdirectory with no graph or git checkout of its own — including a dependency directory such as node_modules or vendor — is unaffected and still gets covered normally. Excluding a dependency directory by name (rather than because it happens to carry its own git checkout) is the adopter's own coverage.excluded config, the first source above.

    A mapping entry that resolves to nothing because every file it would have reached is excluded must say so, naming exclusion as the reason: file-mapping-excluded fires for an entry that names one file exactly and that file is excluded, and the aggregate mapping-path-missing check fires when a node's mapping entries, taken as a whole, resolve to nothing this node can enforce because every file they reached is excluded — the same code a stale glob or a deleted file produces, since a node with real, non-empty mapping: and zero enforceable files needs to say so somewhere regardless of why the files vanished. A glob or exact entry's own per-entry existence check stays silent when the path resolves to real, on-disk content that happens to be excluded — that content is not stale or broken, so blaming it there would be wrong; the aggregate check is what reports the "nothing left to enforce" fact.

A file matching ANY excluded root is dropped entirely, before it is ever sorted into the blocking or non-blocking coverage level — exclusion is absolute, independent of whether a required root also matches it and independent of how specific either root is. Among the files that match no excluded root, any matching required root puts the file in the blocking level. A required root fully contained inside an excluded root can therefore never match anything; yg check warns (coverage-required-shadowed) when both roots are plain (non-glob) paths.

An empty required list means no file can ever fail coverage. That is the shipped default — yg init writes required: [], and so does a mined proposal — and it is deliberate: a brownfield repository is green from its first check. Its consequence would be easy to miss, because the uncovered files are listed either way and only their severity differs. So the report says it where the files are listed: the heading of the uncovered block names the reason they never block.

text
warning[uncovered] 13 files belong to no node — not under coverage.required, so they never block

The block is a warning, never an error, and it disappears the moment nothing is left uncovered; name a path under coverage.required in .yggdrasil/yg-config.yaml and files under it report as error[unmapped] instead, blocking until a component owns them.


Prompt-size gate ​

A tier's optional max_prompt_chars caps the length of the prompt the reviewer assembles for each pair. The prompt for each reviewer pair is composed of: the rule text (content.md), any static reference files, the unit's subject files, and — when the aspect ships a companion.mjs — any companion files the hook resolved for that unit. All of these count toward the limit.

yg check measures the assembled prompt for every expected reviewer pair and reports prompt-too-large — a blocking error — when it exceeds the resolved tier's limit. The check is deterministic and costs nothing; script pairs have no prompt and are never subject to it.

yaml
reviewer:
  tiers:
    standard:
      provider: anthropic
      consensus: 1
      max_prompt_chars: 50000
      config: { model: claude-haiku-4-5, temperature: 0 }

When a pair trips the gate, the remedies in safety order are:

  1. Narrow scope.files on the aspect so non-target payload (fixtures, generated files) drops out of the subject set.
  2. Switch the aspect to per: file — only if the rule is file-local; a per-file reviewer cannot judge a cross-file rule.
  3. Split the node into children.
  4. Raise the limit on the tier. max_prompt_chars is not a verdict input, so raising it re-verifies nothing: the blocked pair is reviewed on the next yg check --approve and every recorded verdict stays. Keep it inside the model's context window. Moving the aspect to a different, higher-limit tier is the costly variant: the tier name is part of each pair's hash, so the move re-reviews every pair of that aspect.

max_prompt_chars is a gate, not a verdict input: lowering it can make an already-verified pair trip the gate without invalidating its recorded verdict.


Quality config ​

yaml
quality:
  max_direct_relations: 10        # Max out-edges per node (high-fan-out warning)

max_direct_relations fires a warning when a node's outgoing relation count exceeds it — a signal that the node may be doing too much. It is the only quality threshold.

Per-node reviewed-seam override ​

A node may declare its own justified ceiling in its yg-node.yaml, which sets that node's own limit, replacing the global default for it:

yaml
max_direct_relations:
  limit: 21
  reason: "Single auditable gateway that concentrates this coupling by design."

The declared limit sets the ceiling for that node only — it may be higher than the global (a genuine single-responsibility seam — one auditable gateway or orchestrator that concentrates coupling by design, where splitting would defeat the architecture) or lower (holding this node to a stricter budget than the rest of the repo). Only the safe direction adds warnings: a limit below the global makes the node stricter. The global default still governs every other node, and this node still warns if it exceeds the number it declares here — so the allowance sanctions a specific, reviewed count rather than loosening the check globally. Both fields are required; a partial or malformed override — including a limit below 1 — is ignored and the strict global default applies. It is a check parameter only — never a verification input — so declaring it re-verifies nothing. The number and its justification are surfaced by yg context --node and yg schemas read node, keeping the exception explicit and auditable in the graph.


Local state — what never gets committed ​

Everything Yggdrasil derives locally lives under .yggdrasil/ and is kept out of git by a single .yggdrasil/.gitignore that yg init writes and every yg init --upgrade tops up (missing lines are appended; lines already there, and any of your own, are left alone). There is no repo-root entry to maintain separately.

EntryWhat it is
yg-secrets.yamlYour local overlay — provider keys and machine-specific tier overrides.
.symbols-cache/A retired predecessor of the cache below. Nothing writes it any more; the entry stays so leftovers in an older checkout keep being ignored.
.ast-cache/The relation pass's content-addressed per-file parse cache — the live one.
.type-class-cache/Type-level coverage's per-file classification cache, keyed by a file's own path together with its raw byte content and the architecture's classifying types — skips re-evaluating a file's classifying-type predicates when none of its path, its content, or the architecture's classifying types have changed.
.debug.logThe opt-in command log written when debug: true.
.yg-lock.deterministic.jsonThe script-rule verdict cache — rebuilt free and keyless by yg check --approve --only-deterministic.
.yg-events.jsonl*The verdict-events telemetry sidecar (see Verdict-events sidecar). The trailing * also covers its .1 rotation.
.refused/The refused-content store: for each refusal a fill records, the subject files as they were and the reason, in <hash>.json named for the verdict's hash (see Refused-content store). A fill writes it only where this line is present — yg init --upgrade adds it — and nothing reads it back.
.yg-fill-divergence.log*Forensic evidence, written only when a single run disagrees with itself because something outside Yggdrasil rewrote a tracked file mid-run (see Running in parallel). The trailing * also covers its .1 rotation.
.feature-field.jsonThe silent structural-deviation index behind the structural-attention hint.
.family-candidates.jsonLook-alike group analysis (the file name keeps an older term) in the shared file earlier releases wrote; rebuilt by rerunning the producer.
.family-candidates.*.jsonLook-alike group analysis, one file per producer (.family-candidates.<producer>.json); freshness-gated when read and rebuilt by rerunning the producer.
.yg-packages-versions.jsonWhat each installed package's source was last seen to publish — a local cache the yg pack commands write while they are already talking to a source.
.yg-*.lockThe run-exclusion lock files — .yg-approve.lock, held for the length of yg check --approve, and .yg-log.lock, held for the moment a log entry is written (see Running in parallel). They exist only while a command runs; yg init --upgrade adds this line to an older graph's .gitignore.
*.tmpAn atomic write's half-finished temp file, orphaned by a hard kill. yg check sweeps stale ones on startup; this keeps one from showing up as untracked noise before that. The same line covers what the package commands leave while they run: pack-fetch-<hex>.tmp/ (a clone of a marketplace, removed when the command ends and swept by the next pack command after a hard kill) and pack-command.lock.tmp (the lock that lets one pack command change the package record at a time).

Every one of them is rebuildable, so a fresh clone missing all of them is a normal state, not a broken one. The only thing a fresh clone notices is the absent verdict cache: those pairs read as unverified until yg check --approve --only-deterministic rematerializes them.

The committed side is the graph itself — yg-config.yaml, yg-architecture.yaml, the model/, aspects/ and flows/ trees, the two committed lock files, the incident ledger, the attention-decision record (advise-decisions.jsonl), the imported attention record (advise-imported.jsonl), (when opted in) the shared events stream, and — in a repository that installs packages — the package record yg-packages.yaml with the copies under aspects/packages/. The package record says what is installed: for each package its source, its install directory, the version, what was asked for (a pinned version or latest), the tag and commit it was taken from, and what every copied file hashed to. It is not a verdict lock; see Packages.


Upgrading ​

bash
yg init --upgrade

Lifts the graph's config version to the current one and refreshes the agent-rules files this project carries (the AGENTS.md digest block, the CLAUDE.md import, and .clinerules/yggdrasil.md, minus anything rules_artifacts switches off) to the installed CLI's current content — no flag says which agent to write for, since the rules are identical in every artifact; the only choice is which files carry them, and --no-agents-md / --no-claude-md / --no-clinerules record that in the config rather than having to be repeated. It also sweeps away any file a retired per-platform installer left behind from an older CLI. (Upgrading a pre-5.1.0 graph also removes the now-retired on-disk schemas/ directory; schemas are read with yg schemas read <name> instead.)

The legacy single-section reviewer format (flat provider keys + reviewer.active) is not migrated — the upgrade leaves the reviewer: block untouched, so a config still in that shape then fails yg check with a config-reviewer-unknown-key error on active. Convert it to reviewer.tiers by hand (see the tier fields above). The upgrade also removes fields earlier releases read and this one refuses, naming each one it removes: quality.max_node_chars and quality.max_mapping_source_files, a tier's config.references: size caps, config.max_tokens and config.context_length_field, a node's or node type's sizeExempt:, a relation's failure:, and a rule's language:, stability:, anchors: or id:. Until 6.1.0 these were ignored without a word; now yg check refuses any of them the upgrade has not removed, and any other key the schema does not know (with the key it is probably a typo of) — those are yours to fix. The files are edited in place, keeping every other key and comment, but the writer may normalize indentation. Run from the repository root only. Review the diff before committing.


Auto-approve config ​

auto_approve controls what bare yg check does when you run it without explicit --approve, --no-approve, or --only-deterministic flags.

ValueBehavior
false (default)Read-only: recomputes hashes, validates, reports. Writes no verdict and no committed file, makes no LLM calls, needs no keys; it does maintain its own gitignored caches under .yggdrasil/ (and sweeps stale *.tmp files).
deterministicBehaves like yg check --approve --only-deterministic — fills only script pairs (free, keyless), writes the gitignored cache.
fullBehaves like yg check --approve — fills the unverified pairs that run answers for, reviewer pairs included.

Precedence: explicit CLI flags always override auto_approve. Passing --approve, --no-approve, or --only-deterministic on the command line takes effect regardless of what the config says.

CI note: CI and pre-commit scripts should always use explicit flags — the cache rebuild yg check --approve --only-deterministic, then the gate yg check --no-approve. The CI-is-free-and-keyless guarantee is about explicit flag use, not auto_approve. Set auto_approve for local developer convenience only.

Under CI, full is held back. yg-config.yaml is committed, so one developer's auto_approve: full reaches every pipeline. When the CI environment variable is set (any value except empty, 0 or false; GitHub Actions, GitLab CI and most runners set it), a bare yg check ignores auto_approve: full, stays read-only, calls no reviewer, and prints auto-approve: full ignored — CI is set on stderr. An explicit --approve still fills. auto_approve: deterministic is not held back: that fill is free, keyless, and the same as the recommended cache-rebuild step.

When a fill triggered by auto_approve produces a PASS, the result line says · auto-filled after the finding counts (yg check: PASS 2 warnings · auto-filled …) to indicate that verdicts were written during this run.

yaml
# .yggdrasil/yg-config.yaml
auto_approve: deterministic   # fill the script-rule cache automatically on bare yg check

Progressive mode ​

progressive names a branch your changes are measured against. It is absent by default, and absent means off: every run answers for the whole project, exactly as it always has.

KeyTypeDefaultEffect
referencestring (non-blank)noneThe branch or ref changes are measured against, e.g. origin/main.
yaml
# .yggdrasil/yg-config.yaml
progressive:
  reference: origin/main

With it set, a plain yg check blocks only on the obligations your change reaches. Everything inherited from that branch is still listed and still counted, as a warning that does not fail the build, and the header says how much of it there is. yg check --full answers for the whole project instead.

Three things about the key itself:

  • It is read from the committed file only. A yg-secrets.yaml overlay can neither introduce nor re-point it, so how much of the project a run answers for is the same for everyone on the branch.
  • The block must name reference, and nothing else. A misspelled key, a blank value, or a block that names nothing at all (progressive: {}) is a hard config-progressive-unknown-key / config-invalid error rather than a silent no-op — the alternative would leave you reading a config that says the mode is on while every run behaved as if it were off.
  • Where the measurement cannot be made honestly — the named branch is unknown locally, the clone is too shallow to share history with it, the graph does not sit at the repository root, a submodule pointer moved, or the verdict record committed at the reference cannot be read — the run answers for the whole project and says which of those it hit and what to do about it.

The full picture, including what decides whether a finding is yours and the two CI legs this needs: Progressive mode.


Signals ​

signals is an optional section that turns attention-layer hints on or off. It is absent by default, which leaves every signal at its default.

KeyTypeDefaultEffect
attentionbooleantrueThe advisory "structurally unusual" note in yg context --file. Set false to silence it.
yaml
# .yggdrasil/yg-config.yaml
signals:
  attention: false   # silence the "structurally unusual" note in yg context --file

The note is purely advisory: it never blocks a check and never changes any verification result whether it is on or off. attention must be a boolean, and signals accepts no other key — a misspelled key is rejected so a typo can't silently leave the note enabled. See Structural attention for what the note means and its honest limits.


Events ​

events is an optional section that controls where reviewer verification events are recorded. It is absent by default, which keeps every event in a local, gitignored file (see Verdict-events sidecar).

KeyTypeDefaultEffect
committed_llmbooleanfalseOpt into a committed, team-shared record of reviewer verification events.
yaml
# .yggdrasil/yg-config.yaml
events:
  committed_llm: true   # commit + share reviewer verification events (default off)

When committed_llm is on, each reviewer verification event is appended to a committed file, .yggdrasil/yg-events.llm.jsonl, instead of the local sidecar — a single home per event, so nothing is double-counted. The committed file is:

  • Reviewer rules only. Script checks, drill runs, and diagnostic runs stay local, so a free, keyless CI run (yg check --approve --only-deterministic) adds nothing to it — zero churn.
  • Union-merged. yg init marks it merge=union in .gitattributes, so events appended on different branches combine on merge instead of conflicting.
  • Sealed by month. When the first event of a new month is appended, the file is renamed to .yggdrasil/yg-events.llm.<YYYY-MM>.jsonl, named for the month its first line was written in, and a new yg-events.llm.jsonl begins. The file every fill appends to therefore holds about one month; the sealed months stay committed, and every reader reads them, oldest first, so no history is lost. yg init marks all of these files linguist-generated, so a review diff collapses them the way it does the lock.
  • Rationale-stripped. The refusal reason is omitted from the shared copy (it can carry code fragments); the local copy keeps it.

Turning the opt-in on or off never changes any verification result or its hash — it invalidates nothing. committed_llm must be a boolean, and events accepts no other key — a misspelled key is rejected so a typo can't silently leave the shared record disabled. A machine on an older CLI writes only locally and does not contribute to the shared file, so a reader that combines the two says as much rather than treating the committed record as complete.


Type law ​

type_law decides whether law that reaches a whole node type must be admitted by the user before it blocks. A rule a node type lists (or one such a rule implies) governs every file of that type, so it is shared vocabulary: the agent proposes it at status: advisory, and the user admits it with yg log add --aspect <id> --ratify --by '<who>' (details).

KeyTypeDefaultEffect
ratificationbooleanfalsetrue: a rule standing enforced on a node type without a ratification of its current version in its own log is a blocking type-law-unratified error. false or absent: no ratification is asked for and nothing is reported.
yaml
# .yggdrasil/yg-config.yaml
type_law:
  ratification: true

A new project starts with it on: yg init writes the block above. An existing project takes it up with yg init --upgrade, once: when yg-config.yaml says nothing about type_law, the upgrade writes one entry into the log of every rule that stands enforced on a node type at that moment, recording it as the law the graph already had (the entry says so, and names the upgrade rather than a person), and then writes ratification: true. Until then nothing is asked for, so upgrading never turns a build red over law nobody changed, and running the upgrade again never records a rule written since: once the key is there, the upgrade leaves it and the logs alone. If a rule's log refuses the entry, the upgrade leaves the setting off and says which log to fix. A configuration that already sets ratification: false is your answer, and the upgrade keeps it.

The setting is read from the committed yg-config.yaml only: whether law must be admitted is decided for the whole team, so a local yg-secrets.yaml can neither turn it on nor off. ratification must be a boolean, and type_law accepts no other key.


Rules artifacts ​

rules_artifacts names which of the three agent-rules files this repository wants Yggdrasil to write and to keep in sync. It is absent by default, and an absent block — or an absent key inside one — means true: the file is installed by yg init and its drift is reported by yg check's rules-digest-stale warning. The block only ever turns an artifact off.

KeyArtifactDefault
agents_mdThe AGENTS.md digest block.true
claude_mdThe @AGENTS.md import line in CLAUDE.md.true
clinerulesThe standalone Cline-native copy, .clinerules/yggdrasil.md.true
yaml
# .yggdrasil/yg-config.yaml
rules_artifacts:
  clinerules: false   # no Cline on this team — stop writing and checking .clinerules/yggdrasil.md

The rules themselves are identical in all three — the only question is which files an agent will find them in. A project whose team uses no Cline can set clinerules: false and stop being told .clinerules/yggdrasil.md is missing on every run; yg init --upgrade stops writing it in the same breath, and the gate keeps reporting the artifacts still switched on exactly as before.

Set it by hand, or let yg init --no-agents-md / --no-claude-md / --no-clinerules write it for you (see the CLI reference). There is no flag that turns one back on — that is an edit to this block, where the decision lives.

Three rules about the block itself:

  • It is read from the committed file only. A yg-secrets.yaml overlay can never change which files the repository carries, since one machine writing a file every other machine does not is exactly the drift this key exists to end.
  • Values must be booleans, and no other key is accepted. An unknown sibling is a hard config-rules-artifacts-unknown-key error and a non-boolean value a config-invalid one — so a typo (clinrules: false) cannot leave you believing an artifact was switched off while yg check goes on asking for it.
  • claude_md may not stay on while agents_md is off (config-rules-artifacts-orphan-import). The whole content of the CLAUDE.md artifact is an import of AGENTS.md, so that combination asks for a pointer to a file Yggdrasil no longer maintains. --no-agents-md switches claude_md off with it.

Switching an artifact off never deletes a file already on disk. yg init names it — still there from an earlier install, now maintained by nobody — and leaves it; delete it by hand when you mean to.


Field reference ​

yg-config.yaml ​

Generated from the schema the parser enforces — the same table yg schemas read config prints.

KeyTypeRequiredMeaning
versionstringyesThe graph schema version, a quoted three-part string managed by yg init --upgrade. A malformed value is ignored: a version that is not a string is refused before the file is parsed, by every command that loads the graph and by yg init --upgrade, each with its own message.
qualitymappingnoQuality thresholds.
quality.max_direct_relationsinteger ≥ 1noThe relation count above which a node gets the high-fan-out warning. Default: 10.
reviewermappingonce a reviewer rule is in effectThe reviewer tiers reviewer rules are judged by.
reviewer.defaultstringyes, with more than one tierThe tier a reviewer rule without reviewer.tier uses.
reviewer.tiersmapping of <tier> to mappingyes, at least oneNamed reviewer configurations; a tier name starts with a letter and default is reserved.
reviewer.tiers.<tier>.providerollama | openai | anthropic | google | openai-compatible | claude-code | codex | gemini-cli | copilot-cliyesWhich reviewer the tier calls.
reviewer.tiers.<tier>.consensusinteger ≥ 1yes1 for a single call, or an odd number for a majority vote.
reviewer.tiers.<tier>.configmappingyesThe provider settings.
reviewer.tiers.<tier>.config.modelstringyes, except for claude-code, codex and gemini-cliThe provider's model identifier; when it is absent, claude-code uses sonnet, codex uses o4-mini, gemini-cli uses gemini-2.5-flash.
reviewer.tiers.<tier>.config.endpointstringyes, for openai-compatibleThe API endpoint URL (ollama defaults to http://localhost:11434).
reviewer.tiers.<tier>.config.temperaturenumber ≥ 0noSampling temperature; the CLI providers ignore it. Default: 0.
reviewer.tiers.<tier>.config.timeoutnumbernoPer-call timeout in seconds, a positive number. Default: 300 for the CLI providers and ollama, 60 for the hosted APIs.
reviewer.tiers.<tier>.config.api_keystringnoThe provider API key; put it in yg-secrets.yaml, never in the committed file.
reviewer.tiers.<tier>.max_prompt_charsinteger ≥ 1noThe longest prompt a reviewer pair on this tier may send (prompt-too-large above it). Default: 50000.
parallelinteger ≥ 1noReviewer-rule pairs reviewed at once. Default: 1.
debugbooleannoAppend all CLI output to .yggdrasil/.debug.log. Default: false.
auto_approvefalse or deterministic | fullnoWhat a bare yg check fills: false nothing, deterministic the script pairs, full every pair (held back under CI). Default: false.
signalsmappingnoAttention-layer switches.
signals.attentionbooleannoThe advisory "structurally unusual" note in yg context --file. Default: true.
eventsmappingnoThe committed-events opt-in.
events.committed_llmbooleannoKeep a committed, shared record of reviewer verification events. Default: false.
coveragemappingnoWhich files must be mapped to a node.
coverage.requiredlist of stringnoRepository-relative roots every file under which must be covered. Default: ["/"].
coverage.excludedlist of stringnoRoots removed from every check, not only from coverage: an excluded file gets no coverage finding, no review pair, no type classification and no rule read, even when a node's mapping names it. A subtree with its own nested .yggdrasil/ or its own .git is excluded the same way. Default: [].
coverage.type_levelbooleannoEnforce per: file rules of classifying types on files no node maps. Read from the committed file only. Default: false.
progressivemappingnoProgressive mode: block only on what a change reaches. Absent means off.
progressive.referencestringyes, when progressive: is presentThe branch a change is measured against (e.g. origin/main). Read from the committed file only.
rules_artifactsmappingnoWhich agent-rules artifacts are written and kept in sync. Read from the committed file only.
rules_artifacts.agents_mdbooleannoWrite and check the Yggdrasil block in AGENTS.md. Default: true.
rules_artifacts.claude_mdbooleannoWrite and check the @AGENTS.md import in CLAUDE.md (refused while agents_md is off). Default: true.
rules_artifacts.clinerulesbooleannoWrite and check .clinerules/yggdrasil.md. Default: true.
type_lawmappingnoWhether law that reaches a whole node type must be admitted before it blocks. Read from the committed file only.
type_law.ratificationbooleannoA rule enforced on a node type must carry a ratification of its current version in its own log, or type-law-unratified blocks; false or absent asks for none. Read from the committed file only. Default: false.

Retired keys, refused by name with what became of each (yg init --upgrade removes them): quality.max_node_chars (removed in 5.0.0 with the per-node character budget; the per-tier max_prompt_chars cap replaced it); quality.max_mapping_source_files (removed in 5.0.0 with the wide-node warning); reviewer.tiers.<tier>.config.max_tokens (removed in 5.0.0; the reviewer no longer caps its reply); reviewer.tiers.<tier>.config.context_length_field (never read by any release since 5.0.0); reviewer.tiers.<tier>.config.references (removed in 5.0.0 with the per-tier reference size caps; the per-tier max_prompt_chars cap replaced them). Any other key is refused by name, with the key it is probably a typo of.

yg-secrets.yaml ​

Generated from the schema the parser enforces — the same table yg schemas read secrets prints.

KeyTypeRequiredMeaning
versionstringnoThe graph schema version, a quoted three-part string managed by yg init --upgrade. A malformed value is ignored: a version that is not a string is refused before the file is parsed, by every command that loads the graph and by yg init --upgrade, each with its own message.
qualitymappingnoQuality thresholds.
quality.max_direct_relationsinteger ≥ 1noThe relation count above which a node gets the high-fan-out warning. Default: 10.
reviewermappingnoThe reviewer tiers reviewer rules are judged by.
reviewer.defaultstringnoThe tier a reviewer rule without reviewer.tier uses.
reviewer.tiersmapping of <tier> to mappingnoNamed reviewer configurations; a tier name starts with a letter and default is reserved.
reviewer.tiers.<tier>.providerollama | openai | anthropic | google | openai-compatible | claude-code | codex | gemini-cli | copilot-clinoWhich reviewer the tier calls.
reviewer.tiers.<tier>.consensusinteger ≥ 1no1 for a single call, or an odd number for a majority vote.
reviewer.tiers.<tier>.configmappingnoThe provider settings.
reviewer.tiers.<tier>.config.modelstringnoThe provider's model identifier; when it is absent, claude-code uses sonnet, codex uses o4-mini, gemini-cli uses gemini-2.5-flash.
reviewer.tiers.<tier>.config.endpointstringnoThe API endpoint URL (ollama defaults to http://localhost:11434).
reviewer.tiers.<tier>.config.temperaturenumber ≥ 0noSampling temperature; the CLI providers ignore it. Default: 0.
reviewer.tiers.<tier>.config.timeoutnumbernoPer-call timeout in seconds, a positive number. Default: 300 for the CLI providers and ollama, 60 for the hosted APIs.
reviewer.tiers.<tier>.config.api_keystringnoThe provider API key; put it in yg-secrets.yaml, never in the committed file.
reviewer.tiers.<tier>.max_prompt_charsinteger ≥ 1noThe longest prompt a reviewer pair on this tier may send (prompt-too-large above it). Default: 50000.
parallelinteger ≥ 1noReviewer-rule pairs reviewed at once. Default: 1.
debugbooleannoAppend all CLI output to .yggdrasil/.debug.log. Default: false.
auto_approvefalse or deterministic | fullnoWhat a bare yg check fills: false nothing, deterministic the script pairs, full every pair (held back under CI). Default: false.
signalsmappingnoAttention-layer switches.
signals.attentionbooleannoThe advisory "structurally unusual" note in yg context --file. Default: true.
eventsmappingnoThe committed-events opt-in.
events.committed_llmbooleannoKeep a committed, shared record of reviewer verification events. Default: false.
coveragemappingnoWhich files must be mapped to a node.
coverage.requiredlist of stringnoRepository-relative roots every file under which must be covered. Default: ["/"].
coverage.excludedlist of stringnoRoots removed from every check, not only from coverage: an excluded file gets no coverage finding, no review pair, no type classification and no rule read, even when a node's mapping names it. A subtree with its own nested .yggdrasil/ or its own .git is excluded the same way. Default: [].
coverage.type_levelbooleannoEnforce per: file rules of classifying types on files no node maps. Read from the committed file only. Default: false.
progressivemappingnoProgressive mode: block only on what a change reaches. Absent means off.
progressive.referencestringnoThe branch a change is measured against (e.g. origin/main). Read from the committed file only.
rules_artifactsmappingnoWhich agent-rules artifacts are written and kept in sync. Read from the committed file only.
rules_artifacts.agents_mdbooleannoWrite and check the Yggdrasil block in AGENTS.md. Default: true.
rules_artifacts.claude_mdbooleannoWrite and check the @AGENTS.md import in CLAUDE.md (refused while agents_md is off). Default: true.
rules_artifacts.clinerulesbooleannoWrite and check .clinerules/yggdrasil.md. Default: true.
type_lawmappingnoWhether law that reaches a whole node type must be admitted before it blocks. Read from the committed file only.
type_law.ratificationbooleannoA rule enforced on a node type must carry a ratification of its current version in its own log, or type-law-unratified blocks; false or absent asks for none. Read from the committed file only. Default: false.

Retired keys, refused by name with what became of each (yg init --upgrade removes them): quality.max_node_chars (removed in 5.0.0 with the per-node character budget; the per-tier max_prompt_chars cap replaced it); quality.max_mapping_source_files (removed in 5.0.0 with the wide-node warning); reviewer.tiers.<tier>.config.max_tokens (removed in 5.0.0; the reviewer no longer caps its reply); reviewer.tiers.<tier>.config.context_length_field (never read by any release since 5.0.0); reviewer.tiers.<tier>.config.references (removed in 5.0.0 with the per-tier reference size caps; the per-tier max_prompt_chars cap replaced them). Any other key is refused by name, with the key it is probably a typo of.


Notes ​

  • yg-node.yaml is a reserved filename in model directories.
  • Node types are defined in yg-architecture.yaml, not yg-config.yaml.