Skip to content
Lessons

Lessons your agents actually keep.

An agent repeats a mistake because nothing durable recorded what was learned. A lesson in a chat log is gone with the session; a lesson in a conventions file is read at the top of every session and not at the moment it matters. Here a lesson is a row with a lifecycle: filed when something goes wrong, observed again each time the same shape returns, and advanced — forward only, refused at the database — to enforced, which the database will not grant until someone writes down the mechanism that prevents the recurrence. That sentence is what becomes a rule.

lesson/typed-params-under-lock-hints

observed · documented · enforced · archived
A lesson filed A ledger row marked observed, dated 07-14 — the lesson as first written down. 07-14 observed A second sighting A ledger row marked observed, dated 07-29 — the same shape seen again. 07-29 observed The pattern written out A ledger row marked documented, dated 08-05 — the pattern write-up added. 08-05 documented The gate passed A ledger row marked enforced, dated 08-11 — the prevention mechanism written down. 08-11 enforced

forward only · enforced needs a prevention mechanism · illustrative

How it works

File. Observe. Enforce. Promote.

Four verbs, in the order a lesson lives them. Each tool links to its row in the catalogue.

  1. File

    A slug unique to the project, a one-line title, and the applicability tags that say where the lesson bites. It lands observed — a thing that happened once, written down before it is understood. Pass a pattern write-up and it lands documented instead; filing at observed refuses one, because a pattern is what documented means.

    file_lesson
  2. Observe

    The second time the same shape appears, record the sighting against the lesson rather than filing a new one: what you saw, a short source label, and the backlog item, build session, commit, feature or file it turned up in. Each one bumps the lesson’s last-observed time. Observations are the evidence that it recurs — and recurrence is the whole argument for a rule.

    append_lesson_observation
  3. Enforce

    Advance it: observed, documented, enforced, archived — forward only, refused at the database, never back. Reaching documented needs the pattern write-up; reaching enforced needs the prevention mechanism as well — the sentence that says what now makes the mistake impossible. A database CHECK constraint refuses the row without it.

    update_lesson_status
  4. Promote

    An enforced lesson becomes a rule the next session is handed: the prevention mechanism becomes the rule’s body, the applicability tags seed its project_attributes, and source_lesson_id points back. A lesson that is not enforced is refused — which is what the gate two steps up was for.

    derive_rule
A lesson becomes a rule: candidate, two observations, promotion, one badge Four ledger rows on the left — a lesson candidate, two observations of the same shape, and a promotion — converge with a bracket into a single rule badge on the right, marked enforced with an amber dot. day 1 lesson a race test that hopes to land in the window is not a guardrail day 3 observation same shape, second suite day 9 observation same shape, review round day 12 promoted human gate · status: enforced rule/lock-order returned by query_applicable_rules retrieval counted on every query a rule is never invented — it is a lesson that was seen again and promoted through a human gate
One lesson, seen twice, promoted once. The rule on the right is what the next session is handed — the incident on the left is what it no longer has to repeat.

The problem is that nothing durable records what was learned. An agent works out, painfully, that a race test which merely runs both sides and hopes proves nothing — and the finding lives in a transcript that ends with the session. Writing it into a conventions file moves it somewhere durable and somewhere unread: that file is loaded whole at the start of every session, before anyone knows which change is coming, so the line that matters is read at the one moment it cannot be applied. The other shape is a record with a lifecycle. A lesson is filed when the mistake happens, gains an observation each time the same shape returns, and is only called enforced once someone has written down what now prevents it — at which point it can be promoted into a rule an agent asks for by context.

In the session protocol this is the last step, and it is the one that closes the loop: the session records its decisions, files what it did not finish into the backlog, reports what it consumed — and files or advances a lesson for the pain worth not repeating. Step two of the next session is the retrieval that hands the resulting rule back. Nothing here is enforced by the server: the protocol is a recipe we run, and the lesson tools are ordinary calls any client can make.

The ladder and the gate

Four rungs, forward only, and one the database will not grant on request

This is the distinctive part, so it is worth being exact about which piece of the system enforces which claim. The lifecycle is a trigger; the gate is two CHECK constraints; both sit beneath the service, so neither is a matter of the API being polite.

