docdog
Context management for AI agents. Your project's decisions, specs, and notes live as markdown files with YAML frontmatter — docdog gives agents a fast retrieval surface over them: hybrid search, a relationship graph, and eight MCP tools. No services, no migrations, no lock-in.
Files are the source of truth. The index is a disposable local
SQLite cache (.docdog/cache/, gitignored), rebuilt from disk by
docdog index. Deleting the cache is always safe. Deleting docdog
leaves you with a folder of readable markdown.
Why
Agents re-derive project context every session — grepping for decisions that are recorded somewhere, re-asking questions the repo already answers, missing the constraint that made yesterday's design choice. Docdog turns the corpus of decisions/specs/notes into something an agent can actually query.
Measured on docdog's own 250-record corpus (40 frozen queries with gold answers, single-shot, no reformulation on either side):
| Hit@1 | Hit@5 | MRR@10 | median tokens to locate the answer | |
|---|---|---|---|---|
| docdog hybrid search | 80% | 95% | 0.871 | ~143 |
| grep, generously ranked | 30% | 63% | 0.446 | — |
| grep, raw transcript | 3% | 5% | 0.047 | ~6,500 |
Honest caveats: the corpus is small and self-describing (agent-authored
titles and descriptions do real lifting), and the gold judgments were
made by the system's builder. Method, harness, and the frozen query set
live in tests/eval/; the full analysis is
OBS-010.
How it works
- A record is a markdown section with frontmatter:
id,title,collection,status,description, and arelationships:block. - The graph is frontmatter. Every relationship edge is born in the
source record's
relationships:block — the cache's edge table is purely derived.git diffshows you graph changes. - The cache is one embedded SQLite file: FTS5 for BM25 keyword search, in-process embeddings (ONNX, no API calls) for the vector leg, reciprocal-rank fusion to merge them.
- No migrations, ever. Schema change? The cache drops and rebuilds from disk.
- Zero service dependencies. Node 20+ is the whole install story.
First
docdog indexdownloads the embedding model once.
A record looks like this (collection: must be one your project
registers — see vertex_collections in .docdog/config.yaml):
---
id: DD-014
title: "Auth tokens move to httpOnly cookies"
collection: decisions
status: current
description: "JWTs leave localStorage; the API gains /refresh. XSS was the driver."
relationships:
- supersedes: DD-009
context: "localStorage tokens were XSS-exposed"
---
# DD-014: Auth tokens move to httpOnly cookies
Ordinary markdown body...
Quickstart
npm install -g @yasnikoff/docdog
# --template structured registers decisions/requirements/principles/…;
# the default `minimal` registers only notes + concepts, and a record
# whose collection is not registered is skipped at index time.
docdog init --name my-project --template structured
docdog index # build the cache from your scan paths
docdog search "why did we drop the legacy auth flow"
docdog serve # MCP server (init wires .mcp.json for you)
Point your agent at the MCP server and it gets the full tool surface.
Records are created by the agent (via MCP) or by hand — write markdown,
run docdog index, done. Deletion is rm + docdog index; files and
git are the archive.
Using docdog from Claude Cowork (desktop app)
docdog init wires .mcp.json for Claude Code. The Cowork desktop
app doesn't read a project's .mcp.json — register docdog serve in the
app's own config instead, and it proxies the tools into your session. See
docs/cowork-mcp-setup.md.
Indexing offline
The first docdog index downloads the embedding model
(nomic-ai/nomic-embed-text-v1, ~550 MB) from huggingface.co. On a machine
that can't reach it, drop the model files in yourself — docdog looks there
before the network:
.docdog/models/nomic-ai/nomic-embed-text-v1/
onnx/model.onnx
config.json
tokenizer.json
tokenizer_config.json
Copy them from any machine that has already indexed (they land in
node_modules/@huggingface/transformers/.cache/) or from the model's
Hugging Face page. The embed.provider: ollama alternative in
.docdog/config.yaml needs a reachable ollama server, so it is not an
offline answer by itself.
MCP tools
| Tool | What it does |
|---|---|
docdog_search |
hybrid retrieval (BM25 + vector, rank fusion) |
docdog_get |
fetch a record by id |
docdog_traverse |
walk the relationship graph from a record |
docdog_relate |
add an edge — patches the source file's frontmatter |
docdog_create |
create a record file at a caller-supplied path |
docdog_update |
patch a record's frontmatter/body |
docdog_index |
rebuild/refresh the cache |
docdog_status |
corpus and cache stats |
CLI
Every MCP tool above has a CLI counterpart wherever the shell can express
its inputs, so an agent without MCP is not a second-class caller. The two
exceptions are docdog_create and docdog_update: they take a markdown
body, which is the one input shell quoting mangles. From a shell you write
the file and run docdog index — the files are the source of truth, so
that is the same operation, not a workaround.
docdog init # create .docdog/, config, MCP wiring
docdog index # (re)index scan_paths into the embedded cache
docdog search <query> # hybrid search from the terminal
docdog get <id> # print a record
docdog traverse <id> # walk the relationship graph --depth, --direction
docdog relate <a> <b> # record an edge — patches a's frontmatter --type, --context
docdog status # corpus and cache stats — and any id two files claim
docdog suggest-edges # report undeclared id mentions as edge candidates, for review
docdog renumber <old> <new> # rename an id and rewrite every inbound edge --prose, --rename-file
docdog serve # start the MCP server
docdog add <file> # stamp frontmatter onto raw markdown
docdog split <file> # split a multi-section file into one-file-per-section
docdog gc # evict stale embedding-cache rows
docdog templates # list/inspect record templates
docdog run <name> # run a project script from .docdog/scripts/
docdog skill install # emit agent-facing navigation skills
Every read command takes --json.
docdog init also installs a git merge driver for the relationships:
block. Two branches each adding an edge to the same record is a set union,
but git diffs lines and calls it a conflict — the driver unions them, and
still conflicts loudly on anything that needs a judgment call.
Design stance
Docdog's code is strictly mechanical — ranking, indexing, file surgery.
Semantic judgment (what a record means, where it belongs, what relates
to what) stays with the agent and the user. That principle and the
others are recorded in specs/principles/, and
the whole design history is in specs/decisions/
— docdog manages its own specs, so this repo doubles as the living
demo. Point docdog search at it and ask it why it exists.
Status
Pre-1.0, self-hosted daily. Node ≥ 20; Windows, macOS, Linux. MIT.