Skip to content

Self-improvement — how Tyran learns your repo

agents/retro.md · skills/retro/SKILL.md · hooks/scripts/retro-gate.mjs

This is Tyran’s centerpiece: you bring the harness, it does the improving. The more initiatives you run, the better it fits your repo and your style.

After every closed initiative, the tyran-retro agent reads the initiative’s journal, decision ledger, and agent reports — the densest source of signal — plus the git history of the work itself. Then it may improve Tyran (never your product code):

  • distill a repo rule (“full test suite, not just touched files — a regression slipped in sideways once”),
  • write a repo-specific skill for recurring work,
  • tune an agent prompt variant,
  • or delete/weaken one of its own earlier rules — the loop self-corrects downward, too.

Not a log of a real install — the shape the loop is designed to produce:

Initiative 1 Infers your stack, your validation commands, your deployment
style from how the repo is actually worked. Asks once, about
what it genuinely could not establish.
Initiative 5 Knows your shared-file hot zones, your flaky tests, your
review taste. Its retro has already DELETED two of its own
rules that weren't earning their keep.
Initiative 20 Has written repo-specific skills for your recurring work and
merged two agents that overlapped. You mostly approve gates.

The anti-bloat filter (default answer: change nothing)

Section titled “The anti-bloat filter (default answer: change nothing)”

Every candidate change must pass all of:

  1. Will it recur — in another initiative, another domain? One-off circumstances don’t qualify.
  2. Was the cost real and measured — a lost agent cycle, a rework wave? Hypothetical discomfort doesn’t qualify.
  3. Is it already covered by an existing rule? Then the problem is enforcement, not documentation — don’t add words to a rule that was ignored; build a mechanism or do nothing.
  4. Can it be a one-sentence edit instead of a new section? Then only that is allowed.

Plus the extraction curator (adapted from the best pattern we found in the wild): is it non-googleable? specific to THIS repo? earned through real debugging? Generic programming knowledge is never extracted.

Stated in the agent’s own contract (agents/retro.md), because a filter with no ceiling is a preference rather than a limit:

  • At most three changes to the conductor skill per retro. More than that is bloat with certainty rather than risk; the surplus is recorded as candidates for next time and not implemented.
  • Deletion is preferred to addition, in that order: (a) delete or merge what duplicates, (b) fix an existing sentence, (c) only then add. Several retros in a row with no deletion means the filter is too soft.
  • Do not breed agents. A new agent only when the work has a genuinely different mode — different tools, different model tier, different lifecycle — never merely a different topic. A roster nobody can hold in their head is its own cognitive tax.

Guardrails (designed to be enforced, not promised)

Section titled “Guardrails (designed to be enforced, not promised)”
ClassExamplesWho decides
AUTOknowledge facts, rule tweaks, .tyran/config.yaml, agent overrides (since 0.1.44), new repo-specific skills (must pass an activation test)retro commits autonomously; ledger entry; git revert rolls back
GATEDnew/changed hooks, CLAUDE.md, budgets, deleting safety rulesretro proposes, you approve
KERNELthe enforcement hooks, the rollback mechanism, this classification itselfhumans only, by hand

The classification is a file in your repo (.tyran/policies/autonomy.yaml) and is enforced by a PreToolUse hook on write paths — the boundary does not depend on the model behaving.

.tyran/config.yaml is AUTO and it holds the deployment class, which is the one place that table trades safety for usability on purpose. It was GATED until a real install showed what GATED costs there: setup had inferred pnpm test, which in that repo is bare vitest — watch mode, never exits — and the agent that discovered every future agent would hang could not repair the file that said so. If that trade is wrong for your repo, set the rule back to GATED (see policy gate, which also measured the same escalation happening under GATED wherever Write is allow-listed).

Every AUTO entry carries confidence and usage/helpfulness counters; entries that stop earning their keep get degraded or retired by later retros. Two learning loops run at different speeds: fast and local (your repo), slow and curated (pull requests to the core — reviewed by humans).

The counters are fed by a closed loop rather than by promises. Every handoff carries a knowledge briefscripts/knowledge.mjs brief selects the entries whose applies_to globs intersect the story’s predicted files, into a budget — and the agent’s final report owes a verdict on the entry ids it received. The retrospective folds those verdicts into the counters at close (the procedure lives in the retro skill, its one home). An entry nobody reports as helpful is a retirement candidate on evidence, not on taste.

