Concepts
The operating loop
The permission model decides what an agent may do. This is the loop it runs inside: where state comes from at the start, what catches drift while you work, what gets written back at the end, and how repeated work turns into tooling instead of habit.
A brake on its own is a good day's protection. What breaks a project over months is quieter: documents that were true in March and are confidently wrong in July, state that lives in a chat window and disappears with it, and the same manual sequence done for the ninth time because nobody counted.
Key idea
The two ends of a session#
A session that reads state at the start and writes it back at the end is what makes “files are the source of truth” actually hold. Both ends matter, and the opening one is the one people skip.
Opening#
Read the rules, then the state file, then only the docs for the area you are about to touch — not all of them. Then run the project's checks before doing any work.
The order is the whole point. If the last session left drift, you want it now, while it is cheap and clearly not yours. Finding it after two hours of building on top of it means choosing between rework and knowingly shipping on a bad foundation.
/keel-skills:session-startClosing#
Reconcile what happened into the files: resolved items move to history, new verified facts go where they belong, open questions get recorded as open. Re-run the checks. Leave a short handoff.
/keel-skills:session-closeTwo rules keep the close from becoming its own problem:
- One line of history per close. The reasoning belongs in the document it is about — the decision, the process doc, the draft — not in the history entry. Entries that grow into essays are how a changelog gains tens of kilobytes a month and stops being read.
- The close records; it does not build. No new work, no reopening decisions. If it finds something worth doing, it writes it down as open and stops.
Note
Documents that don't age#
Written rules decay — not because anyone disagrees with them, but because the world moves and the document doesn't. Three shapes stop that.
Split state from history#
| What it holds | Read when | |
|---|---|---|
| State | What is true now, what is open | Every session |
| History | What happened and why, dated | Rarely, on purpose |
When something closes, its body moves to history and the state keeps one dated line. Resolved items left in the state file are the most common way a state file quietly triples in size.
Cut history out of a document when any one of these is true — each bar exists because the one before it let a real case through:
- Size. The history section passes roughly 30 KB.
- Proportion. History passes about two thirds of the file, even if it is small. You pay for what is read, not for what the file measures.
- Nature. A closed item is history even when it lives in a section not called “History”. The first two bars only look at the labelled section, so a “Pending” section can be more than half closed items and neither one sees it.
A bootstrap carries no state#
The file an agent reads first must point at where state lives, not contain it. State written into a bootstrap ages invisibly and, worse, gets read as criteria rather than as a fact — nobody re-checks it, because it looks like a rule.
The same failure hits numbers written into method documents. “Runs 14 checks”, “takes about 1.5 seconds” — each one is a fact filed inside a rule, and no one looks at it again. Either keep the number where it can be verified, or don't write it.
Date every snapshot, and prefer generated over maintained#
When a document genuinely must hold a picture of the world, mark it Current state (2026-08-14) and say it is a snapshot. A dated snapshot can be measured; an undated one just rots.
Better still: if a listing can be generated from the filesystem at run time, generate it and delete the copy. An index built at run time cannot go stale.
Checks that catch what the rules didn't#
A rule tells a careful reader what to do. A check notices when it didn't happen. You want both, and they fail differently.
Add a check when a real drift already got through undetected — not speculatively. That keeps the suite small enough that every failure means something and nobody learns to ignore it. Write down why each one exists, right next to it.
Three levels, not pass/fail#
A finding means different things depending on whether someone has that file open right now, so version control decides:
| Level | The file is… | What to do |
|---|---|---|
| FAIL | committed and clean | Real drift — it survived a session close. Safe to fix: nobody has it in flight |
| WARN | modified or untracked | Someone is working on it. Report; don't fix what isn't yours |
| info | — | Needs human judgment. Look, don't auto-correct |
Watch out
Ratchets#
For anything that must not grow — startup context cost, bundle size, an index loaded every session — check against the last recorded value, not a generous ceiling. When a ratchet fails, remove content; do not raise the ceiling. A ceiling raised on every failure is a log, not a limit.
A runnable starter — five universal checks, the FAIL/WARN split, and a documented extension point — ships with the plugin:
cp "$CLAUDE_PLUGIN_ROOT/templates/checks/keel_checks.py" scripts/
python scripts/keel_checks.pyUnattended runs#
A scheduled sweep, a CI job, a background agent. Here the rule doesn't get looser — it gets stricter:
Key idea
An unattended run may look, measure, and report with a recommendation. It must not commit, push, deploy, send, delete by inference, mark anything approved, or “fix” what it finds broken. If the job needs any of those, it was scoped wrong.
Write those limits into the routine's own prompt. Every run starts with no memory of any conversation, so a limit agreed once in chat does not exist for it. The enforcement hook does the mechanical half: a hot action degrades from ask to deny when nobody can answer.
command /keel-skills:hygiene — a read-only sweep for uncommitted work, leaked secrets, state drift and stale drafts. Run it on demand, or schedule it from templates/routines/weekly-hygiene.md.
Turning repetition into tools#
Encapsulate what has already been done three times. Fewer is premature abstraction: you pay maintenance forever and — the real cost — one or two occurrences don't show you what varies, so you build the wrong shape and then work around it.
Count carefully, because the number lies in both directions:
- Normalize before counting. The same command with a different id inside is the same command. Counting raw text splits one repetition into five.
- Then check the opposite. A form appearing 200 times may be a truncation artifact. Look at whether the occurrences do the same thing or merely start the same.
Test banks need a case that must fail#
A script that touches a live system can only be exercised on the state things happen to be in — which is the state where nothing is wrong. Build a bank that substitutes the reads, runs the real routine, and captures what would have been sent.
Watch out
Capture is free, adopting is not#
Repetition is invisible from inside a session: each one starts blind, so nobody looks across them. The loop has three separate times:
| Time | When | Who |
|---|---|---|
| Capture | during real work | the agent — one line, then keep going |
| Harvest | when capacity is spare | its own session |
| Adopt | on review | the human only |
Capture is the asset. If the harvest has to begin by thinking of what to improve, it doesn't happen. So: notice the friction, write one line, keep working. Do not design the fix in the moment — that derails the task, and it is exactly why nothing ever gets written down.
Adopting is the green light. A backlog file decides nothing and deleting it breaks nothing, so writing to it is free. The moment an item becomes a tool, a command, or a rule something obeys, it changes how the system works. The agent fills the funnel; the human opens the gate.
Record discards with their reason — a discard without one gets proposed again next quarter.
/keel-skills:harvest