The four lesson statuses: what each means and what the database requires to reach it
statuswhat it meanswhat reaching it requires
observed It happened, and someone wrote it down before understanding it. Nothing beyond a slug and a title — and a pattern write-up is refused here, because writing one is what documented means.
documented The shape is understood and written out: what happens, and why. pattern_markdown — CK_build_lessons_status_requires_pattern refuses the row without it.
enforced Something now makes the mistake hard to repeat — a constraint, a guard, a test. prevention_mechanism_markdown as well — CK_build_lessons_enforced_requires_prevention refuses the row without it. This is the rung derive_rule accepts.
archived Retired: it no longer applies, or the code it was about is gone. Nothing — but it is the end of the line. Archived is absorbing, and there is no surface that moves a lesson back.

Forward only, at the database

status_rank is a persisted computed column — observed 0, documented 1, enforced 2, archived 3 — and the trigger rolls back any UPDATE that lowers it. archived is absorbing, and no tool, route or button offers a way back. The service refuses a backward move first, so a caller gets a conflict rather than a broken transaction.

trigger:tg_build_lessons_status_monotonic

The gate is a CHECK constraint

enforced without prevention_mechanism_markdown is not a row the database will store; its sibling refuses documented or enforced with no pattern write-up. A lesson can be filed only at observed or documented — enforced is reachable only by advancing it, on purpose.

constraint:CK_build_lessons_enforced_requires_prevention

Identity fixed at insert

The same trigger rolls back any UPDATE touching the workspace, the id or the slug. That is not tidiness: it is what makes the rank comparison unevadable, since a row that could rewrite its key could slip the join the trigger compares on. The project can still be corrected.

columns:lesson_id · lesson_slug

Say the caveat with the claim: the gate is structural, and the human in it is convention. Any credential holding write_session_state can advance a lesson to enforced — the database does not check who is asking, and there is no separate approval step. What it does check is that the prevention mechanism is present, which means the promotion cannot happen as a side effect of an agent tidying up: somebody had to decide what now makes the mistake hard to repeat, and write the sentence. That is a discipline made unskippable, not an identity check, and it is the honest version of the claim.

Underneath the ladder sits the evidence. An observation is one sighting — the write-up, a short source label, and optionally the backlog item, build session, commit, feature or file it turned up in — and appending one bumps the lesson’s last-observed time, so a lesson that keeps biting is visible as such. They are append-only by construction rather than by trigger: the only writes anywhere in the codebase are the insert behind append_lesson_observation and the cascade the hard delete performs itself, and that table carries no trigger because it needs none. Be exact about the consequence — a mis-filed lesson can be hard-deleted by an authorised credential, and deleting it deletes its observations in the same transaction; archived is the retire-without-delete path.

The bounds are checked twice, once by the service as a malformed-request error and once by a database constraint: a slug of 1–100 characters unique to the project, a title of 1–300, a pattern write-up up to 32,000 characters, a prevention mechanism up to 8,000, an observation of 1–4,000, and up to 32 applicability tags drawn from a controlled vocabulary of 12. The two markdown caps are written as byte bounds rather than character counts on purpose, so a value padded with trailing spaces cannot slip past a length check that ignores them.

Candidates

The system proposes; a person decides

A candidate is a proposal that a pattern recurs, waiting for judgement. It is the one object here you cannot write by hand — and the pass that produces it is off until you turn it on.

There is deliberately no create-candidate tool and no create route on any transport. A candidate exists to record that a signal noticed something, so letting a caller hand-write one would turn its signal source from a fact into a claim. What you can do is judge it: a candidate leaves pending a single time and never returns to it — promoted into a lesson, rejected with a stated reason, or superseded by a duplicate that says the same thing better. Those three are peers rather than rungs, which is why the database enforces them with tg_lesson_candidates_terminal_absorbing instead of a rank: once a candidate is decided, no UPDATE can move it to another decision, and the same trigger fixes its identity. Two refusals are worth knowing about because they are about evidence, not bookkeeping: a rejected candidate cannot be named as a successor, and a candidate that others were superseded into cannot itself be rejected — that would discard theirs.

Promotion lands the lesson at documented, never at enforced. It creates the lesson one rung short of the gate, carries every observation across as its evidence and back-links both rows — lesson, audit row and link in one transaction — so accepting a proposal can never skip the prevention mechanism. Promoting takes a slug you choose, and lets you fix the detector’s wording first; rejecting takes a reason the call refuses to do without, because a rejection with no stated cause teaches the next person nothing.

