All Recipes

Lessons-ledger: Corrections as Code (Phase 5.5)

Phase 5.5 of the Harness Engineering series: your "wrong" becomes code, not a forgotten chat. A /wrong command captures the correction, normalize splits the fork: a computational lesson (machine-catchable) goes into the rails as a sensor edit, an inferential lesson (judgement needed) becomes a markdown file in lessons/<boundary>/ that the agent reads before the task (the retrieve rule). maintain dedups and promotes matured inferential lessons into sensors. The health metric is how many lessons graduated into sensors, not the size of the notebook. Stack-agnostic, with copyable prompts and an interactive.

AdvancedAI DevOps25 minClaude Code, .claude/commands, AGENTS.md, git, boundary-linter
1

The ledger is staging, not a warehouse

You don't teach the model to know the project — its knowledge is stochastic and unversioned. Instead you turn each of your corrections into two things: text the agent reads before a task, and a rule it can't ignore. That's the lessons-ledger — the layer between context engineering (what the agent will try) and the architectural barriers (what it's allowed to ship). By position in the stack: an ADR explains "why we decided this" (for humans), the rails (lint / boundaries / sensors) hold "what's forbidden" (for the machine), and the ledger catches "exactly how the agent erred" and gradually hands matured lessons down into the rails. The key is the operating mode. The ledger is staging, not a warehouse. Its health is measured not by size ("how many lessons piled up") but by throughput ("how many lessons graduated into sensors"). A growing notebook that never releases anything into barriers is a write-only graveyard: written to, never read, and the agent repeats the mistakes anyway. Keep this in mind from day one.

🟥 Warehouse (write-only)

  • Metric — the notebook keeps growing
  • Lessons pile up, never reach barriers
  • The agent repeats mistakes anyway

🟩 Staging (drains into rails)

  • % of lessons graduated into sensors
  • Matured inferential → computational barriers
  • Knowledge hardens into checks
If a month passes and not one lesson graduated from the ledger into a sensor — that's not a reason to write more lessons but a sign the normalize fork is miscalibrated: anything deterministic should go to the rails immediately, not settle as text.
2

Life of a lesson: 5 stations and two forks

A lesson has a five-station pipeline: capture (catch "wrong" in the moment) → normalize (an LLM pass structures it and splits the fork) → store (markdown in git) → retrieve (the agent pulls it before a task) → maintain (background cleanup and promotion). Not every lesson goes through all five: at the normalize fork it either drives down into the rails (computational) or up into the ledger (inferential), and only inferential lessons reach store/retrieve/maintain. The explorer below shows this in two modes. Click the stations to open each one's detail and its characteristic gotcha. Then run the two examples: computational (UI hits the DB directly — expressible as an import boundary, drives into the rails at once) and inferential (what to name it / how to load data — a judgement that stays a lesson and matures). The difference between them is the heart of the whole phase.

Lessons-ledger — corrections as code

STAGING → RAILS

You don't teach the model to know the project. You turn your corrections into two things: text the agent reads before a task, and rules it can't ignore. The ledger is staging, not a warehouse: its health is measured by how many lessons graduated into sensors, not by how many piled up.

Where it lives: between ADR and the linter

ADRArchitecture Decision Records
"We chose X because Y"
for humans · changes rarely · decision context
↑ ADR explains "why" · ↓ the ledger catches "exactly how"
LEDGERLessons ledgerNEW
"The agent did X — wrong, should be Y"
for the agent · accumulates · staging → rails
promotion of a stable lesson ↓
RAILSlint · boundaries · sensors
deterministic barriers — can't be bypassed
for the machine · railed · "correct" is measurable

Life of a lesson · the pipeline

01 · Capture
Capture — catch it in the moment

You say "wrong" right where you saw it — an agent slash-command or one CLI line. Mining corrections from git diffs can come later, but it's noisy: a diff mixes the fix, the style, and unrelated changes. Start with explicit capture.

/wrong (Claude Code)lessons add (CLI)Cursor rule
High friction kills it. If capture takes more than a couple of seconds, nobody does it — including you.

