--- name: okf-config description: "The okfit config file: discovery order, the three project-local locations and the XDG fallback, the TOML schema table by table, lint severities, and what the software-project profile contributes. Use when writing or editing .okfit.toml, okfit.toml or .config/okfit.toml, or explaining why a type, tag, or lint severity behaves the way it does. Trigger phrases -- \"add a new type to the config\", \"change a lint severity\", \"where does okfit look for its config\", \"what does software-project add\", \"set actors.agent\"." allowed-tools: - read - grep --- # okf-config ## Discovery order and the three file locations With no `--config` flag, `okfit` walks upward from `[path]` (default: the current directory), checking each directory for `/.okfit.toml`, then `/okfit.toml`, then `/.config/okfit.toml` before moving up one level -- so a child directory's `okfit.toml` always beats a parent's `.okfit.toml`. Past the project it falls back to `$XDG_CONFIG_HOME/okfit/config.toml`, then the OS-native config directory, then `/etc/okfit/config.toml` on Linux and macOS. First match wins; nothing merges across levels. `--config ` bypasses all of it: no upward walk, no XDG probe happens once it is given. The upward walk never stops at `$HOME`, so `~/.config/okfit.toml` and `~/okfit.toml` are project-tier files that shadow the XDG tier and anchor the project root at `$HOME` -- personal defaults belong at `$XDG_CONFIG_HOME/okfit/config.toml` instead. `okfit init` scaffolds `.config/okfit.toml` with a `#:schema` directive pointing at the published JSON Schema (`schemas/1.0/config.json` in this repo), so a Tombi- or taplo-aware editor gets completion and validation on the file without any further setup. ## The TOML schema, table by table - `okf_version` -- a string. `DEFAULTS` sets `"0.2"`. - `[bundle]` -- `path` (default `"okf"`), `profile` (default `"software-project"`). - `[concepts]` -- `required` (array of frontmatter keys every concept must carry) and `tags.required` (array of tags every concept must carry). - `[lifecycle]` -- `default_stale_after`, a `StaleAfterDuration`: either `^(\d+)(h|d|w)$` or Effect's `" "` form. `DEFAULTS` is 90 days. This shorthand is a *config* value only: a concept's own `stale_after` frontmatter is an absolute ISO 8601 instant with an explicit offset, and `stale_after: 90d` on a concept fails `family-invalid`. - `[actors]` -- `agent` (a branded `Actor`) and `humans` (an array of `Actor`). `DEFAULTS` sets only `humans: []`, so `actors.agent` is absent unless a config or profile sets it. - `[lint]` -- one `LintLevel` (`off | info | warn | error`) per lint code, snake_case keys. The D-34 default table, reproduced verbatim: | Key | Default | | --- | --- | | `broken_links` | `warn` | | `missing_index` | `warn` | | `unknown_type` | `error` | | `required_key_missing` | `error` | | `field_value_unknown` | `error` | | `require_verified_unmet` | `error` | | `family_invalid` | `error` | | `generated_at_drift` | `warn` | | `computation_runtime_missing` | `error` | | `footnote_source_unknown` | `warn` | | `footnote_undefined` | `warn` | | `log_frontmatter` | `warn` | | `actor_prefix_unknown` | `info` | | `legacy_timestamp` | `info` | | `config_unknown_key` | `warn` | | `stale` | `info` | | `walk_unreadable` | `warn` | | `status_missing` | `off` | | `source_resource_missing` | `warn` | | `generated_missing` | `warn` | | `publication_drift` | `warn` | | `publication_orphan` | `error` | | `surface_unmatched` | `warn` | Two extra facts worth stating: `unknown_type` is forced `off` when the merged config declares no types at all, and `status_missing` (a concept with neither `status` nor `verified`, which the spec reads as `stable`) is the one severity the software-project profile raises, to `warn`. - `[types.]` -- `description?`, `guidance?`, `required?: string[]`, `require_verified?: boolean`, and a `fields.` sub-map (each field is `{description, values?, kind?: "path"}`). - `[tags.]` -- `description?` only. Unlike a type, a tag has no `guidance` key at all -- the asymmetry is deliberate, not an oversight. - `extensions` -- unknown top-level keys are preserved verbatim rather than erroring; `config_unknown_key` warns about them. - Merge order is `DEFAULTS < profile < file`, applied by the CLI, never by core. Plain objects deep-merge key-wise; arrays and scalars in the override replace wholesale -- a config's `concepts.required` replaces the profile's list rather than appending to it. ## What software-project contributes The `software-project` profile (`bundle.profile`'s default) sets sixteen types and thirteen tags on top of `OkfitConfig.DEFAULTS`, plus `concepts.required = ["title", "description"]`. Types, one sentence each: - `Project` -- "The repository's root concept: its purpose, boundaries, and non-goals." - `Module` -- "A unit of code with an owner and a boundary." - `Decision` -- "A choice made, the alternatives rejected, and why." - `Convention` -- "A rule contributors and agents must follow." - `Interface` -- "A contract others depend on." - `Reference` -- "Mirrored external material kept under the references directory." - `Runbook` -- "A repeatable operational procedure with a trigger and an observable end state." - `Glossary` -- "A term this repository uses in its own sense, one term per concept." - `Limitation` -- "A known edge of a contract: something that does not work, and why that is acceptable." - `DataModel` -- "An internal source-of-truth structure that other artifacts are derived from." - `Gotcha` -- "A state or result that looks like one thing and is the opposite: breakage that is transient, or success that did nothing." - `Consumer` -- "An external application that consumes this repository and thereby scopes it." - `Roadmap` -- "A gate and the forward-looking work behind it, held as intent rather than as a Decision." - `Measurement` -- "A dated empirical result: what was measured, how, and what the numbers ruled in or out." - `Invariant` -- "A property the code holds by construction: enforced by the type system or pinned by a test, not followed by people." - `Incident` -- "A dated production failure: what shipped broken, what it looked like to the consumer, the root cause, and the guard that now stops it." Choosing between the near neighbours: a Limitation is "this cannot do X"; a Gotcha is "this looks broken (or looks fine) and is the opposite". A Runbook is followed in order and has no staleness cadence; a Convention is a rule re-examined on one. A DataModel is documented from the maintainer's side (what breaks if an entry is wrong); an Interface from the consumer's. A Glossary term earns a concept on a collision or a trap, not merely because a word is used. A Roadmap is queued work behind a gate, not a choice made: a draft Decision that decides nothing is a Roadmap. A Measurement is the evidence a Decision cites, kept out of the Decision's body so it can rot on its own `stale_after`. A Consumer is a downstream repository, not a Module of this one. An Invariant is held by a brand, a union, or a pinned test and nobody "follows" it; a Convention is a rule a contributor could ignore. An Incident is one dated narrative -- what shipped broken, how it looked, the root cause, the guard -- where a Gotcha is only the misleading signal and a Decision only the guard; write the Incident and link the other two if they exist. A known bug nobody is scheduled to fix is a Gotcha with a `stale_after`, not a Roadmap; it becomes a Roadmap once the fix is planned. A Gotcha whose signal comes from outside the repository (a GitHub platform behaviour, a consumer's bundler) omits `resource` and names the outside system in the body rather than pointing `resource` at a directory that merely sits nearby. Each type's full `guidance` string is longer than is worth reproducing here; read it from the merged config (`okfit context --format json`, which also lists each type's `required` keys, `require_verified`, and declared `fields` with their enum values) or `packages/profiles/README.md` rather than trusting a paraphrase. Tags: `architecture`, `testing`, `release`, `security`, `performance`, `dx`, `ci`, `compat`, `bundle`, `observability`, `deps`, `github`, `docs` -- each a one-sentence `description`, no `guidance`. `bundle` is install weight and reachability (tree-shaking, subpath entrypoints, edges declined for their cost) where `performance` is runtime cost; `observability` is how the system reports on itself; `deps` is how third-party dependencies are declared, pinned, and distributed; `github` is the GitHub platform surface (the APIs, Apps and tokens, Actions, Packages, check runs, pull request conventions) where `ci` is the unattended path wherever it runs; `docs` is the documentation itself (provenance, rot, re-derivation, what would falsify a claim) where `dx` is tooling. Framework tags are deliberately not in the profile. A concern that names one framework -- Effect layer memoisation, React hook ordering, Django migrations -- is a fact about the repository, not about the software-project shape, so a repo that keeps hitting one declares it locally as a plain tag with a one-sentence description: ```toml [tags.effect] description = "Concerns an Effect v4 idiom: layers, services, error channels, or the test runner." ``` Name it after the framework, not `framework:` (a colon in a tag is legal but reads as a namespace the profile does not define). A migration that finds the same framework idiom recurring across three or more concepts should add the tag then, not tag by hand afterwards. Details that surprise: - `Module` requires `resource` and `kind` (`workspace | package | website | plugin | action | worker | harness | config-dependency`; `worker` is a detached sidecar or worker bundle another module spawns, with its own lifecycle, that is not itself a package or action); `DataModel` requires `resource`; `Interface` requires `kind` (`api | cli | config | wire | mcp | runtime`); `Consumer` requires `repository` (free text, a URL or an owner/name pair, since it lives outside this repository); `Incident` requires `occurred` (free text, an ISO 8601 date -- core has no date field kind) -- the other types have no `required` list at all. - `Module` also declares two optional structured fields: `layer` (free text, the repository's own layering label such as `L2`) and `pins` (the sibling Module paths this one is exact-version-pinned with). `Roadmap` declares an optional free-text `gate`; `Measurement` an optional path-kind `justifies` (the Decisions it supports). `Invariant` declares an optional path-kind `resource` (the type or test that enforces it); `Incident` an optional path-kind `guard` (what now stops the failure). - `Decision` sets `require_verified = true` -- a Decision is not settled until a human verifies it. That is exactly the field `okf-authoring`'s rule 2 forbids the agent from writing. A Decision with `status: draft` is exempt from `require-verified-unmet`, so a freshly authored bundle can validate clean before anyone has run `okfit verify`. The profile also carries a `layout` (`root.{index,log,project}` plus fifteen directories: `modules/`, `decisions/`, `conventions/`, `interfaces/`, `references/`, `runbooks/`, `glossary/`, `limitations/`, `models/`, `gotchas/`, `consumers/`, `roadmaps/`, `measurements/`, `invariants/`, `incidents/`) that `okfit init` scaffolds from. That layout is **not** part of `OkfitConfig` itself -- it never appears in a config file, and it does not bound where concepts may live. `okfit sync` and the `missing-index` lint work from the bundle on disk, not from the layout: any directory that holds a concept gets an `index.md`, so a custom `[types.Consumer]` under `okf/consumers/` (or any directory name you choose) is indexed exactly like a layout directory (issue #71). ## actors.agent must be set for this plugin's agent `actors.agent = "okfit/claude-code"` must be present in the repository's config for `okfit:okf-docs` to have an actor identity to stamp `generated.by` with. Nothing in this plugin writes that key: the profile states the reason as doctrine -- "`actors.agent` is deliberately unset (P-17): which agent writes is a fact about the repository, not about the software-project shape." The session hook nudges when it is null; a human or a skill-guided edit sets it. ## Linting the bundle with markdownlint If the repository lints markdown, the bundle needs two exemptions from markdownlint's defaults, and neither can be satisfied at the source: MD025 (one top-level heading) counts a concept's frontmatter `title:` as its H1, so the body H1 the spec requires reads as a second one, and the spec shapes every `index.md` as `# Section` groups, one H1 per concept type. `log.md` is fine: `init` and `sync` both start it with `# Log`. Add this to a `.markdownlint-cli2.jsonc` at the repository root, adjusting the bundle path if `bundle.path` is not `okf`: ```jsonc "overrides": [ { "combine": "merge", "config": { "MD025": { "front_matter_title": "" } }, "filter": ["okf/**/*.md", "!okf/**/index.md"] }, { "combine": "merge", "config": { "MD025": false }, "filter": ["okf/**/index.md"] } ] ``` Do not reach for a nested `okf/.markdownlint-cli2.jsonc` as a shortcut. markdownlint-cli2 does not merge a nested file's `config` block into the root's; it replaces it wholesale. A one-line nested file carrying only `{ "config": { "MD025": false } }` therefore re-enables every rule the root turned off -- in a Silk repository the `changeset-*` custom rules, disabled at the root, fire CSH001/CSH002 on every concept. If the root config genuinely cannot express `overrides`, the nested file must restate the whole root `config` block with `MD025` off; the bundle loader ignores non-markdown files, so the nested file itself is harmless to okfit. ## A worked example Load `references/example-config.toml` when: writing a config from scratch. The smallest useful edit is just naming the profile and the agent: ```toml [bundle] profile = "software-project" [actors] agent = "okfit/claude-code" ```