mcpField
O
okl — Observed Knowledge Ledger

okl — Observed Knowledge Ledger

Local
github.comMemory & Knowledge4Updated 2d ago

About okl — Observed Knowledge Ledger

okl stores engineering lessons for coding agents, briefs relevant records before tasks, and marks them stale when governed code changes. Checks chosen by the team prove each lesson, while push hooks or CI can gate unresolved drift.

Key features

  1. 01Brief lessons selected by task wording
  2. 02Record rules, decisions, and known defects
  3. 03Verify lessons with chosen checks
  4. 04Detect drift when governed code changes
  5. 05Gate pushes or CI on unresolved drift

Use cases

  1. 01Prevent agents from repeating known defects
  2. 02Brief agents on project conventions and decisions
  3. 03Reverify lessons after relevant code changes

MCP support

Provides MCP tools such as `okl_check` and `okl_record` for MCP agents to retrieve task briefings and record lessons. Claude Code hooks can also brief tasks automatically.

Configuration

Local server config is the same for every client. Copy it into your client’s MCP config file.

mcp.json · Universal config
{
"mcpServers": {
"emeraldleaf-okl": {
"command": "uvx",
"args": [
"observed-knowledge-ledger"
]
}
}
}
Run commanduvx observed-knowledge-ledger

Local servers need a runtime on your machine (e.g. Node.js 18+). Config file locations differ by client; see your client’s docs.

Packages
pypiobserved-knowledge-ledger0.9.0

Tools

6

Saved tool list · updated 2d ago

ParameterTypeRequiredDescription
repoanyNo
taskstringYes
limitanyNo
compactbooleanNo

Details

From the project README
okl logo: a tree's growth rings around a healed scar, in an engraved frame with okl in the gap

okl — Observed Knowledge Ledger

Verified memory for coding agents, and the loop that keeps it true.
Your rules, architecture decisions, past defects, documentation and diagrams: recorded once, briefed to your agent before every task (by a hook in Claude Code; any MCP agent can ask for the same briefing), each proven by a check you choose, flagged before you push (or in CI) when what it covers changes, and corrected or retired as you build. Prompts advise; the gate enforces. Use it in one repo, or share one store across every repo your team runs.
Under the hood: typed records, keyword search and one-step links. No embeddings by default, no model calls in the loop, no automatic capture; an optional local embedding arm exists, measured and off until you turn it on (REPORT §4q). A lesson is written on purpose and proven by a check.

PyPI Python ci okl-verify Claude Code plugin License: MIT

🚀 Quickstart · 📘 Getting started · 📥 Load your rules · 🔬 How it works · 📊 Results · 🧪 Eval report · 🧩 Plugin

pipx install 'observed-knowledge-ledger[mcp]'   # the command it installs is `okl`
okl init --repo my-repo                         # hooks, MCP server, push gate, starter lessons

42% → 7%. On eight tasks written to invite defects the store already knew about, 68 of 163 unbriefed attempts repeated one against 11 of 164 briefed, pooled over seven A/B runs with a different model judging than generating (the receipts, §5 finding 7). Small sample, one-shot code on bait tasks; what it does and does not show.

Load your rules. Hold your agent to them. Keep learning as you build. Five stages: your rules, ADRs, standards and specs plus starter lessons for your stack, loaded up front; the store, one local file; a briefing in front of the agent before every task; gated as code moves, on every push; and it keeps learning as you build, with new lessons from mistakes, drift and failing tests, updates that keep their proof and retirements that keep their reason. The briefing before every task and the session-end question are Claude Code hooks; other MCP agents can call okl_check, and the push gate works with any agent. Six cards: one shape for every lesson (when you see, do, why); scope decides reach; selected, not skimmed; a stamp is evidence, and a stamp with no run behind it is unproven; stale blocks the push; corrected, not only added. It fails closed, keeps every check run, and starts in four commands: install, okl init, okl record with the files a rule governs, and okl verify with the check that proves it.

What goes wrong, and what catches it

WhenWhat okl does
The agent is about to repeat a defect you already paid forthe briefing puts the lesson in front of it before the task, selected by the task's words
Code a lesson covers changes after its last checkokl drift goes red; the pre-push hook holds the push (or CI fails) until the check passes again or the code is back to what passed; the next briefing marks the lesson STALE
A lesson turns out to be wrongokl retire --reason keeps it, briefed as AVOID so nobody restates it; nothing is deleted
A lesson is replacedokl retire --by <new-id>; the briefing points to the replacement
The store cannot be reachedthe Claude Code hook blocks the prompt and says why; the MCP tool says it was not briefed. Never a clean "no rules apply"
A session changed files and recorded nothingthe Stop hook asks once what was learned, and names the lessons the session touched