The budget is pressure, and somebody has to look at the total. The brief names what it omitted on every run, but nothing added those omissions up — and measured on a real install the store had reached the state where brief '**' returned 1 of 31 entries: 104,178 codepoints of hard-won detail, read by nobody, while a per-entry oversize warning fired five times and read as a small untidiness each time.

Terminal window
node scripts/knowledge.mjs audit --dir .tyran/knowledge

It answers the aggregate question — how many entries could reach ONE brief, how many could not, and which are so wide they cannot appear even alone, where no budget a caller passes will ever reach them. doctor --state carries the same number as knowledge-store-unreachable.

It is a MEASUREMENT and never an edit. Which of two overlapping entries is the true one is a judgement, and a script that guessed would delete exactly the detail the store exists to hold. That has not changed: nothing scores text similarity, and no tool picks a winner.

What the store gained is a way to record the judgement without paying for it. A merge is an append: write the entry that states the fact once, give it a supersedes: naming every id it replaces, and touch nothing else. knowledge.mjs brief drops any entry named by another entry’s supersedes, so the replaced ones stop reaching handoffs immediately — while keeping their bytes, their provenance and the counters they earned over months, because no file holding them was edited.

That shape is the whole safety argument. A bad merge is not a lost history to be prevented by review; it is one file to delete, after which every entry it retired comes back whole. The audit reports the reclaimable total — superseded entries cost nothing in any brief, so deleting them is a git rm whenever you feel like it, and never a tool.

The one mechanical failure is a supersedes naming an id that is not in the store: the retirement silently does not happen, so the old entry keeps competing and the new one adds to the total. audit names those, and doctor --state counts live and superseded separately so its number still reconciles against the files on disk.

Retirement without a replacement is the other half, and it now runs on evidence rather than taste. audit lists the entries whose counters have written them off — reported wrong more often than helpful, or delivered three times or more and never once reported helpful. An entry delivered once is never a candidate: that is absence of evidence, not evidence. And if no entry in the store carries a non-zero counter at all, the audit says so plainly — the fold at retro close is not happening, which is a more useful finding than any list it could print instead.

This page promised a consolidation step that did not exist until 0.1.35 corrected it, and knowledge.mjs audit printed the same promise in its own output. It may describe one now because selectEntries genuinely suppresses a superseded id — a guard test asserts that by running a brief, not by grepping for a function name.

Where a lesson lands: four stores, four questions

Section titled “Where a lesson lands: four stores, four questions”

A fourth store is only defensible if it answers a question none of the others answers. MISTAKES.md — plain prose at your repository root — does, and the question is recurrence.