Run a single lesson

promoteforkcapture/wrongnormalizeLLM-passstoregit · mdretrievescopemaintaincron + LLMrails · lintboundaries
step 1/4You catch it in review, say "wrong". You log it via /wrong — one line, without breaking flow.

Team and gate

The team (e.g. 12 devs)
PR on lessons / a curator
Architecture owner · arbiter
Shared ledger in git

Without a gate the ledger quickly fills with conflicts and personal taste disguised as rules. So new lessons pass through a gate: a PR on lessons/ or a single curator as arbiter — keeping the ledger coherent and free of contradicting rules. Generic lessons can be delegated, while promotion-to-sensors and _core stay with the curator; keep the gate lightweight, or the arbiter becomes a bottleneck.

Health metric

The ledger's size grows — an alarm: a write-only graveyard
% of lessons graduated into sensors — that is what should grow

What is genuinely new here. "Capture → rule" is already probed in embryo (Cursor auto-rules, model memory), but per-tool and locked into a vendor. The open niche:

model-agnosticteam-sharedgit-nativesoft → deterministic

The discipline of "ledger as staging, not a dump" is what hyped products skip — and what turns "yet another memory" into a harness tool.

Run both examples back to back: you'll see the computational lesson never enters the ledger at all (this matters — otherwise the notebook bloats with duplicates of what's already railed), while the inferential one lives as a nudge and is flagged for promotion only once it has stabilized.
3

The normalize fork: what goes to rails, what to the ledger

Normalize is the heart of the system. An LLM pass takes a raw "this is wrong" and does two things: it structures it (scope, "Wrong"/"Right" blocks, a short "Why") and splits the computational vs inferential sensor fork. This is exactly the distinction the whole harness rests on: context decides what the agent will try; deterministic constraints decide what it's allowed to ship. Computational — the rule is expressible as a static check (import boundary, forbidden call, structural test). Then its place is NOT the ledger but the rails directly: an edit to the boundary-linter, while the notebook keeps only a status: promoted entry — "why the rule appeared". Inferential — judgement, taste, a contextual choice catchable only by an LLM-judge or a human. Then it's a lesson file in lessons/<boundary>/. Unsure — mark it inferential + promotable: true and state when it would mature. And a hard taboo: never write a chase-the-metric rule ("raise coverage to N%") — that's Goodhart pressure, the agent will weaken asserts for the number. The right analog is a policy ("don't edit existing tests"), not a number.

⚙️ Computational → rails

  • Catchable by a static check
  • Lives in the boundary-linter, NOT a lesson file
  • The notebook keeps only a status: promoted entry
  • A barrier: the agent physically can't repeat it

🧠 Inferential → ledger

  • Judgement / taste / contextual choice
  • Caught by an LLM-judge or a human
  • A lesson in lessons/<boundary>/, read before the task
  • A nudge: ignorable, so it matures toward promotion
# prompts/normalize.md — превращает сырую коррекцию в урок и разводит развилку
# turns a raw correction into a lesson and splits the fork
You are the lesson normalizer for the project's agent harness.
The architecture and its boundaries live in AGENTS.md / ADR. Rely on those, not on
generic best practices. Input: a raw correction (what the dev marked "wrong") + an
optional file path / diff from context.

1. SCOPE — where the rule applies. A glob over THIS project's structure, or a named
   boundary / module, or a task type. Narrower is better. Never widen beyond the example.
2. RULE — two blocks on a CONCRETE example: "Wrong" (what the agent did) and
   "Right" (what it should be), plus a short "Why".
3. SENSOR — the fork:
   computational — expressible as a static check (import boundary, forbidden call,
     structural test, fitness function) -> belongs in the architectural constraints
     (the boundary-linter), NOT the ledger. Give the concrete rule/config.
   inferential — judgement / taste / context, catchable only by an LLM-judge or human
     -> belongs in the ledger as a lesson.
   Unsure -> inferential, promotable: true, and state the condition under which it matures.
   NEVER write a rule that chases a metric (e.g. "raise coverage to N%"): that creates
   Goodhart pressure. The right analog is a policy ("don't edit existing tests"), not a number.
