Code-anchored, staleness-aware, git-native memory for coding agents.
Documentation · Quickstart · Concepts · MCP tools · Comparison
Coding agents are stateless: every session re-reads your repo, re-derives old
decisions, and repeats debugging attempts that already failed. Memory
frameworks remember conversations but are code-blind - a memory never knows it
was about src/sync/worker.py:120 and never notices when that code changes.
legendary merges the two sides:
- Anchored - memories link to a file / symbol / line range at a commit
- Staleness-aware - when the anchored code changes, recall flags the
memory
stale; when it disappears,orphaned - Typed -
decision(why it is this way),episode(tried X, failed because Y),convention,reference - Git-native - memories are markdown files in
.legendary/memories/, committed with your code, diffable in PRs, shared with your whole team - Local-first - no cloud, no accounts, no API keys, no embeddings; SQLite FTS5 does search
The 30-second demo
$ legendary recall "strip None"
strip() breaks on None [fresh]
$ vim app.py # edit the anchored function
$ legendary recall "strip None"
strip() breaks on None [stale - parse changed since 8fa2c31]
That flag is the whole point. Every other memory system would still serve you that memory with full confidence.
Quick start
cd your-repo
uvx --from legendary-mcp legendary init # scaffolds .legendary/, prints MCP + hook setup
Add the printed MCP snippet to your client (Claude Code, Cursor, any MCP host). Your agent now has five tools:
| Tool | Purpose |
|---|---|
remember |
save a memory anchored to code |
recall |
search; results carry fresh/stale/orphaned flags |
list_memories |
browse by type/tag/file |
deprecate |
soft-delete with a reason |
stale_report |
all memories whose code moved on |
Optional auto-capture (Claude Code): the printed hooks run
legendary inject at session start (context injection) and
legendary extract at session end (LLM pass over the transcript, saved with
source: auto-extract provenance).
CLI
legendary init | search <q> | reindex | doctor | extract [transcript] | inject | mcp
legendary mcp serves stdio by default; --transport http serves stateless
streamable HTTP for containers and shared team deployments.
Recall quality
Search uses SQLite FTS5 with Porter stemming, so an agent asking about
deadlock finds a memory that says "deadlocked", and transactions finds
"transaction" - word-form drift between how you ask and how it was written
doesn't lose the memory. Ranking combines text relevance, overlap with the
files you're editing, recency, and a penalty for staleness.
How staleness works
At write time each anchor stores a normalized content hash of the anchored
region (symbol body, line range, or file). At recall time the region is
re-resolved (symbols may move) and re-hashed. Changed hash => stale;
missing file/region => orphaned. Stale memories still surface - the why
often survives a refactor - but ranked lower and clearly flagged.
Whitespace-only changes do not invalidate a memory, and a symbol that merely
moves down the file stays fresh, because anchors are re-resolved by symbol
before hashing.
Architecture
flowchart TB
subgraph host["MCP host - Claude Code / Cursor / Codex / any"]
agent["Coding agent"]
end
subgraph legendary["legendary (uvx --from legendary-mcp)"]
mcp["MCP server<br/>remember - recall - list_memories<br/>deprecate - stale_report"]
cli["CLI<br/>init - search - reindex - doctor<br/>extract - inject"]
svc["service layer"]
subgraph core["core"]
store["markdown store"]
index["SQLite FTS5 index"]
anchor["anchor resolve + hash"]
stale["staleness verdicts"]
rank["weighted ranking"]
end
end
subgraph repodir[".legendary/ in your repo"]
md["memories/*.md - committed"]
db["index.db - gitignored"]
end
agent -- "MCP tools (stdio)" --> mcp
agent -. "session hooks" .-> cli
mcp --> svc
cli --> svc
svc --> store
svc --> index
svc --> anchor
svc --> stale
svc --> rank
store --> md
index --> db
stateDiagram-v2
[*] --> fresh: remember() - region hashed at commit X
fresh --> stale: anchored region edited
stale --> fresh: memory re-anchored
fresh --> orphaned: file / symbol deleted
stale --> orphaned: file / symbol deleted
fresh --> deprecated: deprecate(reason)
stale --> deprecated: deprecate(reason)
orphaned --> deprecated: doctor cleanup
What a memory looks like
---
id: mem-a1b2c3d4
type: episode
title: Retry logic in sync worker breaks under SQLite WAL
created: 2026-08-14T15:30:00Z
source: agent
status: active
anchors:
- file: src/sync/worker.py
symbol: SyncWorker.run
lines: [120, 164]
commit: 8fa2c31
content_hash: sha256:9f8e...
tags: [sqlite, concurrency]
---
Tried wrapping retries in a transaction (attempt 1) - deadlocks under WAL.
Working approach: PRAGMA busy_timeout.
Human-readable, PR-reviewable, and it merges like code.
How this differs from other tools
| Graphify / Serena | mem0 / Zep | legendary | |
|---|---|---|---|
| Models code structure | yes | no | anchors only |
| Remembers decisions | no | yes | yes |
| Remembers failed attempts | no | partly | yes (episode) |
| Memories tied to code entities | n/a | no | yes |
| Detects when a memory goes stale | n/a | no | yes |
| Team-shared via git | graph committed | no (service) | yes |
| Retrieval needs an LLM | no | embeddings | no |
Code-graph tools answer "what is this code?"; legendary answers "what do we already know about it, and is that still true?" Running both is a good setup.
Documentation
Full docs at ashhadahsan.github.io/legendary — quickstart, concepts, MCP tool reference, CLI reference, benchmark, and FAQ.
Contributing? See CONTRIBUTING.md.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file legendary_mcp-0.1.1.tar.gz.
File metadata
- Download URL: legendary_mcp-0.1.1.tar.gz
- Upload date:
- Size: 177.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8c6021193c4a32224ebb2b25bdff621183895ede9526df90b3228b897e61cc0f
|
|
| MD5 |
030ec20ed844f187efed720ad156d903
|
|
| BLAKE2b-256 |
edda6e45e72e27dc4910a9e493c1cbf55b67b86c5adc1a301470710654bd5c40
|
File details
Details for the file legendary_mcp-0.1.1-py3-none-any.whl.
File metadata
- Download URL: legendary_mcp-0.1.1-py3-none-any.whl
- Upload date:
- Size: 23.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
451bf7c7320228b390dbc529f7e4810bbf0f4723fcbcb090fe19ca79f5f6de03
|
|
| MD5 |
6047523da18f0cecc165659051fb5389
|
|
| BLAKE2b-256 |
c48cfc87f1a48e2cdf5e31ffdd1447748ac593aae9ef2f6e7354097f766f18f9
|