Repository navigation
Releases: zypher-systems/Ryter
Release list
Ryter v0.43.0
A reply that stops short no longer passes for a finished one, the audit keeps what you edited while it ran, a Claude model on OpenRouter keeps its reasoning through a tool call, and what Ryter writes on your disk now carries a schema number, with a kept copy of each release's files in the tests.
Upgrade
ryter updateComing from 0.42.0:
- A dropped stream is an error. A provider's stream that closed before the provider said the reply was over used to be read as finished: half an answer stood as the answer, and a half-written tool call ran. The turn now fails and says so, what arrived stays on the screen, and nothing of it reaches the conversation. A server that closes without
[DONE]after giving its finish reason is fine, as before. - The audit keeps your edits. The audit's rollback put back a file you edited in your editor while it ran, and reported that as the audit's change. Ryter now watches the tree around the audit's calls that can change it, MCP tools among them, and around your hooks wherever they fire: what changed while none ran is yours and stays as you left it, even where the audit or a hook wrote to the same file, and what the audit changed goes back from a checkpoint that
/redocan restore until the next build turn./undoafter that/redotakes back the same files and leaves yours. - OpenRouter's reasoning goes back within a tool call.
reasoning_detailswere dropped, so a model through OpenRouter lost what it had reasoned at each tool call, where Anthropic's own connection keeps it. They now go back to the same model, within the running tool loop. - The project map's first line is
./ (name). Written like a folder, it sent a model looking forname/a.txt; one then told the user the project's files were missing. - Project memory is shared out.
ROADMAP.md,DECISIONS.mdand your notes each get a third of the 48 KB and pass on what they don't use, and a cut line says what was left out. A large roadmap used to fill it alone. - The gate no longer loses a command behind a quote or a
#.echo ${f:-a #x}; rm -rf /read the#as a comment, andecho "${f:-"'"}"; rm -rf /; echo "'"read thermas quoted, so a second command went unjudged and the line passed.${…},$(…)and backticks now have the quoting the shell gives them, in all three places the gate reads a command, and what stands between backticks is read as a line of its own. This was so in every earlier release. Where the sandbox is on, it still held what such a command could reach. - A
forloop's values are read as the shell will.for f in a b; do sed -i … $f; doneis judged value by value, patterns and lists included, andechoandprintfno longer ask because of a path in their text.f[0]=x,"${f}",${f[0]}and${f[@]}count as setting and reading$f, which they did not before, and outside quotes a pattern in a value is a pattern:g='.en*'; cat $gis refused. Where a name is set under a loop or a branch, or something else can change it, every value the command gives it is judged and the strictest verdict holds:A=notes.txt; if x; then A=/etc/hosts; fi; sed -i … $Aasks, where it used to pass asnotes.txt. A value with a space in it is judged split as well as whole, for loops and forF="a b". - Making something else of a variable is refused.
${f%.txt},${f:-x},${f/a/b},${f:1},f+=xand((n+=1)), and a copy of a variable the same command set (A=x; B=$A) can't be checked before they run. They used to pass, ask or be judged as something else; now the model is told to write the value out. This can stop a command that used to run, such asfor f in *.md; do mv "$f" "${f%.md}.txt"; done. None of 1,573 commands from real sessions was one.((n++))still runs. - A word that may come to nothing is judged without it as well.
{,sudo} idcan runsudo id, so it asks as root as that does, andgit {,push} xasks asgit push xdoes: a{a,b}list is written out first, as the shell writes it, and a word the shell may be left with nothing of is no word, pieces included. A list of more than 256 words is a part the gate can't read, and asks. - A
gitverb the gate can't read is refused, as a program it can't read is:git $v push,git $(x),git "$@". The model is told to write the verb out. An argument it can't read asks where a flag would change the verdict (git reset $x,git checkout $BRANCH) and passes elsewhere (git log $BRANCH).set -- …is refused: the words then stand wherever$1or$@does. - A routing change applies whole or not at all. A key gone since connecting used to leave the old rules live under a screen showing the new ones.
- The screens say what is so.
/budgetand the guide say the cap stops the next call, so the last can pass it by its own cost./contextcalls a window nothing states assumed./sessionsandryter sessionsread as the rail does:$0for no calls,$?.??for nothing priced,≥for a total that leaves calls out, and no$0.00for a session that cost something. ryter doctorsays what the sandbox covers and leaves, for the profile you are on.- As root, a listing that goes down the tree from above root's own folders is refused:
sudo ls -R /etc,sudo find / -name x,sudo du -a /etc. One level and a total (sudo ls -la /etc,sudo du -sh /var) are still a question. - MCP has limits. A line can be 4 MiB from a client and 32 MiB from an outbound server, a listener serves 32 connections, and a TCP client has ten seconds to authenticate. A finished session now closes its socket: a refused client was left connected.
- What Ryter keeps carries a schema number. Sessions, spend records,
config.toml,settings.toml,routing.toml, a new.ryter/run.tomland.ryter/plan.md. Nothing you have is rewritten for it, and a file with no number reads as it did. A file from a newer Ryter stops it, says so, and is left as it was. The guide's new section says what persists where.
Why
These come from the second outside review of the code, against 0.42.0, and from the path to 1.0 written after it. Most are places where the screen said more than was so. The dropped stream and the map's first line were not in the review: they were found by replaying what providers really send, and by a live run on a second model.
Good to know
- Going back to 0.42.0. Once this release has saved
routing.toml, 0.42.0 can't read the file: it warns, ignores your rules, and replaces them if you save from/provider. Keep a copy if you may go back, and don't run both on one home. - OpenAI and SpaceXSI have had no live tool round trip through this harness. Their wire is tested against a stand-in only. Anthropic and OpenRouter are proven on recordings of their own bytes, and
docs/provider-proof.mdkeeps the dated record. - The reasoning echo is proven on two models,
anthropic/claude-sonnet-4.6anddeepseek/deepseek-v4.1-flash. A provider that refuses it would show as a 400 namingreasoning_detailson a tool round. - A bare
$fthe gate can't read is still not judged as a path:for f in $(cat list); do sed -i … $f; donepasses, and the sandbox is what holds it.$f.txtand$f/xask. - During an audit, a command left running in the background, writing after its call has returned, reads as you; and what you edit while a card is up on one of the audit's own commands, or while a hook runs, reads as the audit's.
sudo findfrom above root's folders is refused even with-maxdepth 1.sudo ls -ladoes that job.- A Unix-socket MCP client has no time limit, and an idle one holds its place among the 32.
hats.toml,permissions.toml,hooks.toml,mcp.toml,trusted.json,reasoning.toml,review.tomlandlast.tomlcarry no schema number.- Open from earlier releases: the scout has no seat in
/models; the live measurement of check rounds avoided; selecting with the mouse untried by a person; the bench on macOS; a real run on macOS; WSL2 by hand.
Tried before release
The full local check and CI on every patch, Linux and macOS. Live on real models, by hand and dated in docs/provider-proof.md: tool round trips, parallel calls and truncation on claude-opus-5-5 through Anthropic ($0.09, a thinking signature handed back and accepted), anthropic/claude-sonnet-4.6 through OpenRouter ($0.08, the same through reasoning_details) and deepseek/deepseek-v4.1-flash through OpenRouter ($0.004); the Anthropic and Claude-on-OpenRouter recordings are kept and replayed in every test run, with cancellation and dropped streams cut from them. In the real TUI on the simulated provider: two dropped streams and the recovery after them, the audit's rollback with an edit made while it ran and with a hook writing during it, then /redo and /undo over it, /context, /budget, /sessions and /doctor. The gate's verdicts on 1,573 commands from real sessions, old gate against new: none changed. A session written by the real 0.42.0 listed, resumed and carried on, losing no field; a settings.toml from a newer Ryter stopped it with the file untouched.
Not tried: OpenAI and SpaceXSI live; the MCP limits against a real client or a real server (scripted ones only); the root listing rule under a real sudo (the gate's decisions only, as a session never authenticates); doctor's sandbox lines on macOS; your own existing sessions.
Ryter v0.42.0
The host that serves you on OpenRouter is your choice, and shown on every call: press r on the OpenRouter row in /provider to set the routing rules, /spend names the host behind each row, and the lists in /models, /sessions and the bench keep their columns still while you scroll.
Upgrade
ryter updateComing from 0.41.0:
- Routing rules on the
/providerscreen. Pressron a row OpenRouter serves. The form has every field of OpenRouter'sproviderobject as a row, grouped as the order of hosts, what a host must offer, speed, and price, with a line under the focused row saying what it does:sort(OpenRouter's choice, price, throughput, latency) and what to sort within, hosts to try first, fallbacks past them, hosts to use only or never, quantizations, every parameter required, data collection, zero data retention, distillable text only, a throughput floor, a latency ceiling as a figure or asp50=2 p90=5, and a ceiling in dollars per million for prompt, completion, request and image.←/→change a choice, Enter types a list or a figure, a clear row puts every rule back,^ssaves, Esc asks first when you have edits. A figure that cannot be read marks its row and the save waits. The rules go to~/.ryter/routing.toml, the connection's row then says the rule in a word (cheapest host,fastest host,soonest host,host rules), and the next call carries them without a restart.ron a row OpenRouter does not serve says why it does not apply. - The same rules in the file. A
[connections.<name>.routing]table inconfig.tomltakes every field under OpenRouter's own name, and the screen's file lays over it. A closed word or a price below zero is refused at load; host and quantization lists go as written. A routing table on a connection that is not OpenRouter's is refused inconfig.tomland skipped with a warning inrouting.toml. There is no region field: a region is a segment of an endpoint slug (baseten/fp8/us) in the order, only or ignore lists, or a base URL on an OpenRouter business plan. The guide's "Which host serves you on OpenRouter" has the table and the trade-offs. - The host on every call. The host that served each OpenRouter call is read off the stream and kept with the spend record.
/spendhas a row per connection and host (openrouter · Baseten),ryter spendsaysvia <host>, and the bench table names the hosts behind each run. When no host passes the rules, OpenRouter's refusal is said as the step its funnel stopped at (stopped at: Filter by Max Price), not as a page of metadata, and points atron the connection's row. The rules set on the screen reach a hat or the scout on another connection in the same session too, not only the session's own connection. - Columns that stay put. The
/modelslist, the/sessionslist and the bench's model picker sized their columns from the rows on screen, so the context, price, reasoning and connection columns jumped as a longer name scrolled into view. They are sized by the whole list now and stay where they are while you scroll or search. - Compare: an ablation arm.
bench/compare.pytakesryter@label=connection:modelas a second Ryter arm on the same model and--ryter-config label=FILEfor that arm's project config, so one piece of the harness can be switched off for one arm and the two compared on the same tasks, model and billing.bench/ablations/no-map.tomlturns the project map off. The summary gains a "Median rounds" column, each tool's own count of its rounds with the model.
Why
On a bench run in October 2026, every one of Ryter's calls to GLM 5.3 on OpenRouter landed on a host charging $2.10 per million input tokens while another tool's calls, on the same account and model, landed on one charging $0.08. Neither tool had asked for a host. OpenRouter's own choice is not the cheapest, and until this release Ryter gave you no way to make the choice and no way to see which host you got. The default stays OpenRouter's, since the cheapest hosts serve lossier quantizations; the rules are yours to set, on the screen, in a minute, and the spend rows tell you what they did.
Good to know
- The
/modelspicker lists OpenRouter's plain model ids; a:flooror:nitrosuffix cannot be chosen there. The routing rules do the same job for every model on the connection. - A
[connections.openrouter]row of your own inconfig.tomlreplaces the built-in one whole, so a routing table there needs the full row (base URL, key, default model). The screen's file does not have that cost: it lays over the built-in row. - The order of the three places: a trusted project's own
.ryter/config.tomltable wins over~/.ryter/routing.toml(the screen's file), which wins over~/.ryter/config.toml. So a bench arm's--ryter-configpin holds whatever the screen has saved, also when it restates the home file's choice; a project row without aroutingtable leaves the rules alone, and an empty table is the project's clear.routing.tomlis read afterconnections.tomland before the connections are validated; a name it holds that is no connection is skipped. Clearing on the screen saves an empty table for the name, which clears the rules and holds over a table inconfig.tomlon the next launch. ryter connectionson the command line does not show the routing rules; the/providerrow does.- Open from earlier releases: the scout has no seat in
/models; the live measurement of check rounds avoided; selecting with the mouse untried by a person; the bench on macOS; lint and format scoped to files; cargo--test <name>for a file directly undertests/. New: a Ryter-against-another-tool cost table on OpenRouter waits on both tools being pinned to the same hosts; the first person to use the routing screen found it workable but somewhat confusing, and what confused them is not yet known.
Tried before release
The full local check and CI on every patch. The routing object proved on the real API with the one-word probe: the default route served by Fireworks at $0.0174, price-first with fp8 or better by AkashML at $0.0016, price alone by Wafer at $0.0006; the full object (price, fp8+, max price, zero data retention, every parameter, latency percentiles) served by Sail Research; a max price no host meets refused as Filter by Max Price. The screen driven in the real binary offline with a connection of OpenRouter's kind: no provider object on the first request, the saved rules on the next, none again after clearing on the screen, and the saved file read back. The lists driven through 32 models with three long names in the middle: the price column at the same place in every window. The user tried the routing screen and the spend rows on a real OpenRouter call before the release was cut.
Ryter v0.41.0
The project's verbs as tools, so the model stops guessing commands the harness already knows: test narrows to a name or to the files it edited with the runner's own flag, build, check, lint and format run the project's own commands from the run file, a project with no run file has its verbs read from its manifest for you to approve, and the hard bench level grows to ten tasks.
Upgrade
ryter updateComing from 0.40.0:
test, with a name or with files. The model runs the project's tests with a tool of that name, from the run file you approved.filterruns the tests whose name matches and Ryter adds the runner's own flag (cargo test's positional name before a--, pytest-k, go test-run, jest, vitest and bun test-t, mocha--grep), throughnpm test,pnpm run,yarn,npx,uv run,poetry runandpython -m, with apackage.jsonscript read to find the runner behind it.filesruns the tests that cover those files: a Cargo file as its package (-p, with--workspacedropped so it is heard), a Python or script source as the test file named for it, a jest file through--findRelatedTests, a Go file as its directory. The model gets the digest under the command as it ran, a line for each file the run does not cover, and a last line saying the whole suite has not run; a name that matched nothing is said in words. A command Ryter can't narrow (make test, several commands in one) is listed as not run rather than run whole under a narrow name. The chat row names the command as it ran.- The edited files' tests first. A bare
testin the build hat, with files edited this session, runs their own tests first and the whole suite only once those pass. A failure comes back in seconds under the names the model just touched, with the whole suite not run, said in the last line. The chat row names both commands. The audit hat runs the whole suite whatever was edited. build,check,lint,format. The run file gainsbuild,lintandformatbesidecheck, proposed and approved like the rest, and each is a tool of that name.build,checkandlintcome back as the check's report: the errors as the compiler or linter printed them, capped, or one word (built,passed,clean).formatruns the formatter over the project and comes back with the files it changed, read from the project before and after by content, and those files count as the round's edits: the check runs after them and the session's notes carry them.formatis the build hat's alone; the audit changes nothing. The chat shows each as its own row with the word or the count.- No run file: the manifest is read for you. A verb in a project with no run file no longer sends the model off to read the project and propose one. Ryter reads the manifest at the root (
Cargo.toml,package.jsonwith its scripts and lockfile,go.mod,pyproject.tomland its tools, aMakefile's targets) and puts the commands it found on the approval panel with a note naming the file. A yes saves them as the run file and the verb runs in the same call; a no leaves things as before. The product'sstart,readyandstopare never guessed. - Ten hard bench tasks. Six more at
level = "hard", two per language:config-interpolationandrate-limiter(Python),rust-lru-ttlandrust-glob(Rust),ts-json-patchandts-cron(TypeScript), each with a reference the runner proved. Ten tasks at each level, so a solve-rate step is ten points instead of twenty-five.ryter bench --level hardand/benchmarkrun them.
Why
The build hat ran most tests through bash, guessing the runner, its filter syntax and the directory, and a guess that missed cost a round; a guess that ran the whole suite to look at one test cost minutes and a wall of output that was then cut. The run file the user approved named the runner all along. The same holds for the build, the linter and the formatter, and for a project whose manifest says in plain terms what the model was about to spend a round reading. Each verb is one guess the model no longer makes and one answer that comes back small, exact and true.
Good to know
run_project testandrun_project checkstill work and run the same code as the new tools, as a whole run; old transcripts and the simulated bench use them.run_projectkeepsstart,stopandstatus.- A narrowed test run does not count as the project's check:
last_check, the notes and done-means-checked read whole runs only. A narrowed run is not written into Ryter's notes either. - The runner is never swapped for another program:
npm teststaysnpm test -- -t name. A cargo filter is a substring of the test's path; go's-runis a regex. Cargo files map to-ponly, not--test,--libor--bin. - A source file maps to the test named for its whole stem (
button.component.tstobutton.component.spec.ts), the one beside it first. When files are given, a test folder the command itself names (pytest tests) is dropped so the run is really the files' tests; a path or pattern in the command that is not a folder here, or apackage.jsonscript that names paths itself, can't be narrowed by files and says so (a filter still works). Go's flag values are kept and every package word gives way to the files' directories. The command's own words go back on the line as they were written, so a"$VAR", a$(…)or aNAME="value"prefix you approved still expands; only what Ryter adds is quoted. A word the shell expands where a test path or a Go package would stand (pytest ${TEST_DIR:-tests},go test ${FLAGS:--race} ./...) can't be narrowed by files and says so. formatchanges files, so it checkpoints like a write and/undocovers it. After a format the model is told to read a file again before editing it, since a rewrite of a file changed since it was seen is refused.- The run file's new fields are optional; an older file reads as it did. The approval panel shows
build,lintandformatrows;propose_runtakes them..ryter/run.toml's header comment names them. - Detection runs only for the verbs, only with a user present, and only when there is no run file at all;
run_project startis untouched. A detected file that no hat could run falls back to the old message. A project with both a manifest and aMakefiletakes the manifest. - The ten-hard-tasks commit reached
devwith CI red: a TUI test counted four hard tasks. Fixed in the next commit; the release commit's CI is green. - Open from earlier releases: the scout has no seat in
/models; the live measurement of check rounds avoided; the/sessionspanel's columns; selecting with the mouse untried by a person; the bench on macOS. New: lint and format scoped to files; cargo--test <name>for a file directly undertests/; a live hard-level run.
Tried before release
The full local check and CI on every patch. The real binary driven offline for each piece: a fake pytest recording its arguments, test with a name and with a file, the chat rows naming the narrowed commands; build, lint and format rows reading built, 1 error and formatted 1 file; an edit followed by a bare test running the edited file's tests and then the whole suite; a project with only a package.json, its verbs on the approval panel with the note, approved once, the test run in the same call. Each new bench task validated: the baseline fails the hidden tests, the reference passes everything.
Ryter v0.40.0
Six changes to what the model is handed and held to, so that the same model finishes with fewer redos: a test run comes back as its failures, a map of the project and Ryter's own notes on it open every session, the choices made in the work are recorded beside the plan, a rewrite needs a read, and a build turn does not end on "done" while the project's check fails.
Upgrade
ryter updateComing from 0.39.0:
- A test run reaches the model as its failures.
run_project test, the check after edits and abashtest run too long to keep whole hand the model each failed test with its assertion and where, and the runner's totals, for cargo test, go test, pytest, unittest, jest, vitest and mocha. The last sixty lines, which is what it used to get, carried the names and the totals and not the assertion, and the model spent a round running one test again to read it. A run that passed is its totals line; a runner Ryter does not recognise still comes back as its last lines. - The project map. When a session's first turn begins, the model and the scout are handed the project's folders and the signatures of its Rust, Python, TypeScript, JavaScript and Go files: public functions, types, classes and their methods, exports; other source files named, data files counted. It goes to the file instead of listing and reading to find it. Signatures only, no bodies and no line numbers, so a file is still read before it is edited. Held for the session, so the prompt's cached prefix survives the model's own edits, and remade when the conversation is compacted.
[map] charsinconfig.tomlbounds it (12,000;0leaves it out); hidden folders, what.gitignorenames, and build output and installed packages are not in it. - Decisions beside the plan.
record_decisionnow also records the choices made in the work, a library, a data model, a structure, a workaround, and what you decide when the model asks, under## workin.ryter/decisions.md, no plan needed. Every hat is shown the file's entries at the start of a session, so a later session or another hat builds on what was decided instead of deciding again, and the audit's brief points at them. The file stays yours to edit. - Ryter's own notes on the project. From what happened, never from what the model said: the commands that ran here (project verbs, slow commands, the run file's own) with how they came out, how long they take and which runner is not on this machine, and the last five sessions with their title, turns, edited files and whether the check was passing at the end. Kept in Ryter's home as
projects/<project>.observed.json, not in the project, and shown to the next session at its first turn. - A rewrite needs a read.
writeover an existing file is refused unless the model has read the file as it is now: never read this session, or changed since the model last read or edited it, by you in your editor, by a formatter, by a command. The tool says which and tells it to read first or usesearch_replace; asearch_replacewhose lines are not found says plainly when the file changed since the read. Your edits between turns are never overwritten unread. - Done means checked. In the build hat, the model's "done" with the run file's check still failing is not the end of the turn. The chat says
not done: cargo check still fails · round 1 of 3, the failures go back to the model, and the turn ends when the check passes, when the model says in a line startingBlocked:what is in the way (the chat shows it), or after[limits] check_roundssuch rounds (3;0turns it off). The fast check alone, not the test suite; a check whose program is not on this machine is not held against the model; and nothing about the gate changes: what asked before still asks.
Why
A harness does not make a model smarter. It can stop the model guessing what the harness already knows, and make what it hands back small, exact and true. Each of these six removes a guess or a redo seen in the bench's hard tasks: the assertion that was in the output all along, the file found by listing three folders, the choice re-decided by the next session, the test suite's duration rediscovered, the user's edit rewritten from memory, the confident summary one round before the fix.
Good to know
- The map keeps no default values (
def connect(password=…)) and does not read or name a path the gate holds secret (.env, a key file, a credential path); its root line honours.gitignorelike the rest. A test digest with more totals lines than fit keeps the failing ones;cargo test -- --show-outputblocks of passing tests are not counted as failures. A read whose offset is past the end of the file is an error and does not count as having read it. - The map, the notes and the decisions all go into the system prompt. The map and the notes are held for the session for the prompt cache; the decisions file is read each turn, so a recorded decision costs one cache miss.
.ryter/decisions.mdhas a new header paragraph and a## worksection; the plan sections are as before, and an older file reads as it did.- The notes live in Ryter's home,
projects/<slug>.observed.json. A session is not shown its own note. Twelve commands and five sessions are kept. - The refusal of an unread rewrite applies to the model's
writetool only; a shell redirect is the gate's business as before. A new session,/newincluded, starts with nothing seen, so the first rewrite of each file needs a read; what the scout read does not count as the hat having read it. - A command's output is captured whole up to a megabyte while it runs, so the failures digest sees a failure in the middle of a long test run; the tool result the model gets is still cut to 32,000 bytes after, with the digest in front.
- A command that carries what looks like a secret (a password or token in an option or an assignment, an auth or cookie header, a key by its shape) is not written into Ryter's notes. A shell's "not found" (bash's and dash's) counts as a program missing on this machine; a program's own "not found" is a failure.
- Done means checked holds only where an approved run file has a
check. Headless runs (ryter -p) and the bench get the same loop, which is what makes its effect measurable there. - A test in the cancelled-check path cancelled after a fixed wait and flaked on CI once the first turn builds the map; it now cancels when the check starts.
- Open from earlier releases: the scout has no seat in
/models; the live measurement of check rounds avoided; the/sessionspanel's columns; selecting with the mouse untried by a person; the bench on macOS.
Tried before release
The full local check and CI on every patch. The real binary driven offline for each piece: a failing test run reaching the model as its assertion with the chat row carrying the totals; the map in the first request's prompt and in the scout's, with the root's own source files mapped; a choice recorded under ## work, the chat's "decision recorded" row, and the next turn's prompt carrying it; two sessions through /new, the second told what the first ran, edited and left failing; a rewrite refused until the model read the file, your edit between turns kept; a build turn going on past "done" until the check passed.
Ryter v0.39.0
The bench is in the binary: /benchmark in the TUI, or ryter bench from a shell, runs the bench's tasks on your own model and key and tells you tasks solved per dollar; several models run in one go and come back ranked, so the cheapest model with a clean sheet is the one to seat; and four hard tasks join the ten, each turning on a rule the brief states and the visible tests leave alone.
Upgrade
ryter updateComing from 0.38.0:
/benchmarkandryter bench. The bench's fourteen tasks ship inside the binary, so no checkout is needed. The form takes a connection, the models, the tasks (standard,hardorall), a budget and the sandbox; Enter on the models row lists the connection's models with context and prices as/modelsshows them, typing filters, Enter adds or removes one, and models from several connections can sit together. Each task runs in a fresh copy of its project under the build hat as its ownryterprocess with its own session cap, asking nobody anything; the hidden tests are copied in afterwards and run in a bare environment. The panel shows the tasks ticking through with a result, a cost and a time each;ccloses it while the run goes on,Escasks before stopping after the task in progress. The table lands in~/.ryter/bench/<date-time>-<model>/RESULTS.mdbesidereport.jsonand each task's logs. The same run isryter bench --level hard --budget 5from a shell;--listnames the tasks. The run spends real money on your key: the budget is the cap, with a $0.10 reserve kept back before each task, and a task whose toolchain is missing (cargo,node) is skipped and says so.- Several models compared in one run. Pick more than one model and they run in turn on the same tasks, each with the budget to itself. The finish is a comparison ranked by tasks solved first and cost second, with a column naming what each model did not solve; the chat line names every model's score and cost. The folder is
<date-time>-compare/with a folder per model inside and aRESULTS.mdholding both the ranking and the task-by-task table with a cost in every cell. From a shell,-mrepeats and-m connection=modelseats a model on another connection. A cancel stops after the task in progress and skips the remaining models; a model whose own run stops does not stop the next. - Four hard tasks.
expr-precedence(Python: four precedence levels, a right-associative power, unary minus, errors with the exact message and position),migrate-dependencies(Python: a stable dependency order, cycles reported, a run that survives a failure, a state file replaced by rename),rust-csv-borrow(Rust: a quoted-field reader whose fields borrow from the input unless they must not, errors with their place) andts-debounce(TypeScript: debounce with leading, trailing and maxWait against an injected clock, and throttle on it).--level hardon every runner picks them; the standard ten remain the default. On this level the hard tasks separate models: in the runs made before release, four flash models solved three of four each, three larger models solved one to three, and one solved all four.
Why
The published number was a cost, measured on this machine with other tools installed. A number is believed when you can make it yourself: on your model, with your key, at your prices, without a checkout. And the question behind it is which model to seat for a kind of work, which is answered by the same tasks run on several models side by side, not by one table. The ten standard tasks measure cost alone, since a current model solves them all, so the hard level was added for the models to differ on.
Good to know
- The bench never touches your workspace: each task is unpacked to its own copy under
~/.ryter/bench/, the sandbox confines writes to that copy, and the copies are deleted after scoring. The logs and the report stay. The run leaves your default connection and last model as they were. - The bench runs on Linux only. The build hat runs under a leader process that is a Linux child subreaper, so every process the model started, detached or not, is ended before the hidden tests are written into a copy of its tree and run under the same sandbox; a task whose tree could not be emptied is not scored. macOS has no equivalent (its kernel refuses
kqueue'sNOTE_TRACK), soryter benchand/benchmarkrefuse there with that reason instead of running unguarded. - The Landlock sandbox now also keeps a sandboxed command from signalling processes outside the sandbox, on Linux 6.12 and later: what the build hat started, not Ryter, not your other processes, and not the bench's guard. On an older kernel the rest of the sandbox holds as before. For the bench this is what keeps a model from killing the guard that ends its processes; where that cannot be kept (the sandbox
off, an older kernel), the run says so at its start, and a guard lost that way is a task unscored and the run stopped, withryter benchending what was left behind. - tokio moved from 1.53.1 to 1.54.1 for RUSTSEC-2026-0386.
- Ctrl-C on
ryter bench, or quitting the TUI during a run, ends the task in progress with everything it started and skips the rest; a task cut short, in the model's turn or in its scoring, is not scored, and what it cost still counts in the total.ryter benchexits 1 whenever the run did not go to its end. - The hidden tests travel in the binary deflated and masked, so a model that reads the binary from inside the sandbox does not find them with
grep. That is a cover, not a secret. - The hard level's briefs were sharpened twice before release, after nine models in a row read two of ts-debounce's rules the same wrong way; the hidden tests did not change, and a strong model then passed the level in full. What the first runs turned up, and what was fixed, is in
DECISIONS.md(2026-10-23). - Headless, the build hat used to be told to answer with its plan when it presented one; it is now told to make the change. A bench or
-pbuild run wants the edit. - A rebuild of the binary under a running
ryterused to make every bench task fail with "No such file or directory"; the bench now says the binary was replaced and to restart. - The bench stays head to head on the same tasks. A ranking by language or task kind was considered and set aside: with two Rust and two TypeScript tasks it would be noise.
- Open from earlier releases: the scout has no seat in
/modelsand its answer is in the event log only;max_usdis checked before a round; the live measurement of check rounds avoided; the/sessionspanel's columns; selecting with the mouse untried by a person.
Tried before release
The full local check and CI. Four live comparison runs on the hard level through OpenRouter on 2026-10-23, from the TUI: four flash models for $0.22 all told, then three larger models three times as the briefs were sharpened, the last with Kimi K3.1 Pro solving all four for $0.28. The real binary driven offline: a ryter bench run stopped with Ctrl-C while the build hat's shell command slept, leaving no process behind and a report that says cancelled; a run scored under the Landlock sandbox; a two-model run picked from a priced list, the panel saying model 1 of 2 then 2 of 2, the finish ranking the full-marks model first with the failed task named against the other, the chat line naming both, and the run folder holding a folder per model with RESULTS.md and compare.json.
Ryter v0.38.0
Easier to start, easier to review, cheaper to run: a first run with no key opens the provider list by itself, /changes drops one change of a file at a time, and the bench now compares Ryter with other coding agents by tasks solved per dollar, with two published runs.
Upgrade
ryter updateComing from 0.37.0:
- Tasks solved per dollar.
bench/compare.pygives the bench's ten tasks to Ryter and to the coding agents installed on the machine (Claude Code, Codex, OpenCode, Kilo), each in a fresh copy of the same project with the same brief, each run so that it asks nobody anything, each scored by the hidden tests and costed by what the tool itself reports. Two runs are published underdocs/bench/and summed up in the README: onclaude-opus-5-5all three of Ryter, Claude Code and OpenCode solved all ten, for $0.73, $2.42 and $1.17; onmistralai/mistral-large-4-0through OpenRouter, Ryter, OpenCode and Kilo each solved nine, for $0.30, $0.52 and $0.41. Claude Code's figure is its own estimate at list prices and is marked so; a second Opus pass the next day came within three percent per tool. One budget for the whole run, a reserve before each task, one attempt per task;--dry-runprints the commands and calls nothing,--intoruns one tool again into an earlier report. /changesdrops one change at a time. A file's diff is shown taken apart change by change, as the chat shows an edit: each change under a heading that says which it is and the lines it covers, highlighted like the file.←→picks a change;xdrops that change alone after the one question, Enter yes andnno, keeping the file's other changes;Xputs the whole file back, whichxused to do. With a model connected Ryter snapshots first, so/undobrings a dropped change back, and the chat says which change went back; with none, the chat says the drop was not recorded. A binary file, a link, a file that is not text, or one that is gone has nothing to take apart, soxthere asks about the file.- A first run with no key opens
/providerby itself. With no key on any connection, the TUI opens the provider list before anything else and says in the chat what to do: pick a connection, pressk, paste the key, or export one of the well-known variables and start again. With a key on any connection nothing opens, and the session starts on a keyed connection as 0.37.0 made it.
Why
After 0.37.0 the question was what Ryter needs to be noticed. A claim about cost is not noticed without a number against the tools people already use, so the bench got a comparison runner and the README got the number. Reviewing an agent's work change by change before keeping it is the thing people show each other in the other tools, and Ryter had it only by file. And a new user's first screen was a session with a warning line and nothing that could answer.
Good to know
- The bench's tasks are small and a current model solves them all, so the published number is cost, not ability. Harder tasks are the next step.
- A comparison run spends real money on every tool you name, under one budget; each tool runs as you have it set up, and no key is read or copied by the runner.
/changesno longer showsrin its legend, to fit;rstill reads the files again.- Open from earlier releases: the scout has no seat in
/modelsand its answer is in the event log only;max_usdis checked before a round; the live measurement of check rounds avoided; the/sessionspanel's columns; the light theme's slab untried by a person.
Tried before release
The full local check and CI. The two published comparison runs, made with the runner on this machine against Claude Code 2.1.309, OpenCode 2.0.28 and Kilo 7.8.12 as installed, plus a second Opus pass. The real binary driven offline: a build turn edited a file in two places, /changes showed both changes, the second was dropped alone, the file on disk kept the first, the chat said put back change 2 of config.rs and a checkpoint was recorded; and a start with no key on any connection opened the provider list with every connection marked no key and the line above saying what to do, with nothing sent to any provider.
Ryter v0.37.0
The plan and audit hats can send a question to a scout, a fresh reader whose reading stays out of the conversation; the build hat gets the project's fast check run for it after every round that edited; and a first run starts on the key you exported.
Upgrade
ryter updateComing from 0.36.0:
- The scout. The plan and audit hats have a
scouttool: one question about the project, and a fresh model answers it with only the four read tools (read_file,list_dir,grep,glob), reading under the asking hat's own gate, so the same secrets are refused. Only its answer enters the conversation, with the paths and lines it rests on, so the hat's context does not fill with files it looked at once. The chat shows one row for the question,scout <the question>, with what it read and what it cost (✓ 1 read · 1 search · $0.004); the spend is the asking hat's. It is not a hat: nothing on the rail, nothing to switch to. It runs on the asking hat's model unless[scout]inconfig.tomlnames a connection and a model, which is where a cheap one earns its place;rounds(12) andmax_usd(0.25) cap one question, under the session budget. A failure of the scout's own request (a dropped connection, a model the provider refuses) comes back as the scout's result with what it had, and the hat's turn goes on; the one exception is a stream that broke before its usage arrived in a session with a budget, which ends the turn on that call, as the hat's own would, since the budget admits no further request on a bill it cannot trust. The scout's spend is on the asking hat's rows; the context ring, a resumed session's size and the re-reading notice keep to the hat's own calls. The build and scribe hats are refused it and told who has it. - The check after edits.
checkin.ryter/run.tomlis the project's fast check:cargo check,tsc --noEmit,pyright,go vet. In the build hat, after each round in which the model edited files, Ryter runs it itself, shows acheckrow with✓or✗ 2 errors, and hands the model the errors as the compiler printed them, capped, before its next round, so it does not spend a round running the check and does not skip it. Only a run file you have approved as it stands runs; nothing is asked mid-round. It runs through the samePreToolUse/PostToolUsehooks as the model's ownrun_projectcall, and its row is answered however it ends,cancelled by the useron Esc included.run_projecthas acheckaction too, and the model is told to propose a check with the rest of the file. - A first run starts on the key you exported. With only
OPENROUTER_API_KEYset, the quick start used to fail on the built-in default connection without a key. Now a connection that has a key wins over one without: the--connectionflag as given, then the last-used and the default if keyed, then the first keyed connection in the config. The All hats model named with a connection stays on it, so SpaceXSI'sgrok-4.6is not asked of OpenRouter. - The README screenshots show the current screen. Staged offline against the simulated provider, and
docs/screenshots/HOW.mdsays how. - The test suite no longer looks a name up in the machine's DNS. The web guard's test on what a name resolves to hands the resolver in; a box behind a DNS proxy that answers with a benchmarking address no longer fails it.
Why
An outside gap analysis of 0.36.0 put subagents and LSP at the top of its list and followed the README's quick start to a failure. Reading is where a subagent earns its cost, so the scout reads and nothing is merged. What LSP buys a harness is the model seeing the compiler's errors without a paid round; the run file's check buys the same with nothing to install and no language server running the project's build scripts under the looking hats.
Good to know
- The scout's answer is in the session's event log, not shown in the chat; the asking hat relays what matters.
- The check's row shows the first lines of each error; the model gets the whole block.
- Owed: a live measurement of check rounds avoided; the simulated bench's model is scripted and cannot show it.
- Open from earlier releases: the
/sessionspanel's columns and its$0.00for an unpriced session, the light theme's slab untried by a person, a card not dissolving on closing.
Tried before release
The full local check and CI. The real binary driven offline: the README's quick start with only the OpenRouter key exported, headless and in the TUI, opened on that connection with no key warning and completed a turn; a plan turn sent a question to the scout, whose requests carried the four read tools alone, and the hat's next request held only the answer, with five spend events on the plan hat; a build turn's edit left a type error, the check row showed ✗ 1 error with the rustc block, the model fixed it on its next round and the second check showed ✓. After the review: a scout whose request the provider dropped, its row ✗ nothing read · $0 with the failure under it and the hat's turn finishing; the Context dial at the hat's size while the scout's three rounds landed; Esc during a 30-second check, the row cancelled by the user and the next turn answered; a PreToolUse hook on run_project denying the automatic check, on the row and in the model's note. After the second round: a scout stream dropped mid-answer without a budget, its row ✗ nothing read · $?.?? and the hat's turn finishing; the same with a $5 budget, the turn ending on that call, the next turn refused by the budget and the one after /budget off answered.
Ryter v0.36.0
The All hats seat in /models lists every connection you have a key for, as each hat's seat does, and a model chosen there from another connection moves the session to it.
Upgrade
ryter updateComing from 0.35.0:
- All hats lists every connection. With two keyed connections, each hat's seat listed both connections' models and All hats listed the session's connection alone. Now every seat has the same list, every keyed connection's models, each row naming its connection.
/modelsasks every connection for its list as it opens. - A model from another connection moves the session there. Choosing a row on All hats sets that model for every hat on the row's connection: the key, the prices, the saved route, the model check and the live connection all move with it, and the chat line says so (
model · claude-opus-5.5 · using anthropic), followed as before by which hats had their own model and now follow./model <id>typed by hand still means the current connection. - One id on two connections stays the session's own. A model listed by two connections (Claude through Anthropic and through OpenRouter) has two prices and two windows; opening
/modelsapplies the session's connection's row to the session, never the other's, and the price book prices the session's turns from its own connection's list. - A connection's own list refreshes its rows alone. The list startup asks for, or
/providerchoosing a connection, replaces that connection's rows in/modelsand keeps the others', so the seats never shrink to one connection; the rows of a connection whose key was removed go with the next list. - The price book takes the fresh list. A session's turns were priced from the connection's last list, kept from an earlier session, even after the fresh one came; now the book is built again from the fresh list as it lands.
- "Loading" means it. The panel says "loading" until every keyed connection's list has come or has failed, not until the first one lands, so the other connections are never shown as complete on their built-in rows. A connection whose list comes back empty loses its rows and its cached list. With no keyed connection at all, the panel keeps its built-in rows and stops saying "loading". A connection that didn't answer keeps its note until it answers again, not until some other connection's list lands.
- A list without the session's model prices it from the book. When the session's connection answers without the model in use (an account's filter dropped it, or the list came back empty), the catalog price goes with the row: the header and the saved route show what the turns are charged at, which may be nothing.
- A chosen row's context length is the gauge's. Choosing a model in
/modelssets the context gauge from the row's own length. Before, an id Ryter did not know by name (OpenRouter'sx-ai/grok-4.7) was put at 200k over the length the row had just shown. - A connection added this session is listed. A connection added in
/provideris one/modelslists and the session can use at once. Before, the picker waited on its list for good, and using it said "unknown connection" until the next start. - The
defaultrow names the connection the session is on now, after All hats moved it, not the one it was on when the panel opened. - The saved route is the header.
~/.ryter/last.tomlnow records the price the header shows, an unpriced model included, so a price the list dropped does not come back at the next start. - The header is priced as the turns are charged. Choosing a model, or a list landing, prices the header from the same book the session's turns are charged from: the connection's own list first, the shipped and pulled tables behind it. Before, Enter could show the table's price while the turns ran at the list's.
- A connection removed mid-fetch is gone. Removing a connection in
/providerwhile its list was still coming no longer puts its rows back, writes its cache or leaves a note when the answer lands. - One fetch per connection. Opening
/modelswhile startup's list for the same connection is still out no longer fetches it twice. A connection removed and added again takes the new fetch's answer, never an older thread's. - A session saved under
spacexaiis priced and sized. Such a session runs onspacexsi, and its list, window and price now reach the screen; before, the header and the gauge never took that connection's list for the whole session. - The first frame is priced as the turns are. The opening screen takes its price from the same book the agent is built with, the connection's own list over the shipped tables, not the tables alone.
Why
The user, on 0.35.0 with Anthropic in use and OpenRouter keyed: "each of the hats shows my anthropic key models plus the open router models which is perfect. But if I go to all hats it only shows the anthropic models?" A hat has carried a connection with its model since the per-hat seats; All hats kept the one-connection shape from before 0.31.0, so the only way to another connection's models for every hat was /provider first. "It needs to work the same as each hat."
Good to know
- The seats column shows each seat's model, not its connection; the rows in the list do.
- Open from earlier releases: the
/sessionspanel's columns and its$0.00for an unpriced session, the README's screenshots of the previous screen, the light theme's slab untried by a person, a card not dissolving on closing.
Tried before release
The full local check and CI. The real binary driven offline with two keyed connections on the simulated provider: both rows under All hats, the second connection's row chosen, the line model · fixture-model · using second, and the model check and the next turn both sent with the second connection's key. Then with the two connections listing the same model id at different context windows: the sidebar kept the session's connection's window after /models opened, and All hats still listed both connections after /provider chose one. Then with a stale cached list at one price and the live list at another: the picker opened on the cached list ("from 3 h ago · refreshing"), and the turn after the refresh was charged at the live price. Then a cached price for a model the live list no longer carried: the turn before the list was charged at the cached price, the turn after was unpriced, and the saved route dropped the price with the row; a prefixed id chosen from the list put its own 131k on the gauge; and a connection added in /provider mid-session was used at once and listed by /models beside the other, with the list request carrying its key. Then the route after a dropped model carried no price, a model the list priced at $4 and the shipped table knows with its cache tiers was charged and shown at the table's $1 on both sides, /models during a held startup fetch asked once, and a connection removed while its list was out came back as nothing, with the same checks run again after the fifth round's fixes.
Ryter v0.35.0
Every question Ryter asks takes the same keys: Enter is yes, Shift+Enter is yes for the session where that answer exists, n is no. Deleting a session and removing a hook are a plain are-you-sure.
Upgrade
ryter updateComing from 0.34.0:
- One answer for every question. The allow card, the cards asked every time (destruction, a write outside the project, the project's
.env, a command as root), the plan approval, the audit card, the/newquestion, the trust question, the save-or-discard questions in/settingsand/budget, and the remove questions in/rules,/permissionsand/hooksall takeEnterfor yes andnfor no. The legends say⏎,⇧⏎andn.yandastill work unsaid. Shift+Enteris yes for the session. On an allow card that offers a session answer ("edits to files in the project", "cargo testcommands"), a shiftedEntergives it. Where the card has no session answer, a shiftedEnteris a plain yes.- Deleting a session asks.
din/sessionsshows the session's title and id and asks;Enterdeletes,nkeeps. There is no id to type. Removing a hook from/hooks, a connection from/provider, a server from/mcpor a skill from/skillsasks the same way; there is no name or word to type.xin/changesasks the same question before putting a file back. - The trust question is
Enterorn. The first-run card has no cursor to move:Entertrusts,nskips, and the body says which is which. - The half-second guard stays, and a held key is not a press. A card that opens under typing ignores an
Enterthat arrives in its first half second, and anEnterstill held down when that half second ends: a terminal without key-release reports sends a held key as a run of presses, and the card answers only to anEnterfurther than 200 ms from the last. A long rules change still has to be read to its end beforeEnterapproves it. They-only rule on the destruction and root cards, which existed for the same moment, is gone.
Why
The user, after 0.34.0: "all of the asks, they are not consistent across the different areas. Some still ask for y for yes, other say enter. I like enter for yes, shift enter for yes and don't ask again and n for no. Enter should be the consistent yes for all questions." Sixteen questions had five schemes between them. Typing a session's id to delete it was a ceremony for one of the smallest things in the app.
Good to know
Shift+Enterreaches Ryter as its own key only in a terminal that speaks the kitty keyboard protocol (kitty, WezTerm, Ghostty, foot, recent Windows Terminal), which Ryter asks for at startup. Elsewhere it arrives as a plainEnterand allows once;ais the session answer there./yolois unchanged. It has answered every ask of the gate yes since 0.14.0, a command as root excepted; what is refused stays refused.- The
/sessionspanel still lacks the old page's columns, and the README's screenshots still show the previous screen.
Tried before release
The full local check and CI. The real binary driven offline at 132 columns: a Shift+Enter sent as the protocol sends it, on a git push card, was recorded as the session answer; Enter answered the /new question; in /sessions, d put up the question, n kept the session, and d then Enter deleted it. After the review: an Enter held down across a destruction card's opening left it up, and a press after letting go answered it; the trust card, /changes and the three removes answered Enter and n.
Ryter v0.34.0
The screen loses its views: no Hats or Sessions pages, no tabs on the top bar, and no keys to focus the rail. The conversation, the rail and the / panels are the whole of it.
Upgrade
ryter updateComing from 0.33.0:
- No views. The Hats page (
Alt+2), the Sessions page (Alt+3), the three tabs on the top bar andAlt+1–Alt+3are gone. The top bar carries the brand alone. A hat's description and reasoning level are read in/models, where they are set; the saved sessions are/sessions, which filters as you type, renames, deletes and starts a new one. - The rail is a readout.
F6andF7, the cursor on the hats andEnterto wear one are gone. A click on a hat row still puts that hat on, in place, andTabandShift+Tabreach every hat from the prompt in two presses. The hairline beside the rail is always the rule colour. - Nothing said twice. The hint bar's right end is empty while the rail is up, since the hat and the spend are on the rail; with the rail folded by
F2it readshat · spend. The welcome names/sessionsonly when the project has an earlier session with something in it to go back to; the count behind it is read again when the open session changes (/new, a resume) and after a delete. [ui] capsrounds the ends of the hat chip; there is no tab to round any more.
Why
The user asked what the three views gave the application. The Hats page was read only (its Enter did nothing) and said what the rail has said since 0.33.0. The Sessions page only resumed, where /sessions does everything. F6/F7 was a slower third route to a hat switch. Each one was a place to go with nothing to do there, and the rule since the one-rail release is that nothing on the screen is said twice.
Good to know
- Below 100 columns there is no rail, so the four hats at once are only in
/modelson a narrow terminal. - The
/sessionspanel did not take the page's columns (hat, messages, spend, updated). - The README's screenshots still show the previous screen with its tabs; new ones are owed.
- The light theme's slab is still untried by a person, and a card does not dissolve as it closes.
Tried before release
The full local check and CI. The real binary driven offline at 132 columns: each of F6, F7, Alt+1, Alt+2 and Alt+3 left the idle screen byte for byte as it was; Tab changed the hat; F2 folded and restored the rail; /sessions opened with its own keys; a turn ran to its reply. Then, for the welcome's hint: a fresh project showed none, /new after a turn showed /sessions resume one, deleting that session from /sessions took it away again, and the binary started a second time on the same project, with that session on disk, opened with the hint.