Skip to content

Releases: zypher-systems/Ryter

Ryter v0.43.0

Choose a tag to compare

@github-actions github-actions released this 31 Oct 18:15
da921cd

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 update

Coming 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 /redo can restore until the next build turn. /undo after that /redo takes back the same files and leaves yours.
  • OpenRouter's reasoning goes back within a tool call. reasoning_details were 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 for name/a.txt; one then told the user the project's files were missing.
  • Project memory is shared out. ROADMAP.md, DECISIONS.md and 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, and echo "${f:-"'"}"; rm -rf /; echo "'" read the rm as 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 for loop's values are read as the shell will. for f in a b; do sed -i … $f; done is judged value by value, patterns and lists included, and echo and printf no 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 $g is 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 … $A asks, where it used to pass as notes.txt. A value with a space in it is judged split as well as whole, for loops and for F="a b".
  • Making something else of a variable is refused. ${f%.txt}, ${f:-x}, ${f/a/b}, ${f:1}, f+=x and ((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 as for 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} id can run sudo id, so it asks as root as that does, and git {,push} x asks as git push x does: 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 git verb 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 $1 or $@ 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. /budget and the guide say the cap stops the next call, so the last can pass it by its own cost. /context calls a window nothing states assumed. /sessions and ryter sessions read as the rail does: $0 for no calls, $?.?? for nothing priced, ≥ for a total that leaves calls out, and no $0.00 for a session that cost something.
  • ryter doctor says 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.toml and .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.md keeps the dated record.
  • The reasoning echo is proven on two models, anthropic/claude-sonnet-4.6 and deepseek/deepseek-v4.1-flash. A provider that refuses it would show as a 400 naming reasoning_details on a tool round.
  • A bare $f the gate can't read is still not judged as a path: for f in $(cat list); do sed -i … $f; done passes, and the sandbox is what holds it. $f.txt and $f/x ask.
  • 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 find from above root's folders is refused even with -maxdepth 1. sudo ls -la does 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.toml and last.toml carry 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

Choose a tag to compare

@github-actions github-actions released this 28 Oct 13:57
869694c

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 update

Coming from 0.41.0:

  • Routing rules on the /provider screen. Press r on a row OpenRouter serves. The form has every field of OpenRouter's provider object 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 as p50=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, ^s saves, 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. r on a row OpenRouter does not serve says why it does not apply.
  • The same rules in the file. A [connections.<name>.routing] table in config.toml takes 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 in config.toml and skipped with a warning in routing.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. /spend has a row per connection and host (openrouter · Baseten), ryter spend says via <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 at r on 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 /models list, the /sessions list 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.py takes ryter@label=connection:model as a second Ryter arm on the same model and --ryter-config label=FILE for 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.toml turns 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 /models picker lists OpenRouter's plain model ids; a :floor or :nitro suffix cannot be chosen there. The routing rules do the same job for every model on the connection.
  • A [connections.openrouter] row of your own in config.toml replaces 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.toml table wins over ~/.ryter/routing.toml (the screen's file), which wins over ~/.ryter/config.toml. So a bench arm's --ryter-config pin holds whatever the screen has saved, also when it restates the home file's choice; a project row without a routing table leaves the rules alone, and an empty table is the project's clear. routing.toml is read after connections.toml and 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 in config.toml on the next launch.
  • ryter connections on the command line does not show the routing rules; the /provider row 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 under tests/. 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

Choose a tag to compare

@github-actions github-actions released this 25 Oct 19:30
23b7275

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 update

Coming 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. filter runs 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), through npm test, pnpm run, yarn, npx, uv run, poetry run and python -m, with a package.json script read to find the runner behind it. files runs the tests that cover those files: a Cargo file as its package (-p, with --workspace dropped 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 test in 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 gains build, lint and format beside check, proposed and approved like the rest, and each is a tool of that name. build, check and lint come back as the check's report: the errors as the compiler or linter printed them, capped, or one word (built, passed, clean). format runs 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. format is 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.json with its scripts and lockfile, go.mod, pyproject.toml and its tools, a Makefile'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's start, ready and stop are never guessed.
  • Ten hard bench tasks. Six more at level = "hard", two per language: config-interpolation and rate-limiter (Python), rust-lru-ttl and rust-glob (Rust), ts-json-patch and ts-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 hard and /benchmark run 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 test and run_project check still work and run the same code as the new tools, as a whole run; old transcripts and the simulated bench use them. run_project keeps start, stop and status.
  • 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 test stays npm test -- -t name. A cargo filter is a substring of the test's path; go's -run is a regex. Cargo files map to -p only, not --test, --lib or --bin.
  • A source file maps to the test named for its whole stem (button.component.ts to button.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 a package.json script 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 a NAME="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.
  • format changes files, so it checkpoints like a write and /undo covers 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, lint and format rows; propose_run takes 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 start is untouched. A detected file that no hat could run falls back to the old message. A project with both a manifest and a Makefile takes the manifest.
  • The ten-hard-tasks commit reached dev with 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 /sessions panel'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 under tests/; 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

Choose a tag to compare

@github-actions github-actions released this 25 Oct 03:55
a798f83

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 update

Coming from 0.39.0:

  • A test run reaches the model as its failures. run_project test, the check after edits and a bash test 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] chars in config.toml bounds it (12,000; 0 leaves it out); hidden folders, what .gitignore names, and build output and installed packages are not in it.
  • Decisions beside the plan. record_decision now 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 ## work in .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. write over 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 use search_replace; a search_replace whose 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 starting Blocked: what is in the way (the chat shows it), or after [limits] check_rounds such rounds (3; 0 turns 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 .gitignore like the rest. A test digest with more totals lines than fit keeps the failing ones; cargo test -- --show-output blocks 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.md has a new header paragraph and a ## work section; 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 write tool only; a shell redirect is the gate's business as before. A new session, /new included, 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 /sessions panel'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

Choose a tag to compare

@github-actions github-actions released this 24 Oct 22:42
20b6a9e

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 update

Coming from 0.38.0:

  • /benchmark and ryter 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, hard or all), a budget and the sandbox; Enter on the models row lists the connection's models with context and prices as /models shows 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 own ryter process 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; c closes it while the run goes on, Esc asks before stopping after the task in progress. The table lands in ~/.ryter/bench/<date-time>-<model>/RESULTS.md beside report.json and each task's logs. The same run is ryter bench --level hard --budget 5 from a shell; --list names 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 a RESULTS.md holding both the ranking and the task-by-task table with a cost in every cell. From a shell, -m repeats and -m connection=model seats 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) and ts-debounce (TypeScript: debounce with leading, trailing and maxWait against an injected clock, and throttle on it). --level hard on 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's NOTE_TRACK), so ryter bench and /benchmark refuse 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, with ryter bench ending 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 bench exits 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 -p build run wants the edit.
  • A rebuild of the binary under a running ryter used 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 /models and its answer is in the event log only; max_usd is checked before a round; the live measurement of check rounds avoided; the /sessions panel'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