What people see

Four dashboard pages: the catalogue, a lesson, the queue, and one decision

app.postmq.com/lessons is the catalogue — title, slug, status and applicability tags — with a project filter, a status filter, a tag filter and a search over the title and the pattern, a count badge per status, and a form to file a lesson (project, slug, title, the tags, and an optional pattern write-up, which is what decides whether it lands observed or documented). app.postmq.com/lessons/{id} shows the overview, the pattern, the prevention mechanism and the observations, with an Advance form whose picker offers only the statuses ahead of this one — and a prevention-mechanism field that appears only when the target is enforced — a form to record an observation, and a delete behind an explicit confirmation. app.postmq.com/lesson-candidates is the triage queue with its depth per status, filters for project, status and signal, and a table of the proposal, its signal, its confidence and its evidence count; app.postmq.com/lesson-candidates/{id} is the one decision: the proposal, the evidence behind it, and a Decide card offering promote — which takes the new lesson’s slug — or reject, which takes the reason.

Three honest gaps, and they are the reason this section is short. Superseding a candidate, appending evidence to one and running a detection pass are MCP and REST only — the triage page displays a supersede that happened elsewhere but offers no control for it. A lesson’s title, slug and tags cannot be edited after filing on any surface; the slug and the id are fixed at insert by the trigger, and the rest is simply not offered. Both list pages show the first 100 rows that match the filters and ask you to narrow them rather than paging further. And an archived lesson reads as read-only with its lifecycle fixed, though it keeps its delete.

Every file, advance, observation, deletion, detection, promotion, rejection and supersede is a row on the workspace’s audit chain — 9 of the session-state lifecycle event types, written into the same tamper-evident SHA-256 chain as messaging, in the same transaction as the row each records; an advance that changes nothing writes none. A workspace can browse that chain in the dashboard and read it over REST and MCP; the mechanism is on the security page.

API surface

14 tools, 15 routes, four pages

The MCP tool, the versioned REST route under /v1 and the dashboard page all run the same service layer, and REST and MCP serialize the lesson, its observations, the candidate and the detection result through one shared session-state serializer, tested byte-identical.

file_lesson
MCP tool
{
  "method": "tools/call",
  "params": {
    "name": "file_lesson",
    "arguments": {
      "lesson_slug": "typed-params-under-lock-hints",
      "title": "An interpolated id parameter scans under a lock hint",
      "applicable_to": ["ef-migration", "test-discipline"],
      "status": "documented",
      "pattern_markdown": "An interpolated string binds as NVARCHAR while the id column is CHAR(26); the implicit conversion defeats the seek, and under a lock hint the scan takes locks on other tenants' keys."
    }
  }
}
REST
POST /v1/lessons
Authorization: Bearer pmq_…redacted…
Content-Type: application/json

{
  "lesson_slug": "typed-params-under-lock-hints",
  "title": "An interpolated id parameter scans under a lock hint",
  "applicable_to": ["ef-migration", "test-discipline"],
  "status": "documented",
  "pattern_markdown": "An interpolated string binds as NVARCHAR while the id column is CHAR(26); the implicit conversion defeats the seek, and under a lock hint the scan takes locks on other tenants' keys."
}
Dashboard
app.postmq.com/lessons                 File a lesson: project, slug, title, Applies to, Pattern write-up
app.postmq.com/lessons/{id}            Overview, Pattern, Observations — the page the file form lands on
update_lesson_status
MCP tool
{
  "method": "tools/call",
  "params": {
    "name": "update_lesson_status",
    "arguments": {
      "lesson_id": "01K…",
      "new_status": "enforced",
      "prevention_mechanism_markdown": "Bind a typed SqlParameter of SqlDbType.Char length 26, never string interpolation — and add a cross-key race test, because a same-key one is green either way."
    }
  }
}
REST
POST /v1/lessons/01K…/status
Authorization: Bearer pmq_…redacted…
Content-Type: application/json

{
  "new_status": "enforced",
  "prevention_mechanism_markdown": "Bind a typed SqlParameter of SqlDbType.Char length 26, never string interpolation — and add a cross-key race test, because a same-key one is green either way."
}
Dashboard
app.postmq.com/lessons/{id}            Advance: the picker offers only the statuses ahead of this one
                                       Prevention mechanism — the textarea shown only when the target is enforced
