Skip to content

Repository files navigation

memkeep

A small, deterministic CLI that keeps agent-written project memory honest.

memkeep is the reference implementation of the Earned Memory convention: a memory store where every entry is a falsifiable claim with provenance — what was learned, how it was earned, and what would make it wrong. Staleness is computed mechanically from git, with no LLM and no global re-verification. The harness owns epistemics; the agent owns ergonomics.

Status: Slices 1–3 shipped — the read path (index/status), full lint depth, and the verify write path (per-type bumps, --all-safe batch heal, --dry-run preview) are live and dogfooded in this repo's own agent-memory/ store. The opt-in ref_missing referential signal is live; multi-repo federation (sibling_missing, multi-anchor verify) is spec'd but not yet built.


Why

Two failure modes bracket agent memory: document-everything instructions produce context-file slop that measurably hurts task success, and free-form memory rots silently — diverging from reality without any visible error.

Earned Memory keeps the free-form body (that freedom is what worked) and formalizes the envelope around it: each claim carries evidence, a git anchor, and a scope answering "what would have to change for this to become wrong?" memkeep then flags claims suspect — never silently, never auto-deleting — the moment a change touches their scope.


Quick start

Requires Node.js 20+ and a working git on PATH.

npm install -g .      # build + link the `memkeep` command on $PATH
memkeep status        # run from inside a repo with an agent-memory/ store

Or build without installing:

npm run build         # → dist/index.js (runnable via `node dist/index.js status`)

This repo dogfoods itself: agent-memory/ is a real store describing memkeep's own behavior.


Commands

Every command resolves a store (an INDEX.md + entries/ directory), walks up from cwd git-style to find it, and exits 0 clean / 1 violation-or-suspect / 2 tool error.

Command What it does Exit 1 when
memkeep index Regenerates INDEX.md from the entries' frontmatter.
memkeep status Computes the suspect set and prints a summary (--json for machine output; --hook <event> for the Claude Code lifecycle hooks). any entry is suspect (plain / --json; --hook <event> always exits 0)
memkeep lint Validates frontmatter schema, the type/status taxonomies (incl. model and tombstone rules), the explains dependency graph, status-conditional evidence, scope globs, size caps, and INDEX consistency. --fix regenerates INDEX on drift. any violation
memkeep verify <id> Re-runs an entry's check (or a model's safe predictions), heals on pass (anchor := HEAD, verified_at := today), marks stale on fail. --all-safe batch-heals every suspect entry with a safe check; --dry-run previews the grouped suspect set without running or writing; --manual records an out-of-band re-test. a safe check fails (exit 1); a tool error is exit 2 and leaves the entry untouched
memkeep status --json
memkeep lint
memkeep verify npm-package
memkeep verify --all-safe --dry-run
memkeep index --store ./somewhere/else

Global flags (precedence: explicit > --store-dir > .memkeep.yml > default agent-memory):

  • --store <path> (or MEMKEEP_STORE) — use a store at an explicit path, bypassing discovery.
  • --store-dir <name> — the store directory name at the repo root.
  • -C <dir> — run as if started in dir (for store discovery).

Optional .memkeep.yml

store_dir: agent-memory       # folder name for discovery
siblings:                     # multi-repo: repo name → checkout path (context-repo entries)
  microservice-a: ../microservice-a
referential_check: false      # opt-in DOCER-style second signal (off by default; see below)

How staleness works

memkeep computes the suspect set mechanically — zero LLM, near-zero cost — as a fixpoint over the entries' explains graph:

suspect(e) =
    scope(e) ∩ files_changed(anchor(e)..HEAD) ≠ ∅      # scope_hit
  ∨ today − verified_at(e) > ttl_days(e)               # ttl_expired
  ∨ ∃ d ∈ explains(e): suspect(d) ∨ status(d)=stale    # dependency_suspect
  ∨ anchor unreachable from HEAD                        # anchor_unreachable
  ∨ referential_check ∧ body path ref ∉ HEAD tree       # ref_missing (opt-in)

Implemented: scope_hit, ttl_expired, anchor_unreachable, and dependency_suspect (transitive, cycle-safe by monotonicity — terminates in ≤ N passes even on a cyclic graph), plus the opt-in ref_missing (DESIGN §3.6).

Spec'd, not yet built: sibling_missing (multi-repo).

