agentleFS
jjanczurjjanczur/tyran/docs/agents.md

tyran

AGENTS.md81 starsChanged 4 months agoApache-2.0View on GitHub

agents/ · routing in scripts/tiers.mjs Four agents ship in agents/. The conductor (/tyran:run) spawns them; you can also invoke one directly when you want just that mode of work. For what each agent assumes before it will work, and for the fourteen skills alongside them, see what Tyran ships. Two of these grants are deliberate rather than incidental: - The reviewer has no Edit, Write or NotebookEdit. A reviewer who can fix what they found ends up approving their own…

  • Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.

    Of the AGENTS.md files we read, the 31,774 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.

  • In this repository15 other files it gives its agents.

The file

We split this agents.md into the rules it gives an agent

3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Read the table as a claim about where model strength changes the outcome.

    line 127
  2. Very specific:

    When every tier a role may use is unavailable, tiers.mjs exits 2 rather than returning the bottom of the ladder.

    line 194
  3. Distinctive:

    STOP that is a directory, an empty STOP: all of them stop.

    line 256

≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
agents.md

The roster, and how models are chosen

agents/ · routing in scripts/tiers.mjs

Four agents ship in agents/. The conductor (/tyran:run) spawns them; you can also invoke one directly when you want just that mode of work. For what each agent assumes before it will work, and for the fourteen skills alongside them, see what Tyran ships.

AgentNamespaced asToolsWhat it is for
scouttyran:scoutread-only plus Bashreconnaissance: what the repo does, what the data actually contains, what the docs claim
implementertyran:implementerallone story, from plan to commit or PR on its own branch
reviewertyran:reviewerEdit behind a forfeit — a diff it touched can only come back REVISED — never Writeindependent quality control on somebody else's diff
verifiertyran:verifierread-only plus Bashmechanical validation: runs the named commands, reports counts and exit codes, never fixes
retrotyran:retroallafter an initiative closes, improves Tyran itself and nothing else

Two of these grants are deliberate rather than incidental:

  • The reviewer has no Edit, Write or NotebookEdit. A reviewer who can fix what they found ends up approving their own patch. This removes the easy path, not every path — Bash can still write a file — and the agent file says so rather than pretending otherwise.
  • The scout has Bash because reconnaissance needs git log, ls and command -v. That is the entire justification; the agent is instructed to treat side effects as disqualifying.

Choosing models

Model names appear in exactly one file: .tyran/config.yaml. Everything else — skills, agents, policies, this document — is written in role names. A model deprecation is then a one-line edit rather than a sweep through every prompt in the repo, and a sweep through prompts is the kind of change that reliably gets 90% done.

tiers:
  cheap: haiku # scout, mechanical sweeps, ledger bookkeeping
  work: sonnet # DEFAULT: implementation and ordinary review
  deep: opus # root-cause diagnosis, hard implementation, risky review
  top: fable # security review, arbitration, final acceptance

Resolve a role to a model:

node scripts/tiers.mjs                                  # the whole map, as JSON
node scripts/tiers.mjs --role reviewer                  # -> one alias on stdout
node scripts/tiers.mjs --role reviewer --risk high      # escalate one step
node scripts/tiers.mjs --role implementer --effort xhigh # same model, think harder
node scripts/tiers.mjs --role scout --field json        # -> {tier, model, effort}

The conductor reads the map once at the start of an initiative and passes the resolved values as the model and effort parameters at spawn.

Two dials, not one

Model and reasoning effort are separate on purpose, because most of the time you want one without the other. A mechanical sweep on a strong model still does not need deep reasoning; a subtle diagnosis on the middle model very often does. Collapsing them into a single "power" setting is what makes cost modes blunt enough that people stop using them.

TierDefault effort
cheaplow
workmedium
deephigh
topxhigh

--risk high shifts both ladders one step; --risk low shifts both down. A pinned --tier still lets risk move the effort, so "use the cheap model, but think hard about it" is expressible — otherwise the conductor reaches for an expensive model just to buy the reasoning.