4. Return EXACTLY one lesson markdown file (frontmatter + blocks). File only — no preamble.
5. Last line — the verdict:
   VERDICT: computational -> <concrete place in the constraints>
   VERDICT: inferential  -> lessons/<boundary>/
One rule = one file. Several different mistakes in the input -> split into several files.
A one-line fork test: "can this be baked into a linter?". Yes → rails, and it has no business in the ledger. No → a lesson. If the fork keeps answering "yes" — that's good: it means your architecture is legible and almost everything is deterministic.
4

Capture without friction: /wrong and the lesson template

Capture is the most fragile link: high friction kills it. If logging a correction takes more than a couple of seconds, nobody does it — including you. So capture is one command in the moment, not "I'll write it up later". In Claude Code that's a macro in .claude/commands/wrong.md (the agent picks up markdown from that folder), invoked as /wrong <what's wrong>. For another agent it's just an alias or a saved prompt with the same body. The command never writes a file silently — it shows the lesson and the VERDICT line and waits for your "ok". The lesson itself is a markdown file with frontmatter, organized by the project's boundaries/modules (lessons/<boundary>/<id>.md). Key fields: scope (a glob over your project's actual structure), sensor (computational | inferential), promotable (will it mature into a barrier), status (active | promoted | archived | conflicting), seen (how many times it has occurred — grows). To avoid starting from an empty ledger, run bootstrap over the last N PRs / review threads and extract the corrections that genuinely recur (seen ≥ 2) — inventing nothing.

Lesson template — frontmatter

scope — a glob/boundary for YOUR project (narrower is better)
sensor — computational | inferential
promotable — will it mature into a computational barrier
status — active | promoted | archived | conflicting
seen — recurrence counter (grows, a promotion signal)
Capture longer than a couple seconds — nobody does it
The command writes the file without your "ok"
# .claude/commands/wrong.md — Claude Code подхватывает команды из этой папки
# other agents: same body as a saved prompt / alias. Invoke: /wrong <what's wrong>
---
description: Capture an agent correction as a lesson (lessons-ledger)
---
I marked something as done wrong. The correction: $ARGUMENTS
If the current context has a file/diff, note its path and which boundary it touches.

Run normalize (prompts/normalize.md):
1) decide scope  2) write the "Wrong"/"Right" blocks  3) decide sensor: computational vs inferential.

Show me the lesson file and the VERDICT line. AFTER my "ok":
- inferential  -> write the file to lessons/<boundary>/<id>.md (status: active)
- computational -> propose a boundary-linter rule AND log a lesson with status: promoted
                   (the record of "why this rule appeared")
Do not write any file before my confirmation.
Mining corrections from git diffs looks tempting but is noisy: a diff mixes the fix, the style, and unrelated changes. Start with explicit /wrong; auto-mining diffs and team-wide sharing are not for the first week.
5

Retrieve: the agent reads the boundary lessons itself

A written lesson is useless if the agent doesn't see it at the right moment. Retrieve is how a lesson reaches the agent before a similar task. The cheap, reliable mechanism is not semantic search but a filter by the boundaries you touch: touch persistence/* → load lessons/_global.md, lessons/_core/ and that boundary's lessons. Module boundaries give an almost free relevance signal. "The agent looks it up itself" is a rule line in the file it reads at startup: AGENTS.md (universal) and/or CLAUDE.md. The rule is simple: before working, identify the touched boundaries, read their active lessons and treat them as hard constraints — if a solution violates a lesson, stop and report, don't do it silently. Lessons with status: promoted | archived are NOT served on retrieve (promoted are already in the sensors — no point duplicating). The hard part here is timing: over-pour the context (200 lessons) and the agent ignores them while cost rises; under-pour and the needed one didn't fire. Hence scope by boundary, not "everything at once".
Task touches a boundary
Load _global + _core + <boundary>
Lessons = hard constraints
violates
Conflict → stop and report
no violation
No conflict → proceed
## Lessons-ledger (rules learned from mistakes)   ← put in AGENTS.md / CLAUDE.md

