Important
This library is under development and still being dogfooded. The documentation below is AI-generated and not reviewed yet.
Feel free to use or explore (and if you do please let me know what issues you run into!), but please know this is mostly here for the sake of building in public and not considered ready for release yet.
A bi-temporal, epistemically-typed knowledge graph for AI coding-agent memory.
Owned, portable, inspectable — a structured replacement for the auto-memory
pile your agent accumulates on its own. It reads your CLAUDE.md/AGENTS.md
as high-trust instruction files and audits them against the code and the
graph.
Every fact is a reified edge carrying a metadata bundle: valid time + transaction time, confidence, epistemic class (observation / commitment / preference), source type, scope, and provenance (episode). Nothing is ever hard-deleted: contradictions close a validity interval, so the graph answers both "what do we currently believe about X" and "what did we believe in March, and why did it change."
$ bin/claim history --subject AuthService --predicate has-version
1.0.0 t-invalid: 2026-06-10T00:07:14Z (superseded)
2.0.0 t-invalid: null (current)
Onboarding is designed to be delegated. Tell your coding agent:
Set up claimgraph as this project's memory system: clone https://github.com/kirahowe/claimgraph somewhere stable (e.g.
~/tools), run itsscripts/setup.sh, then runclaim setupin this project and follow the "next" steps it prints.
Prerequisites. claimgraph runs on two native binaries — babashka (bb)
and the Datalevin pod (dtlv), no JVM — and scripts/setup.sh installs
both, so the prompt above is genuinely the whole setup. With Homebrew on
PATH it installs through each project's official tap (borkdude/brew/babashka,
huahaiy/brew/datalevin); without brew it falls back to pinned GitHub
release downloads into a user-writable dir — either way, no sudo. They are
hard requirements: claim refuses to run without bb, and claim setup
checks for dtlv before wiring anything (a SessionEnd hook without its pod
binary would just be session-end noise, so a failed preflight blocks with
the fix attached). The LLM tiers additionally want an authenticated claude CLI
(or any command via $CLAIMGRAPH_LLM_CMD) — that one is optional: without
it, extraction and judging sit out while every deterministic layer works.
Likewise optional: the TypeScript/JavaScript code analyzer shells out to a
pinned dependency-cruiser via npx, so it wants Node on PATH — a TS repo
has that by definition, and without it the analyzer just skips with a hint
while every other language still ingests.
Or by hand — the same two binaries, then one command in your project:
git clone https://github.com/kirahowe/claimgraph ~/tools/claimgraph
~/tools/claimgraph/scripts/setup.sh # babashka (bb) + the Datalevin pod (dtlv)
# via Homebrew (falls back to pinned GitHub
# downloads) + a global `claim` launcher
cd ~/your-project
claim audit # start here: score the memory pile you
# already have — read-only, no store
claim setup # the whole onboarding, one idempotent commandclaim setup creates and seeds the store (./.claimgraph/db), gitignores
it and every sibling it writes — the write lock, the curation lock and log,
the evidence dir, the oplog, the retrieval log, the format stamp — in one
marker-delimited block, so a later release that adds a sibling edits the
block instead of appending a second one (the committable artifacts are
claim dump output and .claimgraph/config.json), installs the agent skill into
.claude/skills/claimgraph/ (the judgment layer: when to consult, when to
record, how to phrase facts), and wires the ambient loop — a SessionEnd hook
so every session ends by feeding the graph and the next one starts with its
compiled view injected. Every step reports as JSON, re-running is always
safe, --dry-run shows the plan without writing, and --mcp additionally
registers the MCP front-end in .mcp.json. Any non-default location you
pass (--db, --notes-dir, --settings-file, ...) is persisted to
.claimgraph/config.json so later commands need no flags — see
Configuration.
scripts/uninstall.sh [PROJECT_DIR] [--purge] [--global] undoes claim setup in a project, by the same markers setup wrote: the hook entries, the
.mcp.json registration, the agent skill, and the gitignore and
compiled-view marker blocks. It keeps .claimgraph/ (the store and its
config) unless run with --purge, and --global removes the claim
launcher — bb and dtlv are shared tools and stay.
Every agent-assisted repo accumulates a memory pile — the auto-memory notes
Claude Code, Codex, and friends write to on their own — and nothing ever
checks it for internal consistency, or against the CLAUDE.md/AGENTS.md/
rules files you actually wrote. claim audit points claimgraph's conflict
machinery at both: it scores the pile for self-contradiction and staleness,
and — the marquee check — flags where your agent's memory contradicts your
standing instructions, an instruction-conflict, before you install
anything:
$ bin/claim audit --scorecard
87 claims extracted from 4 files
7 contradictions (opposed claims coexisting in the pile)
2 instruction conflicts (agent memory at odds with your instruction files)
12 disagreements (same subject, different values — the last one read silently wins)
9 stale (contradicted by what the code says today)
23 restatements (the same fact maintained in more than one place)
3 name clusters (AuthSvc / auth-service / AuthService)
41 KB injected per session against a ~25 KB window ** over budget **
16 KB over — largest injected: CLAUDE.md 22 KB, AGENTS.md 7 KB
(of which 8 KB is claimgraph's compiled view)
15 KB of on-demand notes scanned, not injected
It runs entirely in a throwaway in-memory store: nothing is written, the
real store is never opened, and dtlv isn't needed — the only
prerequisites are bb and an extractor command (claude -p by default).
Every finding carries verbatim quote receipts, an LLM judge pass filters
the false positives (skip it with --no-judge), and --out report.json
keeps the full JSON. The scorecard is what a terminal gets and what
--scorecard forces anywhere; piped or --json output is the findings as
JSON, so audit | jq and audit read by a human are the same run. Instruction
scanning matches what a harness actually injects — up-tree CLAUDE.md/AGENTS.md
files and the user-global one, not just the project root. The
injected-KB line only counts what a harness actually loads at session start
— instruction files plus whichever note is the harness's live inject
target — so the pile's other notes show up as on-demand KB, scanned for
consistency but never charged against the window. The staleness-vs-code prong covers every language the
analyzer registry detects (Clojure, Kotlin, TypeScript/JavaScript, plus
anything added via code-analyzers) and skips honestly when nothing is
detected; every other finding class works on any repo. The findings are precisely the
diseases the graph cures: post-adoption, staleness goes to ~zero by
construction, contradictions become tracked open conflicts, restatement
becomes reinforcement, and name drift becomes aliases.
Audit's model-call bill is stated up front: one preflight round-trip to
confirm the extractor answers, then one call per scanned file plus one per
judged conflict pair, hard-capped at --budget (default 20, the same knob
curate uses) — anything past the cap is deferred and named in the
scorecard rather than silently dropped. Calls run a few seconds to ~2
minutes each, so a typical pile scores in single-digit minutes, narrated as
progress lines on stderr (--quiet to silence them). claim audit --no-llm
runs the deterministic subset — file scan, injection arithmetic, the code
baseline — in seconds, no extractor needed. A run that can't reach its
extractor blocks before scanning anything and exits 1, instead of failing
partway through a pile with an unpredictable slice already spent.
# Mechanical code-analysis pass — no LLM, high confidence, idempotent.
# Runs every detected language analyzer (Clojure, Kotlin, TS/JS) in one pass.
# This alone replaces most of what people stuff into CLAUDE.md.
bin/claim ingest-code
# Record a human decision (a commitment — it will never be silently clobbered)
bin/claim assert --subject api-layer --predicate decided-against \
--object GraphQL --class commitment --source-type decision-record
# Record a preference
bin/claim assert --subject AuthService --predicate prefers \
--object "Result types over exceptions" --class preference
# Valid time is first-class on both ends: record history as it happened
bin/claim assert --subject svc --predicate deployed-via --object Heroku \
--object-kind literal --valid-from 2026-01-01 --valid-until 2026-03-01
# Query
bin/claim facts --entity AuthService --pretty
bin/claim facts --entity claimgraph.store --direction in # who depends on it?
bin/claim neighbor --entity AuthService --depth 2 # BFS expansion
bin/claim search "GraphQL" # full-text
bin/claim facts --entity AuthService --as-of 2026-03-01 # time travel
bin/claim history --subject AuthService --predicate depends-onScope is a namespace on the entity before it is a label on the fact. Every
entity lives in one (project by default) and a name only resolves within
it: --subject-scope / --object-scope place the entities an assert
mints, --entity-scope (--subject-scope on history) picks the scope a
read resolves in, and the entity verbs take --scope. A fact's own
--scope is carried on the claim and filtered on read — asserting --scope module:auth alone leaves the subject and object in project, which is
where the next read goes looking for them.
A query is a bare word or a flag — search "GraphQL" and search --query "GraphQL" are the same call, as are recall, coach, and outcome accepted / outcome --valence accepted. Put flags before the bare word:
option parsing stops at the first one, so search "GraphQL" --db other/db
reads the query and silently ignores the store you named.
Commands emit JSON to stdout (--pretty for humans) and JSON errors to
stderr. Three exit codes, because a typo and a failure are different
problems: 0 success, 1 the command ran and could not do what it was
asked, 2 the command line itself was wrong (unknown verb, a verb missing
its subcommand, a flag value that would not parse — the error names the
option and the value). Four verbs answer with something other than one JSON
object: dump writes JSONL, mcp speaks JSON-RPC over stdio, audit
prints its scorecard at a terminal (above), and coach --hook prints
nothing at all unless the gate fires. bin/claim version says which
claimgraph and which persisted format is running; bin/claim help is the
full verb list.
No file location is assumed. Every setting resolves through one precedence
chain — CLI flag > environment variable > .claimgraph/config.json >
default — and claim config prints each setting's resolved value, which
layer set it, and the fully resolved paths.
A setting's name is its flag, always: --notes-dir sets notes-dir. Where a
flag was renamed the old spelling still works on the verbs that took it —
--dir for the notes dir, and for ingest-code's --project,
ingest-adr's --adr-dir and each of audit's repeatable --scan-dir
(both spellings add, neither shadows); --min-confidence for judge and
consolidate's --min-verdict-confidence, which gates a verdict's own
confidence rather than filtering facts on read.
The config file is JSON keyed by the kebab-case setting names
({"harness": "codex", "notes-dir": "/mnt/notes"}), lives at
$CLAIMGRAPH_CONFIG or <project>/.claimgraph/config.json — project
defaulting to cwd, so nothing changes for a bare claim <verb> run inside the
project it manages, and a --project-taking verb resolves it (and a relative
--db) against the project it was given instead — and is committable
— claim setup writes the non-default choices you pass it, so one person's
choices hold for every writer of the repo. It carries a config-version
stamp (see below), and a key claimgraph does not recognise — a misspelling
resolves to nothing and changes nothing — is named on stderr and in claim config rather than silently ignored. Two more env vars sit below the
settings layer: $CLAIMGRAPH_DTLV (path to the pod binary, default from
$PATH) and $CLAIMGRAPH_WRITER (this machine's oplog writer id, and only
until the store has one of its own — see Maintenance).
One structured setting lives in the config file only (structured values
don't fit flags/env): a code-analyzers map tunes the language-analyzer
registry by id — override a built-in's command, disable one
("typescript": false), or add a new language whose command emits the
interchange format ({"rust": {"detect": "**.rs", "command": "my-rust-deps <roots>"}} — one JSON object per source unit with unit, file,
requires, language; JSONL or a JSON array). Rust or Python support is a
ten-line script in your repo, no claimgraph change.
claim version says what is running — the release (0.1.0-alpha), the
persisted-format version (1), and the sha of claimgraph's own checkout when
it runs from one, with "dirty": true beside it when that checkout has
uncommitted or untracked changes and the sha therefore doesn't describe the
code that ran. That is the thing to quote in a bug report.
Two numbers because they answer different questions. The release moves
whenever anything ships; the format version moves only when an old reader
would get a new file wrong, and it is the integer stamped into every
artifact that crosses a machine or a version boundary: the dump's header
line, every oplog line and applied.json, the store's <db>.version
sibling, and config-version in .claimgraph/config.json. Each of those is
gated on the integer, never on the release string. A stamp above this build's
number is refused with "upgrade claimgraph"; a stamp that isn't a format
number at all is refused differently and never overwritten, because no
claimgraph wrote it and upgrading fixes nothing. The store's stamp is a
sibling file rather than a row inside the store because Datalevin merges a
schema as it opens: a stamp in the store would be written by the very call
meant to check it. The known cost is that cp -r db/ alone leaves the stamp
behind — along with the oplog, which is the actual record.
When a new fact contradicts a currently-valid fact with the same (subject, predicate) on a single-valued predicate, the resolution defaults from the epistemic class:
- observation / preference → supersede. The predecessor's interval closes
at the successor's
--valid-from(non-lossy — it stays in history), so the new truth begins exactly where the old one ends and--as-ofbetween two versions returns exactly one. A successor backdated to start before its predecessor is a valid-time contradiction, not a handoff — it flags asbackdated-overlapinstead of inverting an interval. - commitment → flag. Both facts stay valid, the conflict is linked and the
candidatesare returned. A human decision is never silently overwritten by new evidence — it surfaces for review. - Caller override:
--on-conflict supersede|flag|ignore. - Multi-valued predicates (e.g.
depends-on) accumulate. Exact duplicates reinforce: the world (or the user) just confirmed the fact, so its disuse clock resets and its confidence may rise toward a per-source ceiling — never above it, never down, and never by repetition alone. - A stronger class escalates instead of reinforcing. Re-asserting the same claim with an explicitly stated stronger epistemic class (observation → preference → commitment) supersedes, so "this isn't just something we noticed, we decided it" reads as a change in history rather than another reinforcement of an observation — which would keep the fact fading by disuse and superseding silently. Only a class the caller stated counts: the predicate registry's default is not a statement, or every mechanical re-ingest would supersede its own facts and grow the store without bound.
- Exclusion groups widen detection across predicates: registry rows can
declare mutually-exclusive stances toward the same object (
prefers/decided-againstshare the:stancegroup;supersedes/superseded-bythe:revisiongroup), matched loosely across the entity/literal divide —decided-against "GraphQL"collides withprefers GraphQL. Stance collisions always involve a commitment, so they flag by composition. Groups are deliberately conservative: a false conflict nags the human, so only clearly-opposed pairs are declared; everything fuzzier goes to the sweep.
An invalidated fact records what closed it, in fields rather than in prose:
invalidation-kind (superseded, judged-superseded, judged-duplicate,
merge-duplicate, reconcile-duplicate, code-absent, manual),
successor (the fact-id that took its place — absent when nothing did, as
when the code stopped saying it), and invalidation-reason, a sentence for
humans that nothing parses. The kinds are an open set: a kind from a newer
writer arrives verbatim and matches nothing, rather than being coerced into
one this build happens to know.
Flagged conflicts stay open until resolved. claim conflicts lists them;
claim judge runs an LLM over each pair and classifies the relation —
contradicts, duplicate, supersedes, or compatible. By default it only
reports; with --resolve it invalidates duplicates and superseded facts and
unlinks compatible pairs, gated by --min-confidence (0.8). A contradicts
verdict is never auto-resolved — genuine contradictions always go to the
human. The judge command is pluggable like the extractor (--command /
$CLAIMGRAPH_LLM_CMD, default claude -p).
claim judge --sweep generates the candidates the write path can't see —
multiple values of a :value-exclusivity :exclusive predicate (two prefers
on one subject tend to be alternatives, not accumulation), and same-subject
facts sharing an object across predicates where one side is a decision
(depends-on X while decided-against X stands). Generation is pure and
bounded per-subject, never O(graph²); the LLM never runs on the write path.
Verdicts feed the same pipeline: compatible pairs are dropped silently,
genuine hits are linked, contradictions wait for the human. consolidate
runs the sweep as a stage.
23 curated core/* predicates across four categories (structural, procedural,
decision, provenance), each a first-class queryable row in the same store with
object-kind, cardinality, default epistemic class, and a maps-to anchor to an
established standard (PROV-O / SPDX / DOAP / Dublin Core).
Unknown predicates throw with a did-you-mean fuzzy suggestion — the cheap
defense against LLM-driven predicate proliferation. New relations are coined
in the x/* staging namespace (auto-registered with :testing status) and
promoted once proven. bin/claim predicates --usage shows what's earning
its place.
Functional core, imperative shell. All decision logic — conflict resolution,
epistemic/object-kind rules, bi-temporal filters, BFS folds, decay plans — is
pure functions over plain values in claimgraph.logic (time and fresh ids are
passed in; decisions come back as effect plans). claimgraph.core is the thin
shell that gathers store reads, asks logic for a decision, and executes the
plan. Mutation is concentrated there and in the store implementations.
CLI / skill front-end src/claimgraph/cli.clj arg parsing, JSON in/out
│
imperative shell src/claimgraph/core.clj gather reads → pure decision
│ → execute effect plan
functional core src/claimgraph/logic.clj PURE: conflict policy, temporal
│ src/claimgraph/predicates.clj filters, BFS folds, decay plans,
│ vocabulary + validation
┌────┴─────────┐
│ Store protocol│ src/claimgraph/store.clj the storage abstraction
└────┬─────────┘
│
datalevin impl src/claimgraph/store/datalevin.clj the only layer that
in-memory impl src/claimgraph/store/memory.clj knows Datalog/datoms
- Storage: Datalevin via Babashka
pod — the
dtlvbinary is a GraalVM native image that speaks the pod protocol, so the whole stack is two fast-start native binaries. The pod is loaded from$PATH(override with$CLAIMGRAPH_DTLV) at a pinned release. The storage abstraction keeps other engines pluggable; the test suite runs identically against the in-memory implementation, which is the proof of it. - Bi-temporality is modeled, not engine-native: explicit
t-valid/t-invalid/recorded-atattributes, identical in shape across backends. Valid time is settable on both ends of every write; transaction time (recorded-at) is append-only. Full XTDB-style correction history (auditing how a belief about the past evolved) is deliberately out of scope. - Objects are entities or literals (RDF-style): traversal only follows entity-kind objects; preferences live as literal facts without minting junk nodes. Enforced at write time per the predicate registry.
- Inverses are computed at query time (
--direction in|both), not stored as twins — nothing to keep consistent on invalidation. has-statusis a predicate, so ADR status history accumulates bi-temporally and status changes flow through the conflict machinery.- Entity resolution is layered: lookups resolve exact names, then aliases,
then a unique case/separator-insensitive match (type-guarded, so a namespace
can't silently match a class). A detected collision (two or more normalized
matches) never guesses AND never creates — it errors with the candidates
attached, and ingestion routes such facts to the error bucket instead of
minting a third entity. Write-path near-matches self-heal by recording the
queried name as an alias. Curation
verbs handle the rest:
entity renamekeeps the old name as an alias with facts and history intact,entity mergerepoints facts and collapses the exposed duplicates non-lossily,entity splitrecordsderived-fromlineage, andentity duplicatesreports likely-duplicate clusters.
ingest-code— mechanical multi-language analysis (no LLM) through a registry of analyzer adapters: Clojure internally via edamame, Kotlin via an internal line parse, TypeScript/JavaScript by shelling to a version-pinned dependency-cruiser via npx (missing tooling skips that analyzer with a hint, never an error), and anything else via thecode-analyzersconfig seam. Every detected language runs in one pass under one episode; all analyzers emit the same interchange format (one JSON object per source unit) and feed the same driver:defined-in,depends-on,written-infacts at 0.95 confidence under a:codeepisode ref'd to<git-sha>[+<dirty-digest>]. Each pass reconciles the store against the code: facts the analysis no longer produces (deleted files, removed requires, dropped units) are invalidated mechanically, unchanged facts no-op, a unit that moves files supersedes its old location, and imports that can't be resolved locally become external-scoped facts — never a wrong local edge. Reconciliation is language-guarded: a skipped analyzer's facts are exempt, so degradation never invalidates what it didn't look at. The graph tracks the code with no LLM in the loop, and the ambient loop keeps it fresh (see Maintenance).ingest-session(session-extractstill dispatches, and always will — the old name is in installed skills and hook command lines) — LLM extraction of durable knowledge (preferences, decisions, gotchas, conventions) from a session transcript — plain text or Claude Code session JSONL. The extractor is pluggable: defaults to an already-authenticatedclaude -p(subscription-as-judge, ~$0 marginal), overridable via--extractor/$CLAIMGRAPH_LLM_CMD. The prompt carries a bounded roster of known entities (top by fact count, with aliases) as a prior — "use these exact names when you mean them" — so the extractor aligns synonyms instead of coiningAuthSvcnext toAuthService; normalization catches typographic drift, the roster catches semantic drift, and ambiguity detection backstops both. Session-derived facts are second-class evidence by design: confidence capped at 0.7, source-typesession-log.--dry-runshows what would be ingested.ingest-notes— the ambient tier (docs/consuming-auto-memory.md): consumes the harness's auto-memory notes (Claude Code's~/.claude/projects/<project>/memory/;--harness codexfor Codex's~/.codex/memories/, making claimgraph the cross-harness consolidator — notes from both harnesses about the same project merge into one graph, entity resolution aligns their vocabularies, and restatement across harnesses reinforces) as an extraction substrate — already LLM-distilled, delta-detected per file so only changed notes reach the extractor, one episode per (file, revision) so provenance answers "which note file, at which state, said this." Notes flatten who-said-what, so everything ingests as agent inference: source-typeagent-note, confidence capped at 0.65, and never a commitment — a decision reported by a note is demoted to an observation; genuine decisions arrive viaassert. No reconciliation: the harness compacts notes under space pressure, and absence-by-compaction isn't falsity — un-restated facts just fade by disuse. The marker-delimited managed section claimgraph writes intoMEMORY.mdis stripped before hashing and extraction (the echo-loop guard), so the graph never re-consumes its own compiled view.ingest— batch JSONL (file or stdin) under one episode. Each line goes through the full conflict machinery;classis accepted as an alias for the epistemic field.assert— one fact, interactively or from a skill.
Extraction decides what to keep before knowing what a future query will
hinge on (the write-before-query barrier), so ingest-session and
ingest-notes also keep their raw input: immutable, content-addressed
artifacts in <db>.evidence/, pointed to by the episode they were extracted
under. A pointer names the function that made it — sha256-<64 hex>, a
hyphen because the string is a filename before it is an identifier — and
pointers written before the tag existed still resolve. bin/claim evidence --episode ID returns the exact bytes —
provenance past the summary, and nothing an extractor drops is
unrecoverable. Notes-as-primary, transcripts-as-fallback: the artifacts are
a local audit trail and don't ride the dump; the pointer does.
Disuse decay is a view, not a job. Facts store a base confidence and a
last-reinforced-at anchor; reads compute effective confidence — the base
halved per 90-day half-life since last reinforcement (floor 0.05) — and
return both. --min-confidence filters on the effective value, search ranks
fact hits by it, and --as-of queries see period-appropriate decay.
Commitments and decision-record facts never fade.
Reinforcement is what counts as "use": re-asserting an existing fact (a
session restates it, the code ingester re-derives it) resets its clock and
raises its base toward a per-source ceiling (decision-record 1.0, code
0.95, user-assertion 0.9, session-log 0.7, agent-note 0.65, inferred
0.6) — so a fact re-derived 500 times stays distinguishable from a human
decision. The ceiling binds at birth too: --confidence 0.99 on a
session-log fact mints it at 0.7, because reinforcement is a high-water
mark that never claws a base back down, so a fact born above its ceiling
would sit there permanently. Raising a fact's confidence means raising the
trust of the source it claims, not the number. The
ingester synergy does most of the work: every code pass reinforces what the
code still says, reconciliation invalidates what it stopped saying, and
decay is left fading the session-derived facts nobody restates. Maintenance
scans never reinforce — only intent writes do.
hooks install/hooks run— the ambient loop, automated: a Claude Code SessionEnd hook (wired byhooks installinto the project's hook settings — default.claude/settings.json, overridable via--settings-file) runsingest-code-if-changed→compile-context→ spawncurate, at the end of every session. The session's exit waits on nothing: the installed command ishooks run --detach, which opens no store and spawns that pass detached (log:<db>.capture.log), so quitting costs a process start whatever the code delta is. Runhooks runyourself and it does the same work attached, with the report on stdout. Capture is deterministic either way and never waits on a model. The code stage runs first (so the curator's entity roster and conflict ground truth are fresh) and is delta-gated: every code pass closes its episode with ref<git-sha>[+<dirty-digest>], and the stage skips in milliseconds when the current ref matches the newest code episode's — teammates' pulled changes reconcile mechanically at the next session end, no agent judgment in the loop.code-ingest: manualopts a project with an expensive analyzer out; non-git projects always run (matching manual semantics). Stages report independently — an analyzer failure never blocks the deterministic recompile. Capture in, injection out.curate— the detached run the hook hands off to, and where every model call in the ambient loop lives:ingest-notes(the just-ended session's knowledge is the freshest), thenconsolidate(enrich-only), thencompile-contextso the next session's injected view carries what curation learned. Spawned, never awaited; its output goes to<db>.curate.log. One model-call budget spans the run (--budget, default 20) and every call lands a durable outcome — a recorded verdict, a closed episode, a recorded enrichment attempt — so what is left to do is derived from the store, each run shrinks the remainder, and a converged store makes the pass a free no-op. Whatever the budget did not reach is named in the report and picked up next run. A singleton: a second curator finding the curation lease (<db>.curate.lock) held reportsalready-runningand exits 0, because the live one's work covers it. It holds no standing write lease — it takes one per applied outcome and never across a model call, so a session's capture is never queued behind somebody else's completion.compile-context— the write-back half of the ambient loop (docs/consuming-auto-memory.md): compiles the graph's current view into a marker-delimited managed section at the head of the file the harness auto-injects (Claude Code:MEMORY.md), so every session starts with it for free. Deterministic (no LLM), budgeted (12.5 KB default — half the 25 KB injection window, since the view shares it with every instruction file a harness injects), idempotent. Priority order: standing decisions (never relitigate), open conflicts, recent supersessions ("Heroku → Fly on 2026-06-02" — the what-changed briefing), top current facts by effective confidence, with code-derived facts excluded — the injected view carries only what the code can't say.ingest-notesstrips the managed section before hashing, so compile → ingest → compile is a fixed point: the graph never re-consumes its own view.consolidate— the Dreaming-style offline pass: LLM-summarizes and closes open episodes (summaries are full-text indexed, so episodic history becomes searchable — "why did we do X" is a query), judges open conflicts, sweeps for conflict candidates the write path can't see, and reportsx/*predicates earning promotion review. Falls back to a mechanical digest when the LLM is unavailable, so the pass always makes progress.judge— LLM review of open conflicts on its own (see "How conflicts resolve").- Multi-writer works the local-first way. Every mutation appends an
effect line to this writer's own append-only log in
<db>.oplog/<writer>.jsonl, stamped with a hybrid logical clock. The live store is a materialized view; the logs are the record. Each machine only appends to its own file, so any file syncer (git, rsync, Syncthing) can move logs between machines and a merge conflict cannot occur in transport.claim reconcileapplies unseen foreign effects in canonical clock order, matches entity identity by name (each machine keeps its own display name and picks up the other's as an alias), collapses claims both writers made independently (closed, not erased), and queues contradictions neither writer could see for the judge. This is deliberately not a CRDT: when two machines disagree, the job is to show a human the disagreement, and open conflicts are already how claimgraph does that. - On one machine, concurrent writers serialize through a lease
(
<db>.lock: atomic, token-guarded). A holder renews it while it works, so the 30s TTL bounds only how long a dead writer's lease outlives it — aconsolidatepass that shells out to a model fifty times no longer expires under its own duration and hands a waiting writer a lease it was right to think dead. A writer that loses the lease anyway (a suspended laptop, a hand-deleted lock file) still finishes its work and then fails withlease-lostand the report of what it wrote: nothing may report an unserialized write as a serialized one. A blocked writer waits--lease-waitms (default 5000) before erroring with the holder's name. Reads never take it. dump/load— the portability story, two-way:dumpexports everything as JSONL (the live LMDB directory is gitignored; the dump is the committable artifact),loadrestores a fresh store from it — fact/episode ids, validity intervals, invalidations with their kind and successor, and conflict links round-trip exactly (a raw restore; the conflict machinery does not re-run). The first line is a header record naming the format version, andloadrefuses anything it cannot read in full before it writes a single row: a non-empty target, a file with no header and nothing claimgraph stamped, a format above this build's, a pre-alpha dump (whose entity types were destroyed at write time — re-dump from the source store). Restoring the part it understood would leave a store that looks whole and isn't. Multi-machine users of the ambient loop converge through the committed dump.
bin/claim mcp serves the graph over MCP stdio — the store (and the
Datalevin pod) opens once per session instead of paying ~350ms of cold start
per CLI call, which the ambient loop's per-prompt coach hook made worth
fixing. Every tool is memory_ plus its CLI verb — one rule instead of a
list to memorize — so the eight are memory_facts, memory_neighbor,
memory_search, memory_recall, memory_history, memory_conflicts,
memory_coach, and memory_assert (write-lease-guarded, full conflict
machinery, valid time on both ends like assert). Argument names are
accepted in MCP's snake_case and claimgraph's kebab alike; results stay
kebab, because they are the same records claim facts prints and claim dump commits. A tool name this server doesn't have fails at the transport,
where the client's wiring is what's broken; a failure inside a tool comes
back as a result carrying the CLI's exact error payload, so the model
driving it reads the same hint a human would have seen on stderr. Wire up:
claim setup --mcp (writes the project's .mcp.json) or claude mcp add claimgraph -- claim mcp. The CLI and the skill remain the primary surface;
MCP is the low-latency second front-end the handoff doc trigger-gated.
No LongMemEval/LoCoMo equivalent exists for codebase memory, so bench/ is
the seed of one: a synthetic project (shoply) that lives through three
sessions and three code passes from January to now — decisions made and
relitigated, a namespace renamed and merged, a hosting migration, a
dependency taken up against a standing rejection, an observation nobody ever
restates. Two layers, split by determinism:
bb bench # mechanics: recorded LLM outputs, real store and ingesters,
# 33 questions across retrieval / time-travel / history /
# identity / conflicts / forgetting / provenance / ambient
# (the notes loop: restatement reinforces, planted decisions
# demote and flag, compaction ≠ falsity, echo guard holds) /
# staleness (the code contradicts session facts and standing
# decisions without anyone saying so) / abstention (refusal vs
# confabulation when the graph does not know) / poisoning (MINJA-
# style planted content: caps, decay differential, flag-not-
# override, quarantinable provenance) / shift-recovery (Recovery@0
# after the rename, the dropped dep, and the migration) /
# contamination (a swapped name — React, the in-house clojure
# queueing library — must answer from the graph). Reports
# per-read latency and the real CLI cold-start next to accuracy.
# Deterministic; non-zero exit below a perfect score, and it
# runs in the test suite as a longitudinal regression gate.
bb bench llm # quality: the same graph, a real model ($CLAIMGRAPH_LLM_CMD).
# `bb bench llm 5` judges each labeled conflict pair 5 times
# (default 3): accuracy-of-majority with per-pair flip rate —
# a pair that flips is a pair --resolve must not act on.
# Measures extraction precision/recall against annotated
# transcripts, judge verdict accuracy on labeled conflict
# pairs, and entity fragmentation (suspect names that
# normalization can't rescue). Informational; never in CI.CLAIMGRAPH_BENCH_STORE=memory runs mechanics pod-free. Fixture, questions,
and ground truth live in bench/; the harness drives the same core API the
CLI uses, so an MCP front-end won't invalidate it.
bb test # 316 tests / 1925 assertionsThe core-semantics suite runs against BOTH store implementations (the proof of
the storage abstraction); the logic suite tests pure decision functions with no
store at all; the session suite injects a fake extractor and never shells out.
CLAIMGRAPH_TEST_SKIP_DATALEVIN=1 bb test runs pod-free.
book/ holds a full-length book about the project: background and mental
model as prose, and every code-bearing chapter as a real Clojure namespace
that Clay evaluates against this source tree at build time, rendered into a
Quarto book. Build it with bb book (needs a JVM and the
quarto CLI; the tool itself needs neither), or
bb book:preview to serve it locally. Output lands in
book/rendered/_book/.
ROADMAP.md— the current build plan: 28 ordered issues from the July 2026 research round.TODO.md— earlier roadmap and decisions taken (ordering superseded byROADMAP.md).docs/agent-memory-synthesis.md— the conceptual landscape this grew from.docs/claimgraph-handoff.md— design decisions, rationale, and open forks.docs/memagent-2026-review.md— claimgraph vs the ICLR 2026 MemAgents workshop and the 2026 field: validated bets, gaps, benchmark plan.docs/memory-systems-comparison.md— field comparison (July 2026): built-in harness memory, platforms, OSS/research systems, and where claimgraph stands.docs/consuming-auto-memory.md— design note: consume the harnesses' auto-memory as an ingestion tier and compile the graph back into their injection surface (the ambient loop).docs/memory-audit.md— spec + handoff forclaim audit: the scorecard that audits your agent's memory pile together with your instruction files (contradictions, instruction conflicts, staleness, restatement, name drift, injected-vs-on-demand bytes) that runs before claimgraph is even installed. First of the three measurement tiers; shipped (src/claimgraph/audit.clj— the header notes the few implementation deviations).docs/language-adapters.md— spec + handoff for the language-adapter registry (Kotlin + TypeScript via command-shaped analyzers, a bring-your-own-analyzer config seam) and the ambient code-freshness stage (delta-gatedingest-codeinhooks run, so teammates' pushed changes reconcile mechanically). Shipped (src/claimgraph/ingest/code.clj+ the per-language adapter namespaces — the header notes the few implementation deviations)..claude/skills/claimgraph/SKILL.md— the usage judgment: when an agent should consult, write, and how to phrase facts. Generated fromresources/claimgraph/SKILL.md(the templateclaim setupinstalls into projects) — edit the template, not this copy; a test keeps them in sync.
MIT — see LICENSE.
