Skip to main content

legendary

Code-anchored, staleness-aware, git-native memory for coding agents.

Documentation  ·  Quickstart  ·  Concepts  ·  MCP tools  ·  Comparison

CI PyPI Python versions Docs MIT


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 recall call 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) and episode (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/legendaryquickstart, 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

legendary_mcp-0.2.1.tar.gz (2.2 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

legendary_mcp-0.2.1-py3-none-any.whl (24.8 kB view details)

Uploaded Python 3

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

Hashes for legendary_mcp-0.2.1.tar.gz
Algorithm Hash digest
SHA256 58d93c6b500ecb1ee6dc8c305c38a5f53001fb6a38f58c4aa487cbea9365143f
MD5 5f47cfbc54f53aeec9f750ce8984591f
BLAKE2b-256 aad3d52eb44b91b792260cbd800a96ecd8b34743a6f96891352bdd82df384b8a

See more details on using hashes here.

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

Hashes for legendary_mcp-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4728685ab226fe4f81cd0730776810aa81393242cbc083b4121307ccac33e9ef
MD5 a2a192acc7dff9861f251ac8bdd1de94
BLAKE2b-256 e8a5cffcfc94e5ada6437d85dfd6d7ffc86883fdde561a0d5c5b8a5093aa195f

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page