Quickstart

New here? docs/GETTING-STARTED.md walks through setup and the habit of adding lessons as you build a feature. Already have a CLAUDE.md, ADRs, standards or specs? Load your rules shows how to load them, prove the ones that matter, and keep them current.

1. Install the CLI. Python 3.11+ (0.8.9 is the last release for 3.10), git for drift. macOS and Linux; Windows is untested (#85).1

pipx install 'observed-knowledge-ledger[mcp]'

2. Wire your repo, from its root:

okl init --repo my-repo --dry-run   # lists every file it would write; writes nothing
okl init --repo my-repo             # config, Claude Code hooks, MCP server, pre-push drift gate, starter lessons

init detects your stack (*.csproj, package.json, pyproject.toml …), sets the repo's interests from it, and fills the store with 20 starter lessons that hold on almost any codebase plus the bundled packs for your stack, so the first prompt is already briefed (--interests chooses your own subjects; --no-seed leaves the store empty). It wires Claude Code when the repo has a .claude/ directory or claude is on your PATH (--claude / --no-claude), installs a pre-push hook that blocks a push only when lessons have drifted (--no-git-hook skips it; --ci installs the GitHub workflow instead, --ci --git-hook both), and remembers both choices. Every path it writes, and what each is for: What okl init writes.

3. Add a rule of your own. The starter lessons are generic; what pays is what only your codebase knows. Tell your agent — "record an okl rule for this repo: order lookups are scoped to the signed-in customer; governs app/orders.py" — and it records the lesson with okl's okl_record MCP tool (in Claude Code, /record drafts it and asks you first). Underneath, that is one CLI call you can also run yourself:

okl record --type Rule --scope repo --id order-owner-scope \
  --title "Order lookups are scoped to the signed-in customer" \
  --symptom "an endpoint fetches an order by id with no owner filter" \
  --fix "filter by the caller's customer id in the query; return 404 on no match"

Then prove it: okl verify order-owner-scope --run "pytest -q tests/test_orders.py" --expect passed. Loading the rules, ADRs and specs you already wrote, and the bundled packs, is Seed it.

4. Check it works:

okl check --task "add an endpoint that returns an order for the logged-in user"
okl doctor                  # flags other memory tools, double wiring, and where drift is gated
Which agent, which part
Claude CodeAny MCP agent (Cursor, Codex, Copilot …)Any agent, or none
Briefing before each taska hook on every prompt, automatic, fails closedthe okl_check tool, when the agent calls itokl check in the terminal or a script
Recording what was learnedthe Stop hook asks once per session that changed filesthe okl_record toolokl record
Proof and driftokl verify, okl reverify, okl driftthe same commandsthe same commands
The gatethe pre-push hookthe pre-push hookthe pre-push hook, or the CI workflow (okl init --ci)

The hooks are Claude Code's; everything else is a command or an MCP tool and works anywhere. Setting up another agent: Getting started.

What a normal day looks like

  • You prompt as usual. The pre-task hook runs okl check on what you typed and puts the relevant lessons in the agent's context before it starts, and shows you one line — okl · briefed 9 lesson(s): … — so you can see it working (OKL_QUIET=1 hides it).
  • When you learn something worth keeping — a decision, a convention the agent broke, a bug you fixed — say so and the agent records it (okl_record, or okl record), with the files it governs. In Claude Code the Stop hook also asks once, at the end of a session that changed files, what was learned, and names the lessons the session was briefed on and those whose files it changed, so the agent can propose correcting or retiring one (you approve).
  • Using another agent? The briefing and recording are MCP tools (okl mcp); register them and add one line to your AGENTS.md: before each task call okl_check. The hooks that do this automatically are Claude Code's; see Getting started.
  • When a lesson needs correcting, take its [id] from the briefing and change only what is wrong: okl update <id> --fix "..." (or the okl_update tool) keeps the rest, including its proof, unless you change its governed --files: then it needs verifying again, and okl reverify re-runs its stored check. okl show <id> prints the whole lesson, with how often its check has passed and failed: every run of okl verify and okl reverify is kept, so a check that caught a change leaves a record. Recording an existing id again is refused, because it used to replace the lesson and wipe its proof.
  • When a lesson is wrong or replaced, retire it with a reason; nothing is deleted. okl retire <id> --reason "..." keeps a wrong one briefed as AVOID, so nobody restates it; --by <new-id> takes a replaced one out of briefings, which point to the new one; --obsolete is for a lesson whose subject is gone. A fixed defect is not retired: it keeps briefing against its return.
  • Proving a lesson is true is a check you run, not a flag you set: okl verify <id> --run "pytest -q tests/test_orders.py" --expect "passed".
  • When code a lesson governs changes, okl drift goes red until its check passes again, or the code is put back as it was when the check passed (a lesson recorded with --files is also red until its first okl verify). The briefing says so too: such a lesson is marked STALE (or UNVERIFIED, or UNPROVEN) with the file that changed, so the agent confirms it against the code instead of trusting it blindly. okl reverify re-runs each drifted lesson's stored check after you confirm. With the GitHub workflow (okl init --ci), CI reads a committed snapshot, okl-drift.json, which okl verify creates the first time a lesson with --files is verified and keeps current after that: commit it after the code change it verifies. Until then CI warns "Drift not checked", which is expected. Without the workflow, okl verify writes no snapshot unless one is already committed.
  • Headless runs (claude -p, scripts, CI agents) set OKL_DISABLED_HOOKS=encode, or the end-of-session question replaces the printed answer.

The problem it solves

A team, or an agent, fixes a subtle bug, learns why it happened, and writes a rule to prevent it. Weeks later, in a different file or a different repository, the same class of bug comes back, because the person or agent doing the new work never saw that rule. The knowledge existed; it was not in front of whoever needed it, at the moment they needed it. And when it was, it was often out of date: the code it described had moved on.

okl answers both. The relevant lessons are read automatically at the start of a task, not looked up if someone remembers to, and a lesson stays true or goes stale loudly: each is proven by a check you choose and re-opened when the code it covers changes.

This is a v0 starter, not production-hardened. It ships an end-to-end test suite (pytest -q shows the suite and its result in your environment). The core is stdlib-only with zero required dependencies.

What okl is

A store of your engineering lessons, and the machinery that keeps them true. Typed records (rules, architecture decisions, known defects, gates, tombstones, retractions) that are briefed into an agent's context before a task and go stale loudly when the code they describe moves on. Everything measured in evals/REPORT.md measures this.

It is worth being precise about what that store fills up with, because "lessons a codebase has learned" invites the picture of a bug database. In the 183-record corpus in seed/ it is mostly not that: 110 Rules, 20 Decisions and 7 Gates against 36 Defects — conventions the code follows and trade-offs already settled, not a ledger of things that broke. Count it yourself:

python3 -c "import json,glob,collections; c=collections.Counter(
  n['type'] for f in glob.glob('seed/*.json') for n in json.load(open(f))['nodes']); print(c)"

