{"slug": "okl-hold-your-agent-to-rules-proven-by-checks-and-gated-at-push", "title": "Okl – hold your agent to rules proven by checks and gated at push", "summary": "Emeraldleaf released okl (Observed Knowledge Ledger), a CLI installed via `pipx install 'observed-knowledge-ledger[mcp]'` that briefs AI coding agents on a repository's conventions, architecture decisions and checks before each task and gates pushes when the files those lessons govern drift. The tool requires Python 3.10+ and git for drift detection, is tested on macOS and Linux with Windows untested, seeds the store with 20 starter lessons plus stack-specific packs, and wires Claude Code hooks, an MCP server and a pre-push drift gate through `okl init`, with an opt-in GitHub Actions workflow available via `--ci`.", "body_md": "**Guardrails and a learning loop for AI coding agents.**\n\nEngineering rules, architecture decisions, documentation and diagrams: recorded once, briefed to\nyour agent before every task (by a hook in Claude Code; any MCP agent can ask for the same\nbriefing), proven by checks you choose, and flagged before you push (or in CI) when what they\ngovern changes. Each session's lessons are fed back in. Use it\nin one repo, or share one store across every repo your team runs.\n\n🚀 [Quickstart](#quickstart) · 📘 [Getting started guide](https://github.com/emeraldleaf/okl/blob/main/docs/GETTING-STARTED.md) · 🔬 [How it works](#how-it-works) · 📊 [Results](#measured-effect-and-its-limits) · 🧪 [Eval report](https://github.com/emeraldleaf/okl/blob/main/evals/REPORT.md) · 🧩 [Plugin](#install-as-a-claude-code-plugin)\n\nA small store of what a codebase knows — its conventions, architecture decisions and\nwhy they were made, the checks that catch mistakes already made once, and the claims its\ndocs and diagrams make — plus a hook that hands the relevant ones to a coding agent (or a\nperson) **before** they start a task. Each session can record what it learned, so the store\ngrows with the work; it survives past a session and can be shared across a team. And it\nstays true: an entry is verified by running a check, and flagged when the files it governs\nchange, whether those are code, docs or diagrams.\n\nNew here? **[docs/GETTING-STARTED.md](https://github.com/emeraldleaf/okl/blob/main/docs/GETTING-STARTED.md)** walks through setup, the\nhabit of adding to the canon as you build a feature, and keeping your docs from drifting.\nAlready have a `CLAUDE.md`, ADRs, standards or specs? **[Load your rules](https://github.com/emeraldleaf/okl/blob/main/docs/LOAD-YOUR-RULES.md)**\nshows how to load them, prove the ones that matter, and keep them current.\n\n**1. Install the CLI** (the PyPI name differs — PyPI refuses `okl` as confusable with\n`oki` — but everything you type afterwards is `okl`):\n\n```\npipx install 'observed-knowledge-ledger[mcp]'\n```\n\nNeeds Python 3.10+ (and git for drift detection). Tested on macOS and Linux. **Windows is untested**: the core\nshould work, but the Claude Code hooks likely need fixes there\n([#85](https://github.com/emeraldleaf/okl/issues/85)).\n\n**2. Wire your repo.** From the repository root:\n\n```\nokl init --repo my-repo --dry-run   # lists every file it would write; writes nothing\nokl init --repo my-repo             # config, Claude Code hooks, MCP server, pre-push drift gate, starter lessons\n```\n\n`init` detects your stack (`*.csproj`, `package.json`, `pyproject.toml` …), sets the repo's\ninterests from it, and fills the store with 20 starter lessons that hold on almost any\ncodebase plus the bundled packs for your stack, so the first prompt is already briefed\n(`--interests` chooses your own subjects; `--no-seed` leaves the store empty).\n\n`init` wires Claude Code when the repo has a `.claude/` directory or `claude` is on your\nPATH; `--claude` forces it and `--no-claude` skips it. Drift is gated locally by default:\nin a git repository `init` installs a pre-push hook that runs the drift gate and blocks a\npush only when lessons have drifted (`--no-git-hook` skips it). The GitHub Actions workflow\nis opt-in, because it spends a private repo's minutes and does nothing on another CI: `--ci`\ninstalls it instead of the hook, unless an earlier `init` already installed the hook, which\nstays (`--ci --git-hook` gives both), for a team that wants a gate `git push --no-verify`\ncannot skip; a repo an earlier okl gave the workflow keeps it. Later runs remember both choices. **Prefer the plugin?** Install it\n*before* running `init` — `/plugin marketplace add emeraldleaf/okl`, then\n`/plugin install okl@okl` in Claude Code — and `init` leaves the hooks to the plugin, so\nnothing is wired twice. (Installed after? `okl doctor` reports the double wiring; run `okl init --uninstall`, then\n`okl init` again, which leaves the Claude Code hooks to the plugin and reinstalls okl's\npre-push gate; where another tool owns the pre-push hook, `init` prints the one line to add\nto it instead.)\n\n**3. Add a rule of your own.** The starter lessons are generic; what pays is what only your\ncodebase knows. Tell your agent — *\"record an okl rule for this repo: order lookups are\nscoped to the signed-in customer; governs app/orders.py\"* — and it records the lesson with\nokl's `okl_record` MCP tool (in Claude Code, `/record` drafts it and asks you first).\nUnderneath, that is one CLI call you can also run yourself:\n\n```\nokl record --type Rule --scope repo --id order-owner-scope \\\n  --title \"Order lookups are scoped to the signed-in customer\" \\\n  --symptom \"an endpoint fetches an order by id with no owner filter\" \\\n  --fix \"filter by the caller's customer id in the query; return 404 on no match\"\n```\n\nMore: `okl seed` lists every bundled pack; `/seed-from-docs` has your agent propose cited\nrecords from the rules, ADRs, standards and specs you already wrote, and\n`/seed-from-codebase` from your own code. Both come with the Claude Code plugin or\n`okl scaffold .` ([Seed it](#seed-it-so-the-very-first-check-returns-something)).\n\n**4. Check it works:**\n\n```\nokl check --task \"add an endpoint that returns an order for the logged-in user\"\nokl doctor                  # flags other memory tools, double wiring, and where drift is gated\n```\n\n- **You prompt as usual.** The pre-task hook runs`okl check` on what you typed and puts\nthe 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).\n- **When you learn something worth keeping** — a decision, a convention the agent broke,\na bug you fixed — say so and the agent records it (`okl_record` , or`okl record` ), with\nthe files it governs. In Claude Code the Stop hook also asks once, at the end of a\nsession that changed files, what was learned, and names the lessons the session was\nbriefed on and those whose files it changed, so the agent can propose correcting or\nretiring one (you approve).\n- **Using another agent?** The briefing and recording are MCP tools (`okl mcp` ); register\nthem and add one line to your`AGENTS.md` :*before each task call `okl_check`* . The hooks\nthat do this automatically are Claude Code's; see[Getting started](https://github.com/emeraldleaf/okl/blob/main/docs/GETTING-STARTED.md#using-another-agent) .\n- **When a lesson needs correcting,** take its`[id]` from the briefing and change only\nwhat is wrong:`okl update <id> --fix \"...\"` (or the`okl_update` tool) keeps the rest,\nincluding its proof, unless you change its governed`--files` : then it needs verifying\nagain, and`okl reverify` re-runs its stored check.`okl show <id>` prints the whole\nlesson, 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\nexisting id again is refused, because it used to replace the lesson and wipe its proof.\n- **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\nit;`--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\nkeeps briefing against its return.\n- **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\"` .\n- **When code a lesson governs changes,**`okl drift` goes red until its check passes\nagain, or the code is put back as it was when the check passed (a lesson recorded\nwith`--files` is also red until its first`okl verify` ). The briefing says so too:\nsuch a lesson is marked*STALE* (or*UNVERIFIED* , or*UNPROVEN* ) with the file\nthat 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\nGitHub 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\nafter that: commit it after the code change it verifies. Until then CI warns \"Drift not\nchecked\", which is expected. Without the workflow,`okl verify` writes no snapshot unless\none is already committed.\n- **Headless runs** (`claude -p` , scripts, CI agents) set`OKL_DISABLED_HOOKS=encode` ,\nor the end-of-session question replaces the printed answer.\n\nA team (or an AI agent) fixes a subtle bug, learns *why* it happened, and writes a\nrule to prevent it. Weeks later, in a different file — or a different repository —\nthe same class of bug comes back, because the person or agent doing the new work\nnever saw that rule. The knowledge existed; it just wasn't in front of whoever\nneeded it, at the moment they needed it.\n\n`okl` fixes that with one move: **the relevant lessons are read automatically at the\nstart of a task, not looked up if someone remembers to.** You record a lesson once;\nevery future task that resembles it gets the lesson injected before the first line\nof code is written.\n\nIt works for a single repo on day one, and across many repos when you point them at a shared instance — so a lesson learned in one project protects the next one.\n\n**This is a v0 starter, not production-hardened.** It ships an end-to-end test suite\n(run `pytest -q` to see the suite and its current result in your environment). The core is\nstdlib-only with zero required dependencies.\n\n**A store of your engineering rules, and the machinery that keeps them true.**\n\nTwo things ship in the package. They are not coequal:\n\n- \n**The knowledge layer** is the product. Typed records (rules, architecture decisions,\nknown defects, gates, tombstones, retractions) that live outside any one repo, get\nretrieved into an agent's context before a task, and go stale loudly when the code\nthey describe moves on. Everything measured in[evals/REPORT.md](https://github.com/emeraldleaf/okl/blob/main/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/](https://github.com/emeraldleaf/okl/blob/main/seed) it is mostly not that:**110 Rules, 20 Decisions and 7 Gates against\n36 Defects** — conventions the code follows and trade-offs already settled, not a\nledger of things that broke. Count it yourself:\n\n``` python\npython3 -c \"import json,glob,collections; c=collections.Counter(\n  n['type'] for f in glob.glob('seed/*.json') for n in json.load(open(f))['nodes']); print(c)\"\n```\n\n- \n**`okl scaffold`** is a starter kit for the in-repo discipline the store assumes: a\nlean canon file, mechanical gates, registries, a review agent, and an eval harness.\nIt is useful on its own and it has never been measured. Use it to get a new repo to\nthe state where a shared store has something to attach to.\n\n| Piece | What it is | Where it lives | \n|---|---|---|\n| **client** (`okl` CLI + agent tools) | `check` /`record` /`verify` /`drift` /`search` /`seed` | installed per-repo (this package) | \n| **shared layer** (`okl serve` ) | one small service owning the database, so many repos share one store | one place you run it | \n| **scaffold** (`okl scaffold` ) | the in-repo starter files: canon, gates, registries, evals | stamped into each repo, optional | \n\nokl 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:\n\n| If it is… | Put it in | Why not okl | \n|---|---|---|\n| a rule every session needs, whatever the task | `CLAUDE.md` /`AGENTS.md` | those load in full every time; a briefing picks lessons per task | \n| a procedure: how to release, migrate or set up | a skill, runbook or script | a procedure is read whole and in order; a briefing hands over a few short records | \n| commands, and \"for X, read Y\" pointers | `CLAUDE.md` /`AGENTS.md` , or a skill | it is a map the agent needs on every relevant task | \n| formatting and code style | a formatter or linter | a tool enforces it on every line; a lesson can only remind | \n| a bug that is still open | your issue tracker | the tracker owns open work; okl keeps what was learned once it is fixed | \n| secrets, credentials, personal data | nowhere near okl | the store is shared and its lessons are copied into agent context | \n\nEverything with a symptom and a fix belongs here: a convention the code follows, a decision\nmade on purpose, a defect class you have already paid for. The\n[getting-started guide](https://github.com/emeraldleaf/okl/blob/main/docs/GETTING-STARTED.md#store-records-vs-claudemd--agentsmd) has\nthe same split from the side of recording.\n\nKnowledge rots in a specific way: the code changes and everything written *about* the\ncode silently stops being true. Each row below catches a different version of that, and\nit is worth knowing which one catches what, because they do not overlap.\n\n| Drift | Caught by | How it works | Fires when | \n|---|---|---|---|\n| **A rule vs. the code it governs** | `okl drift --gate` | a record declares the path globs it governs, and `okl verify` records the commit its check passed at | those 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 | \n| **A retired identifier reappearing in prose** | `check-tombstones.sh` | greps the working tree's source, docs, comments and config for every tombstoned name | any non-allowlisted hit | \n| **A withdrawn claim being restated** | `check-retractions.sh` | greps the working tree's markdown for the exact quoted claim from the retraction registry | the quote appears outside the registry | \n| **A doc nobody links to** | `check-doc-orphans.sh` | checks 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 | \n| **A link pointing at a file that moved** | `check-links.sh` | reads every markdown file listed by `git ls-files` and checks each local link's target exists in the working tree | the target does not exist | \n| **A diagram source with no rendered image** | `check-diagram-pairs.sh` | pairs each editable source with its export; format-agnostic via `OKL_DIAGRAM_SRC_EXT` /`OUT_EXT` | reviewers 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 | \n| **Verification going quietly stale** | TTL + `verified_by` | records 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 | \n\nThree honest limits on that table:\n\n- **Diagram *content* is still a human job.**`check-diagram-pairs.sh` proves the rendered\nimage exists; nothing proves it matches the source it was exported from, or that either\nmatches the code. For that, name the diagram in a record's`--files` alongside the code\nit depicts, so changing the code turns the drift gate red until someone re-verifies the\npicture. This repo holds its README that way; its pipeline diagram is generated from the\ncode instead, and a test fails when the committed render no longer matches.\n- **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\nretraction 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\nto test the claim a comment makes for drift to mean anything.\n- **`okl drift` only watches what a record claims.** A file no record governs is not\nwatched by anything. Coverage is a curation decision, and the gap is invisible until\nsomething breaks — which is why the mechanical gates above scan the whole repo (the\ntombstone and retraction gates grep the working tree) rather than only what is enrolled.\n\n**This is not a novel idea, and you should know that before reading further.** Agent\nmemory is one of the most crowded categories in the field: mem0, Zep, Letta, and Cognee\non the infrastructure side, with enterprise knowledge graphs such as Utopia beside them;\nCursor Memories and Devin Knowledge built into the coding\nagents; AGENTS.md / CLAUDE.md / rules files as the convention standard everyone already\nuses; and the research literature (e.g. Codified Context, arXiv 2602.20478) arriving at\ntiered knowledge + retrieval independently. \"Give the agent your team's knowledge\" is\nthe consensus position of 2026, not an insight.\n\n**So why build it anyway?** Three honest reasons:\n\n1. **The crowded half isn't this half.** Nearly all of that tooling solves*personalization* memory — facts extracted from conversations, per-user context,\nknowledge graphs of what the agent experienced. The*institutional* half — receipted\nengineering lessons with governance over who sees what, injected before work with\nteeth — is mostly served by hand-edited rules files, and inside Copilot by its\nrepository memory. That gap is real even if the category isn't new.\n2. **The bet: memories are treated like tests, not notes.** okl is not alone in tying\nmemory to code: GitHub Copilot's memory (2026) cites the code lines behind each\nmemory and has the agent re-read them before use, and driftlint fails CI when an\ninstruction file names a path or command that no longer exists. okl's version is\nstricter for the lessons that matter. A lesson cites the source it governs and\ncarries a verification receipt from a check you choose (`okl verify` — the CLI will\nnot stamp without a run), can decay on a TTL, and goes stale*loudly* :`okl drift --gate` blocks the push (or fails CI, with`--ci` ) when governed code\nchanged after the lesson was last verified, or it was never verified. Most tools in the\ntable below accumulate, decay with time, or close a memory when a newer fact contradicts\nit; the ones that tie a memory to code check it by re-reading the\ncode, checking a reference, or noticing a file changed, not by running your check.\nThe whole repo is plumbing to get that bet in front of an agent\nbefore the first line of code is written.\n3. **Building it was the point.** This repo exists to make a working method concrete —\nand the things it surfaced would not have come from adopting a product: the eval\nreceipts in`evals/` , the store carrying its own failure log, and the end-to-end\ntest that caught the briefing being delivered to a channel the model never reads\n(`evals/REPORT.md` §8). Wiring a vendor SDK would have taught none of that.\n\n| Tool / convention | What it remembers | What invalidates a memory | \n|---|---|---|\n| mem0 / Zep / Letta / Cognee | extracted facts, conversation graphs, agent-curated tiers | a newer fact that contradicts one: Zep closes it on a timeline of when it held, mem0's model updates or deletes it. Nothing tied to your code | \n| Utopia (enterprise world model) | entities and facts extracted from business documents into an ontology-driven, bitemporal graph | a newer document's contradicting fact can close the old one (or both are kept, or the new one is rejected), with when each held and when it was learned; doubtful cases go to a person. Nothing tied to your code | \n| Cursor Memories / Devin Knowledge | per-project conventions and pinned notes | manual editing | \n| GitHub Copilot Memory (2026) | repo facts the agent saves as it works, each citing the code lines behind it; shared by Copilot's coding agent, CLI and code review | the agent re-reads the cited lines before use and replaces a contradicted memory; memories expire unless re-confirmed — a model's judgment at use time, Copilot only, no CI gate | \n| driftlint, agents-lint, scavi (instruction-file linters) | nothing of their own: they check the claims already in CLAUDE.md / AGENTS.md (driftlint also syncs approved facts into them) | a referenced path, command, link or import that no longer exists; driftlint fails CI. The reference checks don't test whether a rule still holds; driftlint's optional `--llm` mode asks a model whether the code contradicts a prose claim | \n| AGENTS.md / CLAUDE.md / rules files | hand-written canon, loaded whole | hand-editing; no per-task selection | \n| claude-mem / agentmemory (Claude Code plugins) | every tool call, compressed into observations by a model; agentmemory adds confidence and decay | file age (claude-mem skips a note when its file changed); time-based decay (agentmemory) — nothing re-checks a memory | \n| ECC (skills + \"instincts\") | instincts learned from observed tool use, weighted by a model-scored confidence | confidence decay, applied by prompt — no check proves an instinct | \n| beads | work items and short `bd remember` notes — a task tracker, not a lesson store | closing the issue | \n| **okl** | **typed, scoped lessons (Defect / Rule / Decision …), selected per task, fail-closed** | **the drift gate: a lesson whose governed source changed after its last verification (or that was never verified) blocks the push, or fails CI** | \n\n**Read against the Claude Code memory plugins** (claude-mem, agentmemory, ECC, beads — their\nsource, September 2026): they are ahead on capture, retrieval engineering, install polish\nand reach across agents, and okl is not trying to catch them there. None re-checks that a\nmemory is still true, ties one to the code it describes, or measures whether memory\nimproves outcomes — claude-mem's \"~10x\" is a 5-query code-search benchmark, agentmemory's\nevals are retrieval-only. Their capture also costs model calls (claude-mem runs a model per\ntool call; agentmemory's lessons need an API key); okl requires none. claude-mem remembers\nwhat happened; okl keeps what must stay true, and proves it.\n\n**Running one of them alongside okl** works, with three known collisions: okl's Stop hook\nblocks the first stop, so their Stop hooks run twice; capture-everything tools record\n`okl record` too, so a lesson lands in two stores; and each injects its own context beside\nokl's briefing. `okl doctor` names whichever is installed and what to do about it.\n\n**Instead — yes, if your problem is theirs.** If you want semantic recall over what an\nagent has seen, per-user personalization, or conversation-scale memory, use them;\nthey're better at it, and this deliberately isn't that (no embeddings, by\n[recorded decision](https://github.com/emeraldleaf/okl/blob/main/docs/decisions/2026-07-17-flat-retrieval-until-scale.md)).\n\n**Alongside — they compose, because they're different layers.** Memory infrastructure\nremembers what the agent *experienced*; this governs what the org has *verified*. A\nreasonable stack runs both: mem0/Zep for recall, okl for the fail-closed pre-task\nbriefing, the drift gate (before the push, or in CI), and the record/verify loop.\n\n**On top — the discipline is portable; the database is deliberately boring.** The parts\nworth stealing are the typed schema, the org/repo scope boundary, verification-with-\nreceipts, and the fail-closed delivery — not the SQLite file. If your org already runs\na memory backend, reimplementing this loop on top of it is a reasonable weekend; what\nyou'd be adopting is the discipline, not the storage.\n\nOne held-fixed A/B (8 authored tasks, 3 samples per arm per run; generator and blind\njudge are different models; method + raw receipts in [evals/REPORT.md](https://github.com/emeraldleaf/okl/blob/main/evals/REPORT.md)):\n\n- Same model, briefed vs not: in the first 3-sample runs (2026-08-30), defect\nreproduction fell **33% → 4%** (sonnet) and**38% → 12%** (haiku). Later sonnet runs, as\nthe retrieval pipeline changed, read**35–50% → 4–8%** (REPORT §4b–§4i, excluding the\nreverted §4d and the opus-judged §4f; the latest, §4i, 43% → 8%). Pooled over the eight\nruns on that instrument from §4 to §4i, the reverted §4d included:**78 of 186 unbriefed\nattempts repeated a known defect (42%) against 13 of 188 briefed (7%)** ; REPORT §5\nfinding 7 names the receipts and the other ways to cut it. The unbriefed arm\nalone has moved 17 points between runs with nothing changed, so that is the noise floor\n(§2b). Every \"reproduced\" is a defect class this store had already paid to learn — a\nprice-tamper fallback, tokens in web storage, an unpinned CI gate, a script that trusts\nexit 0 without checking its output, an eval summary that does not put its failure\nrate first — not lint noise. Three tasks (an IDOR endpoint, a React fetch, a rate limiter)\nhave never reproduced without the briefing in any committed receipt (27–33 unbriefed\nsamples each), so they cannot show a reduction; the rate limiter has reproduced only\nwith the briefing, 2 of 30 times (REPORT §4d, §5 finding 6).\n- The result worth remembering, from those first runs: **briefed haiku (12%) beat\nunbriefed sonnet (33%)** . It suggests the briefing is a cost lever, not just a quality\nlever — it can hold a cheaper model above a frontier model's unbriefed floor on the\norg's known failure modes. Two caveats: the haiku run was never repeated, and it was\njudged by sonnet where the sonnet runs were judged by haiku.\n- Every run above predates the briefing's **Decisions** section: until recently a\nDecision that won a slot was silently dropped; it now appears under its own heading.\nNo eval task's briefing held one, so the eight measured briefings are byte-identical\neither way.\n- Every run above also predates the **`[id]`** each briefed lesson now carries. That one\nis not byte-identical: the eight eval briefings are 5% larger, with every lesson's\ncontent kept (REPORT §4j). Its run (2026-10-10) read**17% → 0%** , but the unbriefed arm\nitself fell 26 points from §4i's, which points at the generator rather than okl, so it\nstarts a new series and is not pooled with the runs above.\n\nWhat this does **not** show: the tasks were authored to invite defect classes the store\nencodes, so it measures what a briefing does when a directly relevant lesson exists —\nnot general code quality, and not retrieval at scale. n is small; treat it as a pilot\nwith receipts, not a benchmark.\n\nThe overview at the top is rendered from\n[`docs/overview/okl-overview.html`](https://github.com/emeraldleaf/okl/blob/main/docs/overview/okl-overview.html). The anatomy below\nfollows one lesson all the way round, in okl's own output; it is rendered from\n[`docs/overview/okl-anatomy.html`](https://github.com/emeraldleaf/okl/blob/main/docs/overview/okl-anatomy.html), and\n[`docs/overview/anatomy-story.sh`](https://github.com/emeraldleaf/okl/blob/main/docs/overview/anatomy-story.sh) runs the same story in a\nthrowaway repo.\n\n`okl check` is a retrieval pipeline: it searches the task's subject words, then narrows the\nwhole corpus to the twelve records an agent reads. Two stages can drop a record, and only one of them\nis entitled to — the distinction that this project got wrong once and measured its way out\nof ([REPORT §4d](https://github.com/emeraldleaf/okl/blob/main/evals/REPORT.md)).\n\nEvery count in that diagram is **traced**, not transcribed: `docs/render_pipeline_diagram.py`\nruns the real `briefing_terms`, `store.search`, `core._in_scope` and `core._live_hits` against\na store seeded from `seed/`, and `test_pipeline_diagram_is_current` fails if the committed\nrender is not what the generator produces today. Change a BM25 weight or a filter predicate\nand the diagram goes red with it. The generator once searched the raw sentence after\n`okl check` had moved to subject words, and that test could not notice; the trace now\ncross-checks itself against `_live_hits`.\n\n`okl` stores small, typed **notes** and the **links** between them.\n\nA note (internally a \"node\") is one of a few kinds:\n\n| Kind | What it captures | \n|---|---|\n| **Defect** | A specific bug or mistake that happened, and why. | \n| **Gate** | An automated check that catches a class of defect. | \n| **Rule** | A standard to follow (\"do X, never Y\"). | \n| **Retraction** | A claim that turned out to be false and was withdrawn. | \n| **Tombstone** | An identifier (name, file, endpoint) that was retired and must not come back. | \n| **Decision** | A choice that was made deliberately, so it isn't silently reversed later. | \n\nEach note can carry a **Symptom → Cause → Fix**: *when you see this symptom, the\ncause is this, do this fix.* That structure is what makes a note actionable instead\nof just informational.\n\nNotes can be **linked**: a Gate `CATCHES` a Defect; a Retraction `RETRACTS` a Claim;\na Decision `SUPERSEDES` an older one. The links let a lookup pull in the connected\ncontext (\"here's the bug, and here's the check that would have caught it\").\n\nEverything reduces to two actions:\n\n1. \n**`check` — read before you work.** You describe the task you're about to do.`okl` searches the store, keeps only the notes relevant to*your* scope, ranks\nthem, and returns a short briefing that**leads with concrete actions** :*\"FIX: server-controlled price tampering — when you see a request carrying a\nPrice field → compute it server-side instead,\"**\"ARM: run the class-path check\nbefore you finish.\"* Records that are not actions follow under their own headings,\nincluding the Decisions already made on purpose. An AI agent reads this at the top of\nits context; a person reads it in the terminal.\n2. \n**`record` — write after you learn.** When you fix something or decide something,\nyou record it as a note (optionally with its symptom/cause/fix and the files it\ngoverns). From then on, every`check` whose task resembles it surfaces it, with its\nid, and`okl update <id>` refines it later without losing its proof (unless its\ngoverned files change, which needs a new check).\n\nEvery note has a **scope**, and this is the one decision that matters most:\n\n- **`repo:<name>`** — a lesson specific to one project. It only ever shows up for\nthat project. (This repo's quirky build step, a workaround for one service.)\n- **`org`** — visible to every project that reads the store it is in; use it for a\nlesson that holds beyond one project. (A security pattern, an API contract, a\ndata-source gotcha.) By default each project has its own store, so another project\ngets an org lesson through a pack it loads. Being visible is not being briefed: its`applies_to` , the project's interests and the task decide whether a briefing shows it.\n\nChoosing the scope when you record is the human curation step. It's what keeps a\nshared layer from filling up with one project's noise: another project's `check`\nnever sees your repo-scoped notes, only the `org`-scoped ones worth spreading.\n\nOrthogonal to scope, every note can carry **subject tags** from a small controlled\nvocabulary (`react`, `security`, `eval-integrity`, … — see `KNOWN_TAGS` in\n`store.py`): scope answers *who may see* a note, tags answer *what it's about*.\nA repo declares the subjects it cares about at init time\n(`okl init --interests \"python-rag,eval-integrity\"`), and `check` then drops\norg-wide notes tagged entirely outside those interests — so a Python eval task\nisn't briefed on React lessons. Untagged notes and the repo's own notes always\npass. (Decision record: `docs/decisions/2026-07-21-subject-tags-controlled-vocabulary.md`.)\n\nIf `okl` is configured to talk to a shared instance and that instance is\nunreachable, `check` **says so loudly and blocks** — it does not return an empty\n\"nothing found,\" because \"no lessons apply\" and \"I couldn't reach the lessons\" look\nidentical from the outside and the second one is dangerous. Silence is never\nreported as safety.\n\nA single local file by default (SQLite). Point it at a shared service (backed by the same SQLite, or Postgres) when you want several repos to share one body of knowledge. The switch is one environment variable; none of your commands change.\n\n```\n/plugin marketplace add emeraldleaf/okl\n/plugin install okl@okl\n```\n\nThe plugin carries the two hooks, the MCP tools and the seeding commands. It does not\ncarry okl itself: install the CLI first, **with the MCP extra**, because the plugin\nregisters the `okl mcp` server (`pipx install 'observed-knowledge-ledger[mcp]'`), then run\n`okl init` in the repo for the store and the drift gate (a pre-push hook; `--ci` for the\nGitHub workflow) — with the plugin enabled, `init`\nskips the hooks and MCP registration, because registering them twice would brief every\nprompt twice. `okl doctor` reports a repo where both the plugin and the project hooks are\nactive. To load the plugin from a checkout for one session: `claude --plugin-dir <path>`.\n\n```\npipx install observed-knowledge-ledger   # provides the `okl` command\npip install -e .                   # or from a clone of this repo\n```\n\nThe core (local + client + CLI) is **stdlib-only** — zero required dependencies.\nExtras are opt-in:\n\n```\npip install \"observed-knowledge-ledger[service]\"    # FastAPI shared service\npip install \"observed-knowledge-ledger[postgres]\"   # Postgres backend (psycopg)\npip install \"observed-knowledge-ledger[mcp]\"        # MCP server for Claude Code / Cursor / Copilot\npip install \"observed-knowledge-ledger[all]\"\n```\n\nInstalling okl is not free. It is worth knowing exactly what you are signing up for\nbefore you wire it into every prompt. Every number below was measured rather than\nestimated, with one representative task (\"add an endpoint that returns an order for the\nlogged-in user\"; tokens ≈ characters ÷ 4). The sizes come from a fresh store holding every\nbundled seed pack, 183 records (`python3 evals/briefing_size.py`, receipt\n[`evals/results/briefing-size-20261010-2127.json`](https://github.com/emeraldleaf/okl/blob/main/evals/results/briefing-size-20261010-2127.json)); the latency from the 161 seed records bundled before that.\nYour store and your tasks will differ.\n\n**Per prompt, once the hook is installed:**\n\n| Latency | **~0.1s** for the whole`okl check` process (0.07s median warm) — one local SQLite query, no network in local mode | \n| Context | **~1,770 tokens** at the default`--limit 12` , down to**~260** for only the action list at`--limit 3` | \n\n**Per session:** the Stop hook interrupts once at the end to ask what was learned. It\nblocks the first stop only, and answering it is the whole write side of the loop. With\nthe question it lists up to five commands that failed during the session, read from the\nsession's own transcript — candidates, not records: no hook runs on every tool call, and\nno model is called to summarise anything. Then `okl recap` names the lessons the session\ntouched: up to five it was briefed on most often, and up to five whose governed files it\nchanged, each with the command that corrects, retires or re-proves it. The agent proposes\na change in one line and makes it once you agree.\n\n**In your repo:** `okl init` writes `.okl/` (config, the local database, a `.gitignore`\ncovering both) and, when it wires Claude Code, two hook scripts plus their registration. In a\ngit repository it also installs a pre-push hook that runs the drift gate (`--no-git-hook`\nskips it). `--ci` installs `.github/workflows/okl-verify.yml`, which runs the gate on every\nPR, instead of the hook. A hook an earlier `init` installed stays: `--no-git-hook` only stops\n`init` installing it, and `okl init --uninstall` removes it.\nCI has no store of its own (the local one is gitignored), so give it one: once a lesson\ngoverns files, commit `okl-drift.json` (`okl verify` refreshes it; `okl export --drift`\nwrites it; a snapshot of the rules drift reads, no lesson bodies), or set the\n`OKL_SERVICE_URL` secret. Without either, the step warns \"Drift not checked\" rather than\npassing as if it had. Do not commit a snapshot holding zero rules: CI reads a configured\nstore that checked nothing as broken, and fails.\nNot on GitHub Actions? The gate is one command; [Getting started §4](https://github.com/emeraldleaf/okl/blob/main/docs/GETTING-STARTED.md#4-run-the-drift-gate-without-github-actions)\ncovers GitLab, Azure Pipelines and the pre-push hook.\n`okl scaffold` is separate and optional — nothing installs it unless you ask.\n\n```\nokl check --task \"...\" --format actions   # imperatives only, about half the size\nokl check --task \"...\" --format json      # the raw result, for scripts\nokl check --task \"...\" --format hook      # what the Claude Code hook prints: the briefing plus\n                                            #   the one-line notice you see (OKL_QUIET=1 drops it)\nokl check --task \"...\" --limit 3          # fewer records; the briefing says how many it trimmed\nokl init --interests \"python,security\"    # drop records tagged for stacks you do not use\n```\n\n- **`--format actions`** is the single biggest saving and loses the least: you keep every\n\"when you see X → do Y\" and drop the explanatory prose.\n- **`--limit N`** caps how many records are drawn on. The full briefing reports how many\nit trimmed;`--format actions` does not, so a short actions list can hide a miss\nwithout saying so.\n- **The prompt hook takes two of these from the environment** :`OKL_BRIEFING_LIMIT=N` becomes`--limit N` , and`OKL_BRIEFING_COMPACT=1` sends only the action list. They are\nfor a model with a small context window; see[Subagents and small context budgets](#subagents-and-small-context-budgets) .\n- **`interests`** is the one to reach for on a mature shared store. Tags filter\ninclusively — an org record passes when it is untagged or shares any one tag with your\ninterests, so declaring`python` keeps out a record tagged only`dotnet` , but not one\ntagged`dotnet,security` when you also declared`security` . Only a record's`applies_to` excludes by stack.\n- **Scope records `repo:` rather than `org`** when a lesson is local. Org scope is a claim\nthat every project in the organization should see it, and it costs every project's\nbudget to be wrong about that.\n\n`OKL_BRIEFING_LOG=0` stops okl logging which lessons each briefing showed, the log\n`okl metric` reads to report exposure. The log keeps the time, the repo and the lesson ids,\nnever the task: a briefing's task is usually your prompt.\n\n`OKL_QUIET=1` keeps the briefing but hides the one-line *okl · briefed …* notice. Switch a\nhook off by name with `OKL_DISABLED_HOOKS=briefing` (the pre-task read),\n`OKL_DISABLED_HOOKS=encode` (the end-of-session question), or both, comma-separated.\n**Set `OKL_DISABLED_HOOKS=encode` for headless runs (`claude -p`, CI agents, scripts):**\nprint mode emits only the final message, and a blocked stop makes the reply to \"what did\nwe learn?\" that final message — the answer you asked for is then only in the transcript.\nHooks inherit the caller's environment, so the variable set on the `claude` command is\nenough (`evals/REPORT.md` §10). The\npre-task hook is the read side and the Stop hook is the write side, and they are\nindependent — running the read without the write is a reasonable way to start, and turning\noff `encode` is the usual choice beside a tool whose own Stop hooks already run.\n\nNothing is load-bearing on the hooks: `okl check` and `okl record` work from the terminal,\nfrom CI, and through the MCP server whether or not any hook is installed.\n\nTo remove okl from a repo: `okl init --uninstall` (add `--dry-run` to preview). It removes\nthe two hook scripts, their exact entries in `.claude/settings.json`, okl's `.mcp.json`\nserver, `.github/workflows/okl-verify.yml` and its pre-push hook — and nothing else: another tool's hooks in\nthe same events stay, a file you edited is kept and named, and `.okl/` (your store) is\nnever touched; delete it yourself if you mean to. Nothing outside the repo was ever written.\n\nThe two Claude Code hook scripts, the pre-push hook and the CI workflow each carry a\n`# okl-fingerprint:` line, the hash\nof the rest of the file (settings and `.mcp.json` are merged entry by entry instead). That\nis how `init` and `--uninstall` tell an untouched okl file (of any fingerprinted version:\nupgraded or removed freely) from one you edited. Files installed before 0.7 carry no\nfingerprint, so unless one is byte-identical to the current version, `init` keeps it and\n`okl init --force` is what upgrades it. `init` keeps an edited file unless you pass\n`--force`; `--uninstall` always keeps it. There is no local state, so this works the\nsame for a teammate who cloned the repo. okl never writes through a symlink: a hook,\nsettings or workflow path that is a link, or sits under one, is refused and named.\n\nThe kit ships a reviewer that reads a PR diff against your encoded rules and fails the\nbuild on a must-fix finding. It is **off unless you ask for it**, and it is not tied to any\nvendor. Set the `REVIEW_CMD` repository variable to any CLI that reads a prompt on stdin:\n\n```\ngh variable set REVIEW_CMD --body \"claude -p --model sonnet\"   # Claude Code CLI, installed on the runner\ngh variable set REVIEW_CMD --body \"ollama run qwen2.5-coder\"   # local model, no API cost\ngh variable set REVIEW_CMD --body \"llm -m gpt-4o\"              # any other CLI\n```\n\nTwo things worth knowing:\n\n- **In CI, the runner needs the CLI and a credential.** The scaffolded job does not install\nwhatever`REVIEW_CMD` names, and if that command is not on the runner's PATH the step\nskips with a soft pass. Install it in the workflow and give it the secret it needs (the\njob passes`ANTHROPIC_API_KEY` and`OPENAI_API_KEY` through when set). Your personal\nClaude Code login does not reach a hosted runner.\n- **Run locally, `claude -p` needs no separate API key.** It authenticates with the Claude\nCode login you already have. Verified headless with`ANTHROPIC_API_KEY` unset.\n- **Locally you do not need this at all.** The reviewer is a subagent\n(`.claude/agents/architecture-reviewer.md` ); ask your agent to run it on your changes and\nit costs nothing beyond the session you are already in. The CI job exists for the case\nwhere no human and no agent is in the loop — a PR nobody reviewed.\n\nUnset, the step prints one line saying it is off and exits 0. Every other gate in the kit is deterministic and free; this is the only one that calls a model, which is why it is the only one that is opt-in.\n\n```\ncd my-repo\nokl init --repo my-repo        # writes .okl/config.json; wires Claude Code if .claude/ exists or `claude` is on PATH (--claude / --no-claude)\nokl connect https://okl.myorg.dev   # optional: point at the shared service (else local file)\n```\n\nRun `okl init --dry-run` first: it lists every path and writes nothing. In full, `init`\ntouches only the current directory, and only these:\n\n| Path | What it is | \n|---|---|\n| `.okl/config.json` | repo name, subject interests, and the path to your `okl` binary | \n| `.okl/.gitignore` | keeps `.okl/` (config and store) out of git, with no edit to your own`.gitignore` | \n| `.okl/okl.db` | the local store (local mode only; seeded with the starter lessons and your stack's packs unless `--no-seed` ) | \n| `.claude/hooks/userpromptsubmit-okl-check.sh` | **executable** ; runs when you submit a task, injects the briefing | \n| `.claude/hooks/stop-okl-encode.sh` | **executable** ; runs at session end, asks what was learned | \n| `.claude/settings.json` | registers those two hooks (merged in place; your existing keys are preserved) | \n| `.mcp.json` | registers the okl MCP server — only when the `mcp` extra is installed | \n| `.github/workflows/okl-verify.yml` | **a CI workflow** running the drift gate on pull requests — only with`--ci` , or where an earlier okl installed it | \n| `.git/hooks/pre-push` (or your`core.hooksPath` ) | **executable** ; in a git repository with no okl workflow, or with`--git-hook` ; runs the drift gate before a push and blocks only on drift; never written over another tool's hook | \n\nRe-running `init` is safe: it keeps the repo name already configured here (`--repo` renames,\nwhich stops the old `repo:<name>` lessons from briefing), upgrades okl's own files, keeps any you edited (say so\nwith `--force` to replace them), and merges settings without duplicating entries.\n\nThe executable ones deserve a second look before you run it: the Claude Code hooks are\nshell scripts that execute automatically during agent sessions (the check hook can *block* a\ntask when the store is unreachable — that is the fail-closed design), and the pre-push hook\nruns on every `git push` and blocks it on drift (with `--ci`, the workflow runs in your\nActions instead). All are plain text you can read first, in\n[`src/okl/scaffold/hooks/`](https://github.com/emeraldleaf/okl/blob/main/src/okl/scaffold/hooks),\n[`src/okl/scaffold/git-hooks/`](https://github.com/emeraldleaf/okl/blob/main/src/okl/scaffold/git-hooks) and\n[`src/okl/scaffold/ci/`](https://github.com/emeraldleaf/okl/blob/main/src/okl/scaffold/ci). Nothing executes at install time; nothing\nis written outside the directory you run `init` in; nothing contacts a network unless you\nrun `okl connect` and point it somewhere yourself.\n\n`init` writes `.okl/config.json`. When it wires Claude Code (the repo has a `.claude/`\ndirectory, `claude` is on PATH, or you passed `--claude`), it also installs two hooks:\na `UserPromptSubmit` hook that runs `check` on\nthe prompt you actually typed and puts the briefing into the model's context (the\nenforced read — it must be this event: `PreToolUse` stdout never reaches the model,\nwhich an end-to-end test caught the hard way), and a session-end hook that blocks the first stop\nof a session that changed files with one question — *did this session learn anything\nworth `okl record` ing?* — so the write side of the loop gets a mechanical prompt too,\nnot just a convention. It fires once per session and never loops.\n\n**Other agents (AGENTS.md):** `okl scaffold` (not `init`) writes the repo canon to both\n`CLAUDE.md` and `AGENTS.md` — one content, two filenames, so Codex/Cursor/anything\nreading the AGENTS.md convention gets the same rules Claude Code does (byte-identity is\ntest-enforced). The hooks themselves are Claude Code-specific; other agents get the\ncanon via AGENTS.md and the store via the MCP server (`okl mcp`).\n\nThat split matters: on Claude Code the pre-task read is *enforced* (fail-closed hook);\neverywhere else it is *available* (a tool call or a shell command), which is\ndiscretionary — the thing enforcement exists to avoid. The hook scripts themselves are\nplain bash reading JSON on stdin, so nothing in them is Claude-specific; what is missing\nfor other agents is the config that registers them, and whether the agent fires an event\nearly enough to matter. Codex CLI documents a `userpromptsubmit` hook, which is the right\nshape; Copilot, Gemini CLI and Cursor have hook systems worth checking against your\nversion; OpenCode's plugin API captures tool events but, as of this writing, no\npre-prompt event — so there the read stays a tool call rather than a gate. Verify against\nyour agent's current docs before trusting any of that. Wiring one up is a well-shaped\ncontribution — see [CONTRIBUTING.md](https://github.com/emeraldleaf/okl/blob/main/CONTRIBUTING.md).\n\nHooks run in whatever environment the agent harness spawns — often without your venv or\npipx bin dir on PATH — so both hooks resolve the `okl` binary in layers: the `OKL_BIN`\nenv var, then the `okl_bin` path `init` pins into `.okl/config.json` (machine-local),\nthen PATH, then any `python3` that can `import okl` (`python3 -m okl`). `OKL_BIN` and\n`okl_bin` hold a path to okl (spaces are fine) or `<python> -m okl`; a launcher with\narguments, such as `uv run okl`, belongs in a script they point at. If a command\nresolves but cannot start (a venv `okl` whose python was removed), the check hook moves\non to the next layer. If nothing runs, the check hook blocks with install\ninstructions (fail closed) while the encode reminder silently disables (best-effort by\ndesign). `OKL_OFFLINE=1` lets the prompt through anyway, and says so: the agent is told\nit was not briefed and why, and you see one line saying the same.\nWith no shared service configured it uses a local `.okl/okl.db` — single-machine\nmode, good for trying it before you deploy anything.\n\n```\n# 1. READ the relevant lessons before starting a task (the load-bearing move)\nokl check --task \"add an endpoint that returns an order for the logged-in user\"\n#   add --format actions --limit 3 for a ~260-token version (subagents, CI)\n\n# 2. RECORD a lesson after you learn it, with an actionable symptom/cause/fix\nokl record --type Defect --scope org --tags \"security\" \\\n  --title  \"Trusting a client-supplied price lets the client set it to anything\" \\\n  --symptom \"a request body carries a price/amount/status/isAdmin field\" \\\n  --body    \"cause: the handler saved the client's value instead of computing it\" \\\n  --fix     \"drop those fields from the request; compute them server-side\" \\\n  --files   \"**/orders/*.py\"         # prints the new record's id\n\n# ...then prove it with a check, rather than asserting it\nokl verify <id> --run \"pytest -q tests/test_orders.py\" --expect \"passed\"\n\n# 3. SEARCH the stored lessons directly\nokl search \"price tampering\"\n\n# 4. LINK a check to the defect it catches (so a lookup pulls in both)\nokl link <gate_id> CATCHES <defect_id>\n```\n\n`--symptom`/`--fix` are what make `check` emit a leading **\"Do this\"** action list\n(*\"FIX: … — when you see: …\"*) instead of a wall of prose. `--files` tells `okl` which\nsource files a lesson governs, which powers drift detection (below).\n\n```\nokl verify <id> --run \"pytest -q\" --expect \"passed\"\n                     # run the named check and stamp the node verified ONLY on an observed\n                     #   pass; the command + result is stored as the evidence trail.\n                     #   --expect requires a positive success signal in the output, so an\n                     #   exit code alone can't self-certify. (`record --verified` is\n                     #   refused; historical receipts import through `okl seed`.)\nokl reverify         # re-run the stored check of every drifted lesson, and of any last checked\n                     #   on another branch, and re-stamp the passes;\n                     #   lists the commands first and runs them only after you confirm\n                     #   (or --yes), because they come from the store; --dry-run lists only\nokl drift --gate     # flag lessons whose governed source changed after they were last verified\n                     #   (exit 1 on drift, 2 when nothing could be checked; the pre-push hook\n                     #   and CI run it; lessons last checked on another branch are listed\n                     #   below the report, exit code unchanged)\nokl export --drift   # write okl-drift.json, the committed snapshot CI's drift gate reads\n                     #   when it has no store; `okl verify` creates it for the first lesson\n                     #   with --files and refreshes it after that (with no CI workflow, it\n                     #   only refreshes a committed one).\n                     #   CI reads the COMMITTED copy, and refuses an entry whose timestamp\n                     #   does not match its verify evidence, so editing the timestamp alone\n                     #   cannot clear it (editing both fields can; review is the guard).\nokl doctor           # names other agent-memory tools installed beside okl (claude-mem,\n                     #   agentmemory, ECC, beads) and how each collides with okl's hooks,\n                     #   and whether the pre-push drift gate is installed and CI gates\n                     #   drift; reads settings and git config, changes nothing.\nokl --version        # the installed release, for a repo whose hooks run a pinned okl\nokl coverage         # ratio of encoded-knowledge lines to code lines — a health signal\nokl bootstrap        # cold-start a new repo: propose starter notes from its own\n                     #   git history + docs into a reviewable file you edit, then seed\nokl metric           # recurrence: defect classes that came back, split by whether a\n                     #   gate existed, with how many defects the number can speak for;\n                     #   and exposure: how many briefings okl has logged, the lessons\n                     #   never shown, and those shown most with no stored check\n```\n\nA full briefing costs roughly **1,770 tokens** on the measurement above — fine for a main session with a large\nwindow, punishing for a subagent working in a few thousand. That asymmetry matters\nbecause subagents are exactly where org rules get lost: a focused worker handling one\nsubtask has the least context and the most need for \"here is the mistake this codebase\nalready made.\"\n\n`--format actions` solves it by dropping everything except the imperative list:\n\n```\nokl check --task \"add an endpoint returning an order for the logged-in user\" \\\n  --format actions --limit 3\nOKL — 3 rule(s) apply before you start:\n- FIX: Missing ownership scope check is an IDOR (CWE-639) [seed:dotnet-defects:nc_idor] [when:\n  an endpoint fetches an entity by id with no owner/tenant predicate]\n  -> add the caller's owner id to the WHERE clause; return 404 (not 403) on no match\n...\n```\n\n**Measured on every bundled seed pack (183 records), one representative task**\n([receipt](https://github.com/emeraldleaf/okl/blob/main/evals/results/briefing-size-20261010-2127.json)): ~260 tokens at `--limit 3`, ~350 at `--limit 5`, ~590 at\n`--limit 8` and ~940 at `--limit 12`, against ~1,770 for the full briefing. Cheap enough\nto call per subtask.\n\nThe same holds for a main session on a model with a small context window, where the\nbriefing arrives with every prompt. Set `OKL_BRIEFING_COMPACT=1` and the prompt hook sends\nonly the action list (`okl check --compact`); set `OKL_BRIEFING_LIMIT=5` and it draws on\nfive records instead of twelve. Together that is ~350 tokens instead of ~1,770 on the\nmeasurement above. Set them where the agent starts: hooks inherit its environment. A\nlimit that is not a whole number above zero is ignored, and an okl too old for\n`--compact` briefs in full rather than blocking the prompt. Like `--format actions`, the\ncompact briefing does not say how many records it trimmed.\n\nThe full briefing is itself capped: `check` keeps the top `--limit` records (12 by\ndefault) from the ranked, filtered set and says how many it trimmed. Historically, before\nthat cutoff existed, one task on this repo's store at the time returned 20 records and\n~4,400 tokens. The cutoff did\ncost one retrieval: `exit_code_trust`'s governing rule ranks below the top 12 for that\ntask's wording. Run outcomes hid it (the briefing still prevented the defect, through\nother records); `evals/preflight.py` found it by asking directly whether each task's rule\nis in its briefing, and it is kept in a named register rather than silently. See\n[evals/REPORT.md](https://github.com/emeraldleaf/okl/blob/main/evals/REPORT.md) §4b's correction.\n\nWhat it drops: the bucketed sections (Decisions among them), the prose bodies explaining *why* each record\nexists, prior-art notes, and the stale-record footer. What it keeps is what changes\nbehaviour: the verb, the symptom to watch for, and the fix.\n\n**Wiring it into a subagent.** Three ways, in order of how much enforcement you get:\n\n1. **The MCP tool** —`okl_check(task=..., compact=True, limit=3)` . Any subagent with\nMCP access can call it. Discretionary: the agent has to choose to.\n2. **In the subagent's prompt** — have the spawning agent run`okl check --format actions --limit 3` and paste the result into the subtask description. Not\ndiscretionary, and it costs the parent almost nothing.\n3. **A wrapper script** that runs the check and prepends it to whatever prompt it is\nhanded. This is the enforced version for orchestration you control.\n\n**A caveat worth stating.** `--limit` caps how many records the briefing draws on, and\nranking decides which survive. If a task's most relevant rule ranks fourth and you ask\nfor three, you will not see it, and nothing will tell you. The full briefing exists\nbecause it does not make that trade. Use the compact form where a token budget forces\nthe choice, not by default.\n\nA step reporting \"I succeeded\" and the work actually being done are two different facts, and a loop that accepts the first one compounds garbage confidently. (The founding receipt: a pipeline step that was supposed to write 238 files failed on every one, swallowed the errors, and exited 0 — everything downstream ran happily on an empty folder.) Two clarifications that stop the common misreadings:\n\n- **The grader is usually `ls`, not an LLM.** Checking the work means observing the\nwork product — files exist, counts match, tests ran, the output contains the success\nsignal you named. Boring, deterministic checks. A second model only enters when the\nverify signal is itself a model's*judgment* (LLM-as-judge) — there, and only there,\nthe judge must differ from the generator.\n- **Not every step — every claim the loop acts on.** Verify at decision boundaries\n(mark done, merge, deploy), cheap invariants in between.\n\n`okl` applies this to its own knowledge in four escalating rungs:\n\n1. **Assertion is refused at the CLI.**`okl record --verified` (a bare claim, no\nevidence) exits 2 and points at`okl verify` . Two doors stay open:`okl seed` imports\nhistorical, already-verified stamps, and the shared service's API still accepts`verified: true` on a record. What closes the loop is the drift gate: the live-store\nscan (and so the pre-push hook) reports a stamp with no`okl verify` evidence as drift,\nCI's snapshot reader refuses a snapshot that carries one (exit 2), and the briefing\nmarks it*UNPROVEN* .\n2. **Observed check with a stored trail** —`okl verify <id> --run \"pytest -q\" --expect \"passed\"` runs the check itself, reads the real outcome, requires the\npositive signal (exit 0 alone can't self-certify), and stores command + result +\ntimestamp on the node (`verified_by` ). Every stamp is inspectable and re-runnable;\na lazy check becomes a visible artifact instead of an invisible belief.\n3. **An independent actor re-checks** — with`okl init --ci` (or your own CI), CI runs`okl drift --gate` and the method gates on every PR: a mechanical grader with no stake\nin the original claim. The default pre-push hook runs the same gate locally, but it is\nper-clone and`--no-verify` skips it, so it is an early warning, not an independent grader. Nothing\nshipped writes`VERIFIED_ON` receipts by default; a gate script can emit one with`okl link <gate_id> VERIFIED_ON <defect_id>` when it watches a gate prove itself.\n4. **Time attacks every stamp** —`drift` re-grades verifications the moment governed\nfiles change after their last verification; a record given`--ttl-days` decays into`STALE` when\nnobody re-earns it (there is no TTL by default);\nand`okl metric` scores the whole system on outcomes — defect classes that came\nback — the one number it can't flatter itself on. It earns that by stating its own\ncoverage (how many defects have a gate it could speak for) and by counting\nrecurrences with no gate separately: lessons written down that came back anyway.\nUntil issue #31 it printed a bare \"0 ✓\" while the store held three recurrences.\n\nAn empty store returns nothing, and says so — a check against an empty store reports that it proved nothing rather than reporting \"no rules apply\". Four ways to fill it:\n\n**1. See what ships, then choose.** A bare `okl seed` imports nothing; it lists the\nbundled packs with their record counts and subject tags, marking the ones that match\nthis repo's declared interests:\n\n``` python\nokl seed                              # list the packs, import nothing\nokl seed rag-defects                  # import one, by name (a path to any pack file works too)\nokl seed --all                        # import every pack (explicit on purpose)\n```\n\nThe packs hold real, dated records from production codebases (a .NET service, a\ngeospatial ML pipeline, a Python RAG service, a React app), plus `python-canon`: the .NET\npacks' engineering rules translated to Python from cited sources (PEPs, the mypy, pytest\nand import-linter docs, Google's style guide), with the okl incidents that prompted them. They are org-scoped, so\nimporting packs for stacks you do not use fills every briefing here with noise about\nframeworks you will never touch — which is why `--all` is opt-in rather than default.\n\n**2. Load the rules you already wrote.** `/seed-from-docs` has your agent read your rules\nfiles (`CLAUDE.md`, `AGENTS.md`, `.cursor/rules`), ADRs, standards and specs, keep the\nstanding rules and drop the work items, and propose records that cite the line each came\nfrom. It writes a reviewable file and imports nothing; review it, then `okl seed <file>`.\nIt comes with the Claude Code plugin or `okl scaffold .`; with another agent, give it the\nprompt itself, `src/okl/scaffold/claude/commands/seed-from-docs.md`. Three things to know:\n\n- Imported records are briefed, not watched. Give the ones that govern specific code\n`files` in the lessons file, run`okl seed` again, then prove them with`okl verify` .\n- Leave superseded ADRs out, or retire an imported one with `okl retire <id> --by <new-id>` .\n- Running `okl seed` on the edited file again updates the same records, but it keeps a\nrecord you deleted from the file, and it overwrites corrections made with`okl update` .\nMake corrections in the file.\n\nThe whole walk-through, with ADR status, teams, and keeping docs and comments current, is\n[docs/LOAD-YOUR-RULES.md](https://github.com/emeraldleaf/okl/blob/main/docs/LOAD-YOUR-RULES.md).\n\n**3. Generate records from this codebase.** If you use a coding agent, the scaffold\nstamps a `/seed-from-codebase` command that has the agent read your repo — the guard\nrails already in the code, what CI enforces, the fix commits, the existing canon — and\npropose records with a `file:line` citation each. Everything it proposes is repo-scoped\nand unverified by design; it writes a reviewable file and imports nothing, because a\nplausible rule no file supports is worse than an empty store.\n\n**4. `okl bootstrap`** greps git history and file names for candidates. It is the weakest\nof these and comes up empty on young repos; prefer options 2 and 3 when an agent is available.\n\nWhichever you use, review before importing. Choosing a record's scope is the curation step that keeps a shared layer from filling with one project's noise.\n\nBeyond the knowledge store, `okl` can stamp a **starter set of engineering-discipline\nfiles** into a repo, so a new project begins with the guardrails already in place\nrather than accumulating them by hand:\n\n```\nokl scaffold .                 # stamp the starter files into the current repo\nokl scaffold . --plugin        # also emit a Claude Code plugin manifest\nokl scaffold new-repo --profile python-rag --profile react   # include stack rule packs\n```\n\nIt writes a lean project-instructions file, a set of automated **checks** (scripts\nthat fail CI when a retired identifier reappears, a withdrawn claim gets restated, a\ndoc becomes unreferenced, or the instructions file grows too large), a small\nbehavior-evaluation harness, and optional **stack profiles** — ready-made rule packs\nfor common stacks (`dotnet`, `geospatial`, `python-rag`, `react`). Stack-specific\nblanks are marked `<<FILL>>`; after scaffolding, `grep -rn '<<FILL' .` lists every\none to complete.\n\nIt also stamps two **first-party method skills** — `encoding-loop` (turn a finding\ninto a promoted, recorded lesson) and `verify-before-claiming` (evidence before you\nassert a result). The broader engineering-discipline skills (systematic debugging,\nTDD, plan writing/execution, git-worktree isolation) are **not bundled** — they're\nbest maintained in third-party collections, so `.claude/skills/RECOMMENDED-COMPANIONS.md`\npoints at those instead of vendoring someone else's work and its cross-references.\n\nThe scaffold runs with no store at all; the store works in a repo that never scaffolded. okl's own drift gate is\n`okl init`'s job (a pre-push hook, or the GitHub workflow with `--ci`), so scaffold leaves it alone.\nThey are complementary, not a package deal.\n\n**Storage is swappable** via one environment variable — your commands never change:\n\n```\n# default: no variable at all — the store is .okl/okl.db beside the repo's config\n# an explicit SQLite file, e.g. one several local repos share\nexport OKL_DATABASE_URL=\"sqlite:///path/to/okl.db\"\n# a shared database when several repos need one store\nexport OKL_DATABASE_URL=\"postgresql://user:pass@host/okl\"\nokl serve --port 8080\npip install \"observed-knowledge-ledger[service]\"\nOKL_DATABASE_URL=\"postgresql://user:pass@host/okl\" OKL_TOKEN=\"a-shared-secret\" okl serve\n# repos then: okl connect https://your-host --token a-shared-secret\n```\n\n**Set `OKL_TOKEN`.** With it, every route requires the bearer token except `/health`\n(left open so schedulers can probe it). Without it, every route is open — including\n`GET /nodes`, which hands the whole store to anyone who can reach the port. A mature\nstore is a catalogue of your known defects and internal architecture, which is a map of\nwhere you are weak. It is a single shared secret with no per-repo scoping or rotation;\nput a real authenticating proxy in front if you need more.\n\nFull instructions, including a throwaway Postgres for trying it locally and what the\nfailure modes look like: **[docs/DEPLOY.md](https://github.com/emeraldleaf/okl/blob/main/docs/DEPLOY.md)**.\n\n```\npip install \"observed-knowledge-ledger[mcp]\"\nokl mcp     # register in your coding agent's tool config\n```\n\nExposes six tools to a coding agent: `okl_check` (read lessons before a task),\n`okl_record` (a new lesson), `okl_update` (refine one by id, keeping its proof),\n`okl_retire` (withdraw one, with a reason), `okl_get` (one lesson whole) and\n`okl_search`. `okl_check` **reports an outage loudly** — if a configured\nshared instance is unreachable it says so rather than returning a reassuring\n\"nothing found,\" because those two look identical from the agent's side and only one\nis safe. Unlike the hook, a tool result cannot block the agent; it can only warn it.\n\n- **Read before you work, automatically.** The value is entirely in the lesson being\nin front of you at the start — not in a database you*could* have searched. So the\nread is a hook / a first step, not an optional lookup.\n- **It fails closed.** An unreachable store blocks or warns; it never reports \"clean.\"\nSilence and safety are different things.\n- **The scope decision is human curation.**`org` reaches any repo that loads it, through\na pack;`repo:<name>` stays local. A person picks which when recording — that's what keeps a shared store\nfrom filling with one project's noise.\n- **Staleness demotes, never deletes.** A note carries when it was last verified and\nhow long that's good for; past that it's shown as`STALE` , not removed — deleting it\nwould lose the record that it was ever true.\n- **Start simple, grow on evidence.** A stdlib-only core and a single SQLite file by\ndefault; add the shared service, Postgres, or anything heavier only when a concrete\nsymptom demands it (recorded as a decision in`docs/decisions/` ).\n- **Records, not an ontology store.** Lessons are typed records found by ranked full-text\nsearch, with one-step relationships for reporting. An RDF triple store would answer\nprecise structural questions okl does not ask, and could not rank a task sentence\nwithout adding back a search engine.[The decision](https://github.com/emeraldleaf/okl/blob/main/docs/decisions/2026-10-05-records-not-an-ontology-store.md) explains how lessons are stored and found now, and when a one-way RDF export would earn\nits place.\n\n```\nsrc/okl/\n  store.py        # the database: note + link schema, swappable SQLite/Postgres backend\n  core.py         # check / record / search / link — the logic, independent of transport\n  client.py       # resolves local-file vs. shared-service; fails closed\n  cli/            # the `okl` command, one module per command group (cli/__init__.py maps them)\n  drift.py        # source-vs-spec drift detection\n  ownership.py    # okl-fingerprint lines: which installed files are okl's, and untouched\n  coexist.py      # `okl doctor`: detects other agent-memory plugins and double wiring\n  bootstrap.py    # propose starter notes from a repo's git history + docs\n  service.py      # the shared web service (the [service] extra)\n  mcp_server.py   # coding-agent tools (the [mcp] extra)\n  seed.py         # load a JSON seed file\n  scaffold_cmd.py # the `okl scaffold` starter-files stamper\nseed/             # starter lesson files (examples + genuinely useful defects)\ndocs/decisions/   # design decision records\ntests/            # end-to-end tests\npip install -e . --group dev   # from a clone of this repo (pip 25.1+; or `uv sync`)\npytest -q                 # full suite\n```\n\nSome tests skip rather than fail where git is unavailable (or `git init` is blocked) or\nan optional extra such as `service` is not installed.\n\nMIT.\n\nSee [CONTRIBUTING.md](https://github.com/emeraldleaf/okl/blob/main/CONTRIBUTING.md) for setup, the repo's own rules (mirror files,\ndrift, evidence-based verification), and where help is most useful.\n[docs/DEVELOPING.md](https://github.com/emeraldleaf/okl/blob/main/docs/DEVELOPING.md) walks through how okl is developed end to end: the\neveryday loop, CI, pull requests, releases, the evaluations and what you can observe.\nSecurity policy and the deployment threat model: [SECURITY.md](https://github.com/emeraldleaf/okl/blob/main/SECURITY.md).", "url": "https://wpnews.pro/news/okl-hold-your-agent-to-rules-proven-by-checks-and-gated-at-push", "canonical_source": "https://github.com/emeraldleaf/okl", "published_at": "2026-10-18 14:04:53+00:00", "updated_at": "2026-10-18 14:17:49.529773+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "agent-protocols", "mlops"], "entities": ["Emeraldleaf", "okl", "Observed Knowledge Ledger", "Claude Code", "MCP", "GitHub Actions", "PyPI"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/okl-hold-your-agent-to-rules-proven-by-checks-and-gated-at-push", "markdown": "https://wpnews.pro/news/okl-hold-your-agent-to-rules-proven-by-checks-and-gated-at-push.md", "text": "https://wpnews.pro/news/okl-hold-your-agent-to-rules-proven-by-checks-and-gated-at-push.txt", "jsonld": "https://wpnews.pro/news/okl-hold-your-agent-to-rules-proven-by-checks-and-gated-at-push.jsonld"}}