neru-add-config-option
Add or change a Neru config.toml option end to end: struct field, shared and platform defaults, validation, examples, docs, and hot-reload behavior. Use whenever a change adds, renames, or removes a k
Files2
View on GitHubSKILL.md.agents/skills/neru-add-config-option/SKILL.md
GitHubAdding a config option to Neru
Config changes fail in review when one link of the chain is missing, such as a default
that exists on macOS but not Linux, a validator that never runs, or an example
file that still shows the old key. internal/config/AGENTS.md states the
contract and which test fails per link. This skill gives the order to do it in.
Five steps are universal. Then check the conditional four, the ones that turn a one-line option into a ten-file one, and that no guide used to name.
The universal five
- Schema. Add the struct field in
internal/config/config.gowith atoml:"snake_case_name"tag matching the surrounding section. Follow the existing nesting. Options live in their feature's sub-struct, not at root. Then runjust genschema, which renders the field intoschemas/config.schema.jsonfor editors.TestConfigSchema_IsGeneratedFromTheConfigStructsfails until you do. - Shared default. Set it in
newDefaultConfig()ininternal/config/config_defaults.go. Assign the field by name even when the value you assign is the zero value, and say in a comment what the zero means, because a zero nobody wrote cannot be told from a forgotten one.TestEverySchemaFieldHasAnExplicitDefaultfails on an omission. - Example. Add the line to
configs/default-config.toml, which is the file a user copies. Comment it out if the option has no default, because an uncommented empty value asserts a default that does not exist.TestConfigOptionsAppearInTheDefaultExamplefails when the line is missing. Update the mode-specific examples (grid-only-config.toml,hints-only-config.toml,recursive-grid-only-config.toml) only if they show the affected section. They are deliberately partial, and only their keys are checked, byTestShippedExamplesWriteOnlySchemaKeys. Those four are the whole shipped set.configs/embed.golists them, and anything else inconfigs/is a working file rather than a project artifact. Onlydefault-config.tomlis embedded, and the rest are read from disk by the tests that check them. - Docs. Add the row to
docs/reference/configuration.md. That file is the single home for config reference facts. Do not also describe the option indocs/contributing/architecture.mdor README. No test catches a missing row. This is the one universal link a reviewer has to check by eye. - Platform column. Add the option's path to
PlatformSupport()ininternal/config/platform_support.go, saying which of macOS, Linux and Windows writing it actually does something on. Say so even when the answer is all three, because a column nobody wrote cannot be told from a forgotten one, andTestEveryConfigOptionDeclaresItsPlatformSupportfails on an option that is neither. A narrower column needs a note saying why, in the words the user reads, because it reaches the load-time warning, theneru doctorplatform_supportrow and the published table, which you regenerate withjust gensupportref. Aconfig.Coloris declared at the field, not at itslightanddarkleaves (docs/adr/0013-parity-is-measured-in-words-not-subsystems.md).
Steps 2 and 3 exempt three shapes, and an option with one of them passes both
tests whether or not you do the work: the light/dark leaves under a Color,
a collection that ships empty, and any field of a repeated table nobody ships
entries for (app_configs, layers). Do the work anyway. See
internal/config/AGENTS.md.
The conditional four
Each is skippable, and each is a whole file when it is not.
- Platform override. If a platform needs a different default, set it in
applyPlatformDefaults()in the matchingconfig_<os>.go(config_darwin.go,config_linux.go,config_windows.go,config_other.go). Both layers are reached viaDefaultConfig()inconfig_platform.go, so never callnewDefaultConfig()directly from feature code. Nothing checks this, so pin the difference in a test. - Validator. Add checks to the relevant
Validate*method on*Configininternal/config/validators_*.go(hints, grid, style, hotkeys, macros, pointer, appconfig, …). Invalid values must fail validation with a clear message, not get silently clamped. New enum-ish strings get an allowlist. A brand-newValidate*method must be called fromValidateWithWarningsinvalidate.go, orTestEveryConfigValidatorRunsInTheLadderfails. - Derivation helper. If the value the daemon runs on is not the value the
user writes, resolve it once after the configuration is assembled rather than
in each consumer.
internal/config/grid_labels.goandheld_repeat.goare the worked examples. The declared default stays what a user would type. - Cross-field rule. If the option only makes sense alongside another, and a
user could act on the mismatch, collect it into
Warningsso it rides out onLoadResultand reachesneru config validate.logger.Warnreaches nobody (ADR 0002,docs/adr/0002-severity-tiered-config-validation.md).
Hot reload
Config is hot-reloadable via internal/config/loader (load/watch/persist/
set-field). If the option should be settable at runtime with neru config set,
confirm the field path resolves through the loader's set-field lookup. If it
intentionally requires a restart, say so in the docs.
Tests
- Table-driven validator tests next to the validator you touched
(
validators_*_test.go), covering the default, a valid custom value, and at least one rejected value. - If the default differs per platform, pin it in
config_defaults_test.goor the platform-tagged config test.
Verify
just test-foundation # the fast cross-platform-safe slice
That one recipe now runs every package this skill's guardrails live in: the
config package, its loader, and internal/architecture.
Then the standard pre-commit gate from AGENTS.md.