Imported from kstenerud/yoloai (
AGENTS.md). Install upstream withnpx skills add kstenerud/yoloai. Copyright stays with the author.
AGENTS.md
yoloAI runs AI coding CLI agents inside disposable sandboxes (containers, Tart VMs, macOS Seatbelt) with a copy/diff/apply workflow. Go binary, no runtime deps beyond the backend. Public beta: breaking changes are allowed, but must be recorded — rule 1.
This is the contract for changing yoloAI, and the canonical instruction file for every agent
working on it. CLAUDE.md imports it. Keep it short; detail lives behind the pointers.
Docs last swept: 2026-07-15 (D116, D124). Docs drift silently — nothing executes prose. If
that date is more than ~3 months ago, say so, unprompted, before relying on a doc being right.
Bump it when a sweep lands. Why this exists: general-principles.md §16.
Deprecations reviewed: 2026-07-17 (D127) — nothing due; next 2026-09-12. Compatibility code
is invisible once written, so it is never retired. docs/contributors/deprecations.md registers
each with the date it was incurred and a per-entry due date. If today is past the date above,
read the register and say so, unprompted — the owner decides what retires, but only if asked.
This is a nag, not a gate: nothing fails, ever. Bump the date when a review lands.
The marker is a claim, so treat it as one. When first written it named a sweep that had never
opened architecture/, which at that moment documented a mode retired two releases earlier and
a package that has never existed — the drift-detector's own first reading was wrong. D124 made
it true and gated the names, so architecture/ now fails loudly rather than drifting quietly.
No other tier has that backstop.
Where things are
Docs are organised by role. Pick your tier, then read that directory's README.md:
| Tier | Directory |
|---|---|
| Users — running yoloAI | docs/ |
| Integrators — embedding it as a library | docs/integrators/ |
| Contributors — changing it | docs/contributors/ |
Two you will want by name: architecture/where-to-change.md maps a change onto the files
that make it. backend-idiosyncrasies.md catalogues backend behaviour that contradicts its
own docs — read it before diagnosing any backend problem; add to it when you find more.
Nothing under docs/contributors/archive/ is a specification. It is frozen and unmaintained,
and it usually describes code that has since moved; each file says so in its own opening lines.
Search will put you there — plans read like specs, and an archived plan reads exactly like a live
one. Use it for "did we consider X?", never to build from. The live answer is in the tiers above,
and if the only thing that answers your question is archived, that is a gap to file, not a source.
next-release.md stages whatever ships next: it carries the next release version (a fact — it
escalates on its own when something breaking lands) and points at the work considered for the cut.
It is permanent and drains at each release, like ## Unreleased. It never carries an item's
status, and never its own reason — an entry there is an ID and the record's own title, nothing
more. "What's left for this release?" and "why is this in?" are both answered by following its
links, never by trusting the page. Finishing an item does not remove its line; entries stay
until the release drains the file, because "done" is the record's to say and a list that shortens
as work lands is a checkbox wearing a disguise. Repoint the link if the record moved; leave the
entry. The scope reason lives in the record, in a - **Rides:** field
naming the kind of release the fix needs (any / breaking / a migration) and which half
qualifies when only one does. Add that field to anything you propose for a release. It exists
because a reason composed on the staging page cannot be checked against anything, so a right one and
a wrong one read identically — and both duly happened, in opposite directions, on the same day.
agent-failures.md is its sibling for the component that writes the code. The trigger is an
owner correction: when the owner contradicts a claim you made, or asks a question whose answer
turns out to be "I was wrong", that is the event — record it there before moving on. Not every
fumble; only what reached a durable artifact or would have. A correction is a fact about a
repeating behaviour, and it is the only input from which the next gate gets built — the two
scripts/check_*.py gates exist because someone wrote the specimen down. On the evidence so far,
the owner's question is the most reliable detector in this repo, which is the problem the file
exists to fix.
Preparing a PR
Full detail and the reasoning behind each rule:
docs/contributors/procedures/pull-requests.md. Ordered by how likely you are to trip — but
the numbers are cited from other documents, so a new rule appends rather than inserts. Rule 10 is
the newest and belongs about third. Rule 13 belongs last: it only fires on an edit to four
narrowly-scoped documents, a narrower surface than any of the other twelve govern.
- A user-visible break needs a
docs/BREAKING-CHANGES.mdentry in the same PR — under## Unreleased, never under a## vX.Y.Zheading (those are frozen once tagged). Renamed or removed flags and config keys, changed defaults, newly-rejected input. That file's preamble has the format and the trap. This is the most-missed rule here. A removed or renamed config key or flag is now gated on the PR (scripts/check_breaking_changes.py); a changed default or newly-rejected input is not, and stays yours to notice. A break also escalates the next release version — bump the field innext-release.mdwhen you land one. - Invalidate a name, sweep every surface that names it. Config keys, flags, and agent
names are mirrored verbatim into shipped text that nothing typechecks — including
internal/cli/helpcmd/help/*.md, which is//go:embeded shipped UI despite living underinternal/.make checkcatches none of this. Append-only history (BREAKING-CHANGES,archive/,decisions/, the*-resolved/*-deferred/*-abandonedsinks) is exempt from the sweep — but not from rule 1. Surface list: see the procedure. - Code commits carry a prose body saying what was wrong or missing and why this fixes
it — not a restatement of the diff.
feat/fix/refactor/perf. Near-universal here. - Subject is
type(scope): summary—feat,fix,docs,test,refactor,build,ci,chore,perf. Imperative, no trailing period (0 of 2137 have one). Breaking gets!after the scope; never aBREAKING CHANGE:footer. Don't infer the format fromgit log— pre-June-2026 commits use a superseded convention. - Attribute AI work by model:
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>. Never a bareClaude, never a🤖 Generated withfooter. Humans and bots omit it. - Cite the rationale ID (
D<n>decisions,DF<n>findings) wherever you write rationale. Never invent one; never strip one. No decision behind the change? Cite nothing. Allocating a new one? Runscripts/next-id.sh D/scripts/next-id.sh DF; it prints the next free number. Don't grep for the highest — every duplicate ID this repo has had came from a grep that missed a sink. - File the defects you don't fix in
design/findings-unresolved.md. Fixing in scope is fine if you record it; silently working around it never is. And its converse: when a fix establishes a convention, the convention graduates out of the finding — intostandards/,architecture/, or the interface's own docstring, with the finding citing it rather than holding it. A resolved finding is archaeology:*-resolved.mdis append-only history that nothing sweeps and no router points at, so a rule living only there is a rule the next contributor cannot find. Both conventions PR #44 broke were sitting infindings-resolved.mdand nowhere else (DF151). Before fixing one, grep for its shape — the same kind of defect on a sibling path or another backend. Three of a kind means the architecture is generating them and the edge is a symptom; and a sibling path may already solve it, making your fix a second mechanism for one job. Naming that in the finding is mandatory; acting on it is a decision (GEN §18). - An idea worth building is a plan file in
design/plans/— not a bullet in a README, which is where a backlog goes to be invisible. Each carries a metadata list under its title:- **Status:**(UNSPECIFIEDno design yet /PLANNEDdesigned /IN-PROGRESSpartly built /IMPLEMENTED/ABANDONED) and- **Depends on:**(live plan filenames, or—). A one-commit fix needs no plan.design/plans/holds only the unfinished three: once the work is done or dropped the plan is archaeology, and it moves whole toarchive/plans/in the same PR. Gated, so none of it is something to remember. - Writing a migration? Register it in
docs/contributors/deprecations.mdwith the date you incurred it (D127). A migration is a deprecation: it commits the project to supporting the old form, on the day it lands. Same for any compatibility reader, alias, or shim. The entry costs three lines and is the only thing that will ever make retiring it possible — the register's own audit found 16 such mechanisms, of which 0 recorded a date. And it does not go tomain— see rule 12. - Every behavior change in the PR carries a test that fails when that change is reverted
(D128, D129). Every is the load-bearing word: count the behavior changes, then count the
tests that would go red on revert, and make the numbers match. Not "the package has tests",
and not "the headline fix has one" — the second change in a PR is the one that escapes.
Now gated on the PR (
scripts/check_revert_red.py): it reverts each changed file and requires something to fail, so you no longer have to remember to try. It judges only files touched by afeat/fix/perfcommit, and a change that genuinely cannot go red in the suite — a smoketest-only symptom, a path the single-principal CLI cannot reach — is declared with aVerified-By:trailer naming what you actually ran. That is a claim on the record, not a bypass; without it the only route past the gate would be mislabelling the commit. Red-on-revert is necessary and not sufficient: reverting a line inside a function nothing can reach still turns its test red, so the check proves a test is wired to the code, never that the code is wired to the program. A fake is free to invent a capability combination the product does not contain, and when it does, its tests certify a dead path — six of them once did. So for anything capability-guarded, name the backends that pass the guard and the statuses that reach it, and confirm the intersection is non-empty before writing the first test; prefer aruntime/runtimetestconformance case, which no fake can satisfy. Common cases, none of them the definition: an argv handed to a subprocess (assert the flags, and which paths must be absolute); which environment a subprocess draws from; an error's content, not just that one was returned. The fake-binary fixture covers all three cheaply —runtime/apple/build_test.go. Three traps, all drawn from the first two outside PRs: asserting only that an error was wrapped tests the wrapping, not the fix; a test whose comment explains why the code does the wrong thing has pinned the defect as intended behavior; and a change that is invisible in the single-principal CLI — because two values coincide there — is exactly the change no manual check will ever catch. - Multi-commit work goes on a
work/<topic>branch, cut from an up-to-datemain, merged back intomainwhen it is done, and deleted once merged. A one-commit fix needs no branch. The prefix is there so a branch's name says which procedure it followed — the 2026-08 cleanup found 45 local and 29 remote branches where answering that took reading each one's commits againstmain, and four had landed their content through a different branch than the one that started it, so "is it merged?" was not even the right question. Outside the scheme and not work:release-vX.Y.Z(release staging, deleted at the tag) anddependabot/…(GitHub's, tied to a PR — deleting one closes it). - A migration never reaches
mainunreleased (D131). The moment work is known to need one — the momentLibrarySchemaVersionwould move — cutrelease-vX.Y.Zat the escalated version and merge the migration work there.mainstays at the last released schema, so buildingmainat any commit gives a binary whose migrations are all already published. Otherwise someone trackingmainpulls, is told to runsystem migrate, gets stamped at a schema that never ships in that form, and is left with on-disk state no released binary can read — orphaned for doing what the tool told them. "Merge the migration last" does not fix this: it shortens the window, and a short window is still open. Non-migration work keeps going tomainand must be kept merged into the release branch, which is the real cost and is the price of the guarantee. - A load-bearing architectural claim gets a
TestArch_citation. The test is: if this sentence were false, would a reader give a wrong instruction or build the wrong thing? If yes, write (or find) a test namedTestArch_<Claim>, make it genuinely fail when the claim stops holding, and cite it in the prose by its bare name in backticks —`TestArch_ProfileIgnoresPersonalDefaults`— so "is this still true?" is answered bymake check, not by trust. Reserve the prefix for this alone; it is what makesscripts/check_claim_citations.pya grep instead of a judgment call. The scope is deliberately narrow —docs/contributors/architecture/*.md,docs/contributors/design/config.md,docs/contributors/principles/*.md, anddocs/contributors/decisions/working-notes.md, nowhere else — for the same reasoncheck_citation_provenance.pyis scoped toresearch/*.md(D122): a gate that fires on prose it was never meant to police gets disabled, and findings/plans/docs/GUIDE.mdare full of claims that are current status, not standing architectural guarantees. "Nothing — this is a judgment, not a claim" is a legitimate answer. Most sentences in these documents assert an opinion, a tradeoff, or a decision's reasoning, none of which a test can check; only mark the ones asserting a checkable fact about what the code does.
The quality gate
make check must pass before every PR (Claude Code runs it via .claude/settings.json
hooks). It is your local gate, not all of CI, and it does not read prose — shipped help
text and docs/ sit outside it. See the PR procedure for the CI jobs and required tooling.