The conductor is expected to override this

The table is a starting point, not a prediction of the task in front of it. The conductor may set --tier or --effort for a single subtask whenever it can see the default does not fit — that is the intended use, not an escape hatch. Raise effort for root-cause diagnosis, a failure nobody can reproduce, an arbitration between two agents who disagree: anything where the first plausible answer is probably wrong. Lower it for sweeps, bookkeeping, and re-runs of a recipe that already worked.

Two things keep that from becoming drift. Every deviation is recorded as a decision event naming the subtask, the default, what was used and why — an override you cannot justify later is indistinguishable from a habit. And an override cannot go below a role floor: security-review and arbitration stay at top/max and high whatever is asked. When a floor corrects a request, the tool says so on stderr rather than quietly handing back something other than what was asked for, because a floor that silently "fixed" the request would teach the conductor that its overrides take effect when they did not.

The routing table

Roles down the side, cost profile across the top:

Roleecobalanced (default)full
scoutcheapcheapwork
implementerworkworkdeep
reviewerworkworkdeep
security reviewtoptoptop
arbitrationtoptoptop
acceptancedeeptoptop
retroworkworkdeep
authoring (a skill, an agent, a prompt)toptoptop
bookkeepingcheapcheapcheap
conductor (advisory)deeptoptop

authoring is separated from retro because retro does two unlike things with one agent: folding a ledger, which the middle model does fine, and writing the text every future session will obey. It sits at top with a max effort floor on a stronger version of the security-review argument — a bad security verdict costs one merge, while a bad prompt misroutes every run that reads it, for as long as it ships, and it is the one output nothing downstream checks. A skill that reads plausibly passes review.

conductor is the one row nothing can enforce: the conductor is your own session, and no plugin can change a running session's model. The row records the choice in the one file where model names may live, and tiers.mjs says so on stderr when you resolve it.

Read the table as a claim about where model strength changes the outcome. A scout reports what a file says; a stronger model does not make the file say something else. A security reviewer decides whether a hole is real, and a miss there survives every downstream check, because everything downstream trusts it. That asymmetry, not the price list, is what the table encodes.

--risk high shifts one tier up, --risk low one tier down, and both clamp at the ends. Security review and arbitration have a floor: they resolve to top under every profile and every risk level. Without it, --profile eco --risk low would be a one-flag downgrade of the two judgements the whole design leans on. The floor is applied last, after the shift, so it cannot be shifted out of. conductor has a deep floor for the same reason, advisory or not: one weak coordinator spends a whole team's budget on the wrong plan.

Implementers and reviewers may raise an operator ask rather than stopping — node scripts/journal.mjs ask <journal> <init> --question '...' --default '...' [--ticket T-n] — and must NOT set ticket.status on the asked ticket: the ask already lanes it waiting-operator, and an override on top hides the question.

The failure this design is actually guarding against

resolveModel throws when a tier has no alias. It would be easy to return undefined and let the spawn fall back to the session default — and that is precisely the bad outcome, because routing that silently does nothing is indistinguishable from routing that works. You would read profile: eco in your config, watch every agent run on whatever the session happened to be using, and have no signal at all. A test pins this.

The same reasoning covers a missing config: tiers.mjs falls back to the plugin's shipped template so a repo that has not run setup still works, but it says so loudly on stderr. A silent fallback would let a repo believe it had adopted a policy it never wrote.

When a tier's model is out of capacity

A tier resolves to a model, and a model can be temporarily unavailable — usually because it has hit a rate limit while the account still has budget on everything else. Measured on a real run: the strongest tier hit its limit and the subagents failed rather than finishing on the tier below, which had capacity the whole time.

node scripts/tiers.mjs --role acceptance --unavailable fable --field json
# tiers: FELL BACK top -> deep because the top model is unavailable.
# {"tier":"deep","model":"opus","effort":"xhigh","floored":false,"fell_from":"top"}

