| name | config-system |
| description | Use when reading, writing, extending, or testing Flemma configuration — covers the CoW layer store, schema DSL, proxy/lens API, and common patterns. Trigger on any work touching flemma.config, flemma.opt, config.get, config.writer, config.materialize, config.lens, schema definition, DISCOVER callbacks, or setup/frontmatter wiring. |
Flemma Config System
Copy-on-Write layers with a declarative schema. One system, one API, one mental model.
Architecture
4 layers store operations (not values). Reads replay ops across layers.
| Priority | Layer | Scope | Written by |
|---|
| L10 | DEFAULTS | global | Schema materialization at boot |
| L20 | SETUP | global | require("flemma").setup(opts) |
| L30 | RUNTIME | global | :Flemma switch, runtime cmds |
| L40 | FRONTMATTER | per-buffer | flemma.opt.* in frontmatter |
Scalars: Top-down (L40 first), first set wins.
Lists: Bottom-up (L10 first), accumulate set/append/remove/prepend.
Module Map
lua/flemma/config/
init.lua -- Public facade (flemma.config)
store.lua -- Layer store: ops log, resolution
proxy.lua -- Read/write/list/lens proxy metatables
schema/
init.lua -- Schema DSL factory (s.string, s.object, etc.)
types.lua -- Node type hierarchy
definition.lua -- THE schema (source of truth for all config)
navigation.lua -- Schema tree traversal helpers
API Quick Reference
local config = require("flemma.config")
local symbols = require("flemma.symbols")
local cfg = config.get(bufnr)
cfg.tools.timeout
cfg.thinking
local t = config.materialize(bufnr)
local w = config.writer(bufnr, config.LAYERS.RUNTIME)
w[symbols.CLEAR]()
w.provider = "openai"
w.tools.modules:append("my.tool")
config.lens(bufnr, "tools.bash")
config.lens(bufnr, { "parameters.anthropic", "parameters" })
config.inspect(bufnr, "provider")
config.LAYERS
get() vs materialize()
materialize() expands a $preset model reference into its concrete provider,
model, and parameters. get()/inspect() return the raw configured value with
the $preset alias preserved — setup's one-time presets.resolve_default reads
the raw alias via get(), and UI that shows the configured value uses inspect().
| Need | Use |
|---|
Static key reads (cfg.tools.timeout) | get(bufnr) |
pairs() / iteration / dynamic keys | materialize(bufnr) |
vim.deepcopy() / vim.inspect() | materialize(bufnr) |
| Callbacks receiving config | materialize(bufnr) |
Concrete model (expand $preset alias) | materialize(bufnr) |
Raw configured value (keep $preset) | get(bufnr) / inspect(bufnr, path) |
Key Invariants
- Store is schema-free. Callers pass
{ is_list = true } to resolution. The facade owns the schema.
- Providers are request-scoped. Created in
send_to_provider(), GC'd after. No global instance.
- All config reads go through the facade —
config.get() or config.materialize().
materialize() expands $preset model aliases; get()/inspect() keep them raw. The store holds the raw alias (source of truth); expansion is a read-time concern, so no caller has to remember a separate resolve step.
- DISCOVER callbacks lazy-require.
definition.lua is pure data — inline require() in callbacks avoids circular deps.
auto_approve always defaults to { "$standard" }. "Not configured" doesn't exist.
- Hybrid objects (
tools) support sub-key navigation AND list ops on the same path.
Reference Files
Read these when you need depth on a specific area:
| File | When to read |
|---|
schema-dsl.md | Adding config fields, extending schema, understanding validation/coerce |
store-and-proxy.md | Resolution mechanics, proxy behavior, lens composition, write patterns |
patterns-and-pitfalls.md | Writing/testing config-dependent code, avoiding known gotchas |
All reference files are in this skill's directory (.claude/skills/config-system/).