promote_lesson_candidate
MCP tool
{
  "method": "tools/call",
  "params": {
    "name": "promote_lesson_candidate",
    "arguments": {
      "candidate_id": "01K…",
      "lesson_slug": "typed-params-under-lock-hints",
      "title": "An interpolated id parameter scans under a lock hint"
    }
  }
}
REST
POST /v1/lesson-candidates/01K…/promote
Authorization: Bearer pmq_…redacted…
Content-Type: application/json

{
  "lesson_slug": "typed-params-under-lock-hints",
  "title": "An interpolated id parameter scans under a lock hint"
}
Dashboard
app.postmq.com/lesson-candidates/{id}  Decide: Promote to a lesson (a slug) or Reject (a reason)
app.postmq.com/lessons/{id}            the new lesson, at documented, carrying the candidate’s evidence
detect_lesson_candidates
MCP tool
{
  "method": "tools/call",
  "params": {
    "name": "detect_lesson_candidates",
    "arguments": {
      "project_id": "01K…",
      "max_candidates": 5,
      "lookback_items": 60
    }
  }
}
REST
POST /v1/lesson-candidates/detect
Authorization: Bearer pmq_…redacted…
Content-Type: application/json

{
  "project_id": "01K…",
  "max_candidates": 5,
  "lookback_items": 60
}
Dashboard
app.postmq.com/lesson-candidates       Queue depth and the queue itself — a pass files into it
                                       detection is not started from this page: it is a tool, a route, or the sweep

The 14 MCP tools · from the server's own manifest

Reusable build lessons with a forward-only lifecycle, and the candidates a detection pass proposes. PostMQ speaks standard MCP, so any MCP-capable client can call them; each name links to its row in the catalogue.

The lessons MCP tools, from docs/mcp/tool-surface.json
toolwhat it doesneeds
file_lesson File a new reusable build lesson. Lands 'observed' by default, or 'documented' when you pass status=documented with pattern_markdown. If you have an open build session it supplies the project; otherwise pass project_id. applicable_to tags are a controlled vocabulary. A slug already used in the project is a conflict. Requires the write_session_state scope. required: lesson_slug, title write_session_state
get_lesson Get a lesson and its observations (oldest-first). Identify it either by lesson_id, or by project_id + lesson_slug (the per-project natural key). Open to any authenticated caller. authenticated
query_lessons List build lessons in lifecycle order (least-advanced first, newest first), offset-paged with a total. Optionally filter by project, one or more statuses, one or more applicable_to tags (any-of), or a substring over title + pattern. Returns a lightweight summary shape. Open to any authenticated caller. authenticated
update_lesson_status Advance a lesson forward through observed -> documented -> enforced -> archived (never backward; archived is absorbing). Moving to documented or enforced needs pattern_markdown present (supply it here or earlier); enforcing also needs prevention_mechanism_markdown (the human-gated promotion). Requires the write_session_state scope. required: lesson_id, new_status write_session_state
append_lesson_observation Append an evidence sighting to a lesson (bumps its last_observed_at). Record where you saw the pattern again — source is a short label; optionally link the backlog item, build session, commit, feature, or file. observed_at defaults to now. Requires the write_session_state scope. required: lesson_id, observation_markdown, source write_session_state
delete_lesson Hard-delete a mis-filed lesson and all its observations (archived is the retire-without-delete path). Requires the write_session_state scope. required: lesson_id write_session_state
list_lesson_candidates List lesson candidates (auto-detected proposals that a recurring pattern may deserve a curated build lesson). Ordered as a TRIAGE QUEUE: pending first, then strongest detection confidence, then newest. An unscored proposal sorts last within pending. Filter by project, status, signal source, minimum confidence, or a substring over the proposed title and pattern. authenticated
get_lesson_candidate Get one lesson candidate by ULID, together with the evidence observations that back it (newest sighting first). Read this before deciding: the observations are the case for promotion. required: candidate_id authenticated
count_lesson_candidates Count lesson candidates per lifecycle status (optionally within one project) — the triage queue-depth header. Every status is returned, with an explicit zero when empty. authenticated
observe_lesson_candidate Append an evidence sighting to a PENDING candidate: you hit the same pattern again. Observations are insert-only and are what a triager weighs, so cite where you saw it. A candidate that has already been decided is a conflict. Requires the write_session_state scope. required: candidate_id, observation_markdown, source write_session_state
promote_lesson_candidate Promote a PENDING candidate into a curated build lesson: creates the lesson at 'documented', back-links it, and carries the candidate's observations across as the lesson's evidence. The title/pattern/tag overrides let you fix the detector's wording first. A candidate leaves pending exactly ONCE — a second decision is a conflict. Requires the write_session_state scope. required: candidate_id, lesson_slug write_session_state
reject_lesson_candidate Reject a PENDING candidate with a REQUIRED reason — a rejection with no stated cause teaches the detector nothing and leaves a later triager unable to tell a considered 'no' from an accidental one. A candidate leaves pending exactly once. Requires the write_session_state scope. required: candidate_id, reason write_session_state
supersede_lesson_candidate Supersede a PENDING candidate by another candidate that says the same thing better — the two are duplicates and only one should reach a decision. The successor must not be rejected, and the chain must not form a cycle. Requires the write_session_state scope. required: candidate_id, superseded_by_candidate_id write_session_state
detect_lesson_candidates Run one LLM detection pass over a project's RESOLVED backlog items, filing each recurring pattern it finds as a PENDING candidate for a human to judge. Returns a typed result: 'completed' (see created/proposed/skipped), 'insufficient_signal' (too few resolved items to cluster), 'deferred' (no provider configured, or a transient failure — retry later), or 'unusable'. An unknown project is a not-found error, not a result. Requires the write_session_state scope. required: project_id write_session_state