It falls DOWN the ladder, never up, and that is the answer to the question this feature was blocked on for a while. Climbing would spend more than the routing table promised, silently, at exactly the moment nobody is watching — and the whole point of the table is that what a run costs is legible before it runs. Down is also the direction the incident needed.

Three things the fall does not do:

  • It does not lower the effort. "This task needs deep reasoning" did not stop being true because a model ran out of capacity, and dropping both dials at once is a second downgrade nobody asked for. fell_from records the tier it came from so the substitution is visible in the ledger.
  • It does not cross a role floor. A security review that ran on the cheapest model is not a security review. The floors in the routing table bound the fall.
  • It does not pretend. The substitution is announced on stderr in the same breath as the resolution, saying plainly that this is a weaker model than the table asked for.

When every tier a role may use is unavailable, tiers.mjs exits 2 rather than returning the bottom of the ladder. That case is not a routing problem — there is nothing left to substitute — and the thing that already knows how to wait for capacity is overnight mode.

Re-trying a ticket that already failed

The escalation rule used to live in the conductor's memory, which iron rule 7 already names the least reliable store in the system: after a compaction, or under a second conductor, a ticket that has come back changes-requested twice is re-spawned at the tier that failed both times, burns the same money and fails the same way.

node scripts/tiers.mjs --role implementer \
  --journal .tyran/state/<init>/journal.jsonl --ticket T-3 --field json
# tiers: ESCALATED work -> deep after 2 failed attempt(s) on T-3 (ceiling deep).

The journal is what remembers. --journal --ticket counts the reviews on that ticket whose verdict was not an approval, and climbs one tier per failed attempt — capped twice over, at two steps and at a ceiling of deep. A ticket that has failed five times does not need the most expensive model in the table; it needs a human, and the changes-requested lane is where that is already visible. A role that already resolves above the ceiling is not dragged down to it.

The same read picks up any model an error event has recorded as out of capacity — {class: 'model-unavailable', model: '<alias>'} — and feeds it to the fallback above. That convention is a convention on purpose: the failure surfaces inside a subagent's API call, where no hook can see it, so something has to write it down before routing can act on it. Detection is still the operator's or the conductor's job; this is what makes the decision durable once it is made.

State the weakness rather than let someone find it: "did this attempt fail" is APPROVING_RE, which was written for lane assignment. "Approved with nits" reads as an approval and escalates nothing; "looks fine" does not and escalates. A journal that cannot be read routes as a first attempt, loudly — routing must never depend on a readable journal.

The brake

An operator can halt a running initiative without killing the session:

echo "wrong branch — hold everything" > .tyran/STOP

The conductor and the implementer check scripts/stop-check.mjs before every spawn, before every merge, and before any long unattended stretch. Exit 1 means stop and report where you got to. Delete the file and work resumes.

The value is that it needs no session. Someone who sees an overnight run going wrong can stop it from a phone with a one-line commit, and does not have to kill a process and lose the state that would explain what happened. (The idea is borrowed from pro-workflow's file kill-switch; the code and the semantics here are our own.)

stop-check fails closed. Everything else in this codebase fails open, on the principle that a broken gate must not block ordinary work — but a brake that releases itself when damaged is not a brake. An unreadable STOP file, a STOP that is a directory, an empty STOP: all of them stop.

.tyran/STOP is classified KERNEL in the shipped policy, so the write guard refuses an agent that tries to delete it through a writing tool. The policy gate's own documented limits still apply — a shell command that removes the directory without naming the file is one of them. This stops a loop that is misbehaving; it does not stop one that is trying to escape.

Overriding an agent in your own repo

Copy the file into .claude/agents/ and edit it. Your copy wins. Note the trade-off the shipped policy already flags: an override freezes that agent at the version you copied, so plugin updates stop reaching it. That is why .claude/agents/** is classified GATED rather than AUTO — it is a decision worth making on purpose.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.