A second, optional thing ships in the package: okl scaffold, a starter kit for the in-repo discipline the store assumes (a lean rules file, mechanical gates, a review agent, an eval harness). It has never been measured; use it to get a new repo to the state where a store has something to attach to.

What okl is not for

okl holds lessons: what an agent would see or do (the symptom), what to do instead (the fix), why, and where it can be proven, the check. Most of a good agent setup is other things, and they work better elsewhere:

If it is…Put it inWhy not okl
a rule every session needs, whatever the taskCLAUDE.md / AGENTS.mdthose load in full every time; a briefing picks lessons per task
a procedure: how to release, migrate or set upa skill, runbook or scripta procedure is read whole and in order; a briefing hands over a few short records
commands, and "for X, read Y" pointersCLAUDE.md / AGENTS.md, or a skillit is a map the agent needs on every relevant task
formatting and code stylea formatter or lintera tool enforces it on every line; a lesson can only remind
a bug that is still openyour issue trackerthe tracker owns open work; okl keeps what was learned once it is fixed
secrets, credentials, personal datanowhere near oklthe store is shared and its lessons are copied into agent context

Everything with a symptom and a fix belongs here: a convention the code follows, a decision made on purpose, a defect class you have already paid for. The getting-started guide has the same split from the side of recording.

What it keeps from drifting, and how

Knowledge rots in a specific way: the code changes and everything written about the code silently stops being true. Each row below catches a different version of that, and it is worth knowing which one catches what, because they do not overlap.

