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 is a delivery-and-verification layer: it holds the knowledge that has no home in your codebase, and pushes it back — verified — at the moment the agent needs it.
- Pushed, not fetched - hooks deliver memories when the agent opens a file
or when a failure it has seen before reappears. No
recallcall required. - Verified - every memory is anchored to a file/symbol/commit and
content-hashed; when that code changes, the memory arrives flagged
stale - Two types -
decision(why it is this way) andepisode(tried X, failed because Y). Conventions belong in CLAUDE.md; docs belong in docs. - Git-native - markdown in
.legendary/memories/, committed with your code, reviewed in PRs, shared with your team - Local-first - no cloud, no accounts, no API keys, no embeddings
The 30-second demo
Your agent hits an error it has hit before. Without being asked, legendary interrupts with what you already learned:
$ # agent runs the tests, sees: AttributeError: 'NoneType' has no attribute 'strip'
This failure has been seen before. Recorded episodes:
- [episode] strip() crashes on None (verified against current code):
Use a guard: data.strip() if data else "". Retries do not help.
Then someone changes the anchored function. The same memory now arrives with its trust downgraded:
- [episode] strip() crashes on None [stale - code changed since this was
written; verify before trusting]: ...
That flag is the point. Every other memory system would keep asserting the stale claim as fact.
Quick start
cd your-repo
uvx --from legendary-mcp legendary init # scaffolds .legendary/, prints MCP + hook setup
init installs the hooks for you — that is the primary channel, and it needs
no agent cooperation:
| Hook | Fires when | Delivers |
|---|---|---|
PreToolUse |
agent reads/edits a file | memories anchored to that file |
PostToolUse |
a Bash result contains a stored error signature | the episode that recorded that failure |
Optionally, paste the printed MCP snippet for agent-initiated search. Three
tools: remember, recall, deprecate.
How memories reach the agent
Two push channels, both installed by init:
File-touch (surface) — the agent opens sync/worker.py, and any memory
anchored there is injected before the tool call completes.
Error-signature (guard) — every episode stores the verbatim error
strings that produced it. When one reappears in a command's output, that
episode is pushed back. This is why episodes require triggers: an agent acts
on retrieved experience when the current situation resembles the recorded one,
and a recurring error message is the strongest resemblance signal there is.
Both dedupe per session, and both render imperatively — (verified against current code) when the anchor still hashes, [stale - ... verify before trusting] when it does not.
CLI
legendary init | search <q> | reindex | doctor | surface | guard | mcp
Recall quality
Search uses SQLite FTS5 with Porter stemming, so an agent asking about
deadlock finds a memory that says "deadlocked". Ranking is text relevance
plus overlap with the files you're editing, minus a staleness penalty — fixed
weights, no tuning. Recency is deliberately absent: a memory whose anchor still
hashes fresh has survived, and penalizing it for age would double-count what
staleness already measures.
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)"]
subgraph push["push channel - primary, no agent cooperation"]
surface["surface<br/>PreToolUse: file touched"]
guard["guard<br/>PostToolUse: error signature seen"]
end
mcp["MCP add-on<br/>remember - recall - deprecate"]
svc["service layer"]
subgraph core["core"]
store["markdown store"]
index["FTS5 index + triggers"]
anchor["anchor resolve + hash"]
stale["staleness verdicts"]
rank["ranking"]
end
end
subgraph repodir[".legendary/ in your repo"]
md["memories/*.md - committed"]
db["index.db - gitignored"]
end
agent -- "Read/Edit/Write" --> surface
agent -- "Bash output" --> guard
surface -- "injected memories" --> agent
guard -- "this failed before" --> agent
agent -. "optional search" .-> mcp
surface --> svc
guard --> svc
mcp --> 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]
triggers:
- "sqlite3.OperationalError: database is locked"
---
Tried wrapping retries in a deferred transaction - SQLITE_BUSY on lock upgrade,
and busy_timeout cannot help. Working approach: BEGIN IMMEDIATE.
Human-readable, PR-reviewable, and it merges like code.
Benchmark
Head-to-head on a task where the needed knowledge cannot be recovered from the repository, with the working tree hard-reset between sessions so memory is the only channel. n=10 per arm.
| arm | median rediscoveries in session 2 | s2 cost |
|---|---|---|
| no memory | 9.5 | $0.60 |
| mem0 | 11.5 | $0.77 |
| legendary | 1.0 | $0.60 |
legendary vs mem0: p = 0.00705. mem0 vs no memory at all: p = 0.695 - indistinguishable on this task.
We published an ablation claiming the hooks are not what wins. It has been retracted - both arms were misconfigured (one had no way to write memories at all; the other still had hooks installed, so it was the same configuration as full legendary). Which channel drives the result is unknown. Details and raw data: benchmark.
It is not a universal win. On a second scenario, where the needed knowledge was already in the model's priors, legendary had no effect and cost 54% more. It pays off on arbitrary, environment-specific knowledge - the kind with no home in a comment or a README - and is pure overhead otherwise.
An earlier benchmark of ours reported legendary losing and was retracted for measuring nothing. Full numbers, both scenarios, the retraction, two explanations we tested and rejected, and a harness bug we fixed mid-run in our own favour: benchmark.
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 + triggers) |
| 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 |
| Pushes memory without being asked | no | no | yes (hooks) |
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.2.1.tar.gz.
File metadata
- Download URL: legendary_mcp-0.2.1.tar.gz
- Upload date:
- Size: 2.2 MB
- 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 |
58d93c6b500ecb1ee6dc8c305c38a5f53001fb6a38f58c4aa487cbea9365143f
|
|
| MD5 |
5f47cfbc54f53aeec9f750ce8984591f
|
|
| BLAKE2b-256 |
aad3d52eb44b91b792260cbd800a96ecd8b34743a6f96891352bdd82df384b8a
|
File details
Details for the file legendary_mcp-0.2.1-py3-none-any.whl.
File metadata
- Download URL: legendary_mcp-0.2.1-py3-none-any.whl
- Upload date:
- Size: 24.8 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 |
4728685ab226fe4f81cd0730776810aa81393242cbc083b4121307ccac33e9ef
|
|
| MD5 |
a2a192acc7dff9861f251ac8bdd1de94
|
|
| BLAKE2b-256 |
e8a5cffcfc94e5ada6437d85dfd6d7ffc86883fdde561a0d5c5b8a5093aa195f
|