Before working on a task:
1. Identify which boundaries / modules you touch.
2. Read lessons/_global.md, lessons/_core/ and lessons/<touched-boundary>/*.md.
3. Treat these lessons as hard constraints. If a solution violates a lesson —
   stop and report, do not do it silently.
4. If I say "wrong" during the work, that is the signal to capture a lesson: /wrong.

Ignore lessons with status: archived | promoted (promoted are already in the sensors).
Don't dump the whole ledger into context "just in case". Same principle as the GC checks: the signal must be narrow and relevant. A module boundary is a ready, cheap filter; reach for semantics only if boundaries aren't enough.
6

Maintain: dedup, promotion to sensors, the metric

Maintain is the ledger's "garbage collector" — small continuous payments instead of a one-off cleanup. A background job of the same class as the phase-5 GC checks (on the same scheduler — CI / cron / n8n), weekly or by hand. It only PROPOSES diffs, applies nothing silently. Four functions: dedup/merge of near-duplicate lessons (summing seen); catching contradictions (two conflicting lessons for one scope → mark both conflicting and surface to the human, never pick a winner itself); promotion (stable inferential lessons with promotable: true, unchanged for N days and with seen ≥ M → propose a concrete boundary-linter rule, mark the source lesson status: promoted — archive, not delete); hygiene (promoted/archived aren't served on retrieve). And the headline of the maintain report is not the ledger's size but how many lessons graduated into computational sensors this period. That is the health metric. The ledger must drain into the rails: an inferential lesson is a nudge you can ignore, so the value is that matured knowledge hardens into a barrier rather than sitting forever as text. This closes the autonomy core: corrections the GC can't catch (non-deterministic taste mistakes) pass through the ledger and some become the very phase-3 barriers. Beyond it the series continues with phases 6–8 (skills, workflow, scaling); the full map is in the hub.
PhaseWhatRecipe
HubHarness Engineering series mapharness-engineering — done ✓
5Garbage collection: drift → artifactsharness-garbage-collection — done ✓
5.5Lessons-ledger: corrections as codethis recipe ✓
6Skills: reusable SKILL.mdagent-skills-library — done ✓
# prompts/maintain.md — gardener of the ledger; PROPOSES diffs, applies nothing silently
You are the ledger keeper. Input: all files in lessons/**.

1. DEDUP / MERGE: lessons saying the same thing -> merge into one, keep the clearest
   example, sum their seen.
2. CONFLICTS: pairs that contradict for the same scope -> mark both status: conflicting
   and surface to the human. Do NOT pick a winner yourself.
3. PROMOTION (machine over text): inferential lessons with promotable: true that are
   stable (unchanged N days AND seen >= M) -> propose a concrete boundary-linter rule /
   structural test, mark the source lesson status: promoted (archive, not delete).
4. HYGIENE: lessons with status: promoted | archived are not served on retrieve.

Output: the diffs; a health summary (active / promoted / conflicting counts);
and ABOVE ALL — how many lessons graduated into computational sensors this period.
That is the metric, not the ledger's size.
Dedup by scope, not by wording: two lessons about the same boundary said in different words are one lesson with a summed seen. Otherwise "promote the stable ones" never fires, because seen is smeared across duplicates and the M threshold is never reached.

Result

Phase 5.5 is assembled: your corrections stopped getting lost in chat and became code. You understand the lesson pipeline (capture → normalize → store → retrieve → maintain), the main normalize fork (computational goes to the rails at once, inferential lives as a lesson and matures toward promotion), how to make capture frictionless via /wrong, how the agent pulls a boundary's lessons via the retrieve rule, and how maintain dedups and promotes the matured into sensors. And above all — the health metric: how many lessons graduated into barriers, not the size of the notebook, plus the anti-Goodhart discipline (no chase-the-percentage rules). This is the autonomy core of the series; next come phases 6–8 (skills, workflow, scaling), the full map in the hub.