DivineSWE Configuration Reference
After initialization, edit .divineswe/config.json to customize behavior. The full field reference is documented here.
Full Config Example
{ "version": "1.0.0", "project_name": "your-project", "model_preferences": { "high": "opus", "medium": "sonnet", "low": "haiku" }, "model_authority": "prophet", "worktree_isolation": true, "ledger_enabled": true, "scrolls_enabled": true, "max_parallel_agents": 4, "specs_dir": "docs/specs", "books_dir": ".divineswe/books", "default_branch": "master", "custom_instructions": ""}Field Reference
version
Schema version of the config file. Current value: "1.0.0". This is the config-FORMAT version, deliberately separate from the plugin release version.
project_name
Human-readable project name. Used in retrospective report filenames and TEACHINGS entries.
model_preferences
Maps tier names to model identifiers. Used for tier-based model resolution when no per-agent override is set. The full config example above shows the field in place; the shape is an excerpt, not a pasteable config, because the schema also requires version and project_name:
// excerpt — not a complete config"model_preferences": { "high": "opus", "medium": "sonnet", "low": "haiku"}A / in a value names a DISPATCH CHANNEL, not a vendor. The prefix before the slash names the process family that runs the seat, and codex and openai are the registered channels. An unregistered slash-prefixed id — openrouter/anthropic/claude, or a mis-cased Codex/… — is refused at resolution with a hard error rather than passed through as an opaque vendor id. The schema pattern still permits / because that is what makes the channel settable at all; the registry is enforced by the resolver, exactly once, and fails closed. If you are carrying a provider-prefixed id from an older config, it must become a bare native id or a registered channel.
The high tier stays native in this release: a foreign (codex/… or openai/…) value under high is a resolution error, not a fail-over. Pin a high-tier seat to the channel through agent_models instead (and note that under model_authority: "config" a per-agent pin is inert, so there the only remedy is a native high or a different static tier for the seat).
Channel routing lives only in the dispatch resolver. The Claude Code PreToolUse model-resolver hook is native-only by design: it hands a model id to the Task tool, which cannot carry a channel, so it refuses any slash-bearing id loudly (model_resolution_failed + a denied dispatch) rather than running a foreign-labelled id on the native carrier.
The dispatch surface itself is carrier-agnostic in two of its three beats — precompute the brief, then check its typed exit code — and only the final “hand the compiled prompt to a process” beat differs by carrier. Every dispatch, on any carrier, records per-attempt spend (one dispatch_spend ledger row per dispatch attempt, not per task) whose --source field names the channel that carried the attempt (workflow | agent-tool | codex-exec | codex-retry | openai-exec | openai-retry | backup-failover). Do not confuse that field with resolveDispatch()’s own source return value, which answers a different question — which rung of the resolution chain chose the model id — and is never written to the ledger’s --source. See the Dispatch Carrier Contract & Spend Accounting section of docs/state-model.md for the single authoritative vocabulary table.
model_backups
The per-tier native fall-back map — the sibling of model_preferences, keyed by the same tier names. It answers exactly one question: when this tier’s model cannot be dispatched, what runs instead?
A tier’s backup is consulted when the resolved model is foreign — a channel-prefixed id such as codex/gpt-5.6-luna — and one of three things is true: the seat guard bars that seat from the channel, the running platform cannot carry the channel at all, or the foreign dispatch failed. The backup is then dispatched in its place and the event records source: "seat-guard" or source: "channel-unavailable" so the substitution is never invisible.
A backup is definitionally native. The pattern for these values excludes /, so a codex/… value — or any other channel-prefixed id — is rejected by the schema, and the resolver rejects it a second time at read time because a hand-edited config never passes through validation. That is not tidiness: it is what makes the fail-over chain terminate. One hop, to a model this machine can definitely reach, or a hard config error — never a second foreign hop.
There is no default, deliberately. schemas/config-defaults.json does not seed this key, and the built-in resolution leaves it empty. A defaulted backup would quietly absorb the one error an operator most needs to see: a foreign tier with no declared backup is a terminal config error, because a dispatch that silently lands somewhere unnamed is worse than one that refuses.
// excerpt — not a complete config"model_preferences": { "high": "opus", "medium": "codex/gpt-5.6-luna", "low": "codex/gpt-5.6-luna" },"model_backups": { "high": "opus", "medium": "sonnet", "low": "haiku" }The high tier stays native in this release: a foreign model_preferences.high is refused at resolution rather than backed up. Pin a high-tier seat to a foreign model through agent_models if that is what you want, and read the seat-guard note under model_authority first.
worktree_isolation
Boolean. Currently inert — no dispatch branch consults this key; worktree isolation is decided by the dispatcher. Default: true.
Declared intent: when true, parallel Monkey agents are dispatched with isolated git worktrees. Disposition deferred with the other dead switches in consults/stub-session-hooks-hygiene.md.
ledger_enabled
Boolean. When true (default), all significant actions emit ledger events to .divineswe/ledger.jsonl.
scrolls_enabled
Boolean. When true (default), task scrolls are written and read for context anchoring.
max_parallel_agents
Integer, minimum 1. Currently inert — the key is prose-only; no scheduler reads it, so setting it changes nothing. Default: 4.
Declared intent: the maximum number of parallel Monkey agents during the IMPLEMENT phase. Its disposition (wire it or remove it) is recorded in consults/stub-session-hooks-hygiene.md.
flash_default_enabled
Boolean. When true, consultation silence composes a flash Book: the deprecated oracle-consult reads this key once at Step 1 (alongside the specs_dir read) and applies it at Step 5a’s flash-default application clause — a consultation in which the user expressed no preference about rite depth or review thoroughness composes flash: true frontmatter, and Oracle states aloud in the consultation both that the config default drove the composition and what the default composition omits (no adversarial tests, no audit pair, no Book-Level AUDIT or WU task, and no Mason tasks — commits stay operator-side unless restored), and records that provenance durably in the consultation’s spec decision record, so silence never becomes an invisible switch. An explicit request in either direction always wins, and the trigger is intent, never a literal phrase: any expressed preference about rite depth or review thoroughness — a request for careful review, for the audit pair, for security-sensitive thoroughness — counts as an explicit request for the omitted seats or the full rite; a user who asks for flash gets flash. The key is a default, never an override of stated intent. The flash floor is unaffected: a defaulted-flash Book whose tasks hit a boundary trigger still composes the Rule B wiring task or falls back to the full rite — the default changes what silence means, never what a flash Book is allowed to carry.
Default: false — the conservative direction: silence must not compose flash Books on a repository that never opted in.
Consumer-side strict read. Because no runtime module applies config.schema.json, the typing contract lives at the one reader: only the JSON literal true opens the flag. Any other value — the string "true", the string "false", 1, 0, null — is read as closed (the documented default governs), and Oracle surfaces the malformed value aloud in the consultation so the operator learns the key did not take. The schema still types it as a strict Boolean so any harness or future consumer that does apply the schema rejects the same values — a loose schema accepting the string "false" would read truthily and silently invert the operator’s disable signal.
A composition flag of the simplify_pass_enabled class, not a rite gate: it is deliberately absent from scripts/shared/rite-gates.mjs’s GATE_KEYS. Stated positively, this key has no layering, no resolver, and no suppression semantics — no project→global→default resolution, no dead-switch read-suppression, no global-fallback branch. It is one project-file read by one agent (Oracle, at the deprecated oracle-consult’s Step 1).
PROJECT-ONLY. Composition posture is per-repository, so the key is not declared in schemas/global-config.schema.json — that schema is additionalProperties: false, so a machine-wide ~/.divineswe/config.json declaring it fails global validation wherever that schema is applied rather than silently speaking for every repository on the machine. At consultation time a machine-wide declaration is simply inert and produces no diagnostic — Oracle reads only the project .divineswe/config.json, so an operator who set the key globally will observe nothing happening and should look in the project config. Not seeded into new configs (schemas/config-defaults.json omits it), so an absent key stays absent and the documented default governs.
This key replaces the retired default_mode key. An earlier release removed default_mode from the live config and left it in the schema as a documented-dead artifact; this release deleted the property from schemas/config.schema.json. Stated honestly, that retirement is a documentation-and-test contract change with no runtime effect on straggler configs: no runtime reader applies config.schema.json, so a config still carrying the dead key experiences nothing at runtime. The rejection exists only where the schema is applied — an AJV compile (as the test suite performs, and as any future consumer would) now rejects a default_mode-carrying config as an unknown extra under the schema’s additionalProperties: false. The fix for such a straggler is deleting one dead line.
specs_dir
Directory for spec files, relative to project root. Used by oracle-envision and avatar-forge — and by the deprecated oracle-consult and prophet-fulfill — for reading and writing design specs. Must be a relative path with no .. traversal. Default: "docs/specs".
books_dir
Directory holding the active Book files (book-NN.md). Resolved through scripts/resolve-books-dir.mjs and honored by divineswe-init, the deprecated oracle-consult and prophet-fulfill/resume/revelation, the next-book glob, and the consultation verifier. Default: ".divineswe/books".
More permissive than specs_dir: a relative value is resolved under the project root (no .. traversal allowed); an absolute value is taken as-is — including a path outside the git work tree, in which case the Books there become local-only working state (untracked by the repo, while the in-repo TABLETS.md remains the committed archive of completed Books). A leading ~/~/ expands to the current user’s home; $VAR/$(...)/backticks are treated as literal path characters (no shell expansion), and NUL bytes are rejected.
decrees_dir
Directory holding the deprecated Decree briefs — the initiative layer of committed briefs above the Books, one <slug>.md per Decree. Resolved through scripts/resolve-dirs.mjs and read/written by the deprecated decree domain of scripts/divineswe-state.mjs. Default: "docs/decrees", used only while the deprecated Decrees are switched on.
Classed with specs_dir, not with books_dir, and the asymmetry is deliberate. A Book is local-only working state, so books_dir may legitimately point out of the tree. A brief — deprecated with the Decrees — is a committed artifact: an out-of-tree one cannot be committed, so every Decree: back-link on every Book in its sequence would point at a file no clone of the repository contains. So the value must be repo-relative — an absolute path, a leading ~, a backslash, a .. traversal, an empty string and a NUL byte are all rejected at validation.
Because this is a write target (unlike specs_dir), the resolver goes beyond string guards and adds real containment: it also refuses percent-encoded path characters, shell-expansion forms ($VAR, $(…), backticks — never expanded, so honoring them literally would write intent into a directory named after the unexpanded token), a .git target (nothing in that value looks hostile, which is exactly the danger — a stray markdown file in the object store’s neighbourhood is a repository corruption rather than a mess), a symlinked final directory component, and a symlinked brief file.
None of those guards can see an intermediate symlink: the .. and .git scans read the configured string, and the inode guard lstats only the final component. So the resolver also resolves the whole chain with realpath — falling back to the nearest existing ancestor, since the first deprecated decree create runs against a directory that does not exist yet — and refuses a result that leaves the project root or enters .git. That is what stops a <root>/docs symlink from writing a committed brief out of tree, or into the object store, while decrees_dir sits at its untouched default and every string check passes. The brief-file guard takes the vetted directory the same way, so a symlinked parent cannot present a foreign file as a brief of this repository. The returned path is the logical one, never the realpath: printing a realpath would name a location the operator never configured.
A refusal propagates: the CLI never falls back to the default directory, because the operator’s configured location silently ignored is the worst outcome — intent written somewhere they will never look, at exit 0.
PROJECT-ONLY. Directory layout is per-repository, so a machine-wide ~/.divineswe/config.json may not move it; the key is not declared in schemas/global-config.schema.json at all, which makes the classing enforceable rather than advisory. Not seeded into new configs — schema and docs only.
consults_dir
Directory holding consultation handoff documents. Default: "docs/consults". Same class, same guards, same PROJECT-ONLY posture and the same resolver body as decrees_dir (scripts/resolve-dirs.mjs), so the second resolver can never become the unguarded twin of the first — a home-relative consult handoff is a document no reviewer of the pull request can read. Not seeded into new configs. Its first consumer is the staged-consult glob of a later prophecy; it is resolved and guarded now so the key does not arrive unvetted.
vows_dir
Directory holding the local Vow obligation register. The register filename is fixed as vows.jsonl; this key changes only its dwelling. When vows_dir is absent, the register keeps its existing default dwelling, .divineswe, so the default path is .divineswe/vows.jsonl. Default: .divineswe. The configured value is resolved through scripts/resolve-dirs.mjs, which owns the traversal, NUL, .git, symlink and realpath safety guards for this write target and returns the logical dwelling selected by the operator.
PROJECT-ONLY. Obligation state belongs to one repository, so a machine-wide ~/.divineswe/config.json may not move the register. vows_dir is deliberately absent from schemas/global-config.schema.json, preventing one machine-wide value from relocating every project’s obligations. It is also not seeded into new configs because schemas/config-defaults.json omits it; leaving the key unset therefore continues to mean .divineswe rather than turning the default into a value silently written by initialization.
Unlike decrees_dir and consults_dir, an absolute value is honored as a deliberate local-only escape hatch. An absolute dwelling outside the git work tree is consequently uncommittable: its vows.jsonl register is not part of the repository and cannot be committed or carried into the next clone. Use that escape hatch only when managing the register as local-only state is intentional. Relative values are resolved from the project root, and the filename remains fixed at vows.jsonl in every dwelling.
default_branch
Name of the default branch Mason FF-merges into at Book-level WRAP UP when branch_mode is active. Validated at runtime against the safe-name pattern ^[A-Za-z][A-Za-z0-9._/-]*$ plus git show-ref --verify. Default: "master". Set this to "main" (or whichever branch your repo uses as the integration target) before running a >6-task Book. The SessionStart top-up hook backfills this key if missing.
custom_instructions
Custom instructions injected into all agent prompts under a “Project Custom Instructions” header. Max 2000 characters. AGENT.md behavioral invariants always take precedence. Default: "" (empty).
simplify_pass_enabled
(Added in v0.9.0) Boolean toggle for the AUDIT-phase enhancement that runs a second simplify-style refactor review alongside the correctness/security audit. When true (default), 4eyes emits the two-array Shape A output (findings_audit, findings_simplify) and Prophet reconciles three streams (audit + simplify + Loki-Exploit). When false, 4eyes runs the legacy Shape B single-pass and findings_simplify[] is empty. Read at INIT and cached for the cycle duration; a default flash-composed Book omits AUDIT tasks at compose time, so this flag has nothing to act on there (Prophet dispatches every task present — nothing is skipped at run time). prophet-full-audit always uses Shape B regardless.
agent_models
Optional per-agent model overrides. Maps agent name to scalar model id; takes precedence over the per-agent static fallback in agents/<name>/config.json.model. Example:
{ "version": "1.0.0", "project_name": "your-project", "agent_models": { "monkey": "haiku", "thoth": "opus", "4eyes": "claude-opus-4-20250514" }}Notes (examples appear in config only because JSON files have no comments):
- Resolution priority:
agent_modelsoverride → static fallback (agents/<name>/config.json.model). Themodel_preferencesconfig field remains orthogonal — it is consumed by other resolution paths and not by the per-agent override chain. - Snapshot at INIT: Prophet captures
agent_modelsinto.divineswe/session-state.json.agent_models_snapshotat the start of every cycle. The snapshot is the source thePreToolUsemodel-resolver hook is designed to read, so mid-cycle edits toagent_modelsonly take effect at the next cycle.prophet-full-auditand other non-cycle dispatchers fall back to live config (no snapshot exists). - KNOWN DEFECT — the hook layer does not currently enforce this field. Verified empirically against
claude2.1.220: the subagent tool is surfaced toPreToolUsehooks asAgent, whilehooks/pre-tool-use-model-resolver.mjsactivates only ontool_name === 'Task'. Every dispatch therefore takes the hook’s pass-through branch, and neither the wrong-model deny nor the tier→concrete rewrite is ever emitted — on every dispatch path, not only inside Workflowagent()calls. Read this precisely:agent_modelsis not ignored.scripts/shared/dispatch-resolution.mjsstill resolves the override chain at dispatch time, Prophet still writes the snapshot and themodel_planaudit event, and the value you set here still selects the model Prophet dispatches with. What is missing is the hook-layer enforcement — a dispatcher that passes a model contradicting this field is not blocked. Tracked as deferred item 5 ofconsults/stub-hook-hookeventname-permissiondecision.md; pinned bytests/hooks/pre-tool-use-model-resolver-agent-tool-name.test.ts. The claim is version-scoped — seeclaims-register.md. - Loki shares one entry (Loki L11 limitation): Loki’s three forms (
doubt,break,exploit) all resolve through the singleagent_models.lokientry — there is no per-form override surface in this version. - Audit trail: a single
model_planledger event is written at INIT showing the resolved model id and source (snapshot|config|static) for every agent dispatched in the cycle.
model_authority
Controls who has final say over model selection and effort tier for each agent dispatch.
Enum: "prophet" | "config". Default: "prophet".
{ "version": "1.0.0", "project_name": "your-project", "model_authority": "config"}| Value | Behaviour |
|---|---|
"prophet" | Prophet (the run-cycle orchestrator) decides both model and effort tier per agent, using its full context: task complexity, prophecy phase, Book size, budget remaining. This is the default and recommended setting. |
"config" | Model is clamped to the static tier declared in model_preferences (or agent_models). Prophet still decides effort, but the model selection is resolved from config rather than inferred dynamically. Use when you want deterministic, cost-bounded dispatches at the expense of adaptive model selection. |
The seat guard is the only protection under "config" authority. Read this before setting it, because the interaction is not obvious: under "config" the model is clamped to model_preferences[<the seat's static tier>] before agent_models is consulted, so a per-agent pin written to hold a seat back — the recommended "agent_models": { "styx": "sonnet" } — is inert here. If that tier maps to a foreign (codex/…) model, every seat on the tier clamps to it. What stops the blocking seats from going with it is the seat guard, which is evaluated on the final resolved model on both authority paths: Styx is denied any foreign model outright, and a barred seat resolves that tier’s model_backups entry with source: "seat-guard" instead. Loki is deliberately unguarded, and 4eyes is held to the reviewer floor — at or above the model that implemented the work — by the Avatar and by an ad-hoc Hermes summons, not by this guard. A barred seat with no declared backup for its tier is a terminal config error, not a silent pass.
In-workflow strict-mode note: when DivineSWE runs inside a Claude Code Workflow (<!-- divineswe:claude-code-only:begin/end --> gating), the model_authority resolver switches to advisory mode. The Workflow host may impose its own model constraints; Prophet honours them rather than overriding them — model_authority:"config" is still respected but is advisory input to the Workflow scheduler, not a hard override.
See also Agentic Orchestration Frameworks for the research backing DivineSWE’s model-authority design.
upstream_url
Base upstream repository URL for the Offering pipeline (scripts/offering.mjs, /divineswe:offering) — the target the prefilled new-issue link points at. Optional: when the key is absent the pipeline falls back to the shipped aidragon GitLab base, which schemas/config-defaults.json seeds into every freshly-initialized config. Default: "https://gitlab.com/aidragonhq/divineswe".
Restricted by pattern to http/https with no whitespace, newline or NUL byte. That restriction is load-bearing rather than cosmetic: the value is interpolated into a URL that a human opens, so a hostile scheme or an embedded control character would be a URL/log-injection vector at link-open time. The key is declared explicitly in the schema because the schema is additionalProperties: false — an undeclared upstream_url would be rejected outright.
Sharing itself is never automatic. The pipeline is deny-all by default and contains no network code; nothing leaves the machine until you open the generated link yourself.
openai_base_url
OpenAI-API-compatible endpoint used by the openai/ dispatch channel. These openai_* keys now feed a runtime, not a codex-carried provider block: openai/ drives an in-process Chat Completions agent loop, scripts/hermes-runtime.mjs, imported by the carrier (scripts/hermes-dispatch.mjs) rather than shelled out to. There is no aliveness probe ahead of it — no request precedes turn one (LD-17) — the endpoint is dialed for the first time by the runtime’s own first chat-completion call, and the five tools the runtime offers the model (bash, read_file, write_file, edit_file, submit_report) run confined to the dispatched worktree, with bash additionally run under bubblewrap where the host has it (see skills/hermes-dispatch/SKILL.md for the unsandboxed-shell risk this raises where it does not). This key is settable project → global → default: a project value wins, a machine-wide value in ~/.divineswe/config.json (declared in schemas/global-config.schema.json with the same grammar) fills a project that sets none, and resolveEndpointKeys in scripts/shared/model-routing.mjs resolves each endpoint key on its own — reading the project layer at the discovered project root, so a worktree hears its checkout, and closing the key, never falling to the machine-wide value, when the project layer cannot be read or holds a malformed value. Spec forge-adversarial-model-diversity (M7) lifted its earlier project-layer-only class, so an endpoint set once machine-wide resolves in every project that sets none. It is consulted only when a model on the openai/ channel is dispatched, so a machine-wide endpoint reroutes nothing else. Restricted by pattern to a conservative http/https URL grammar — scheme, a host that is either a bracketed IPv6 literal ([::1], [fe80::1]) or a dotted hostname/IPv4 admitting letters, digits, _ . - and a trailing dot for an FQDN (the underscore covers ordinary docker-compose sidecar names), an optional port bounded to the legal 0–65535 range (:65536 is refused at the schema, not merely at the carrier), and an optional path built only from letters, digits, and . _ ~ % / : + - — capped at 2048 characters overall, with no whitespace, control characters, NUL bytes, userinfo (@), quotes, backslashes, or shell metacharacters ($ backtick ; | & < > ( )). That restriction is load-bearing rather than cosmetic: this config value reaches the carrier on a command line — the carrier argv avatar summon prints for an openai/ model, or Prophet’s hand-composed one — and a shell reads it BEFORE the carrier’s own downstream validateEndpointUrl ever runs, so the grammar must guard the upstream end of the pipe, not rely solely on the carrier’s runtime validation. Nothing at runtime AJV-validates a config, so the runtime read holds the value to the same grammar itself (ENDPOINT_URL_GRAMMAR in scripts/shared/endpoint-url.mjs) and to the carrier’s own validateEndpointUrl; a value failing either is ignored for its layer and reported as malformed_routing_value_ignored:openai_base_url. It is optional and not seeded into new configs.
Two failure classes reach this channel, and they are not the same shape. A transport failure — nothing listening, a timeout, a 5xx, a refused redirect — exits 17 ENDPOINT_UNREACHABLE, which IS retryable: the carrier spends its one OpenAI retry before falling over to the native backup. A reachable and answering hearth that will not speak the tool grammar this runtime demands is a different fact and exits 19 ENDPOINT_NONCONFORMING instead — the turn-one adjudication keys on a call naming a tool this runtime actually OFFERED, not merely on a call being present, because a fabricated tool name previously escaped the check entirely and ran the loop to the step budget against a hearth that had already disqualified itself. Exit 19 is NOT retryable and never triggers backup failover — a nonconforming endpoint does not become conforming on a second attempt — and this is enforced by a module-load assertion in the carrier that throws if 19 ever joins RETRYABLE_EXITS, rather than trusting the verdict’s retryable projection alone.
openai_context_window
Declared, documented, named in the carrier’s invocation form, CLI-flagged (--context-window), parsed, and validated as a positive integer — but read by no runtime. This key is settable project → global → default: a project value wins, a machine-wide value in ~/.divineswe/config.json (declared in schemas/global-config.schema.json with the same grammar) fills a project that sets none, and resolveEndpointKeys in scripts/shared/model-routing.mjs resolves each endpoint key on its own — reading the project layer at the discovered project root, so a worktree hears its checkout, and closing the key, never falling to the machine-wide value, when the project layer cannot be read or holds a malformed value. Spec forge-adversarial-model-diversity (M7) lifted its earlier project-layer-only class, so an endpoint set once machine-wide resolves in every project that sets none. It is not seeded into new configs. A supplied value reaches no wire: it configured the retired codex provider block, and the Hermes runtime (scripts/hermes-runtime.mjs) takes its context budget from its own hardcoded settings, and the hearth from its own n_ctx, neither of which currently derives from any declared config window (scripts/hermes-dispatch.mjs’s --context-window dead-ends with a note saying so). Wiring a config-driven window to the runtime is future work, not something this key currently does.
openai_max_output_tokens
Declared, documented, named in the carrier’s invocation form, CLI-flagged (--max-output-tokens), parsed, and validated — and read by no runtime. No runtime and no carrier opens .divineswe/config.json looking for this key; the code that reads it is the layered config read, resolveEndpointKeys, and avatar summon, which puts the resolved value on the carrier argv it prints for an openai/ model. What is wired is the FLAG: --max-output-tokens now travels into the Hermes runtime’s own maxTokensPerTurn option and reaches the wire as max_tokens on every turn. The key reaches a dispatch only on that flag: avatar summon puts it there for an Avatar summons, and Prophet hand-walks it there on a deprecated cycle dispatch, as skills/prophet-fulfill/foreign-carrier.md instructs — exactly as openai_base_url reaches --base-url. So the key is not inert in the way openai_context_window is: a dispatch composed either way DOES honour it; a dispatch that omits the flag does not, however the config reads. Teaching the carrier or the runtime to read the key itself is deliberately not done.
Bounded above. The flag is refused at argv above MAX_TOKENS_PER_TURN_CEILING (35072 on the hearth this repository was measured against), because the runtime’s prompt budget and this per-turn cap are spent out of the SAME per-dispatch context slot: tokenBudget + maxTokensPerTurn must fit one slot, or the hearth answers a hard ERROR_TYPE_EXCEED_CONTEXT_SIZE rather than truncating. That slot figure is one llama-server launch’s property (LLM_CTX_SIZE over total_slots), not a universal limit. This key is settable project → global → default: a project value wins, a machine-wide value in ~/.divineswe/config.json (declared in schemas/global-config.schema.json with the same grammar) fills a project that sets none, and resolveEndpointKeys in scripts/shared/model-routing.mjs resolves each endpoint key on its own — reading the project layer at the discovered project root, so a worktree hears its checkout, and closing the key, never falling to the machine-wide value, when the project layer cannot be read or holds a malformed value. Spec forge-adversarial-model-diversity (M7) lifted its earlier project-layer-only class, so an endpoint set once machine-wide resolves in every project that sets none. It is not seeded into new configs.
second_opinion_model
The model the second opinion runs on: a second 4eyes, on a model from another family than the implementer’s, carried by the Hermes carrier. One key, resolved project → global → default by resolveSecondOpinion in scripts/shared/model-routing.mjs, with the project layer read at the discovered project root, so a worktree hears its checkout; the default is unset, so no second opinion runs until one is configured. Two shapes:
"none"switches the second opinion off. A project’s"none"beats a machine-wide model.{ "model": "codex/gpt-6.1-sol" }names a carried model — acodex/,openai/orclaude/id. Anopenai/endpoint model has no rung on the model ladder, so it carries the tier you declare for it:{ "model": "openai/qwen3.8", "tier": "high" }. A declared tier is honoured only for such a model.
Choose it with the guided setup, /divineswe:hermes-setup (offered by divineswe-init, and re-runnable), which says where prompts go before it asks and writes this key into ~/.divineswe/config.json; nobody needs to edit a config file by hand to turn it on. Only the claude-code package carries the Hermes carrier: copilot-cli will never ship it, and the codex-cli and opencode packages do not carry it whole, so on those hosts avatar-forge’s closing record says the second opinion did not run.
It closes on doubt. Whether a project’s code goes to another vendor’s cloud is consent, so this key takes the rite gates’ pole, not the routing trio’s: a malformed value — a bare id string, a native id, None, an unknown tier or sibling key — or a layer found and not read resolves closed, reported as malformed_routing_value_ignored:second_opinion_model or the layer’s own token, and the next layer is never asked. A project whose config does not parse never inherits the machine’s model. The key is never seeded into a new config, and it carries no credential.
offerings_enabled
Boolean. Gates the entire Offering subsystem: the /divineswe:offering skill, scripts/offering.mjs, and the compose-time script-opportunity detector inside the deprecated oracle-consult. That compose-time offering walk runs only in a Book consultation, so it is deprecated with the Book cycle; the standalone offering skill stays live.
Default: false. The rite is opt-in — a project that never asks for it spends zero tokens on it, and the switch is off until somebody turns it on.
Resolution is layered and strict-boolean — see The global (machine-wide) config layer for the one authoritative statement of the order and its one exception.
retrospective_enabled
Boolean. Gates the entire WRAP UP retrospective rite: the WU task composition and the 4eyes mode=retrospective dispatch, follow-up-register appends and surface reads, the honor-register append, the spend and exceptions digests, and the once-per-Book terminal acknowledgment. The WRAP UP retrospective runs only inside a Book cycle, so it is deprecated with that cycle.
Default: false. The rite is opt-in — the most expensive tail in the cycle no longer runs unless it is asked for.
Resolution is layered and strict-boolean — see The global (machine-wide) config layer. When the gate is closed the tail is skipped cleanly: no partial retrospective, no half-written register.
teachings_enabled
Boolean. Gates the entire TEACHINGS rite, reads and writes: compile-prompt lesson injection (the per-dispatch token tax), the orchestrator-side ~/.divineswe/TEACHINGS* reads at the deprecated prophet-fulfill INIT and oracle-consult Step 1, the teachings sweep, the tiering pass, and the promotion coda. The TEACHINGS promotion coda runs only at a Book’s WRAP UP, so it is deprecated with the Book cycle; lessons already in TEACHINGS still reach Avatar briefs through compile-prompt.
Default: false. The rite is opt-in. This is the flag with the largest per-dispatch cost behind it, which is precisely why it is off until requested.
Resolution is layered and strict-boolean — see The global (machine-wide) config layer. This is the one key with a documented exception to that order: authorizing a section of the shared ~/.divineswe TEACHINGS register follows the section’s recorded owner, not the project you are running in. Read the exception before you rely on a project-level false to withhold a register section.
atlas_enabled
Boolean. Controls whether the plan-time Atlas swarm runs — the four read-only scouts that chart the repository before planning begins.
Default: true, unlike the three memory rites. The map is load-bearing for plan quality and for accurate task file-paths, so it stays on unless you switch it off deliberately.
When set to false, the deprecated oracle-consult skips the four-scout sweep: no map is produced, and that is never an error — a missing map is simply no map. Resolution is layered and strict-boolean — see The global (machine-wide) config layer.
vows_enabled
Boolean. Controls the Vows — DivineSWE’s obligation register, an append-only event log at .divineswe/vows.jsonl by default, though vows_dir may name it another dwelling; it records what is still owed: deferrals, post-seal steps, preconditions, validations and backlog.
Default: true, on the same dead-switch polarity as ledger_enabled / scrolls_enabled and deliberately opposite to the four rites above. The register is the pipeline’s memory of owed work; a register you have to opt into is empty exactly where it matters, on the machine of the operator who never heard of it. Say so once in your project config to turn it off.
PROJECT-ONLY. Obligation state is per-repository, so a machine-wide ~/.divineswe/config.json may not move it: a global declaration is inert and the verdict reports global_key_ignored:vows_enabled. The key is not declared in schemas/global-config.schema.json at all, which makes the classing enforceable rather than advisory.
When set to false, every vow write is a no-op — exit 0, zero bytes, and an existing register left byte-identical (a closed gate is a skip, never a truncation). Vow reads are count-suppressed, not empty: vow list --json answers {"vows":[],"suppressed":true,...} and vow surface prints a SUPPRESSED line, because “nothing is owed” and “the register was not read” must never be the same answer.
decrees_enabled
Boolean. Controls the Decrees, deprecated with the Book cycle — the initiative layer of committed briefs above the Books: problem, solution shape, constraints, an ordered Book sequence and acceptance criteria.
Default: false — off since the Decrees were deprecated. A project that still keeps the deprecated Decrees sets it true, and the decree verbs work as they did; under the closed gate a decree write says so on stderr (decrees_suppressed_by_gate) and writes nothing. Same PROJECT-ONLY class as vows_enabled (a global declaration is inert and reported as global_key_ignored:decrees_enabled).
It depends on vows_enabled. A Decree — deprecated, but still gated — reaches done only through a judgment recorded as a vow, so decrees_enabled: true over vows_enabled: false is an incoherent pair: the Decree can never close, and the degradation ladder’s middle rung — “skip loudly and record the debt as a vow” — has nowhere to write. The resolver reports that combination as dependent_gate_without_substrate:decrees_enabled and changes neither declaration. That is deliberate in both directions: silently closing decrees_enabled would make your config file and the verdict disagree, and silently opening vows_enabled would manufacture consent you never gave. You are told; you decide.
The rule reads the resolved verdict, not the raw text — a malformed vows_enabled is an absent one, so it falls back to its true default and the pair is coherent again.
canon_enabled
Boolean. Controls the Canon, deprecated with no replacement — the living current-truth capability layer under docs/canon/. In the deprecated design it was the is pole of the reconciliation loop: a Decree states the desired state, the Canon states the declared actual state, the Books are the actuator, and Judgment is the sensor. A Book never regenerates the deprecated Canon; it declares deltas against it in a ## Canon Deltas section and the seal merges them.
Default: false (it was true before the forge path became the default). The deprecated Canon’s declared truth is written only through a Book’s seal or vow judge, and the default forge path — avatar-forge, after oracle-envision for involved work — writes no Book, so a Canon on by default would drift silently behind every forge-built change. A project that keeps the deprecated Canon turns it on once, in its own config: "canon_enabled": true. The key is PROJECT-ONLY, like vows_enabled / decrees_enabled (a global declaration is inert and reported as global_key_ignored:canon_enabled): declared truth is per-repository, so no machine-wide file may move it.
It depends on vows_enabled. Under the non-blocking covenant a seal-time malformed delta skips loudly, marks the section Apocrypha-unmerged, opens a failed-canonization vow, and the seal proceeds; and a delta landing on a judgment-verified section resets it to Apocrypha by recording the voided signature that only vow judge ever writes. With the register dark the deprecated Canon can neither record its own failures nor ever be verified — it degrades from “declared truth, judged over time” to unfalsifiable claims. So canon_enabled: true over vows_enabled: false is an incoherent pair, and the resolver reports it as dependent_gate_without_substrate:canon_enabled while changing neither declaration, for the same reason it does for the deprecated Decrees: silently closing the dependent would make your config file and the verdict disagree, and silently opening the substrate would manufacture consent you never gave.
decrees_enabled and canon_enabled strand independently over that one substrate, and each emits its own token, so warnings[] always names which layer is stranded rather than leaving you to infer it.
daedalus_enabled
Boolean. Controls Daedalus — the two-tier catalog of buildable tools under .divineswe/tools/, the operational arm of the First Axiom: build a script wherever it saves context and LLM work. Gates the whole daedalus scaffold | lint | test | register | describe | list domain of the state facade. A tool is a per-tool directory (project tier <repo>/.divineswe/tools/<name>/ or global tier <home>/.divineswe/tools/<name>/) holding an AJV-validated tool.json manifest, an entry stub, and a node --test exhibit; each tier’s CATALOG.md is a derived index, never hand-kept.
Default: true, on the same dead-switch polarity as ledger_enabled / scrolls_enabled / vows_enabled and deliberately opposite to the three memory rites. The catalog is load-bearing infrastructure — the find-before-hand-executing duty depends on it being present — so it records unless a project turns it off, and a mistyped or unreadable config keeps it live rather than silently darkening it.
PROJECT-ONLY, with no dependency edge. Catalog participation is per-repository, so a machine-wide ~/.divineswe/config.json declaration is inert and reported as global_key_ignored:daedalus_enabled; the key is declared in schemas/config.schema.json only, never in schemas/global-config.schema.json. Unlike decrees_enabled / canon_enabled it stands alone — the catalog neither reads nor writes the Vows register, so there is no incoherent pair to warn about. Not seeded into a new project config (schemas/config-defaults.json omits it) so the polarity is never frozen into an existing repo.
Driving the obligation layer from a consuming project
The Vows and the deprecated Decrees use their vows_enabled and decrees_enabled switches, while the Vows register’s dwelling is independently selected by vows_dir; everything else about that layer is a verb, not a setting. The runnable consumer path — bootstrapping .divineswe/, backfilling obligations with their provenance, migrating off the retired home-scoped follow-up ledger, the judge workflow and its permission ask rule, the --json read surface, and the gate-dependency matrix restated as a table — lives in vows-consumer-contract.md. Every shell recipe published there is executed against the shipped CLI by the test suite, so a recipe that drifts from the binary is a build failure rather than a support ticket.
The global (machine-wide) config layer
Eleven keys can be set machine-wide, in a second config file at ~/.divineswe/config.json: four of the gate keys above — offerings_enabled, retrospective_enabled, teachings_enabled and atlas_enabled — plus the routing trio, model_preferences, model_backups and agent_models, plus second_opinion_model and the three endpoint keys openai_base_url, openai_context_window and openai_max_output_tokens. Nothing else. The file carries no project identity, so neither version nor project_name belongs in it, and its schema (schemas/global-config.schema.json) permits nothing beyond those eleven. The PROJECT-ONLY switches (ledger_enabled, scrolls_enabled, vows_enabled, decrees_enabled, canon_enabled) are inert there and say so, as is model_authority — who wins a dispatch argument is a per-project posture, while which models this machine is willing to spend on is machine policy.
AUTHORITY NOTE (LD-R2-1). The earlier machine-wide narrowing (A9/LD-6) pinned this file at exactly four keys and named model_preferences explicitly as a “project-shaped identity key” that must never join them. That narrowing is superseded for model_preferences/model_backups/agent_models — and for those three keys only (the schema itself carries the supersession sentence verbatim, naming the deciding release). Routing is a policy about which models this machine is willing to spend on, which is machine-shaped rather than project-shaped — the same distinction A9/LD-6 used to keep ledger_enabled and scrolls_enabled out, and those two (plus every dead switch and identity/layout key added since) stay out unchanged. The supersession is scoped to the routing trio alone; it does not reopen the four-key pin for anything else, and it does not touch model_authority, which stays a per-project posture.
SECOND AUTHORITY NOTE (M3, M7). Spec forge-adversarial-model-diversity supersedes the PROJECT-ONLY ruling for openai_base_url, openai_context_window and openai_max_output_tokens, and adds second_opinion_model — the user ruled that the guided setup writes this file and that every project uses it by default. Those four keys are read by scripts/shared/model-routing.mjs (resolveSecondOpinion, resolveEndpointKeys), project → global → default, each on its own, with the project schema’s grammar — and on the consent pole, not the routing trio’s: the project layer is read at the discovered root, and a layer found and not read, or a malformed value, closes the key instead of falling to this file.
The routing trio is read by a different resolver, with a different fail-safe direction. scripts/shared/model-routing.mjs — not rite-gates.mjs — resolves model_preferences, model_backups and agent_models across the same two layers, per key and per tier/agent: a tier the project declares wins outright, a tier it omits is inherited from the machine-wide file, and a tier neither declares falls to the shipped native trio (opus / sonnet / haiku). A project pinning only medium therefore still inherits your machine’s low.
And the fail-safe pole is inverted for routing — deliberately. For the gate keys, a layer that is found and not read closes the rite, because an unknown intent is never consent. For routing there is no consent to protect and a different failure to avoid, so an unreadable, refused, unparseable or non-object layer is simply absent, and what an absent layer resolves to is native: a routing read that fails must never strand a dispatch on a vendor this machine may not be able to reach. A malformed value is absent for that key alone — malformed_routing_value_ignored:model_backups.medium — and never blanks out its siblings or the other layer. Every dropped layer still speaks, in the same vocabulary the gates use (global_config_refused_symlink and friends).
{ "model_preferences": { "medium": "codex/gpt-5.6-luna", "low": "codex/gpt-5.6-luna" }, "model_backups": { "medium": "sonnet", "low": "haiku" }, "agent_models": { "loki": "codex/gpt-5.6-terra" }}That file routes every project on the machine that does not say otherwise. A project overrides any one key by declaring it in its own .divineswe/config.json.
Resolution order: project value → global value → built-in default. scripts/shared/rite-gates.mjs is the single source of that truth; every consumer asks it rather than reading config itself. Within that resolver a project’s .divineswe/config.json wins, so a repository can opt out of a machine-wide policy without touching your home directory. This is the one authoritative statement of the order — the per-key sections above point here rather than restating it.
Which file is “the project layer” is decided structurally, not by where you are standing. The gate is resolved at the repository root: from any base inside a checkout the resolver de-worktrees the path (<repo>/.claude/worktrees/<name> is a checkout of <repo>), climbs the resolved (symlink-followed) ancestry to the nearest ancestor that is a repository boundary — a .git directory, a .git pointer file that actually resolves, or a bare repository — and reads that root’s config. What follows from that, stated in both directions:
- Running a rite from a git worktree, a subdirectory or a monorepo package still honours the enclosing project’s declaration. Before this was structural, a worktree — which never carries a
.divineswe/of its own, because.divineswe/is gitignored — silently found no project layer at all and inherited the machine-wide policy over the project’s explicitfalse. - A
.divineswe/config.jsonthat arrives below the repository root — inside a vendored subtree, a dependency, a monorepo package — cannot open a rite the repository root did not open. That is the protection. - It can still close one, and that is the cost of the protection stated honestly. A
falsedeclared below the root is honoured, and the verdict reportsproject_declaration_below_root_closed. The asymmetry is deliberate: a refusal must survive being read from the wrong directory, while consent must never be manufactured from one. It matters most for the shapedivineswe-initproduces — it seeds at your current directory, never at the git toplevel, so a package-level.divineswe/inside a monorepo is an ordinary thing to have. Such a file will turn its rites off and will not turn them on; put thetrueat the repository root. - A nested repository (a submodule, or a vendored tree that kept a
.gitwhose pointer resolves) is its own boundary and does answer for itself — including opening rites, if you invoke a chokepoint with the dependency’s own directory as the base. No orchestrated path does; treat “never point a rite at a dependency directory” as a caller obligation. - The climb never enters the directory whose
.divineswe/config.jsonis the machine-wide file. A git-tracked$HOME(git init ~, a container home that is a checkout) is an ordinary setup, and without this stop every project beneath it would be redirected to$HOME— reading the machine-wide file a second time as the project layer and movingledger_enabled/scrolls_enabled, which LD-6 forbids. Naming your home directly as the base still works; the walk simply never arrives there on its own. - If there is no repository anywhere above the base, nothing is climbed: the base answers for itself. That is deliberate — an unrelated parent directory must never speak for a project that legitimately has none. The one exception is the
teachings_enabledveto described further down, which in that case keeps climbing to the nearest ancestor that expressed the key, so a non-git project’s declaredfalsestill survives acdinto a subdirectory. The veto can only ever close a rite, so the two ranges differ only in how much can be turned off. - If the walk runs out of budget before it can decide (a path more than 64 components below its repository root), it does not guess and does not go quiet: the project layer is treated as dropped, all four opt-in keys resolve closed, and
project_root_discovery_exhaustedappears inwarnings[].
When the base was redirected, the verdict says so: basepath_resolved_to_project_root appears in warnings[].
A dropped project layer closes the opt-in rites; it does not fall through. If the project layer is found and not read — a symlinked config, a trailing comma, chmod 000, a config that is a directory or parses to an array, or a key declared with a non-boolean value — the project’s intent is unknown, and an unknown intent is never consent. All four globally-settable keys then resolve closed, and the machine-wide value is not inherited. The resolver CLI prints that decision and every script chokepoint acts on it, so the two can never disagree; re-declaring the key elsewhere will not open it — repair the project config named in warnings[].
That applies to a dropped layer below the root too, and warnings[] says which. A package-level .divineswe/config.json that is found and not read is not the same as one that said nothing — it used to be, and one trailing comma (or a symlink to a shared config, an ordinary monorepo idiom) therefore discarded that package’s explicit false and inherited the machine-wide value. A dropped layer at the repository root reports project_config_unparseable and its siblings; a dropped layer below the root reports project_declaration_below_root_dropped. Two tokens because the remedy points at two different files; one rule because unknown intent is never consent, wherever the file sits.
The documented exceptions — teachings_enabled for a TEACHINGS register section. The register under ~/.divineswe is shared by every project on the machine, so authorization for one of its TEACHINGS-<slug>.md sections is bound to that section’s recorded owner — a record kept in teachings-owners.json inside the register directory and consulted by scripts/shared/teachings-gate.mjs — rather than to whichever project happens to be running.
The two TEACHINGS CLIs ask different questions, and they answer a claimed slug differently. That difference is real and reproducible, so it is stated rather than averaged:
teachings-sweep, per destination section — the recorded owner’s layer answers, whoever is asking. A sweep fired from project A may move project B’s claimed section when B’s own resolution istrue, and is refused (owner_closed) when it is not.teachings-tier --project <slug>, per project file — the caller must be the recorded owner. Running it from a co-worker’s checkout is refused (unowned) even when the owner’s own resolution istrue: this CLI rotates, evicts and archives one project’s file destructively, so authority alone is too weak — identity is checked in both directions.
Within that split, the section cases are:
- Unclaimed slug, asked from another project — decided by the machine-wide layer alone. The register holds a name, not a path, so there is no project root to consult: the unclaimed project’s own reachable, explicit
teachings_enabled: falseis never read, and a machine-widetrueopens the section. This is the reason the machine-widetruebelow is not a free action (see the warning under the snippet). - Claimed slug — as split above: the owner’s layer decides for a sweep destination; the caller must be the owner for a tier run.
- Unclaimed slug, asked from its own project root — that project answers for itself, and an authorized run records the claim, after which the case above applies.
- Unreadable owner record — the highest-impact state, and the one an operator is most likely to hit. If
teachings-owners.jsonexists and cannot be trusted (a symlinked leaf, an appended comma, unreadable, not an object, or carrying a non-absolute root), every slug it covers is refused, regardless of either config layer, and the record is never rewritten. One stray comma freezes the whole register. Remedy: repair or delete the file — an unreadable record is a refusal, not an empty one.
A second, smaller departure applies to teachings_enabled generally: a project may always veto a run fired from inside it. The veto is resolved by walking up from the invoking directory to the repository root (bounded at 64 levels), so a cd into a subdirectory cannot silence a project’s declared false. It resolves through the same rule as every other rite gate — first false wins, and a declaration in range can only ever close a rite, never open one — so a subdirectory declaring teachings_enabled: true cannot cancel the repository root’s false, and a subdirectory config that is found but not read closes the rite rather than falling through. When no repository exists above the base, the veto keeps climbing to the nearest ancestor that expressed the key; that is the only respect in which its range differs from the rite gate’s, and it differs only in the closing direction.
Bind a slug to its project once, from that project’s own root, and the ordinary order governs it thereafter:
node $CLAUDE_PLUGIN_ROOT/scripts/teachings-tier.mjs --project <slug> --base <that project's root>Nothing else on this page departs from the order: the other five gate keys, and teachings_enabled outside register-section authorization and the veto walk, are project → global → default.
{ "offerings_enabled": true, "retrospective_enabled": true, "teachings_enabled": true, "atlas_enabled": true}Before you paste that machine-wide
trueforteachings_enabled: by the first bullet of the exception above, it also opens every unclaimed section of the shared TEACHINGS register — including sections belonging to projects that have declaredfalseand were never consulted. Bind the sections you care about first.
Seeing what actually resolved. Every failure mode below is silent by design, and the resolver is also the CLI that reports them:
node $CLAUDE_PLUGIN_ROOT/scripts/shared/rite-gates.mjs <basePath>It prints one JSON line — the six resolved gates plus a warnings[] array — for the project root you name, and always exits 0. An empty warnings[] means every layer it found was honored; anything else names the value or layer it dropped. This is the only way to observe the tokens listed below.
Rules worth knowing before you write that file:
- Exactly eleven keys are globally settable: the four gates
offerings_enabled,retrospective_enabled,teachings_enabledandatlas_enabled, the routing triomodel_preferences,model_backupsandagent_models,second_opinion_model, and the endpoint keysopenai_base_url,openai_context_windowandopenai_max_output_tokens. Nothing else in this reference has a machine-wide layer. The strict-boolean rule below governs the four gates; the other seven carry structured values and are governed by the per-key rules above it. ledger_enabledandscrolls_enabledare project-only. Declaring either one here is inert — the resolver ignores it and recordsglobal_key_ignored:<key>in the verdict’s warnings. They are per-project observability switches, not machine policy, so a global file that sets them is selling a switch that does not exist.- Strict booleans only, and a non-boolean does not fall through.
trueandfalsecount. A quoted"true", a1, or anullis discarded rather than raising an error, and is reported asnon_boolean_value_ignored:<key>(plus a layer-qualifiedproject_…/global_…twin naming whose value it was). What happens next depends on which layer wrote it. In the global file the value is simply absent and the next layer decides. In a project file it marks that project’s intent as not read for the key, and every consumer — the resolver CLI and all ten script chokepoints alike — then resolves the rite closed: an intent nobody could read never inherits the machine-widetrue. Re-declaring the key elsewhere will not open it; repair the project config. This is the quietest way to get a file that looks right and does nothing. divineswe-initnever writes the file itself. Its one writer is the guided setupdivineswe-initoffers,/divineswe:hermes-setup, which writes onlysecond_opinion_modeland the endpoint keys, with your choice, keeping every other key; everything else here you write by hand, and the file’s absence simply means “no machine-wide policy”.- The four keys are deliberately not seeded into a new project config either —
schemas/config-defaults.jsonomits them. An absent project key is what keeps the global layer reachable; a seededfalsewould shadow your machine-wide policy in every project forever. - A refused layer is never silently followed. The resolver declines a
~/.divineswedirectory or aconfig.jsonleaf that is a symbolic link, and any leaf that is not a regular file under a 1 MiB cap. A refusal falls through to the built-in defaults and is reported inwarnings[](global_config_refused_symlink,global_config_refused_irregular,global_config_unreadable,global_config_unparseable,global_config_not_an_object). - The symlink guard covers those two components and nothing above them. A dotfile manager that symlinks
~/.divineswe, or theconfig.jsonleaf inside it, is refused and told so. A symlinked home directory is not: the resolver follows it and reads the layer normally, with an emptywarnings[]. Treat the home path itself as trusted input — the guard stops a config layer redirecting the read out of the directory it was found in, not an operator pointing the resolver somewhere unexpected.