The 15 REST routes

Under the versioned /v1 API. Writes carry write_session_state; reads carry the default authenticated policy. Seven are the lesson itself; eight are the candidate queue.

The lessons and lesson-candidate REST routes
routewhat it doesneeds
POST /v1/lessons file — a slug unique to the project, a title, the applicability tags; lands observed, or documented when a pattern write-up comes with it. The project comes from project_id or the caller’s open build session write_session_state
POST /v1/lessons/{id}/status advance — forward only; documented and enforced need a pattern write-up, enforced also needs the prevention mechanism. A same-rank move is a no-op that writes no audit row write_session_state
POST /v1/lessons/{id}/observations append a sighting — the write-up, a short source label, and optionally the backlog item, build session, commit, feature or file it was seen in; bumps last_observed_at write_session_state
DELETE /v1/lessons/{id} hard-delete a mis-filed lesson and its observations in one transaction; archived is the retire-without-delete path write_session_state
GET /v1/lessons list and search — filter by project, status and applicability tag, or match a substring of the title or the pattern; ordered by status, then newest first authenticated
GET /v1/lessons/by-slug one lesson by project_id + lesson_slug, with its observations authenticated
GET /v1/lessons/{id} one lesson by id — fields, pattern, prevention mechanism, and its observations oldest first authenticated
GET /v1/lesson-candidates the triage queue — filter by project, status and signal source authenticated
GET /v1/lesson-candidates/counts the queue depth, per status authenticated
GET /v1/lesson-candidates/{id} one candidate — the proposal, its signal source and confidence, and every observation behind it authenticated
POST /v1/lesson-candidates/detect run one detection pass over a project’s resolved backlog now; answers deferred and files nothing when no provider is configured write_session_state
POST /v1/lesson-candidates/{id}/observations add evidence to a pending candidate — refused once it has been decided write_session_state
POST /v1/lesson-candidates/{id}/promote accept it — creates the lesson at documented, carries the observations across, back-links both write_session_state
POST /v1/lesson-candidates/{id}/reject decline it, with a reason the call refuses to do without write_session_state
POST /v1/lesson-candidates/{id}/supersede fold it into a better-worded duplicate; the successor may not be a rejected one, and the chain may not form a cycle write_session_state
Guarantees and limits

What holds, how, and what is default-off

A mechanism per row; the badge says whether the row ships on, ships off, or ships with the caveat its note carries. One row here is default-off — detection — and it says so.

