ADR-020: Memory backend is à-la-carte — derived from memory_stack, declinable as none
- Status: Accepted
- Date: 2026-06-24
- Implements: epic #470 (decompose project-init into à-la-carte overlays), WS-A — #466
- Relates to: ADR-001 (deterministic scaffolder), ADR-004 (Obsidian docs integration), ADR-009 (Graphify memory preset), ADR-010 (plugin dual-ship), ADR-013/PI-189 (upgrade round-trip contract), ADR-018 (governance overlay — the opt-in overlay pattern this mirrors)
Context
The base layer force-shipped an Obsidian memory stack (.agents/vault/ +
.agents/memory/) into every scaffold. Every preset listed obsidian in its
layers, and base itself carried the vault/memory starters — so "no Obsidian"
was impossible, even though the scaffold engine has zero dependency on it. The
coupling was purely: prose links, a config.yaml block, preset layers, ADR-001
of the scaffolded project, a .gitignore section, and capabilities.py reporting.
epic #470's goal is to let a user adopt only what they need. WS-A makes the memory backend the first decomposed, self-explaining choice.
Decision
-
Move the memory content into the
obsidianoverlay.memory/**,vault/**,using-memory.md, and the scaffoldedadr-001-memory-stackmove frombaseintotemplates/obsidian/, co-located with thelint_memory.sh/dot_obsidianconfig already there.baseno longer ships a memory backend. -
Derive the memory overlays from
memory_stack, not presetlayers. A newoverlay_layers(memory_stack=...)mapsobsidian-only → [obsidian],obsidian-graphify → [obsidian, graphify](graphify always implies the obsidian vault it exports from), andnone → []. Presets dropobsidian/graphifyfromlayersand keep only thememory_stackvar. The default is"none"so memory-agnostic callers (agent_layers()) are unaffected; scaffold and upgrade pass the resolved stack explicitly at both call sites. -
Memory is default-ON for existing records, NOT a default-off overlay. Upgrade re-derives the overlay from the recorded
memory_stack(legacy records lacking the field fall back toobsidian-onlyvia the existing backfill). It is not modeled via_overlay_off_defaults(). -
One variable contract, three emit paths.
memory/obsidian/graphifyare derived frommemory_stackidentically by_build_variables,_backfill_variables, and_migrate_semantic_config:none → ("", "", ""),obsidian-only → ("true","true",""),obsidian-graphify → ("true","true","true"). The newmemorygate var drives{{#if memory}}blocks. The vault-free stack maps to thecorepreset, notload_preset("none"). -
New
corepreset +--memory none|obsidian|obsidian-graphifyflag. Precedence: flag > interactive prompt > preset var >obsidian-only. The wizard renders arich.Panelexplaining each backend before asking (the WS-D self-explaining-wizard pattern, seeded here). -
Gate prose/config, never delete it. Memory references in
AGENTS.md,project-init.md,config.yaml,skills/README,docs/README,copilot-instructions, and.gitignoreare wrapped in{{#if memory}}.
Scope boundary
core= "no memory backend", NOT "minimal". It still ships the full GitHub lifecycle and toolchain (those become à-la-carte in epic #470's later workstreams).- Shared skills that mention memory (
save_memory,session_summary,audit,status) are not gated. In plugin mode (the default) skills come from a static plugin payload that cannot carry{{#if}}, so per-backend skill gating is impossible there; rather than gate inconsistently, these ship in all backends and degrade gracefully (they use code-span instructions, not dangling markdown links).core's guarantee is: no vault/memory dirs, no memory config, no dangling links. - Backend-switching is a re-scaffold operation, and even then
config.yamlis not rewritten. Becausememory/+vault/are preserved dirs (excluded from the manifest),upgradedoes not add/remove them whenmemory_stackchanges. Andconfig.yamlis a user-owned file (PI-179): once a scaffold record exists, re-running with a different--memoryupdates the record block but leaves the visible config body untouched — so anobsidian-only → nonere-run still shows a stalememory:block. Thecore"no memory config" guarantee therefore holds for fresh scaffolds, not for in-place backend changes (Codex review, PR #473). Targeted config-body sync on a changed backend is a general config-migration concern (not memory-specific) and is out of scope for WS-A; the fresh-scaffold path is the supported one.
Consequences
- A vault-free project is possible (
--preset core/--memory none). obsidian-only/obsidian-graphify/governedrender byte-identically to before — verified by committed pre-move tree snapshots (the move is invisible to the upgrade manifest, which excludes memory/vault, so fresh-scaffold snapshots are the only thing that covers it).upgradeshows zero drift.- Adding
coredoes not change the interactive Enter-default (obsidian-only).