Repositories hold code. They don't hold understanding.
Every AI coding agent that touches a repo starts amnesiac: it greps, opens a few files, builds a fragile mental model in its context window, acts on it, and throws it away. The next agent pays the full cost again and reaches a slightly different conclusion. Understanding gets re-derived thousands of times, inconsistently, and never accumulates anywhere.
CodeBrain compiles a repository into a Brain — a durable, versioned, provenance-carrying model of one software system, stored beside the code and kept true on every commit — so any agent or engineer starting any lifecycle task begins informed instead of blind.
Full blueprint — vision, conceptual design, Claude Code integration, roadmap:
docs/vision.html
P6 — Cortex. Federation across repos: compose several already-built Brains at query time — composed, never re-extracted — and answer cross-repo impact queries with evidence. The vision's long-run gate for this phase (type-change propagation) is not met; what shipped is scoped honestly below.
| Phase | Scope | State |
|---|---|---|
| P0 | Schema, provenance envelope, store, diff, plugin contract | ✅ done |
| P1 | Deterministic core — L0/L1/L4/L5 extractors, Atlas | ✅ done |
| P2 | Context packs, MCP server, hooks, thin L6, eval harness | |
| P3 | Verification by execution, carry-forward, sync, drift gate | ✅ done |
| P4 | L2 behavior, L3 semantics, full L6, rigorous eval | ✅ done |
| P5 | L7 memory, write-back, disputes, decay | ✅ done; gate met on a small sample |
| P5.5 | Session instrumentation — trials, arms, scoring | ✅ built; mechanics proven on one real session (n=1) |
| P6 | Cortex — federation, cross-repo impact | ✅ shipped; type-change propagation not attempted |
Cold build. 331,000 lines of Python (the CPython standard library) → 234,268 records in 24.5s, about 7.4s per 100k LOC against a 60s gate.
Retrieval — parity is the norm, with one outlier. codebrain eval
generates its own benchmark from git history: each past commit is a task whose
correct answer is the files it changed. Packs are compared against keyword
search over the same repository, at identical k.
Run leak-free, with a Brain per case built from that commit's parent so nothing in it can know the answer:
| Repository | Files | Cases | Pack recall@k | Search recall@k | Delta | Better / same / worse |
|---|---|---|---|---|---|---|
| django | 2,928 .py |
20 | 35.0% | 35.0% | ±0.0% | 0 / 20 / 0 |
| a private TypeScript/Next.js app (1) | ~900 .ts/.tsx |
30 | 29.8% | 29.0% | +0.8% | 2 / 27 / 1 |
| a private TypeScript/Next.js app (2) | ~490 .ts/.tsx |
30 | 51.8% | 49.2% | +2.6% | 4 / 23 / 3 |
| a private multi-language desktop app | ~190, mixed Rust/JS/Python | 30 | 90.1% | 82.8% | +7.4% | 7 / 22 / 1 |
All four runs are leak-free, and all four are genuinely unfamiliar repositories the pack has no prior exposure to. Three of four land within a few points of parity — none of those deltas clear enough wins/losses at n=30 to call them more than noise around zero. Only one repository shows a real, repeatable lift. The read stands as it did at three repos: parity is the norm on file retrieval, and one repository so far is a genuine outlier — not a pattern this project can yet explain, and not a claim it makes about which repos will land where.
The fast mode — one Brain built from HEAD — reports +7.3% on django, and most of that gap is leakage. The mechanism is specific: L4 co-change coupling is computed over history that includes the commit being tested, so the precedent facet learns "these files change together" from the very commit it is being asked about. Both arms read the post-change tree, but only the pack gets that extra hint.
What the benchmark does not measure, on either repo, is everything the pack carries beyond its anchors: the contracts, the constraints, the verified runbook, the blast radius, the declared unknowns. Those are five of the six facets and they are the reason the pack exists — but "did it name the right files" cannot see them. Even at parity on file retrieval, those facets plus a budget-bounded pack may be worth it; this harness cannot demonstrate that, and pretending otherwise would be exactly the kind of confident unverified claim the whole project is built to avoid.
Measuring the other facets needs agents completing real tasks.
Write-back. codebrain eval --memory measures the same Brain with and
without a previous session in memory, so the delta is the value of write-back
and nothing else:
| Repository | Cases | With memory | Without | Delta | Better / same / worse |
|---|---|---|---|---|---|
| django | 6 | 38.9% | 16.7% | +22.2% | 4 / 2 / 0 |
| requests | 13 | 84.6% | 84.6% | ±0.0% | 0 / 13 / 0 |
| a private TypeScript/Next.js app | 10 | 70.4% | 28.7% | +41.7% | 9 / 1 / 0 |
Memory pays where retrieval is hard and does nothing where it is already easy.
On requests cold recall is already 84.6%, at its ceiling, and no mechanism
could move it. Where cold recall is lower — django at 16.7%, the third repo at
28.7% — memory shows a real lift, and the largest effect landed on the repo
with the most disambiguation to offer help with: a Next.js App Router codebase
where 241 different files are all literally named route.ts, distinguished
only by their full path. A prior session recording which exact route.ts it
touched is information keyword search structurally cannot replicate — checked
directly on one case: the earlier session and the later task touched the exact
same two files, and memory correctly re-surfaced the ambiguous one that plain
matching could not tell apart from 240 others sharing its name.
Three caveats, stated because the headline numbers are favourable:
Small samples. Six, thirteen, and ten cases. Suggestive, not conclusive.
The harness selects for the favourable case. It only pairs commits whose changed files overlap, so memory always points at files that are part of the answer. Real prior sessions frequently touch unrelated code. Read these deltas as an upper bound on a related task, not as an average over sessions — and this caveat bites hardest on the third repo, whose fast, iterative commit history produced more closely-related consecutive pairs than either of the other two.
What would settle it is traces of two agents working the same problem in sequence — a commit is the end of a session, not a handoff mid-task, so git history cannot supply them. That needs real sessions instrumented.
The knob that would inflate this further — loosening the task-overlap function until unrelated commits boost each other — is the thing not to do.
codebrain eval --cases 60 # fast, leaky, good for iteration
codebrain eval --cases 20 --rigorous # slow, leak-free, the number to quoteA Brain answers questions about one repository. Real systems are systems of systems, and no single Brain can answer "which other services call this route?" The vision's long-run target for this phase is full type-change propagation — "this field's type changed, enumerate every consumer across every repo" — and that needs schema/type extraction this project does not have. This phase does not claim to meet it.
What is buildable without inventing facts: compose several already-built Brains at query time and search each member's own source for evidence of a cross-repo reference.
# .codebrain-cortex.toml
[[member]]
name = "payments-api"
root = "../payments-api"
[[member]]
name = "billing-worker"
root = "../billing-worker"codebrain cortex roster
codebrain cortex impact "POST /v1/charges"POST /v1/charges - defined in payments-api (route)
billing-worker - 1 reference(s)
worker.py:41 requests.post(BASE + '/v1/charges', json={"amount": amount})
Scanned: billing-worker
This is textual matching over each member's own source, not a
resolved import graph or a type-checked call -- a hit is evidence
to go look at, not proof of a real dependency.
Composed, never re-extracted is the rule the whole phase turns on. Cortex
never writes to a member Brain and never merges them into a new on-disk
artifact — it reads whatever codebrain build last left in each .brain/
directory. A member that has drifted is caught by its own drift gate; Cortex
is not a second copy of that job. A member Brain that fails to load is kept in
the roster with its error rather than silently dropped, because a Cortex that
quietly ignores a broken member gives a confidently incomplete answer — worse
than one that admits the gap.
Both results above come from one metric — did it name the right files — which is the single facet keyword search also covers, and which saturates on small repositories. It cannot see whether the constraints stopped a bad edit, whether the verified runbook saved a guess, or whether the blast radius prevented a regression. Those only appear when an agent actually does the work.
codebrain trial records real sessions and scores them:
codebrain trial start --task "add rate limiting" --verify # assigns an arm
# ... the agent works; hooks record edits and commands ...
codebrain trial end --verify # decides the outcome
codebrain trial reportTrial pack-vs-control — 2 recorded session(s)
with pack control
sessions 1 1
task success 100% 0%
broke the build 0% 0%
mean files edited 1.0 2.0
mean pack tokens 1023 —
NOT YET A RESULT — fewer than 10 decided sessions per arm.
The rates above are printed for inspection, not for quoting.
Three rules it is built on:
CodeBrain does not judge its own trial. Success is the repository's own test command, run before and after the session. A harness scored by the system under test will always find that the system under test is doing well.
Arms are assigned before the work, deterministically — a hash of trial name
and session id, fixed at start. Deciding afterwards which sessions count is
how honest people produce dishonest numbers.
A delta without an n is not a result. Below ten decided sessions per arm the
report says so and declines to present the comparison as a finding. A session
whose verification never ran is unknown and is excluded from every rate rather
than being quietly counted as a failure.
This collects; it produces nothing until real sessions run through it. That is the honest cost of measuring the thing that actually matters.
Requires Python 3.11+. No third-party dependencies — the deterministic core must build a Brain offline, on a locked-down machine, with no API key.
pip install -e .codebrain build && codebrain statusBrain built at .brain
1656 records · 457 nodes · 1071 edges · 128 facts
atlas: .brain\ATLAS.md
providers: census, gitmeta, history, structure-py, operations
layer records
L0 corpus █····················· 40
L1 structure ████████████████████·· 1495
L4 intent ██···················· 118
L5 operations █····················· 3
method DERIVED 304 EXTRACTED 1352
status fresh 1655 unverified 1
Other commands:
codebrain atlas --out - # the human-readable Atlas, to stdoutcodebrain providers # what can extract from this repocodebrain validate # structural problems, non-zero on failurecodebrain diff old-brain new-brain --check # compare two Brains directlyEvery command takes the Brain path positionally or as --brain.
| Provider | Layer | Reads | Produces |
|---|---|---|---|
census |
L0 | filesystem | files, sizes, language mix, primary language |
gitmeta |
L0 · L4 | git | head, branch, remote, authors, commit count |
history |
L4 | git log --numstat |
churn, hotspots, ownership, co-change coupling |
structure-py |
L1 | Python AST | modules, symbols, imports, resolved call graph |
structure-ts |
L1 | TS/JS scanner | modules, declarations, import graph, same-file call graph |
structure-rs |
L1 | Rust scanner | modules, symbols (fn/struct/enum/trait/impl), mod graph, same-file call graph |
operations |
L5 | manifests, Cargo.toml, Make, CI, Docker | build/test/run commands, pipelines, CODEOWNERS |
behavior |
L2 | AST decorators/urlpatterns, file-based routes, imports | routes (Flask/FastAPI, Django, Next.js App Router, express), entrypoints, jobs, env vars, data stores |
semantics |
L3 | the Brain itself | bounded-context candidates, ubiquitous language, entities |
constraints |
L6 | the Brain + .codebrain.toml |
reviewers, danger zones, public contracts, untested churn, policy zones |
Two extractors overlap on purpose. census guesses the repo name from the
directory (DERIVED 0.5); gitmeta reads it from the remote (EXTRACTED
0.98). The envelope settles it — a real disagreement resolved by evidence
quality rather than by whichever provider ran last.
codebrain pack "add rate limiting to the payments API"CONTEXT PACK · task: add rate limiting to the payments API · 1653/6000 tokens · brain @f8ed2b1b
ANCHORS payments/api.py:44 charge_endpoint (function)
BLAST RADIUS payments/middleware.py:12 chain (direct) [EXTRACTED]
+ 43 test symbol(s) across 4 test file(s) also depend on this
CONTRACTS payments/api.py:44 charge_endpoint — 4 external caller(s)
PRECEDENT payments/settle.py changes with payments/api.py (7 shared commits, 70%)
CONSTRAINTS payments/api.py needs review from @risk-eng [DERIVED 0.80]
RUNBOOK test make test [DERIVED 0.80] (never executed)
UNKNOWNS TS/JS has no call graph — blast radius across TS call sites is incomplete
Six facets, because six kinds of ignorance cause six kinds of failure: touching the wrong code, breaking callers you did not know existed, changing a shape someone depends on, inventing a pattern the repo already has, violating a constraint, being unable to check your own work, and guessing confidently.
.mcp.json exposes the Brain as an MCP server with brain_pack,
brain_locate, brain_explain, brain_impact, brain_runbook and
brain_constraints. Five hooks push context in without the model having to ask:
| Hook | Command | Does |
|---|---|---|
| SessionStart | codebrain brief |
~400-token orientation |
| UserPromptSubmit | codebrain pack --stdin |
compiles a pack from the task, before the first action |
| PreToolUse | codebrain guard |
checks the pending edit against L6 |
| PostToolUse | codebrain touch |
marks the edited neighbourhood stale |
| Stop | codebrain learn |
(P5) |
Every hook fails open. If .brain/ is missing, stale or corrupt, hooks print
nothing, exit 0, and the session behaves exactly as it would without CodeBrain.
A Brain that can break someone's session gets uninstalled the first time it
does — so this is a tested contract, not an aspiration.
guard warns by default and never blocks. Denying an edit on inferred evidence
would get the hook removed; hard gates wait for real compliance zones in P4, and
are opt-in via --deny-guarded until then.
L3 is the layer people expect an LLM to write, and the temptation is to have one narrate the domain and call the result knowledge. That would be expensive on every build and would produce confident prose nobody can check.
So the part that is derivable is derived. Bounded contexts come from import
cohesion — a directory whose modules import each other far more than they import
outward is a boundary, whatever anyone calls it. Ubiquitous language comes
from the vocabulary the code actually uses, weighted by how widely each term is
shared. On this repository that yields extract, provider, applies, brain, pack, report, constraints — which is, in fact, what this codebase is about.
What is left — business rules, entity relationships, why a boundary sits where
it does — needs a model, and the provider says so in a semantics_coverage_gap
fact rather than leaving the absence to be mistaken for "this repository has no
domain". An LLM-backed provider can register alongside it; its claims will be
INFERRED, ranked below these, and overridden by any human who disagrees.
Everything in L6 is inferred from evidence except one thing. A team can state a
constraint outright in .codebrain.toml:
[[zone]]
name = "pci"
paths = ["payments/", "src/billing/"]
reason = "PCI scope — cardholder data"
requires = ["@risk-eng"]
block_agents = trueThese are ASSERTED, so they outrank anything the machinery derives, and they
are the only constraint permitted to stop an agent. Inferred constraints —
churn, bus factor, missing tests — warn and never block, because denying an edit
on a guess gets the hook uninstalled by lunchtime.
Everything below L7 is re-derived from the code on every build. L7 is what a session leaves behind.
codebrain remember "the limiter store is Redis, not in-process" --about payments/api.py
codebrain learn --session s1 --task "add rate limiting" --lesson "..." --outcome successIt comes back in the next relevant pack:
MEMORY
a previous session on "fix carry-forward of verified claims" succeeded,
touching codebrain/verify.py [EXTRACTED]
learned: carry_forward must match refuted claims on status, not method [INFERRED 0.60]
learned: the envelope must never be mutated during verification [ASSERTED]
Three rules keep it honest:
An agent's claim is not a human's claim. What an agent did — the files it
edited — is EXTRACTED, a fact about the session. What it concluded is
INFERRED, a reading. Only a person gets ASSERTED.
An agent may dispute, not overrule. If extraction says one thing and an agent
says another, the agent files a dispute that packs surface under UNKNOWNS; the
claim itself is untouched. A model that can demote an AST fact by asserting
harder can poison every session downstream, and it will do so confidently. A
human can overrule, with --human.
Memory ages. A lesson from four hundred commits ago is about a different codebase, so it fades on a half-life — computed at read time from the distance between then and now, never by mutating stored confidence, which would make two reads of the same Brain disagree.
"The test command is make test" is a hypothesis until something runs it.
codebrain verifyThat is a dry run. It prints exactly what it would execute and stops:
Dry run — nothing was executed. These commands came from this repository's
own manifests and would run as-is:
test python -m unittest discover -s tests -t . (from pyproject.toml)
Read them, then re-run with --yes to execute and settle the claims.
codebrain verify --yes ok test python -m unittest discover -s tests -t .
passed in 47.4s
1 claim(s) promoted to OBSERVED · 0 refuted
The claim is now OBSERVED with the exit code, duration and command recorded as
evidence. A failing command is refuted, not deleted — it keeps its place with
the reason attached, scores 0.0 confidence, can never reach a context pack, and
packs say "EXECUTED AND FAILED — do not rely on this" rather than sending the
next agent to run something already known to be broken.
Safety. verify executes commands that came out of a repository, which on
an untrusted repo is arbitrary code execution. So it never runs from a hook or
from build; it is a dry run by default and needs an explicit --yes with the
command list already on screen; only commands CodeBrain itself extracted are
candidates; servers are excluded by construction (npm start never returns, and
a verifier that hangs is worse than one that never ran); and every run is
bounded by a timeout.
codebrain syncRebuilds when something moved, and carries verified and asserted claims
forward. This is load-bearing: extraction is deterministic, so a rebuild
reproduces every EXTRACTED and DERIVED claim exactly — but it would also
overwrite the two kinds it cannot regenerate, what execution proved and what a
human stated. Without carry-forward, every build silently erases every
verification and P3 would be pointless by the next commit.
A verification only travels if the claim it proved is unchanged. Verified
make test, and the command is now pytest? The evidence is about a different
claim and is invalidated rather than carried — that would be the Brain lying
with a real receipt attached.
codebrain drift --checkRebuilds into memory, compares against the committed Brain, writes nothing, and exits non-zero when they disagree. That is the CI gate. A stale Brain misleads every agent downstream, which is worse than having no Brain at all.
DRIFT: the committed Brain no longer describes the code.
+2 -0 ~3 records
+ L1:symbol:codebrain/gitutil.py#drift_probe
~ L1:fact:|python_summary (value)
Run `codebrain sync` and commit the result.
codebrain build also writes .brain/ATLAS.md: the onboarding document that is
always true because it is generated, not maintained. What this repo is, how to
run it, the most depended-upon modules, where the risk sits (hotspots,
single-author files, coupled files), who to ask — and a closing section stating
what the Brain does not know, because a document that hides its gaps gets
trusted exactly where it is weakest.
A Brain is eight layers of three record types, each carrying one envelope.
| Layer | Answers | |
|---|---|---|
| L0 | corpus | what is this thing, and what is in it |
| L1 | structure | what exists and how is it wired |
| L2 | behavior | what it does when it runs |
| L3 | semantics | what it means in the business |
| L4 | intent | why it is like this |
| L5 | operations | how to build, test, run, ship it |
| L6 | constraints | what must not break |
| L7 | memory | what we've learned since |
Nodes are things that exist. Edges are typed relationships. Facts are
claims that aren't graph-shaped — and are the unit of verification: "the test
command is make test" is a hypothesis until CodeBrain runs it.
Every record carries a provenance envelope:
Envelope.make(
Method.EXTRACTED, # EXTRACTED | DERIVED | INFERRED | OBSERVED | ASSERTED
source="census", # which provider said so
as_of="d3f91d2", # the commit it was true at
evidence=(Evidence(path="src/api.py", start_line=44),),
)This is the honesty model, and it is load-bearing. An agent can demand
EXTRACTED-only facts before a risky refactor. Confidence decays with status —
a refuted claim scores 0.0 and can never reach a context pack. A Brain that
confabulates silently is worse than no Brain.
Conflicts between providers are settled by the envelope, not by whoever ran last, and the ordering is total — so two builds of the same inputs produce byte-identical Brains, and the drift gate never fires on noise.
.brain/
manifest.json schema version, as_of commit, providers that ran
layers/l0.jsonl … one record per line, sorted by id
.gitignore derived indexes only — never the JSONL
JSONL sorted by readable id, because the Brain is committed to the repository
it describes and reviewed on pull requests. Ids look like
L1:symbol:payments/api.py#charge_endpoint, not a3f9c2e1 — a reviewer has to
be able to read the diff.
from codebrain.providers import Provider, register
from codebrain.model import Fact, Layer, REPO
class MyProvider(Provider):
id = "mine"
layers = (Layer.L5,)
def applies(self, ctx):
return (ctx.root / "Makefile").is_file()
def extract(self, ctx):
yield Fact(layer=Layer.L5, subject=REPO, predicate="test_command",
value="make test", env=...)
register(MyProvider())Two rules: declare the layers you write, and return False from applies when
you can't run. A provider that can't run says so — it does not emit
low-confidence guesses to look busy. A provider that raises is recorded and
skipped, never fatal: a partial Brain beats no Brain.
python -m unittest discover -s tests -t .556 tests, no external test runner required.
MIT © EnterpriseX Platform