Lessons guarantees and limits: what holds, the mechanism, and its scope
what holdshowscope
A lesson only moves forward, and the database is what says soC111 status_rank is a persisted computed column (observed 0, documented 1, enforced 2, archived 3) and tg_build_lessons_status_monotonic rolls back any UPDATE that lowers it; archived is absorbing, and no surface offers a way back. The service refuses first, so a caller gets a conflict rather than a broken transaction shipped
Identity and slug are fixed at insertC111 the same trigger rolls back any UPDATE touching workspace_id, lesson_id or lesson_slug — which is what makes the rank comparison unevadable, since a row cannot rewrite its key and slip the join. The project can still be corrected shipped
Nothing reaches enforced until the prevention mechanism is writtenC112the gate is structural, the human in it is convention: any credential holding write_session_state can enforce a lesson. What cannot be skipped is writing the mechanism down two CHECK constraints, beneath the service: CK_build_lessons_status_requires_pattern refuses documented or enforced with no pattern write-up, CK_build_lessons_enforced_requires_prevention refuses enforced with no prevention mechanism. A lesson can be filed only at observed or documented — enforced is reachable only by advancing it shipped with caveat
Observations are append-only by constructionC113by construction, not by trigger — say it that way: deleting the lesson does delete its observations, in the same transaction build_lesson_observations carries no trigger and needs none — the only writes are the insert behind append_lesson_observation and the cascade the hard delete performs itself. Each records a sighting and its provenance, and bumps the lesson’s last_observed_at shipped with caveat
Every bound is checked twiceC114the two markdown caps are written as byte bounds rather than character counts, so a value padded with trailing spaces cannot slip past a length check that ignores them a slug of 1–100 characters unique to the project, a title of 1–300, a pattern write-up up to 32,000 characters, a prevention mechanism up to 8,000, an observation of 1–4,000, and up to 32 applicability tags from a controlled vocabulary of 12 — each a service check and a database constraint shipped
A candidate cannot be typed inC115 there is no create tool and no create route on any transport; a candidate exists only because a detection pass filed it, which is what keeps signal_source a fact rather than a claim shipped
A candidate is decided once, and the decision is finalC115 promoted, rejected or superseded — three peers, not a ladder, which is why absorption is a trigger rather than a rank: tg_lesson_candidates_terminal_absorbing rolls back any UPDATE that changes status out of a terminal state, and any UPDATE touching the identity columns shipped
Promotion lands at documented, never at enforcedC115 promote_lesson_candidate creates the lesson one rung short of the gate, carries every observation across as its evidence and back-links both rows — lesson, audit row and link in one transaction. So accepting a proposal can never skip the prevention mechanism shipped
A decision has to say somethingC115 reject needs a stated reason; supersede refuses a rejected successor and refuses a cycle; and a candidate that others were superseded into cannot be rejected, because that would discard their evidence shipped
Detection is optional and off by defaultC116nothing is proposed unless someone turns it on, and nothing becomes a lesson until a person promotes it — the model never promotes the detection sweep ships disabled on a daily cadence, the deployment template’s switch defaults to off, and the LLM client is inert until you bring your own Anthropic key — with no provider, detect_lesson_candidates answers deferred and files nothing. The one detector that ships clusters a project’s resolved backlog items default-off
A rule derives only from an enforced lessonC12mis-filed lessons and rules can be hard-deleted by an authorised credential; archiving is the retire-without-delete path derive_rule reads the lesson under an update lock inside the same transaction that inserts the rule and refuses one that is not enforced; the prevention mechanism becomes the rule’s body and the applicability tags seed its project_attributes shipped
9 events on the audit chainC19 build_lesson.filed, build_lesson.status_changed, build_lesson.observation_appended, build_lesson.deleted; lesson_candidate.detected, lesson_candidate.observation_appended, lesson_candidate.promoted, lesson_candidate.rejected, lesson_candidate.superseded — each written into the workspace’s tamper-evident SHA-256 chain in the same transaction as the row it records; a same-rank advance writes none shipped
Writes need one scope; reads need only authenticationC117 the nine writes require write_session_state over REST and MCP alike; the five reads — the lesson, the search, the queue, a candidate, the counts — require an authenticated caller in the workspace, and the workspace always comes from the credential shipped
REST and MCP return the same bytesC05 one shared session-state serializer per aggregate builds the lesson, its observations, the candidate and the detection result, so the two transports are byte-identical by construction and tested that way shipped
Honest limits

What this is not