Choose a tag to compare

@github-actions github-actions released this 23 Oct 14:23
df677d7

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 update

Coming from 0.37.0:

  • Tasks solved per dollar. bench/compare.py gives 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 under docs/bench/ and summed up in the README: on claude-opus-5-5 all three of Ryter, Claude Code and OpenCode solved all ten, for $0.73, $2.42 and $1.17; on mistralai/mistral-large-4-0 through 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-run prints the commands and calls nothing, --into runs one tool again into an earlier report.
  • /changes drops 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; x drops that change alone after the one question, Enter yes and n no, keeping the file's other changes; X puts the whole file back, which x used to do. With a model connected Ryter snapshots first, so /undo brings 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, so x there asks about the file.
  • A first run with no key opens /provider by 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, press k, 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.
  • /changes no longer shows r in its legend, to fit; r still reads the files again.
  • Open from earlier releases: the scout has no seat in /models and its answer is in the event log only; max_usd is checked before a round; the live measurement of check rounds avoided; the /sessions panel'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

Choose a tag to compare

@github-actions github-actions released this 22 Oct 18:28
dcc352d

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 update

Coming from 0.36.0:

  • The scout. The plan and audit hats have a scout tool: 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] in config.toml names a connection and a model, which is where a cheap one earns its place; rounds (12) and max_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. check in .ryter/run.toml is 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 a check row 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 same PreToolUse/PostToolUse hooks as the model's own run_project call, and its row is answered however it ends, cancelled by the user on Esc included. run_project has a check action 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_KEY set, 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 --connection flag 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's grok-4.6 is not asked of OpenRouter.
  • The README screenshots show the current screen. Staged offline against the simulated provider, and docs/screenshots/HOW.md says 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 /sessions panel's columns and its $0.00 for 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

Choose a tag to compare

@github-actions github-actions released this 21 Oct 22:29
2e6df34

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 update

Coming 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. /models asks 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 /models applies 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 /provider choosing a connection, replaces that connection's rows in /models and 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 /models sets the context gauge from the row's own length. Before, an id Ryter did not know by name (OpenRouter's x-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 /provider is one /models lists 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 default row 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.toml now 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 /provider while 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 /models while 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 spacexai is priced and sized. Such a session runs on spacexsi, 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 /sessions panel's columns and its $0.00 for 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

Choose a tag to compare

@github-actions github-actions released this 21 Oct 14:44
88c068d

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 update

Coming 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 /new question, the trust question, the save-or-discard questions in /settings and /budget, and the remove questions in /rules, /permissions and /hooks all take Enter for yes and n for no. The legends say ⏎, ⇧⏎ and n. y and a still work unsaid.
  • Shift+Enter is yes for the session. On an allow card that offers a session answer ("edits to files in the project", "cargo test commands"), a shifted Enter gives it. Where the card has no session answer, a shifted Enter is a plain yes.
  • Deleting a session asks. d in /sessions shows the session's title and id and asks; Enter deletes, n keeps. There is no id to type. Removing a hook from /hooks, a connection from /provider, a server from /mcp or a skill from /skills asks the same way; there is no name or word to type. x in /changes asks the same question before putting a file back.
  • The trust question is Enter or n. The first-run card has no cursor to move: Enter trusts, n skips, 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 Enter that arrives in its first half second, and an Enter still 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 an Enter further than 200 ms from the last. A long rules change still has to be read to its end before Enter approves it. The y-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+Enter reaches 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 plain Enter and allows once; a is the session answer there.
  • /yolo is 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 /sessions panel 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

Choose a tag to compare

@github-actions github-actions released this 21 Oct 06:06
40ab5ac

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 update

Coming from 0.33.0:

  • No views. The Hats page (Alt+2), the Sessions page (Alt+3), the three tabs on the top bar and Alt+1–Alt+3 are 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. F6 and F7, the cursor on the hats and Enter to wear one are gone. A click on a hat row still puts that hat on, in place, and Tab and Shift+Tab reach 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 F2 it reads hat · spend. The welcome names /sessions only 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] caps rounds 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 /models on a narrow terminal.
  • The /sessions panel 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.