Imported from buvis/mkdocs-zettelkasten (
AGENTS.md). Install upstream withnpx skills add buvis/mkdocs-zettelkasten. Copyright stays with the author.
AGENTS.md — mkdocs-zettelkasten
Guidance for AI agents (and humans) working in this repo. Generated by an evolution
assessment on 2026-07-09; see dev/local/evolution-findings.md for the full analysis and
dev/local/prds/roadmap.md for the work queue.
Orientation
A Zettelkasten theme + plugin for MkDocs. It renders pages as zettel cards with
backlinks, and runs entirely at mkdocs build / mkdocs serve time — there is no server.
- Plugin:
mkdocs_zettelkasten/plugin/—plugin.py(5 hooks, orchestrator),features/(one per capability, declaredepends_on),services/(the real logic),entities/zettel.py(the domain model),adapters/(markdown-render transforms),utils/,templates/(generated pages). - Theme:
mkdocs_zettelkasten/themes/zettelkasten/— html templates,css/(25 schemes),js/. - Data flow:
on_filesbuilds aPipelineContext; each feature'scompute()writes only its own context field;RelationshipMaterializer.materialize_allis the sole writer onto zettels; the theme reads the materialized fields.
Running things (READ THIS FIRST)
Use uv, not poetry. make test (~546 unit, fast), make test-e2e (~202 playwright).
If uv run fails with "No interpreter found for Python ", .python-version names an
interpreter you don't have installed. Fix the pin (see PRD 00010) — do not work around it
per-command. CI overrides the pin with explicit python-version inputs, so a broken pin is
invisible in CI and only bites local dev.
Invariants (what agents most often get wrong)
Marked [HOLDS] (true today, keep it that way) or [GAP → PRD] (not yet enforced; fixing it is queued).
- Config single-source [GAP → 00070].
ZettelkastenConfig(plugin/config.py) is the intended single source of truth for options and defaults. Do not read options from the raw mkdocs config dict in plugin logic — use the typedzk_config/ctx.config.config["extra"]is for template-facing values only. (Today some reads still bypass this; don't add new ones.) - Snippet escaping [HOLDS]. Backlink/mention snippets are rendered with
{{ ... |safe }}incontent.html, so they bypass autoescape. Any value sent to a|safesink MUST be HTML-escaped at build time before<mark>insertion (seeentities/zettel.py:418-421andservices/unlinked_mention_service.py:117-123). Never pass unescaped zettel body text to a|safesink. Add a<script>-in-body escaping test when you touch snippets. - Fail loud on resolution [GAP → 00020]. Relationship resolution must not silently
drop an unsatisfied
depends_onor an ambiguous wiki link — warn or raise. (Today both are silent; seefeature.py:63-64,zettel_store.py:44-47.) - Generated pages go to
.build/[HOLDS].tags.md,outline.md,validation.md,workflow.md,graph.jsonare written to a gitignored.build/and registered via mkdocsFileobjects. Never write generated artifacts into the sourcedocs/tree. - Rebuild idempotency [HOLDS, guard incoming → 00030]. The plugin instance persists
across
mkdocs serverebuilds. Per-zettel relationship lists (backlinks, mentions, moc_parents, suggested_links, sequence_*) must be rebuilt eachon_files, never appended across rebuilds. The pipeline context_ctxis rebuilt peron_files(plugin.py:182-190) — keep it that way. - Entity holds data, parser parses [ASPIRATIONAL → 00050]. Parsing should live in
services/zettel_parser.py, not inentities/zettel.py. Until 00050 lands, the entity still hosts the parser; don't add more parsing logic to the entity.
Conventions
- Working docs (plans, PRDs, this assessment) live in
dev/local/(gitignored). PRDs:dev/local/prds/backlog/, numbered lowest-runs-first. - Conventional commits; the release changelog is auto-generated from them.
- ruff
select = ["ALL"]with a curated ignore list (pyproject.toml). Keep it green.