It is a record and a gate, not a learning system. Nothing infers a lesson from your code, your commits or your transcripts: the only detector reads a project’s resolved backlog items, it is off by default, it needs your own provider key, and what it produces is a proposal a person still has to promote. There is no ranking of lessons by importance, no similarity search across them, no automatic linking of a lesson to the code it concerns, and no notification when one is filed. Search is a substring match over the title and the pattern, capped and ordered by status then recency — search, not relevance. A lesson’s title, slug and tags cannot be edited after filing, and a lesson’s detail returns its 500 most-recent observations rather than all of them. Superseding a candidate, adding evidence to one and running detection are MCP and REST only. Rules derived from lessons stay advisory to the agent — enforcement lives in your CI, not here. And there is no CLI command for any of this.

Questions

Seven things people ask

No, and two separate things have to be switched on before it even proposes. Detection — the pass that reads a project’s resolved backlog and suggests recurring patterns — ships disabled, on a daily cadence, and the deployment template’s switch defaults to off; the client that would call the model is inert until you configure your own Anthropic key, so with no provider the call answers deferred and files nothing. And a proposal is never a lesson: someone has to promote it, which lands it at documented, one rung short of enforced. Nothing in the system makes that decision on its own. What the agent does do without any of that is write lessons down and observe them again — filing and observing are ordinary tool calls.

Writing down the prevention mechanism. A lesson advances observed, documented, enforced, archived, and the transition to enforced needs prevention_mechanism_markdown — the sentence that says what now makes the mistake hard to repeat. A database CHECK constraint refuses the row without it, beneath the service, so the gate holds even for a caller that goes around the API. Be precise about the caveat: the gate is structural, the human in it is convention. Any credential with the write_session_state scope can enforce a lesson. What the database makes unskippable is that somebody wrote the mechanism down.

No. status_rank is a persisted column and a database trigger rolls back any update that lowers it, so documented cannot return to observed and archived is the end. The same trigger fixes the id and the slug at insert, which is what makes the rank comparison unevadable — a row cannot rewrite its key and slip the check. Two things are still open to you: the project can be corrected, and a mis-filed lesson can be hard-deleted outright, which removes its observations with it. Archiving is the retire-without-delete path.

Evidence that the pattern recurs. The first time a mistake happens you file a lesson; the second time you append an observation rather than filing a second lesson — the write-up, a short source label, and optionally the backlog item, build session, commit, feature or file you saw it in. Each one bumps the lesson’s last-observed time, so the lessons that keep biting are visible as such. They are append-only by construction: the only writes are inserts, and the table needs no trigger to keep it that way. A lesson’s detail view shows its 500 most-recent observations, oldest first.

From a detection pass, and from nowhere else. There is deliberately no create-candidate tool and no create route on any transport, because a candidate exists to record that a signal noticed something — letting a caller hand-write one would make its signal source a claim instead of a fact. The detector that ships clusters a project’s resolved backlog items with a model on your key. Each candidate is then decided a single time — promoted into a lesson, rejected with a reason, or superseded by a duplicate that says it better — and the database will not let that decision be revisited.

derive_rule, and only from an enforced one. It reads the lesson under an update lock inside the same transaction that inserts the rule, copies the prevention mechanism into the rule’s body, seeds project_attributes from the applicability tags, and links back with source_lesson_id; a lesson that is not enforced is refused. From then on the rule is what query_applicable_rules returns to the change it applies to — the point of the whole ladder is that the next session is handed the conclusion rather than the incident.

No — and today some of it is not there. The four pages cover the common path: file a lesson, filter and search the catalogue, advance a lesson and record an observation, and triage a candidate by promoting or rejecting it. Superseding a candidate, adding evidence to one and running a detection pass are MCP and REST only, and a lesson’s title, slug and tags cannot be edited after filing on any surface. The tools are the whole surface; the dashboard is the part a person needs most often.

Last verified 2026-08-19 against main at 74fa02d: the lesson service and query service, the two migrations and their trigger and CHECK constraints, the detection sweep's defaults, the 14 tools, the 15 routes and the four dashboard pages this page names were read on that day.

Related: rules — where an enforced lesson goes next · session state — the protocol this is the last step of · the backlog a detection pass reads · the lessons tools in the catalogue · the audit chain every write lands on · connect an agent · pricing — lessons and rules are unlimited on every tier.

Write the mechanism down once. Let the ladder carry it.

Create a workspace, connect your agent, and file the next thing that goes wrong as a lesson instead of a note that ends with the session.