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/legendary — quickstart, concepts, MCP tool reference, CLI reference, benchmark, and FAQ.

Contributing? See CONTRIBUTING.md.

License

MIT

Metadata

Release files for legendary-mcp 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for legendary-mcp 0.2.1
File Size Uploaded Provenance
legendary_mcp-0.2.1.tar.gz 2.2 MB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for legendary-mcp 0.2.1
File Interpreter ABI Platform
legendary_mcp-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 2.2 MB

Release files / legendary_mcp-0.2.1.tar.gz

Download URL legendary_mcp-0.2.1.tar.gz
Size 2.2 MB
Tags Source
SHA-256 checksum
How to use checksums
58d93c6b500ecb1ee6dc8c305c38a5f53001fb6a38f58c4aa487cbea9365143f
BLAKE2b-256 checksum
How to use checksums
aad3d52eb44b91b792260cbd800a96ecd8b34743a6f96891352bdd82df384b8a
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release files / legendary_mcp-0.2.1-py3-none-any.whl

Download URL legendary_mcp-0.2.1-py3-none-any.whl
Size 24.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4728685ab226fe4f81cd0730776810aa81393242cbc083b4121307ccac33e9ef
BLAKE2b-256 checksum
How to use checksums
e8a5cffcfc94e5ada6437d85dfd6d7ffc86883fdde561a0d5c5b8a5093aa195f
Upload date
Uploaded using Trusted Publishing?
What is 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}

Security for legendary-mcp 0.2.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

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