suspect is never stored — it is derived fresh on every read, so the flag can never itself go stale. Editing an entry never self-flags it (the entry's own path is excluded from its scope match).

ref_missing is opt-in (referential_check: true in .memkeep.yml): it flags an entry suspect when a file path referenced in its code-marked body spans (backticks / fenced blocks) no longer exists at HEAD — a cheap second signal for wrong-scope escapes the scope globs miss. Expect false positives (~24% in the nearest study), which is exactly why flags mean suspect, never stale.

One verdict per reason. status --json emits a suspect object per (entry × reason), so an entry suspect for two reasons appears twice; summary.suspect is the deduped entry count.


The store & entry format

agent-memory/                      # name is configurable
├── INDEX.md                       # generated — one line per entry: id | type | status | claim
└── entries/
    └── exit-codes.md              # one claim = one file

An entry is markdown with a YAML envelope and a free-form body — the first non-empty body line is the claim sentence (≤140 chars, lint-enforced) and the source of the INDEX line.

---
schema_version: 1
id: exit-codes
type: fact                      # fact | gotcha | recipe | topology | model
status: verified                # verified | observed | inferred | stale | tombstone
written_at: 2026-07-14
verified_at: 2026-07-14         # last time the claim itself was confirmed (≠ last edit)
anchor: 864fd19c47d...          # commit at which the claim was last confirmed
scope:                          # "what would have to change for this to become wrong?"
  - src/cli/**
evidence: |                     # the anti-slop core: what earned the claim
  Encoded in src/cli/cli.ts: the lint handler returns 1 on any violation ...
---
memkeep uses exit codes 0 pass / 1 violation-or-suspect / 2 tool error; verify propagates the wrapped check's own exit.

Claim bodies stay free-form; only the envelope is normative. Agents may add their own frontmatter fields, link entries, and structure entries/ however serves retrieval — lint validates reserved fields and ignores unknown ones.


Project layout

Path Responsibility
src/index.ts Bin entry — thin wrapper over cli.run.
src/cli Subcommand dispatch, handlers, text/JSON rendering, exit mapping.
src/entry Frontmatter model, parse/serialize, content-field classification.
src/glob Root-anchored, positive-only gitignore-style matcher.
src/stale The pure staleness core (RepoView/Clock interfaces + fixpoint compute).
src/refs Pure body path-ref extraction for the opt-in ref_missing signal (code-marked spans → path refs).
src/contracts Reason, Verdict, Summary, StatusReport JSON types.
src/git Shell-out RepoView (changedFiles, ancestorOf, head, trackedFiles).
src/check The check runner — runs an entry's check command; maps exit 0→pass / 1→fail / other→toolError (pure over a Runner seam; ShellRunner shells out via sh -c).
src/store Store discovery, precedence, .memkeep.yml, entry/INDEX IO.
src/lint Schema and contract rules + INDEX consistency.
src/verify The write path — the heal bump planner and the byte-stable frontmatter writer that verify heals with.
src/merr Categorized error (Tool/Violation) → exit-code mapping.
agent-memory/ Dogfood store (memkeep's memory about itself).

Development

npm run build         # tsup → dist/index.js
npm run typecheck     # tsc --noEmit
npm test              # vitest: unit + real-git + subprocess golden + dogfood

Built test-first throughout. src/stale is pure and depends only on the RepoView/Clock interfaces, so staleness is unit-tested with fakes; real-git behavior is covered by src/git integration tests; the CLI's exit-code contract is pinned by subprocess golden tests in test/clitest.test.ts.

Lifecycle hooks

.claude/settings.json wires three read-only, non-blocking hooks that inject the suspect summary for the working agent:

  • SessionStartmemkeep status || true — every session opens with the staleness picture (verbose even when clean).
  • PreCompactmemkeep status --hook PreCompact || true — the suspect set is re-injected right before compaction, so it survives instead of being evicted.
  • Stopmemkeep status --hook Stop || true — the suspect set is re-surfaced before the agent finishes, as a verify-before-finishing reminder.

--hook <event> emits a Claude Code additionalContext envelope (carrying <event> as the required hookEventName) and always exits 0 — it never blocks the session. A blocking Stop (exit 2) would hard-loop on a store with chronic suspects; the non-blocking nudge surfaces state and lets the agent judge. --hook <event> is silent when the store is clean, so a healthy repo adds no noise. The hooks invoke the command by name, so run npm install -g . once per machine before they will fire.


Roadmap

memkeep is built slice by slice:

  • Slice 1: read-path MVP — index / status / minimal lint + the pure staleness core, dogfooded in-tree. (shipped)
  • Slice 2: full lint depth — model/tombstone rules, the explains dependency graph, the ttl_days status cap, status-conditional evidence, and lint --fix. (shipped)
  • Slice 3: the verify write path — per-type bumps, tool-error isolation, --all-safe batch heal, --dry-run preview, the reserved environment: field. (shipped)
  • Slice 4a (shipped): the opt-in ref_missing referential staleness signal (DESIGN §3.6) — body path refs checked against the HEAD tree.
  • Slice 4 (next): multi-repo federation — the sibling_missing signal and multi-anchor (context-repo) verify.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages