{"slug": "one-schema-to-rule-them-all-the-config-v2-rewrite", "title": "One Schema to Rule Them All: The Config v2 Rewrite", "summary": "The akm project has consolidated its config layer into a single Zod schema in the 0.8.0 release, replacing approximately 1,400 lines of legacy parser code. The rewrite eliminates silent failures from unknown keys, corrupt JSON, and missing files by enforcing strict validation on typo-prone fields like registries and profiles. A new config field now requires only a one-line schema addition, with TypeScript types automatically inferred from the schema.", "body_md": "This is part sixteen in a series about managing the growing pile of skills, scripts, and context that AI coding agents depend on. The [0.8.0 release notes](https://dev.to/itlackey/akm-080-cli-redesign-task-assets-and-belief-aware-memory-335a) cover the storage and pipeline changes that shipped alongside this rewrite; [Part thirteen](https://dev.to/itlackey/from-30-minutes-to-8-how-llm-mode-reflect-works-5epl) covers how the new `profiles.improve`\n\nconfig drives the improve pipeline.\n\nConfig files are where projects go to accumulate technical debt quietly. Each new feature gets a new key. Each new key gets a new parser. Each parser has slightly different error handling, slightly different defaults, and slightly different ideas about what \"invalid\" means. Nobody notices until a user files an issue that says \"I had a typo in my config and akm just silently used defaults for three weeks.\"\n\nThat was the state of akm's config layer going into 0.8.0.\n\nThe v1 config had three top-level blocks that grew independently over two years: `llm.*`\n\nfor LLM connection settings, `agent.*`\n\nfor agent process settings, and `llm.features.*`\n\nboolean flags gating per-feature LLM calls. The features block was nested under `llm`\n\nfor historical reasons even though many features used the agent, not the LLM. The agent's per-process map lived under `agent.processes`\n\n, while LLM-gated features used `llm.features.index.metadata_enhance`\n\nstyle dotted paths.\n\nEach block had its own parser function. `parseLlmConfig`\n\n, `parseEmbeddingConfig`\n\n, `parseIndexConfig`\n\n, and a dozen more. The comment at the top of the new `config-schema.ts`\n\nis blunt about it: the Zod schema \"replaces the ~1.4k LOC of legacy per-shape parsers.\"\n\nThe problems that accumulated in that ~1.4k LOC:\n\n**Unknown keys were silently accepted.** If you wrote `llm.temperaure`\n\n(typo), the parser ignored it and fell back to the default temperature. No warning. You tuned a key that did nothing.\n\n**Bad JSON was masked.** The config loader caught JSON parse errors and fell back to `DEFAULT_CONFIG`\n\n— the compiled-in defaults. Your entire config file could be corrupt and akm would start without complaint, using defaults across the board.\n\n**Missing files fell back to defaults.** Same behavior. A missing config file and a present-but-corrupt one looked identical at runtime.\n\n**Adding a field meant adding a parser.** Want a new boolean flag under a feature? Find the right parser function, add the extraction logic, add the type declaration, add the hint string, add the test. The cost of a new field was not one line — it was a small PR touching four or five places.\n\nThe 0.8.0 rewrite consolidates all of that into `src/core/config-schema.ts`\n\n: a single Zod schema that is the source of truth for the on-disk shape.\n\nZod handles the parse, transform, and validate steps that were previously scattered across ~1.4k LOC of hand-written code. A new config field is a one-line schema addition. Type inference means the TypeScript types for `AkmConfig`\n\nare derived from the schema automatically — no parallel maintenance between the schema and the type declarations.\n\nThe schema design makes deliberate tradeoffs between strictness and resilience:\n\nThe top-level object uses `.passthrough()`\n\nso unknown future keys round-trip intact. If a user upgrades and then downgrades, keys added by the newer version survive without triggering errors on the older version. `sanitizeConfigForWrite`\n\ndecides what to strip on write.\n\nNested sub-objects use `.catch(undefined)`\n\nfor field-level shape errors so that a typo in one field does not destroy an otherwise valid config. This preserves the legacy parser's warn-and-ignore semantics for individual fields while still catching structural problems.\n\n`.strict()`\n\nwalls gate the records that are most typo-prone: `registries[]`\n\n, `sources[]`\n\n, and `profiles.*`\n\nsub-shapes. A typo in a profile name or a source type now produces a validation error at load time.\n\nTwo cases are hard-rejected by `superRefine`\n\nrather than silently dropped: the old `stashes[]`\n\nkey (replaced by `sources[]`\n\n) and a legacy source type that had been removed. Both have explicit migration paths — silently ignoring them would mask user data loss.\n\nThe new loader changed three behaviors that were causing silent failures in the field.\n\n**Unknown keys now error at the profile level.** A typo in `profiles.llm.my-profile`\n\nis caught at load time rather than ignored. The error message names the unexpected key and points at the profile block.\n\n**Bad JSON now throws.** If `config.json`\n\nis not valid JSON, akm throws a `ConfigError`\n\nwith the file path and the parse error. No fallback to defaults. The user finds out immediately.\n\n**Missing files stay missing.** A missing config file is a different situation from a corrupt one, and akm treats them differently now. First run with no config: `akm setup`\n\nor an explicit `akm config set`\n\ncreates the file. A missing file during a subsequent run is an error, not a silent fallback.\n\nWith a Zod schema as the source of truth, generating a JSON schema for editor autocompletion is a natural output. The `schemas/akm-config.json`\n\nfile is generated from the Zod schema and checked in. A CI drift test fails if the checked-in file is out of sync with the schema source — there is no manual step to remember when adding a field.\n\nPoint your editor at the schema and you get field completion and inline documentation in `config.json`\n\n:\n\n```\n{\n  \"$schema\": \"https://itlackey.github.io/akm/schemas/akm-config.0.8.0.json\",\n  \"configVersion\": \"0.8.0\"\n}\n```\n\nThe `$schema`\n\nkey is optional. VSCode and other JSON Schema-aware editors pick it up automatically for field completion and inline docs.\n\nThe 0.8.0 shape replaces the scattered `llm.*`\n\n, `agent.*`\n\n, and `llm.features.*`\n\nblocks with a unified `profiles`\n\ntree and first-class feature sections.\n\n| Old location | New location |\n|---|---|\n`llm.endpoint` , `llm.model` , `llm.apiKey`\n|\n`profiles.llm.<name>.endpoint` , `.model` , `.apiKey`\n|\n`agent.platform` , `agent.bin` , `agent.args`\n|\n`profiles.agent.<name>.platform` , `.bin` , `.args`\n|\n`agent.processes.<name>.*` |\n`profiles.improve.<name>.processes.*` |\n`llm.features.index.metadata_enhance` |\n`index.metadataEnhance.enabled` |\n`llm.features.search.curate_rerank` |\n`search.curateRerank.enabled` |\n\nNamed LLM connections live under `profiles.llm.<name>`\n\n, declared once and referenced by name from process entries. Named agent connections live under `profiles.agent.<name>`\n\n. The improve profile (`profiles.improve.<name>.processes.*`\n\n) binds processes to specific LLM or agent profiles and controls per-process gating. Non-improve features (`index.metadataEnhance`\n\n, `index.stalenessDetection`\n\n, `search.curateRerank`\n\n) are first-class top-level entries.\n\nThe `configVersion`\n\nfield is the version gate. Configs without it, or with a pre-0.8.0 value, are auto-migrated at first run.\n\nThe smallest config that gets you a fully functional 0.8.0 installation with a cloud LLM for improve operations:\n\n```\n{\n  \"configVersion\": \"0.8.0\",\n  \"$schema\": \"https://itlackey.github.io/akm/schemas/akm-config.0.8.0.json\",\n  \"profiles\": {\n    \"llm\": {\n      \"openai-mini\": {\n        \"endpoint\": \"https://api.openai.com/v1/chat/completions\",\n        \"model\": \"gpt-4o-mini\",\n        \"apiKey\": \"${OPENAI_API_KEY}\",\n        \"temperature\": 0.3,\n        \"supportsJsonSchema\": true\n      }\n    },\n    \"agent\": {\n      \"opencode-default\": { \"platform\": \"opencode\", \"bin\": \"opencode\", \"args\": [\"run\"] }\n    },\n    \"improve\": {\n      \"default\": {\n        \"processes\": {\n          \"reflect\": { \"enabled\": true, \"mode\": \"llm\", \"profile\": \"openai-mini\" },\n          \"distill\": { \"enabled\": true, \"mode\": \"llm\", \"profile\": \"openai-mini\" },\n          \"consolidate\": { \"enabled\": true, \"mode\": \"llm\", \"profile\": \"openai-mini\" },\n          \"memoryInference\": { \"enabled\": true },\n          \"graphExtraction\": { \"enabled\": true, \"profile\": \"openai-mini\" }\n        }\n      }\n    }\n  },\n  \"defaults\": {\n    \"llm\": \"openai-mini\",\n    \"agent\": \"opencode-default\",\n    \"improve\": \"default\"\n  },\n  \"embedding\": {\n    \"endpoint\": \"http://localhost:11434/v1/embeddings\",\n    \"model\": \"nomic-embed-text\",\n    \"dimension\": 384\n  },\n  \"stashDir\": \"~/akm\"\n}\n```\n\nThis is a trimmed example focused on the core profiles and defaults. The full minimal config in [docs/configuration.md](https://github.com/itlackey/akm/blob/main/docs/configuration.md) also includes `feedbackDistillation`\n\n, `index`\n\n, and `search`\n\ntop-level blocks with their defaults. If you omit those blocks, akm uses compiled-in defaults for them.\n\nFor local models, swap `openai-mini`\n\nfor an Ollama or LM Studio profile and drop the `apiKey`\n\nfield. The `supportsJsonSchema`\n\nflag tells akm to use structured JSON output for providers that support it — set it to `true`\n\nfor OpenAI-compatible endpoints that honor `response_format: {type: \"json_schema\"}`\n\n, leave it off for local models that do not.\n\nIf you are on 0.7.x, you do not need to hand-edit your config. The migration command handles the key remapping:\n\n```\n# Preview the transformation without writing\nakm config migrate --dry-run\n\n# Apply migration — writes a timestamped backup first\nakm config migrate\n```\n\n`--dry-run`\n\nshows which keys move and what the new shape looks like without writing anything. When you run without `--dry-run`\n\n, akm writes a timestamped backup to `~/.cache/akm/config-backups/`\n\nbefore touching the live file. Locate it with:\n\n```\nls -1t ~/.cache/akm/config-backups/ | head\n```\n\nAuto-migration also runs on the first command after upgrade — a one-time notice prints to stderr with the backup path and a reminder to set `AKM_NO_AUTO_MIGRATE=1`\n\nto suppress future auto-migration. That env flag is useful for read-only CI mounts where you want to run `akm config migrate`\n\nexplicitly in a deploy step.\n\nAfter migration, verify the result:\n\n```\nakm config get configVersion\n# \"0.8.0\"\n\nakm config\n# Full config in the new shape\n```\n\nThe complete old-to-new key mapping is in [docs/migration/v0.7-to-v0.8.md](https://github.com/itlackey/akm/blob/main/docs/migration/v0.7-to-v0.8.md).\n\nBefore the rewrite, adding a new per-process option involved touching the parser function, the type declaration, the hint string, and the test. In the Zod schema, the same change is one line in the relevant sub-schema object. TypeScript picks up the new field automatically through inference. The JSON schema regenerates on the next build. The CI drift test catches it if the regeneration step is skipped.\n\nThe cost of that improvement is worth making concrete: the schema file is 641 LOC. The migration logic is another 643 LOC. The config loader itself is 590 LOC. That is 1,874 lines total (approximate) — replacing the ~1.4k LOC of parsers while also adding the migration pipeline, the strict validation, and the structured error reporting that were not present before. The maintenance surface per feature is lower, not higher.\n\nConfig v2 is in akm 0.8.0. The full configuration reference is in [docs/configuration.md](https://github.com/itlackey/akm/blob/main/docs/configuration.md). The [0.8.0 release notes](https://dev.to/itlackey/akm-080-cli-redesign-task-assets-and-belief-aware-memory-335a) cover the broader storage and pipeline changes that landed alongside the config rewrite. If you are running the improve pipeline and want to see how the `profiles.improve`\n\nconfig behaves in practice, [Your Agent Has a Memory That Runs While You Sleep](https://dev.to/itlackey/your-agent-has-a-memory-that-runs-while-you-sleep-20oh) covers 24 hours of autonomous operation with the full process config in place.\n\nIf you are upgrading, start with `akm config migrate --dry-run`\n\nand check that the output matches your expectations before applying.", "url": "https://wpnews.pro/news/one-schema-to-rule-them-all-the-config-v2-rewrite", "canonical_source": "https://dev.to/itlackey/one-schema-to-rule-them-all-the-config-v2-rewrite-4jek", "published_at": "2026-06-04 00:31:43+00:00", "updated_at": "2026-06-04 00:42:10.431205+00:00", "lang": "en", "topics": ["ai-tools", "ai-agents", "ai-infrastructure", "ai-products", "mlops"], "entities": ["akm", "LLM", "Config v2"], "alternates": {"html": "https://wpnews.pro/news/one-schema-to-rule-them-all-the-config-v2-rewrite", "markdown": "https://wpnews.pro/news/one-schema-to-rule-them-all-the-config-v2-rewrite.md", "text": "https://wpnews.pro/news/one-schema-to-rule-them-all-the-config-v2-rewrite.txt", "jsonld": "https://wpnews.pro/news/one-schema-to-rule-them-all-the-config-v2-rewrite.jsonld"}}