DriftCaught byHow it worksFires when
A rule vs. the code it governsokl drift --gatea record declares the path globs it governs, and okl verify records the commit its check passed atthose files differ between that commit and HEAD (a verification with no recorded commit, or one this clone lacks, compares commit times instead) — or the rule has never been verified at all, so a new --files rule is red until its first okl verify — or its stamp has no observed check behind it
A retired identifier reappearing in prosecheck-tombstones.shgreps the working tree's source, docs, comments and config for every tombstoned nameany non-allowlisted hit
A withdrawn claim being restatedcheck-retractions.shgreps the working tree's markdown for the exact quoted claim from the retraction registrythe quote appears outside the registry
A doc nobody links tocheck-doc-orphans.shchecks that each top-level docs/ doc or image is named by a hub file or a docs/*.md (one hop, not transitive)nothing names it, so it drifts unread
A link pointing at a file that movedcheck-links.shreads every markdown file listed by git ls-files and checks each local link's target exists in the working treethe target does not exist
A diagram source with no rendered imagecheck-diagram-pairs.shpairs each editable source with its export; format-agnostic via OKL_DIAGRAM_SRC_EXT/OUT_EXTreviewers would see nothing. A hand-authored image with no source is noted, never failed, and a repo with no diagram sources is a clean no-op
Verification going quietly staleTTL + verified_byrecords carry when they were last verified and by which observed check; a TTL applies only to records given --ttl-days (none by default)past its TTL, a record is shown demoted rather than deleted

Three honest limits on that table:

  • Diagram content is still a human job. check-diagram-pairs.sh proves the rendered image exists; nothing proves it matches the source it was exported from, or that either matches the code. For that, name the diagram in a record's --files alongside the code it depicts, so changing the code turns the drift gate red until someone re-verifies the picture. This repo holds its README that way; its pipeline diagram is generated from the code instead, and a test fails when the committed render no longer matches.
  • Comments are covered only by the tombstone gate. It finds a retired name in .md, .py, .ts, .tsx, .cs, .yaml, .yml, .json and .sh files; the retraction gate reads markdown only. Any other stale comment is not caught, and okl reverify re-stamps a governed file without reading its comments, so a check has to test the claim a comment makes for drift to mean anything.
  • okl drift only watches what a record claims. A file no record governs is not watched by anything. Coverage is a curation decision, and the gap is invisible until something breaks — which is why the mechanical gates above scan the whole repo (the tombstone and retraction gates grep the working tree) rather than only what is enrolled.

How okl differs

  • From memory layers (mem0, Zep, Letta, the Claude Code memory plugins): they recall what the agent experienced; okl keeps what must stay true, and proves it with a check you chose.
  • From rules files (CLAUDE.md, AGENTS.md): a briefing selects the few lessons that match the task; a rules file loads whole, every time, and nothing re-checks it.
  • From instruction-file linters: they check that a path or command a rules file names still exists; okl runs your check, and blocks the push when its code moved.
  • From documentation platforms: they draft and review; okl drafts nothing and blocks a push.
Best forThe trade
A rules filethe five rules every session needsloaded whole; no proof, no per-task selection
A memory layerrecall over what an agent has seena model's judgment at use time; nothing tied to your check or your push
okllessons with a symptom, a fix and a checkyou write them on purpose, and keep them proven

The long version, with the comparison table and how to run okl beside another memory tool: Where this sits. It fails closed by design: an unreachable store blocks or warns, never reports "clean".

What the research literature measured in 2026, with figures read from the papers and a "where okl stands" column beside each result: Related work.

Measured effect, and its limits

One held-fixed A/B (8 authored tasks, 3 samples per arm per run; generator and blind judge are different models; method + raw receipts in evals/REPORT.md):

  • Same model, briefed vs not: in the first 3-sample runs (2026-08-30), defect reproduction fell 33% → 4% (sonnet) and 38% → 12% (haiku). Later sonnet runs, as the retrieval pipeline changed, read 35–50% → 4–8% (REPORT §4b–§4i, excluding the reverted §4d and the opus-judged §4f; the latest, §4i, 43% → 8%). Pooled over the seven runs on that instrument from §4 to §4h, the reverted §4d included: 68 of 163 unbriefed attempts repeated a known defect (42%) against 11 of 164 briefed (7%); REPORT §5 finding 7 names the receipts and the other ways to cut it. The unbriefed arm alone has moved 21 points between runs with nothing changed, and 26 across the §4j boundary, so the noise floor is at least 21 points (§2b, §4m). Every "reproduced" is a defect class this store had already paid to learn — a price-tamper fallback, tokens in web storage, an unpinned CI gate, a script that trusts exit 0 without checking its output, an eval summary that does not put its failure rate first — not lint noise. Two tasks (an IDOR endpoint, a React fetch) have never reproduced without the briefing in any committed receipt (46–48 unbriefed samples each), so they cannot show a reduction; the rate limiter has reproduced 1 of 41 times without the briefing (§4m) and 2 of 44 with it (REPORT §4d, §4m, §5 finding 6).
  • The result worth remembering, from those first runs: briefed haiku (12%) beat unbriefed sonnet (33%). It suggests the briefing is a cost lever, not just a quality lever — it can hold a cheaper model above a frontier model's unbriefed floor on the org's known failure modes. Two caveats: the haiku run was never repeated, and it was judged by sonnet where the sonnet runs were judged by haiku.
  • Every run above predates the briefing's Decisions section: until recently a Decision that won a slot was silently dropped; it now appears under its own heading. No eval task's briefing held one, so the eight measured briefings are byte-identical either way.
  • Every run above also predates the [id] each briefed lesson now carries. That one is not byte-identical: the eight eval briefings are 5% larger, with every lesson's content kept (REPORT §4j). Its run (2026-10-10) read 17% → 0%, but the unbriefed arm itself fell 26 points from §4i's, which points at the generator rather than okl, so it starts a new series and is not pooled with the runs above.

The figure is also a transfer result. The eight tasks are fresh code written from a task prompt, and the defect classes they invite were learned in other projects: each task names its lesson in evals/tasks.jsonl, and those lessons arrive through six of the twelve seed packs. A lesson paid for in one project prevented the defect in code written for another. In the pooled runs the briefing held each task's lesson with three exceptions, whose arms measure the briefing without it: exit_code_trust's ranked below the twelve-record cutoff in every run until the lesson gained a symptom on 2026-10-22 (REPORT §4b, §4o); rate_limiter's was filtered out in the reverted §4d run; and react_fetch's was filtered out in the two runs made with this repo's declared interests (§4h). Running that across an organisation, and what it asks of a lesson, is Across an org.

What this does not show: the tasks were authored to invite defect classes the store encodes, so it measures what a briefing does when a directly relevant lesson exists — not general code quality, and not retrieval at scale. n is small; treat it as a pilot with receipts, not a benchmark.

How it works

The overview at the top is rendered from docs/overview/okl-overview.html. The anatomy below follows one lesson all the way round, in okl's own output; it is rendered from docs/overview/okl-anatomy.html, and docs/overview/anatomy-story.sh runs the same story in a throwaway repo.

Anatomy of okl, verified memory and the loop that keeps it true: load your rules, hold your agent to them, keep learning as you build. A ring around the store runs through six steps: lessons come in, up front from your rules, ADRs, standards and specs plus starter lessons for your stack, and as you build from new rules, mistakes, drift the gate finds, failing tests and reviews; each is written down as a lesson; okl proves it with a check you choose; it is briefed before each task (by a Claude Code hook; other MCP agents can call okl_check); when a file it covers changes, the pre-push hook holds the push until the check passes again or the code is back to what passed; then it is re-checked, fixed or retired. Below, real okl output follows the lesson 'order lookups are scoped to the signed-in customer' from okl record to a blocked push, where re-running its check caught a change that dropped the owner check. Then where a lesson lives (briefing only, watched for drift, build-breaking); what the briefing did (78 of 186 unbriefed attempts repeated a known defect against 13 of 188 briefed, over eight A/B runs on tasks built to invite one); and how it ranks, what it costs and where it breaks.
How a briefing is actually built

okl check is a retrieval pipeline: it searches the task's subject words, then narrows the whole corpus to the five or fewer records an agent reads in full, and names the rest of what matched in a line each. Two stages can drop a record on what it is about, and only one of them is entitled to: the distinction that this project got wrong once and measured its way out of (REPORT §4d).

Then the decision. A record is briefed only when its title, symptom, aliases and triggers hold three of the task's subject words, or two nearly as rare as a word in one record. Triggers are phrasings of the moments a lesson applies, written when it is recorded. The line was set on 282 real prompts with no subject of their own, so that about one in twenty such prompts gets anything; at most five records clear it. Nothing else that matched is hidden: what cleared the line past the five, and what the earlier word floor keeps, follow one line each (id, title and the start of the fix, up to twelve), so the agent can read any of them with okl show <id> (REPORT §4y).

It was measured on a benchmark built to decide: 519 generated queries, 550 real prompts with no subject, and a held-out half run once after the choice was written down. Against the previous pipeline:

previous pipelinethe line, with triggers
held-out recall74.4%90.0%
lessons per query11.84.7
prompts with no subject that got a briefing149 of 2689 of 268

Real prompts are terser than the benchmark's queries. On prompts a person typed, against a store of recorded lessons (REPORT §4x), the line alone surfaced about a third of the applying lessons the earlier rule did. That is why the default names the rest in a line each (REPORT §4y, held out):

real prompts, a real store, held outearlier rule (floor)the line alonethe default: two tiers
applying lessons named, of 1159038102 (38 in full, 64 in a line)
lessons shown in full per prompt7.71.81.8, plus 7.6 in a line
share of the lessons shown in full that apply9.8%17.8%17.8%
median briefing, tokens1,424113721

A team's own lessons need triggers to reach the first tier: okl record asks for them, okl triggers lists those that lack them, and /triggers drafts them in Claude Code. The evidence, the alternatives that lost (rerankers, graphs, query-time generation, conversation context) and that cost are in docs/RETRIEVAL.md and REPORT §4w and §4x and §4y. --decision calibrated gives the line alone, the smallest briefing; --decision floor the earlier rule (two shared subject words, every match in full, up to twelve records).

The corpus; the task's subject words, with messages a harness wrote briefed nothing; a BM25 fetch of three times the limit, triggers weighted like the symptom; a scope gate; the exclusive applies_to filter; the inclusive tags-times-interests filter; the calibrated line of three shared subject words in the title, symptom, aliases and triggers, or two nearly as rare as a word in one record, with retired lessons out; the best five in full; the rest of what matched, what cleared the line past five and what the word floor keeps, named one line each, up to twelve; bucketing by type; and rendering. Each stage shows how many records survive it.

With the optional semantic arm on (okl check --hybrid --decision floor), a second candidate source joins before the gates: the task as one vector, its nearest records by cosine, and a fusion of the two orders. It needs the floor: a record found by meaning alone shares no word with the task, so the calibrated line cannot brief it. Meaning measured best added to the word count instead. EmbeddingGemma-300m took held-out recall from 90.0% to 95.2% that way, but a prompt hook would pay about two seconds to load it on every prompt, so it waits for a process that keeps the model loaded (REPORT §4w). The same generator draws it from the same code path, and REPORT §4q says what it buys and costs. A local cross-encoder over those candidates (okl check --rerank) was measured next (REPORT §4t): one held-out paraphrase in, one bug report out, so it ships as a flag, off.

The same pipeline with the semantic arm on: beside the full-text fetch, the task embedded as one 512-dimension vector and its nearest records by cosine, fused with the full-text order by a convex combination, then the scope gate, the applies_to filter, the tags filter, a floor that also admits a record by a cosine at or above 0.40, the cutoff, the buckets and the renderers

Every count in that diagram is traced, not transcribed: docs/render_pipeline_diagram.py runs the real briefing_terms, store.search, core._in_scope, core._live_hits and decide.select against a store seeded from seed/, and test_pipeline_diagram_is_current fails if the committed render is not what the generator produces today. Change a BM25 weight or a filter predicate and the diagram goes red with it. The generator once searched the raw sentence after okl check had moved to subject words, and that test could not notice; the trace now cross-checks itself against _live_hits.

Lessons and links

A lesson is one of a few kinds:

KindWhat it captures
DefectA specific bug or mistake that happened, and why.
GateAn automated check that catches a class of defect.
RuleA standard to follow ("do X, never Y").
RetractionA claim that turned out to be false and was withdrawn.
TombstoneAn identifier (name, file, endpoint) that was retired and must not come back.
DecisionA choice that was made deliberately, so it isn't silently reversed later.

Each carries a Symptom → Cause → Fix: when you see this, the cause is this, do this. That is what makes a lesson actionable rather than informational. Lessons link: a Gate CATCHES a Defect, a Decision SUPERSEDES an older one, so a lookup pulls in the connected context.

Two actions. check: you describe the task; okl searches the store, keeps the lessons in your scope, ranks them, and returns a briefing that leads with concrete actions (FIX: … when you see … → …), followed by the decisions already made on purpose. record: when you fix or decide something, you write it down, with the files it governs; from then on every task that resembles it is briefed, and okl update <id> refines it without losing its proof.

Scope is the one decision that matters most. repo:<name> stays with that project. org reaches any project that loads the pack it is in, and is briefed only when its applies_to, the project's interests and the task agree. Subject tags from a closed vocabulary (KNOWN_TAGS in store.py, grown with okl record --type Vocabulary) say what a lesson is about; okl init --interests says what a repo cares about (decision). Packs of org lessons across many repositories, through a ledger repo and pull requests, with each repo proving what it loads: Across an org.

The choices behind all this, and why: docs/DESIGN-CHOICES.md.

The data is one SQLite file per clone by default, .okl/okl.db, gitignored. Point several repos at a shared service when you want one store (docs/SCAFFOLD.md).

Use it

# 1. READ the relevant lessons before starting a task (the load-bearing move)
okl check --task "add an endpoint that returns an order for the logged-in user"
#   add --format actions --limit 3 for a ~350-token version (subagents, CI)

# 2. RECORD a lesson after you learn it, with an actionable symptom/cause/fix
okl record --type Defect --scope org --tags "security" \
  --title  "Trusting a client-supplied price lets the client set it to anything" \
  --symptom "a request body carries a price/amount/status/isAdmin field" \
  --body    "cause: the handler saved the client's value instead of computing it" \
  --fix     "drop those fields from the request; compute them server-side" \
  --files   "**/orders/*.py"         # prints the new record's id

# ...then prove it with a check, rather than asserting it
okl verify <id> --run "pytest -q tests/test_orders.py" --expect "passed"

# 3. SEARCH the stored lessons directly
okl search "price tampering"

# 4. LINK a check to the defect it catches (so a lookup pulls in both)
okl link <gate_id> CATCHES <defect_id>

okl record --verified is refused: it exits 2 and points at okl verify, because a stamp is evidence of a run, not a claim. Every other command, with its exit codes: docs/COMMANDS.md. Why a step never grades itself, and the four rungs okl applies to its own stamps: docs/VERIFICATION.md.

--symptom/--fix are what make check emit a leading "Do this" action list ("FIX: … — when you see: …") instead of a wall of prose. --files tells okl which source files a lesson governs, which powers drift detection (below).

What it costs

Installing okl is not free. It is worth knowing exactly what you are signing up for before you wire it into every prompt. Every number below was measured rather than estimated, with one representative task ("add an endpoint that returns an order for the logged-in user"; tokens ≈ characters ÷ 4). The sizes come from a fresh store holding every bundled seed pack, 183 records (python3 evals/briefing_size.py, receipt evals/results/briefing-size-20261027-0132.json); the latency from the 161 seed records bundled before that. Your store and your tasks will differ.

Per prompt, once the hook is installed:

Latency~0.1s for the whole okl check process (0.07s median warm) — one local SQLite query, no network in local mode
Context~1,390 tokens by default on one representative task (five lessons in full, twelve named in a line; ~730 under --decision calibrated, ~1,730 under --decision floor); on real prompts the median is ~720 (REPORT §4y). Down to ~350 for only the action list at --limit 3

Per session: the Stop hook interrupts once at the end to ask what was learned. It blocks the first stop only, and answering it is the whole write side of the loop. With the question it lists up to five commands that failed during the session, read from the session's own transcript — candidates, not records: no hook runs on every tool call, and no model is called to summarise anything. Then okl recap names the lessons the session touched: up to five it was briefed on most often, and up to five whose governed files it changed, each with the command that corrects, retires or re-proves it. The agent proposes a change in one line and makes it once you agree. To read the whole session back afterwards, okl recap --report lists the briefings, the lessons recorded and retired, the checks run and failed, the commits, and what is stale now, from the store's own logs.

A full briefing costs roughly 1,390 tokens on the measurement above (~730 for the line alone, ~1,730 under the floor's twelve), fine for a main session with a large window and still a real share for a subagent working in a few thousand. That asymmetry matters because subagents are exactly where org rules get lost: a focused worker handling one subtask has the least context and the most need for "here is the mistake this codebase already made."

Measured on every bundled seed pack (183 records), one representative task (receipt): the action list alone is ~350 tokens at --limit 3, ~580 at --limit 5 and ~1,010 at --limit 12 (its one-line entries follow the limit), against ~1,390 for the full briefing. Cheap enough to call per subtask; --decision calibrated drops the one-line entries for the smallest list.

The same holds for a main session on a model with a small context window, where the briefing arrives with every prompt. Set OKL_BRIEFING_COMPACT=1 and the prompt hook sends only the action list (okl check --compact); set OKL_BRIEFING_LIMIT=3 and it shows three records and names three more, instead of five and twelve. Together that is ~350 tokens instead of ~1,390 on the measurement above. Set them where the agent starts: hooks inherit its environment. A limit that is not a whole number above zero is ignored, and an okl too old for --compact briefs in full rather than blocking the prompt. Like --format actions, the compact briefing does not say how many records it trimmed.

okl check --format actions is the imperatives only, about half the size; --format json is the raw result for scripts; --format hook is what the Claude Code hook prints; --limit N draws on fewer records and says how many it trimmed.

Every knob, what okl init writes, how to turn parts off, and the opt-in architecture review: Costs and knobs. Subagents and small windows: docs/SUBAGENTS.md.

Questions

How do I stop my agent repeating a bug it already fixed? Record the fix as a lesson with a symptom, a fix and the files it governs, then okl verify it with the test that proves it. Any task that shares its words is briefed with it first; if the code it covers changes, the push waits until the check passes again.

What happens when the code a lesson covers changes? okl drift lists it, the pre-push hook holds the push (or CI fails), and the next briefing marks it STALE. okl reverify re-runs its stored check; a pass re-stamps it. Putting the code back as it was clears it too.

Does it work with Cursor, Codex or Copilot? The store, the commands, the push gate and the six MCP tools do. The briefing on every prompt and the session-end question are Claude Code hooks; other agents call okl_check (how).

Does it capture my sessions automatically? No. A lesson is written on purpose, by you or by the agent when you say so; the Stop hook asks once. Nothing records tool calls, and no model is called in the loop.

Can I run it beside claude-mem, mem0 or another memory tool? Yes; they are different layers. okl doctor names what is installed and the three known collisions (Where this sits).

Is it production-hardened? No: v0, one person's project, with an end-to-end suite and the measurements above. What it is not for and the limits are stated on purpose.

Install as a Claude Code plugin

/plugin marketplace add emeraldleaf/okl
/plugin install okl@okl

The plugin carries the two hooks, the MCP tools and the seeding commands. It does not carry okl itself: install the CLI first, with the MCP extra, because the plugin registers the okl mcp server (pipx install 'observed-knowledge-ledger[mcp]'), then run okl init in the repo for the store and the drift gate (a pre-push hook; --ci for the GitHub workflow) — with the plugin enabled, init skips the hooks and MCP registration, because registering them twice would brief every prompt twice. okl doctor reports a repo where both the plugin and the project hooks are active. To load the plugin from a checkout for one session: claude --plugin-dir <path>.

Install

pipx install observed-knowledge-ledger   # provides the `okl` command
pip install -e .                   # or from a clone of this repo

The core (local + client + CLI) is stdlib-only — zero required dependencies. Extras are opt-in:

pip install "observed-knowledge-ledger[service]"    # FastAPI shared service
pip install "observed-knowledge-ledger[postgres]"   # Postgres backend (psycopg)
pip install "observed-knowledge-ledger[mcp]"        # MCP server for Claude Code / Cursor / Copilot
pip install "observed-knowledge-ledger[all]"

Agent integration (MCP)

pip install "observed-knowledge-ledger[mcp]"
okl mcp     # register in your coding agent's tool config

Exposes six tools to a coding agent: okl_check (read lessons before a task), okl_record (a new lesson), okl_update (refine one by id, keeping its proof), okl_retire (withdraw one, with a reason), okl_get (one lesson whole) and okl_search. okl_check reports an outage loudly — if a configured shared instance is unreachable it says so rather than returning a reassuring "nothing found," because those two look identical from the agent's side and only one is safe. Unlike the hook, a tool result cannot block the agent; it can only warn it.

Layout

src/okl/
  store.py        # the database: lesson + link schema, swappable SQLite/Postgres backend
  core.py         # check / record / search / link — the logic, independent of transport
  client.py       # resolves local-file vs. shared-service; fails closed
  cli/            # the `okl` command, one module per command group (cli/__init__.py maps them)
  drift.py        # source-vs-spec drift detection
  ownership.py    # okl-fingerprint lines: which installed files are okl's, and untouched
  coexist.py      # `okl doctor`: detects other agent-memory plugins and double wiring
  bootstrap.py    # propose starter lessons from a repo's git history + docs
  service.py      # the shared web service (the [service] extra)
  mcp_server.py   # coding-agent tools (the [mcp] extra)
  seed.py         # load a JSON seed file
  scaffold_cmd.py # the `okl scaffold` starter-files stamper
seed/             # starter lesson files (examples + genuinely useful defects)
docs/decisions/   # design decision records
tests/            # end-to-end tests

Test

pip install -e . --group dev   # from a clone of this repo (pip 25.1+; or `uv sync`)
pytest -q                 # full suite

Some tests skip rather than fail where git is unavailable (or git init is blocked) or an optional extra such as service is not installed.

License

MIT.

Contributing

See CONTRIBUTING.md for setup, the repo's own rules (mirror files, drift, evidence-based verification), and where help is most useful. docs/DEVELOPING.md walks through how okl is developed end to end: the everyday loop, CI, pull requests, releases, the evaluations and what you can observe. Security policy and the deployment threat model: SECURITY.md.

Footnotes

  1. The PyPI name differs because PyPI refuses okl as confusable with oki; everything you type afterwards is okl. ↩

Comments

Markdown supported

New to MCP?

From what the protocol is to installing your first server, these answers cover most first questions.

An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.