diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 059d952..4f9387e 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -19,6 +19,12 @@ "source": "./plugins/glm-plan-bug", "category": "development", "description": "Submit case feedback and bug reports for GLM Coding Plan service." + }, + { + "name": "leteo", + "source": "./plugins/leteo", + "category": "development", + "description": "Persistent memory for coding agents: local-first, one binary over one SQLite database. Sessions, prompts and decisions survive compaction and restart." } ] } \ No newline at end of file diff --git a/plugins/leteo/.claude-plugin/plugin.json b/plugins/leteo/.claude-plugin/plugin.json new file mode 100644 index 0000000..41251ca --- /dev/null +++ b/plugins/leteo/.claude-plugin/plugin.json @@ -0,0 +1,11 @@ +{ + "name": "leteo", + "description": "Persistent memory for AI coding agents. Survives across sessions and compactions.", + "version": "0.4.2", + "author": { + "name": "Alexis Sanabria Lopez" + }, + "homepage": "https://github.com/asanabrial/leteo", + "repository": "https://github.com/asanabrial/leteo", + "license": "MIT" +} diff --git a/plugins/leteo/.mcp.json b/plugins/leteo/.mcp.json new file mode 100644 index 0000000..39d1fc7 --- /dev/null +++ b/plugins/leteo/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "leteo": { + "command": "leteo", + "args": ["mcp", "--tools=agent"] + } + } +} diff --git a/plugins/leteo/README.md b/plugins/leteo/README.md new file mode 100644 index 0000000..14691e2 --- /dev/null +++ b/plugins/leteo/README.md @@ -0,0 +1,77 @@ +# Leteo + +Persistent memory for coding agents: one Rust binary over one SQLite database, +local-first, no service to sign up to. + +Each session opens holding what the project already knows, prompts are kept, +sub-agent findings are captured before their context is discarded, and a +compaction is recovered from rather than survived. + +## Install + +```shell +claude plugin marketplace add zai-org/zai-coding-plugins +claude plugin install leteo@zai-coding-plugins +``` + +**The plugin carries configuration, not the binary.** Install `leteo` first, or +the MCP entry and every hook here are commands that are not on `PATH`: + +```shell +curl -fsSL https://raw.githubusercontent.com/asanabrial/leteo/main/scripts/install.sh | sh +``` + +`cargo install leteo` and a Windows script are in the +[project README](https://github.com/asanabrial/leteo#install). + +## What it registers + +| Event | Command | Effect | +| --- | --- | --- | +| `SessionStart` (`startup`, `clear`) | `leteo hook session-start` | Opens the session and hands back the project's recent work, prompts and most relevant memories | +| `SessionStart` (`compact`) | `leteo hook post-compaction` | Puts back what the compaction took, and clears what had been marked as already shown | +| `UserPromptSubmit` | `leteo hook user-prompt-submit` | Keeps the prompt, and names a memory worth having in front of you — never the same one twice in a session | +| `SubagentStop` | `leteo hook subagent-stop` | Captures a sub-agent's Key Learnings, in any of the thirteen languages Leteo speaks | +| `SessionEnd` | `leteo hook session-stop` | Closes the session | + +Alongside them, [`.mcp.json`](.mcp.json) registers the MCP server with the +`agent` tool profile, so the everyday tools are there from the first message, +and [`skills/memory`](skills/memory/SKILL.md) carries the protocol that tells +the agent when to reach for them. Tools without that protocol is the shape that +measured 0 saves out of 8 tasks. + +Every hook is best-effort and bounded: Leteo gives up before Claude Code does, +and a hook that could not do its work says so rather than failing silently. +Memory is an assistant, not a gate. + +## Alternative to the plugin + +`leteo setup claude-code --hooks` writes the same five registrations into +Claude Code's own settings directly. **Pick one, not both** — registered twice, +every lifecycle event runs twice, which stored each prompt twice on the machine +where it was found, 23 identical pairs before anything looked wrong from the +outside. `leteo setup` looks for an installed bundle and refuses rather than +adding a second copy. + +## Design + +The bundle is deliberately thin. Project detection, the stable manual session, +`` redaction, deduplication, the save reminder and its debounce, and +the `.leteo/` import all live in the Leteo binary, shared with the CLI, the MCP +server and every other agent's hooks. These files only say which event maps to +which command, so there is one implementation to keep correct instead of two. + +Which events belong here is not written out twice either: upstream holds one +list per agent and a guard reads each bundle against it, matchers and deadlines +included. Written out here and nowhere held, they drift. + +## Upstream + +Canonical source, issues and the full documentation: +. MIT licensed. Leteo is a +reimplementation of Engram; that attribution is kept in the project's `NOTICE`. + +Leteo also ships a ZCode-targeted bundle upstream, which registers three events +rather than five because that client fires neither `SubagentStop` nor +`SessionEnd`. This entry is the Claude Code one, which is what this marketplace +is for. diff --git a/plugins/leteo/hooks/hooks.json b/plugins/leteo/hooks/hooks.json new file mode 100644 index 0000000..98d8a2c --- /dev/null +++ b/plugins/leteo/hooks/hooks.json @@ -0,0 +1,64 @@ +{ + "description": "Leteo persistent memory hooks — session tracking, compaction recovery, prompt capture, and passive capture", + "hooks": { + "SessionStart": [ + { + "matcher": "startup|clear", + "hooks": [ + { + "type": "command", + "command": "leteo hook session-start", + "timeout": 10, + "statusMessage": "Loading Leteo memory..." + } + ] + }, + { + "matcher": "compact", + "hooks": [ + { + "type": "command", + "command": "leteo hook post-compaction", + "timeout": 10, + "statusMessage": "Recovering Leteo context after compaction..." + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "leteo hook user-prompt-submit", + "timeout": 5 + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "leteo hook subagent-stop", + "timeout": 10, + "async": true + } + ] + } + ], + "SessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": "leteo hook session-stop", + "timeout": 3, + "async": true + } + ] + } + ] + } +} diff --git a/plugins/leteo/skills/memory/SKILL.md b/plugins/leteo/skills/memory/SKILL.md new file mode 100644 index 0000000..9baa81d --- /dev/null +++ b/plugins/leteo/skills/memory/SKILL.md @@ -0,0 +1,304 @@ +--- +name: leteo-memory +description: "ALWAYS ACTIVE — Persistent memory protocol. You MUST save decisions, conventions, bugs, and discoveries to Leteo proactively. Do NOT wait for the user to ask." +--- + +# Leteo Persistent Memory — Protocol + +You have access to Leteo, a persistent memory system that survives across +sessions and compactions. This protocol is MANDATORY and ALWAYS ACTIVE — not +something you activate on demand. + +## AVAILABLE TOOLS + +The plugin registers Leteo with the `agent` tool profile, so the everyday tools +are present from the first message — no `ToolSearch` needed. + +- `mem_save`, `mem_search`, `mem_context`, `mem_session_summary` +- `mem_get_observation`, `mem_timeline`, `mem_suggest_topic_key`, `mem_update` +- `mem_session_start`, `mem_session_end`, `mem_save_prompt` +- `mem_current_project`, `mem_pin`, `mem_unpin`, `mem_review` +- `mem_judge`, `mem_consolidate`, `mem_compare`, `mem_capture_passive`, `mem_doctor` + +Only the three that change or count the whole store are deferred; reach for +`ToolSearch` when you need one: `mem_stats`, `mem_delete`, `mem_merge_projects`. + +**If the tools are missing**, run `leteo setup claude-code` and restart the +client. Verify the store itself with `leteo doctor`. + +## PROACTIVE SAVE TRIGGERS (mandatory — do NOT wait to be asked) + +Call `mem_save` IMMEDIATELY after any of these: + +### After decisions or conventions +- An architecture or design decision was made +- A team convention was established +- A tool or library was chosen, with its tradeoffs + +### After completing work +- A bug was fixed — record the root cause, not just the symptom +- A feature landed with a non-obvious approach +- A configuration or environment change was made + +### After discoveries +- Something non-obvious about the codebase +- A gotcha, edge case, or surprising behaviour +- A pattern worth repeating + +### After the user confirms or rejects something +- The user accepts a recommendation ("go with that", "perfect", or the + equivalent in their language) +- The user rejects an option ("no, better X") +- The user states a preference or constraint + +### Self-check after EVERY task +> "Did we just decide something, fix a bug, learn something non-obvious, or +> establish a convention? If yes, call `mem_save` now." + +## HOW TO WRITE A MEMORY + +- **title**: verb + object, short and searchable — "Fixed N+1 query in UserList" +- **type**: `bugfix` | `decision` | `policy` | `architecture` | `discovery` | + `pattern` | `config` | `preference` +- **scope**: `project` (default), `personal`, or `global` +- **topic_key** (recommended for topics that evolve): a stable key such as + `architecture/auth-model`, so later saves supersede rather than duplicate +- **content**: + - **What**: one sentence + - **Why**: what motivated it + - **Where**: files or paths affected + - **Learned**: gotchas and surprises (omit when there are none) + +Write memories someone else could act on months later. Convert relative dates +("yesterday", "last sprint") into absolute ones. + +### Dense, not short + +The goal is facts per token, not fewer tokens. A vague memory is cheap and +worthless; a precise one costs a little more and is worth reading. Written +loosely and written tightly, the same bug fix came out at 115 and 137 tokens — +and only the longer one named the error, the files, the numbers, the root cause +and how to reproduce it. Optimising for length would have kept the useless one. + +So spend nothing on these: + +- **Filler and hedging.** "It's worth noting that", "basically", "essentially", + "it turns out that", "some kind of", "apparently". They carry nothing. +- **Throat-clearing.** Lead with the fact: "`delete_project` failed with + `FOREIGN KEY constraint failed`", not "we ran into an issue where...". +- **Repetition.** The title already says what happened; **What** should not + restate it, and **Why** should not restate **What**. +- **What the repository already records.** Diffs, file listings and commit + history are not memories. What is not in the code is why, and what surprised + you. + +And spend freely on these, which are what make the memory worth keeping: + +- **Names and paths.** `src/store.rs`, `DeleteProjectResult.sessions_kept`. +- **Numbers.** "27 prompts", "3404 of 3407", "20ms". A number replaces a + paragraph of qualification and costs a token. +- **Error strings, identifiers and commands, verbatim.** These are what a + future search matches on. Never paraphrase or abbreviate them. +- **The reproduction.** "reproducible from `leteo delete project --hard`" turns + a claim into something checkable. + +Write full sentences with their articles. A memory is read by a person in the +TUI as well as by an agent, and clipped grammar saves a token or two while +costing the reader a second guess about what was meant. + +## THE PROJECT A MEMORY BELONGS TO + +Leteo decides the project itself, from the session, the process override, or +the working directory. **Do not invent a project name.** If a write comes back +with `ambiguous_project`, it carries a short-lived recovery token: ask the user +which project they meant and replay the call with their answer. A write naming +a project Leteo has never seen is refused on purpose. + +## WHEN A WRITE COMES BACK `store_busy` + +Nothing happened and nothing was half-written: another Leteo process — a +lifecycle hook, another agent, the background sync — was writing at that +moment. Make the same call again. It is the one failure that is worth +retrying, and it needs no changes to what you sent. + +## CONFLICTS — when a pair is waiting on a verdict + +A save may report that the new memory looks related to existing ones. Judge each +candidate with `mem_judge`, once per candidate, using that candidate's own +`judgment_id`. + +**Settle every verdict yourself** — `related`, `compatible`, `scoped`, +`conflicts_with`, `supersedes`, `not_conflict`. Never put one to the user and +never mention it in your reply. This is Leteo's bookkeeping, not work they asked +for, and a question about two memories they do not remember writing is an +interruption charged against the time memory is supposed to be saving them. A +verdict is not final either: `mem_judge` replaces one wholesale, reason +included, so getting one wrong is a correction rather than damage. + +Judge in the same turn the candidate is reported. A pair you leave is not +deferred to later, it is dropped: `mem_judge` takes a `judgment_id` that only +`mem_save` and a session opening ever hand out, so nothing raises it again in +between. + +The session opening is the second route. When it lists pairs under **Waiting on +a verdict**, those are proposals earlier turns left behind, oldest first, with +the `judgment_id`, the category and the topic key for each. Two memories under +one topic key are revisions of each other; two under different keys rarely +conflict. That is usually enough to rule — read one with `mem_get_observation` +when it is not. A side marked `(deleted since this pair was proposed)` is a +memory removed since — nothing is left to contradict, so `not_conflict` closes +that pair in one call. A trailing line counting pairs `mem_judge` cannot settle +is not work and not yours: leave it and say nothing. + +## WHEN SEVERAL MEMORIES ARE ONE + +When the same fact was saved more than once and no single memory is the one to +keep, `mem_consolidate` writes one replacement and records a judged `supersedes` +relation from it to every source in a single transaction. The sources stop +appearing in search and context but stay readable by id with +`mem_get_observation`. For a single better revision, `mem_update` or a new save +under the same `topic_key` is the smaller move; to correct one span of a body +without rewriting the rest, `mem_update` takes `find` and `replace`, and a +`find` that is absent or matches more than once is refused with +`edit_not_found` or `edit_ambiguous`. + +## WHEN A MEMORY COMES ROUND FOR A REREAD + +A decision, a policy or a preference is saved with a date to look at it again. +When the session's opening block says memories are due, call `mem_review` with +`action: "list"`, read the ones that still matter, and mark each with +`action: "mark_reviewed"` once you have — that is what winds the clock on. A +memory whose answer has changed is a `mem_update` or a new save under the same +`topic_key`, not a note to yourself. + +## AT THE END OF A SESSION + +Call `mem_session_summary` before you say you are done. The stop hook records +the session, but the summary is what makes the next one useful. + +## HOW TO REPORT MEMORY WORK + +Leteo is the store; **Sardi** is the cat who tends it. When you tell the user +what became of their memories, say who did it: + +| What happened | The register | +| --- | --- | +| A memory was saved | 🐈 Sardi kept that one. | +| A search found prior work | 🐈 Sardi remembers three notes about this. | +| Duplicates were folded together | 🐾 Sardi merged it with the earlier decision. | +| Noise was dropped | 🗑️ Sardi discarded 42 redundant notes. | +| Work is under way | 🐈 Sardi is reading your notes... | + +Three rules make this a personality rather than an annoyance: + +1. **Once per reply, at most**, and only when there is something the user would + want to know. A mascot that narrates every tool call is noise. +2. **Your own wording.** The table is the register, not a script — a fixed + phrase repeated verbatim stops reading like a voice within a few turns. +3. **Never in an error.** A failure has to stay precise and actionable, and + there is nothing charming about a cat standing between someone and the thing + they need to fix. + +The data is never touched by any of this: tool arguments, titles, and memory +content stay plain. The voice belongs in what you say, not in what you store. + +## SAVING IS NOT REPLYING + +Memory is bookkeeping for your future self. The user never sees a tool call or +the text you store, so a memory is never an answer. + +- If the answer exists only inside a `mem_save`, the user did not receive it. +- End every turn with the complete user-facing reply, with no tool calls after + it. Save before composing that reply, not after. +- If a save runs late, still write the full answer — do not collapse it into + "saved" or "done". +- If `mem_save`, `mem_judge` or `mem_session_summary` fails or hangs, deliver + the answer anyway and mention the failure in a sentence. A slow memory + operation never blocks, truncates, or replaces what you owed the user. + +## WHEN TO SEARCH + +On any reference to past work — "remember", "what did we do", "how did we solve +it", or the same in whatever language the user writes in: + +1. `mem_context` first. It is the cheapest and covers recent sessions. +2. `mem_search` if that came up short — with the words the memory would carry, + which are not always the words of the question. A memory is written in the + language the conversation was held in, or in the one `leteo setup` pinned — + the session context says which — and the question does not always arrive in + that language. `cobertura` against a store that says `coverage` returns + nothing at all, and an empty result never says why. Identifiers, paths, + numbers and error strings cross languages unchanged, so lead with those, + and try the same idea in the language the store holds before concluding a + thing was never saved. +3. `mem_get_observation` for the whole text once something looks right. The + block a session opens with is an index, not the memories themselves: every + entry in it, the pinned ones included, is a title alone. `mem_context` gives + its newest few a short preview, marked `content_truncated` when it was cut. + Every entry carries its `#id`, and that is what to fetch by. + + Assume the answer is not in what you were handed. A memory here runs to + about two thousand characters, and the names, paths, numbers and error + strings — the part written to be worth keeping — are in the body, never in + a title and rarely in a preview. Fetch before answering from either; + guessing from one is how a version number turns into the wrong version + number. + +Search proactively too: before starting something that may have been done +before, when the user names a topic you have no context on, and on their first +message of a session if it refers to the project, a feature, or a problem. + +## SUBAGENTS + +When you hand work to a subagent, ask it to end with this section: + +``` +## Key Learnings: +1. [Something worth remembering, in one sentence of at least four words] +``` + +A hook reads the subagent's final message when it stops and keeps those lines +as memories on their own, filed under the subagent's name. Nothing else in its +output is kept, so this is the only way work done in a subagent reaches the +store — without it the subagent finishes, its context is discarded, and what it +found is gone. + +Ask only when there is something to learn. A subagent that read three files to +answer one question has no learnings, and an empty section is better than an +invented one: these are saved without review. + +## AFTER COMPACTION + +When you see a compaction notice: + +1. Call `mem_session_summary` with the compacted summary **first**. Without it, + everything from before the compaction is lost to memory as well as context. +2. Call `mem_context` to recover what earlier sessions knew. +3. Only then carry on with the work. + +## SESSION SUMMARY SHAPE + +`mem_session_summary` takes prose. The first line that is not a heading becomes +the memory's title, so make it say what the session was for — every summary +used to be called "Session summary: " and none of them could be found +again. This shape is what makes it useful to read months later: + +``` +## Goal +[What this session was for] + +## Instructions +[Preferences or constraints the user stated — omit if none] + +## Discoveries +- [Findings, gotchas, things that were not obvious] + +## Accomplished +- [What was completed, with the details that matter] + +## Next Steps +- [What is left, for whoever picks it up] + +## Relevant Files +- path/to/file — [what it does, or what changed] +```