StoreThe one question it answersLifetimeWriterHow an agent receives it
journal findingwhat did I discover during THIS run, and what proves itone initiativeany agent, mid-runnothing — it dies with the initiative, correctly
MISTAKES.mdwhat has gone WRONG here, how often, and what would have prevented itthe repository’sthe retrospective, at closenothing — there is no read channel, on purpose
.tyran/knowledge/*.yamlwhat must an agent about to touch THESE paths knowuntil the counters retire itthe retrospectiveknowledge.mjs brief, pasted verbatim into every handoff
CLAUDE.md, inside the tyran:rules fencethe law that binds every session here, whatever it touchesuntil someone deletes the linemistakes.mjs promote --lawit is in context on every turn

Read the axis, not the topics. “Bugs here, conventions there” is unfalsifiable and produces three spellings of one rule inside a month. The axis is shape and delivery: knowledge is forward-looking, path-scoped, machine-delivered and budgeted; MISTAKES.md is backward-looking, narrative, repo-global and counted; CLAUDE.md is law — no scope, no counters, no budget, paid on every turn.

MISTAKES.md is where evidence accumulates, the knowledge store is where it gets delivered, and CLAUDE.md is where it gets enforced.

Setup seeds the file create-only, and deleting it is the whole opt-out — no config knob for a file you can remove. Nothing reads it at session start: an unscoped injection would spend context on every session to deliver what the knowledge brief already delivers scoped.

The graduation pipeline — 3 to knowledge, 5 to law

Section titled “The graduation pipeline — 3 to knowledge, 5 to law”
a correction or a breakage, mid-run
│ the agent appends a `finding` event (claim + proof, + the command)
a `finding` in the journal [one initiative; dies at close]
│ the retro at close: did something actually BREAK, or is this a discovery?
│ mistakes.mjs add --signature <s> --proof F-12
a MISTAKES.md entry, status `open` [repo lifetime, yours to edit]
│ mistakes.mjs repeats --threshold 3 (three OPEN entries, one signature)
.tyran/knowledge/<topic>.yaml [delivered in every matching handoff]
│ mistakes.mjs promote --signature <s> --status knowledge:K-12
│ ...and it happened twice more anyway — 5 open-or-promoted
CLAUDE.md, inside the tyran:rules fence [law, on every turn of every session]
mistakes.mjs promote --signature <s> --law --rule '...'

The signature is the judgement; the count is mechanical. The model decides whether two failures are the same failure — a near-synonym resets the count to one and the lesson never graduates — and the script counts what it is given. Promotion never deletes an entry; it rewrites one status token, because the entry is the evidence that earned the rule.

Why 3 and 5. Three is “not a coincidence, and cheap to act on”: the knowledge store is scoped, budgeted, and retired by its own counters, so a wrong promotion costs one brief line. Five is the expensive one, because law is paid on every turn — and the gap between them is the diagnostic. A signature that reaches five has already been delivered in every matching handoff and broke anyway, which is the strongest argument for a rule there is.

Tyran writes the fence itself. At five occurrences the retrospective edits your CLAUDE.md between two markers, and never a byte outside them:

<!-- tyran:rules start -->
- Link the dependency directory into every new worktree before the handoff. (`worktree-missing-deps`, 5 occurrences — MISTAKES.md entries 2026-05-02, 2026-06-11, 2026-07-03, 2026-07-28, 2026-08-09)
<!-- tyran:rules end -->

One line: the rule, its signature, and the dated entries that earned it — the evidence is not decoration, it is how you disagree well. There is no approval gate. You meet the change the way you meet every other autonomous act — a decision event in the journal, a row on the board, a diff — and you say no by deleting the line, which does not come back (those entries are already at status law). --dry-run prints the line and writes nothing. An absent fence is appended once, with its heading, at the end of the file; a malformed one is refused by name and left alone.

The shipped policy classes CLAUDE.md as GATED, and that bans the hand, not the mechanism: a subagent’s free-hand Write is denied, while the script that demands five recorded occurrences passes. The boundary is not “agents may not change the law” — it is “the law changes only through the mechanism that requires evidence.”

State its floor with it, because a guarantee read as absolute is worse than a narrower one read correctly: the class binds the Write/Edit hand, and only hooks/** and .tyran/policies/** are refused to shell commands as well. A repo-root file is not in those globs, so a subagent’s Bash redirect into CLAUDE.md is not refused — measured, and stated where the class is set (policy-gate). What the mechanism guarantees is that the law arriving through it carries its evidence; what catches the other route is the diff, the same way you meet every other autonomous act.

Seeding has always been create-only, so a repo adopted before this rule shipped keeps its own policy until it copies the line from templates/policies/autonomy.yaml.

Authored, not a projection — and what that costs

Section titled “Authored, not a projection — and what that costs”

MISTAKES.md is prose you edit. It is deliberately not a generated projection of the journal:

  • a projection reports a hand edit as drift and destroys it on the next render — and a human correcting a wrong root cause is the most valuable edit this file will ever receive;
  • journals are per-initiative, so archiving an old initiative would silently shrink a projected history, taking the recurrence count with it;
  • most finding events are discoveries, not mistakes, and projecting them would fill the file with non-mistakes.

The cost, stated rather than discovered: nothing checks MISTAKES.md against the journal byte for byte, so it can drift — a fabricated entry, or a real breakage nobody wrote down. Every entry cites its initiative and the journal event id that proves it, so drift is auditable by a human and not enforced by a gate. That is a weaker guarantee than the projections carry, and no test will catch an entry nobody lived through.

doctor --state surfaces the ledger without nagging: mistakes-repeat-unpromoted (info) when a signature has earned promotion, mistakes-unreadable (warning), and claude-md-fence-missing (info) when entries claim law but no fence exists to hold the rule. An absent file produces no finding at all.

The one part that is not built: the update delta-review

Section titled “The one part that is not built: the update delta-review”