The problem this solves
Most teams have ADRs. They sit in docs/adr/, written in good faith, referenced in onboarding, and then quietly ignored by every AI coding agent that touches the codebase. The agent has no access to them, no enforcement against them, and no way to know when code it generates violates a decision the team already made.
mneme adr import closes that gap without requiring you to rewrite your ADRs or adopt a new format. Add a single section to flag machine-readable constraints, run the import, and those decisions enter the same enforcement pipeline as any other Mneme rule — applied at whichever workflow boundary your integration supports, from pre-mutation prevention to the CI gate.
Step 1: add a Constraints section
ADR files need YAML frontmatter and an optional ## Constraints section. The rest of the body is free-form markdown and is preserved as rationale.
---
id: ADR-005
title: Brand vs Package Namespace Enforcement
status: accepted
priority: foundational
date: "2026-05-04"
scope: code
---
The package namespace is `mneme`. The brand name is "Mneme HQ".
These must never be conflated in code-bearing surfaces.
## Constraints
- FORBID_LITERAL: pip install mneme # correct: pipx install "mneme-hq>=0.11.0"
The canonical, mechanically enforced directive is FORBID_LITERAL. Its value is matched as an exact, case-sensitive token sequence — no stemming, no synonyms, no regex. When a checked artifact contains the literal, the verdict is a deterministic FAIL.
Typed rules with path applicability
Where a rule applies is a separate question from what it matches. The structured form attaches explicit path selectors to the same typed rule:
## Constraints
- FORBID_LITERAL:
value: install legacy-client
include_paths:
- "**/*.md"
exclude_paths:
- "docs/history/**"
include_paths/exclude_paths determine applicability only — they never loosen the exact-match semantics. Every check reports how each scoped rule resolved against the real target path, so you can see which rules applied, which were excluded, and why. Rules without selectors stay corpus-wide.
YAML frontmatter is required. Nygard-style **Status:** Accepted headers are not parsed. Adding a 6-line frontmatter block per file is a one-time, scriptable conversion.
Legacy directives
Three older directive kinds are still accepted for compatibility. They aren’t typed rules and can’t take include_paths: the structured form is a parse error for them. FORBID_PATH and REQUIRE_PATH never change a verdict. FORBID_DEPENDENCY does: it gives a WARN.
| Directive (legacy) | What it compiles to | Deterministically enforced? |
|---|---|---|
| FORBID_DEPENDENCY: X | A "no X" decision constraint |
WARN, whatever the path. A one-word name (mongodb) is checked on every input. A name that splits into several words (left-pad) is checked only when retrieval selects the decision, and then any one of its words triggers it. In strict mode a WARN exits 1, and the Claude Code hook blocks on it |
| FORBID_PATH: glob | Stored verbatim for visibility and audit | No |
| REQUIRE_PATH: glob | Stored verbatim for visibility and audit | No |
| FORBID_LITERAL: value | A typed rule with exact semantics | Yes: FAIL on every input its paths apply to, regardless of retrieval |
New policies should author FORBID_LITERAL first: it’s the only directive that accepts include_paths. Unknown directive kinds raise a parse error rather than being silently dropped, so a typo can’t switch enforcement off.
Step 2: preview the import
The default mode is dry-run. Nothing is written.
mneme adr import docs/adr --memory .mneme/project_memory.json
Output shows the active decision set and each parsed constraint:
ADR import preview
============================================================
Active set (4 ADRs):
[ADR-001] status=active
(no ## Constraints directives)
[ADR-005] status=active
rule: FORBID_LITERAL "pip install mneme" # correct: pipx install "mneme-hq>=0.11.0"
constraint: FORBID_PATH site/use-cases/**/Mneme HQ*
ADRs with status proposed, deprecated, or superseded are excluded from the active set. Explicit supersession via the supersedes frontmatter field is handled silently. Active-active contradictions (two accepted ADRs sharing the same scope, priority, and date) surface as diagnostics and block the apply step until resolved. Two accepted ADRs that share a scope but differ in priority or date raise no diagnostic: the import keeps the one with higher priority, or the newer one when priorities match, and leaves the other out. ADRs with no scope share one global scope and compete the same way. Give every ADR its own scope.
Step 3: apply the import
mneme adr import docs/adr --memory .mneme/project_memory.json --apply
The import writes atomically: the updated memory is serialized to a sibling temp file, then swapped in with os.replace(). A failed write leaves the original file intact. No backup files are created — the original is always recoverable from git history.
On success, the command reports what was written:
Wrote 4 decisions to .mneme/project_memory.json
Step 4: verify enforcement
Once imported, typed rules enter the MemoryStore → DecisionRetriever → check_prompt pipeline unchanged — and unlike legacy constraints, they are enforced corpus-wide: the query does not need to retrieve the decision for the rule to fire.
mneme check \
--memory .mneme/project_memory.json \
--input agent-output.sh \
--query "local development setup"
If agent-output.sh contains pip install mneme (use pipx install "mneme-hq>=0.11.0" instead):
FAIL [ADR-005] FORBID_LITERAL "pip install mneme" -- trigger: pip install mneme # correct: pipx install "mneme-hq>=0.11.0"
Brand vs Package Namespace Enforcement
Result: FAIL
A path-scoped rule additionally reports its applicability outcome against the target path (PATH APPLIED or PATH EXCLUDED lines, or ERROR PATH_APPLICABILITY_UNKNOWN when the path cannot be resolved), so scoped policies stay explainable:
PATH EXCLUDED [ADR-009] docs/history/2025-migration.md via docs/history/** -- an exclude selector matched
Result: PASS
The same input without the forbidden literal passes cleanly with Result: PASS.
Conflict handling
Same-id collision. If an incoming ADR id already exists in the target memory file's decisions[] array, the import refuses. Pass --update-existing to overwrite the existing entry in place. It updates decisions for ADRs that are still active. Deprecating, superseding or deleting an ADR file leaves its decision active and enforced. To retire one, add a new ADR that lists it under supersedes (keep the old ADR file in docs/adr; the import rejects a supersedes reference to an ADR that isn’t there) and re-import with --apply --update-existing: the old decision is marked superseded and stops producing verdicts. Don’t remove it from the memory file by hand; from 0.10.0, a memory file whose decisions no longer match its decision index fails to load.
Active-active contradiction. If two accepted ADRs share the same scope, priority, and date, the compiler cannot pick a winner deterministically. The import surfaces this as a diagnostic and refuses --apply unless --approve-conflicts is also passed. The fix is to edit the contradicting ADRs: mark one superseded, give one a higher priority, or give one a newer date.
Dogfooding
Mneme HQ enforces its own ADRs this way. The docs/adr/ directory in the public repo contains the team's architectural decisions, and ADR-005 (namespace enforcement) is imported into the project's live memory with its FORBID_LITERAL typed rule active. When an AI-assisted task generates content using the wrong package namespace, Mneme catches it deterministically.
The runnable demo is at examples/demo-adr-import.py in the repo. It imports the ADR corpus, applies it to a fresh memory file, and shows the WARN and PASS verdicts end-to-end.