# Rabbit Brain: AGENTS.md Rabbit Brain (`rb`) keeps the record of the ML experiments you run with coding agents, as plain YAML files in `.rb/` next to the code. It holds what the work is trying to find out: questions, as a tree. It holds how each is tested: experiments, each with its hypothesis or purpose, its variants, every setting with where its value came from, and its criteria, each a rule over one comparison, declared by someone at a time. It holds what ran, as runs with a receipt and the resources they read and made, and what was computed from saved outputs, as analyses with the comparisons and intervals they produced. It holds the calls made on the work, as decisions: one recorded in an agent session is proposed until the user confirms it. `rb` tracks what agents and humans do; it never refuses a write because of who makes it. **Anyone records; `rb` checks sources and computes findings; every write records who made it.** **If the project has a `.rb/` directory, run `rb context --model ` before anything else.** It is the state of the work. It replaces anyone's summary of it, including your own from an earlier session. `--model` is the model you run on, as its provider names it: rb keeps it with every write you make in this session. Read this whole file before you run anything. **Do not compute errors, regressions, rankings or findings yourself.** `rb` defines them, applies the same definitions every time, and records how. That is what makes a result comparable across runs, sessions and people. If you find yourself writing a script that decides whether a result holds or which model is better, stop. Record the numbers with `rb run add` (a computation over saved outputs with `rb analysis add`) and read the finding with `rb show`. ## Install ```sh pip install rabbit-brain # rb, with the SDK, in this environment; or: uv tool install rabbit-brain (rb in an environment of its own, which keeps itself up to date), or uvx --from rabbit-brain rb rb version rb tour # a one-minute walkthrough: builds a small real project in ./rb-tour, every command shown with its real output rb compile # what existing reports and results the record can place, and what it cannot; writes nothing rb docs # prints this file rb docs --start # prints the start protocol: recording work already done in a project under way rb skill add # installs rb's records and rules as your coding agent's skill (--claude default; --cursor; --agents-md for Codex and anything that reads AGENTS.md) rb init "" # starts .rb/ here and installs rb's instructions for the agents it detects (--no-agents: leave them alone) rb init --dry-run # every file rb init would write or change, and what later commands add; writes nothing rb remove --dry-run # what rb remove would take out of this checkout, and what it would keep, and why; removes nothing rb feedback # a link to send feedback on rb (--about ): the page shows what it would send; rb sends nothing ``` Python 3.10 or later. The core depends on pydantic and textual (the user's screen); rb reads and writes its own YAML, so the record needs no YAML library. The research record stays on your machine unless someone runs `rb sync --with-record` (optional: a private copy in a rabbitbrain.ai account, where plain `rb sync` sends its page alone). Optional usage analytics sends fixed categories of usage facts to PostHog only after it is enabled; it sends no research content and is off by default. See **Optional usage analytics** below. `rb init` starts the record and installs rb's instructions for the agents it detects in the repository: the skill and rb's MCP server (in `.mcp.json`) for Claude Code, when `.claude/`, `CLAUDE.md` or a Claude Code session says it works here; rb's marked section in `AGENTS.md`, when that file exists or Codex runs it; a rule in `.cursor/rules/` for Cursor, when `.cursor/` or `.cursorrules` is there or Cursor's agent runs it. With each, a session hook in the agent's project file, so rb looks at the repository as a session starts and as each turn ends whether the agent reads rb's instructions or not: `SessionStart` and `Stop` in the `hooks` of `.claude/settings.json` for Claude Code, the same in `.codex/hooks.json` for Codex (when `.codex/` is there or Codex runs it; Codex runs a project's hooks once it is trusted, in `/hooks`), and `sessionStart` and `stop` in `.cursor/hooks.json` for Cursor. The hook runs `rb journal scan --hook `, with a 10-second limit: rb says nothing, exits 0 whatever happens (no record, an error), waits on no other rb, and keeps the look in the journal as the agent's; it tells the session nothing and records nothing. Where rb is found depends on the form: the one Claude Code runs (in `sh`, or Git Bash on Windows) and Codex runs on macOS and Linux tries rb on the PATH, else in `~/.local/bin`, where `uv tool` and `pipx` put it, else in the project's `.venv/bin` (not `.venv/Scripts`, where a Windows venv keeps it), and exits 0 where none is there; Cursor's looks on the PATH alone and exits 0 where rb is not there; Codex's on Windows (`commandWindows`) looks on the PATH alone, and where rb is not there the hook fails and Codex goes on. On Windows without Git Bash, Claude Code runs the hook in PowerShell, which does not read the `sh` form: the hook fails, the session goes on, and rb does not look. It makes the files that are not there, and says what it wrote. An `AGENTS.md`, a `.mcp.json` or a hook file that was there before rb changes only once the user agrees: in their own terminal (standard input and output a terminal, no agent session, no `--json`) `rb init` asks once, showing exactly what it would add (rb's section at the end of `AGENTS.md`, or in place of the section between rb's marker lines where the file holds one, each marker a line of its own: a marker quoted in a sentence is the user's; rb's one member in the `mcpServers` of `.mcp.json`; rb's handler added to each of its events under `hooks`, beside the hooks there, or put in place of rb's handler as an earlier rb wrote it, where the file holds one: an upgrade leaves one handler, in today's form), and Enter, or anything but `y` or `yes`, keeps the file as it is; `--yes` gives the answer up front, for a script. Anywhere else it leaves such a file as it is and says what to add and the one command that adds it (`rb skill add --agents-md --yes`; for `.mcp.json`, `claude mcp add --scope project rabbit-brain -- rb mcp`; for a hook file, `rb skill add --claude --project --yes`, `--agents-md --yes` or `--cursor --yes`), in `data.agents.left` too. A handler that runs `rb journal scan` in a form no rb wrote (edited by hand, or written by a newer rb) is the user's: rb leaves it as it is, adds nothing beside it, and says so in one line. Which handler is rb's: where the ownership ledger has a line for the file, a form rb has written under an event that line says rb added it to; with none (a clone, a ledger lost), any form rb has written. An edit keeps every byte that was there: the section goes in the file's own line endings, the member in its own indentation and spacing, and the file is never reformatted. Where a link takes a write elsewhere (an `AGENTS.md` that is a link to `CLAUDE.md`, a `.claude/` that is a link to another folder), rb names the file it lands in, in the plan, the question and what it says (`AGENTS.md (a link to CLAUDE.md)`; `real_path` in `--json`), and a new file a link takes outside the repository waits for the yes as a change to a file that was there does; `rb remove` takes away the file rb made where a link points and leaves the link, which is the user's. A skill or a Cursor rule already at rb's path that rb did not write (a team's own) `rb init` leaves as it is, `--yes` or not, and names the command that puts rb's in its place. `--no-agents` leaves every instruction file alone, and `rb skill add` puts rb in place for any one agent later, `--agents-md`, `--claude --project` and `--cursor` by the same rule (`--yes`; in an agent session `--yes` agrees to nothing, and the command rb names, without it, is for the user's own terminal): rb's text in place of a file it did not write is kept in the ownership ledger with what that file held, and `rb remove` puts the file back. When `.gitignore` keeps a file it installs out of git, it says so. Run inside an agent session (read as `rb confirm` reads one, a chat's leftover id included; a CI job is none) it installs nothing, since a running session loads no skill or MCP server added during it, and it names the commands that install them for later sessions. `rb reconcile --apply` does the same when it starts the record (with `--no-agents` and `--yes` as `rb init` takes them). `rb init` ends with the one line the user pastes into the agent's chat: `Use Rabbit Brain to track this: read rb context, then record where the work stands.` (`data.paste`); after `--no-agents` it is offered to whoever works with a coding agent, never as a step left undone. ## The record (`.rb/`) `rb` keeps a line of work as plain YAML files in `.rb/`: one file per record, and one directory per experiment, holding a file for each of its variants, notes, criteria, runs and analyses, so agents adding records on different branches or worktrees write different files and their work merges without a conflict. An experiment's own file holds its shared settings, relations and freeze: when two branches change it in the same place, git leaves a conflict there, which `rb doctor` names with how to resolve it. Commit `.rb/` with the code, and the record travels with the branch. `.rb/log.jsonl` records every write: who made it and how `rb` knew, the chat session when the runtime exports one, what rb knows of an agent that made it (`agent`), the user's words when an agent passed them (`words`), through what (`via`: the CLI, the SDK, or MCP), what changed, the file it wrote, and the hash of the content written. `rb init` sets git to merge the log line by line. `rb` also keeps a local copy of each write in `.rb/cache/` (gitignored), which `rb doctor --restore` puts back; a write that came from elsewhere is found in git history. **Worktrees.** A command reads and writes the record in its directory or the nearest one above it. A git worktree nested in the checkout with no `.rb/` of its own (the record was not committed when it forked) writes to the checkout's record, and asks git in the worktree: a run's and an analysis's receipt keep the worktree's commit, branch, uncommitted changes and exact code, and `rb journal run` runs the command there, in the worktree's copy of the record's folder, and lists what it wrote in the worktree's result folders. The SDK's `open()` works the same way where the process works in such a worktree, whatever path names the record. Every write from a linked worktree keeps its path in the data (`worktree` on the log line and on the receipt, and on `rb journal run`'s line). A worktree nested in a checkout is another checkout: the checkout's code, what its journal watches, `rb compile` of a folder holding it, its results folders and databases and the work it says is already done leave out that worktree's files (`rb compile ` reads them). `rb status` and `rb context` there say once that it writes to the checkout's record, with each place's branch and path, and `rb status --json` gives the checkout it ran in (`checkout`). A worktree outside the checkout with no record of its own, where the checkout holds the project's record, gets `E_NO_PROJECT`, which names that record's place and how to bring it here: commit `.rb/` there (where no commit there holds it yet, an ignored `.rb/` too), then `git merge` its branch here, or, where that checkout is detached, the commit that holds the record. `rb init` there is refused with the same words; `rb init --new-project` starts a separate project (a new uid), which **Elsewhere** tells apart. The checkout itself is never pointed at a linked worktree's record: with none of its own, `rb init` starts one there. The user's screens, `rb open` and `rb ui`, say which place they show and can show another: a worktree's own record, or a past version of a record, read only. Nothing is summed across places. ### The files ``` .rb/project.yaml the project: its title, where it was cloned from .rb/notes/.yaml one file per note on the project .rb/questions/.yaml what the work is trying to find out; a sub-question names its parent .rb/experiments//experiment.yaml an experiment: its questions, purpose, hypothesis, why, shared settings, relations, its freeze and amendments .rb/experiments//variants/.yaml one file per variant: its role and its own settings .rb/experiments//notes/.yaml one file per note on the experiment .rb/experiments//criteria/.yaml one file per criterion: what it compares, its rule and role, who declared it and when .rb/experiments//runs/.yaml one file per run: results, per-run settings, inputs, outputs, host, when its output was produced, receipt .rb/experiments//analyses/.yaml one file per analysis: what it read, its method, the comparisons it produced with their intervals .rb/metrics/.yaml the metric catalogue .rb/resources/.yaml datasets, checkpoints, code, environments, artifacts, and who has looked at them .rb/decisions/.yaml calls on criteria, experiments, questions and settings, each with who made it, why, and what it was made on .rb/log.jsonl every write: who (an agent with its name, version, provider and model), in which chat session, how, the path, and the hash of the content written .rb/cache/ local and gitignored: rb's own copies and a hash cache (in a bundle, cache/commits/ holds the sources read at a commit, kept in git) .rb/merges.json in a bundle rb export --with-record wrote: rb's copies of the versions each record git merged from two branches was merged from ``` Every file opens with `rb: 1` (the format) and then `kind:`, so a file travels and validates on its own; the record's fields follow. Format 1 is fixed: a new field means a new format, so a later rb that adds a field to a record writes that file in format 2, and an older rb says to upgrade (`E_STATE_NEWER`) rather than read part of it. A field rb does not know in a format-1 file is therefore most likely a hand edit, and rb says so (below). **Format 2** (rb 0.8) adds four rules to a criterion and nothing else: a count (`over: count`, with `count`, how many units must meet the threshold, and `of`, how many units the rule is of), a best of N (`over: best`, with `of`), a relative change (`measure: relative`) and a cited difference of means (`measure: means`), under **Criteria**. A file is written in format 2 only while it holds one of them, and every other file is `rb: 1`, written as rb 0.7 writes it, so a record that uses none of the four is format 1 throughout and rb 0.7 reads it as before. A record that uses one holds one `rb: 2` file per such rule (its criterion's own file): rb 0.7.x names that file, says to upgrade, reads every other file, fails its gates closed and writes nothing to the record (a note, a criterion, a call, a restore alike) until it is upgraded, and refuses to clone a bundle that holds it; a team that shares such a record upgrades together. This rb reads both formats, and refuses a file that says `rb: 1` and holds one of the four (`E_STATE_CORRUPT`): format 1 never held one. A retracted rule keeps the rule it was, so its file stays format 2. Nor does format 2 hold anything else: a file that says `rb: 2` and holds none of the four (its first line raised by hand) is no valid record either (`E_STATE_CORRUPT`). `rb doctor` names it with its fix (set its first line back to `rb: 1`, or `rb doctor --restore`), every gate fails closed, and rb writes nothing over it until it says `rb: 1` again. A finding, an experiment's state, a question's state and a resource's availability are never in a file: they are computed on every read. A run's file keeps, beside each resource it read or made, where that resource stood when the run was recorded (`inputs[].availability`, `outputs[].availability`): rb says it as then (`present when recorded`) and never reads it as the resource's availability now. An id is the kind's prefix and six random characters (`q67pn3k`, `run4k2xa9`), so two branches do not mint the same one (and when they do, `rb doctor --restore` gives the second record an id of its own), or a name you give (`--id`, a metric's name, a resource's id); the project, its experiments, runs, analyses and resources also carry a global uid (`exp_01J...`) that names them across projects. The files are plain YAML kept to a strict subset, **Rabbit Brain YAML**: YAML with the surprises taken out, so every YAML parser reads a record the same way: - Mappings, lists, text, numbers, `true` and `false`, `null`, and comments. Nothing else: no anchors or aliases, no tags, no merge keys, no directives, one document per file. - A plain value that YAML 1.1 and YAML 1.2 read differently is refused, with the spelling to write instead: `yes`, `no`, `on` and `off` (and `y`, `n`); dates (`2026-09-30`); numbers with leading zeros (`08`), underscores (`1_000`), hex or octal (`0x1f`, `0o17`), or an exponent without a dot (write `1e-4` as `1.0e-4` or `0.0001`); times (`10:30`); `.inf` and `.nan`, and a number too large to hold (`1.0e+999`). Quote such a value to keep it as text: `"yes"`, `"2026-09-30"`. rb quotes every timestamp it writes for the same reason. - Comments belong to the reader. rb keeps them, on the key or list item they sit on, whenever it rewrites a record. **A comment or a reformat is not an edit**: rb compares a record's content (its canonical JSON), never its text, so annotating a record never reads as changing it. Changing a value, deleting a file or moving one is an edit. Config files the settings point at (`configs/train.yaml#optim.lr`) are ordinary YAML, JSON or TOML, read by key; Rabbit Brain YAML is only the record's own format. Three commands work on the files as files: - `rb fmt` rewrites record files in their canonical form (two-space indents, block style, text plain where that is safe and quoted where it is not), comments kept: every record file under `.rb/` by default, or the files and directories you name. `rb fmt --check` changes nothing and exits 1 when a file is not canonical. Content never changes, so nothing is logged. A file that is not Rabbit Brain YAML is named with its line, its column and what to write instead, and `rb fmt` exits 1. - `rb show .yaml` reads any record file (here, in an export, or sent on its own), checks it against the format, and prints it without who made each write (the writer, how rb knew, through what, in which session, with which agent, on which branch); `rb show .yaml --json` gives it back whole (`data.record`) and as canonical JSON (`data.canonical`: sorted keys, no whitespace), with `data.valid` and `data.problems`. A file that reads but is not a valid record exits 1; one that is not Rabbit Brain YAML fails with `E_YAML`. - `rb schema all` prints the whole format as one JSON Schema (draft 2020-12): the shape of every file, chosen by its `kind`. It covers formats 1 and 2 and is committed as `schema/rb-v2.schema.json`; its `$id` is . Format 1's own, which rb 0.7 enforced, stays beside it as `schema/rb-v1.schema.json`, and every file rb writes in format 1 satisfies it. rb writes nothing it does not satisfy, and `rb schema all > rb-v2.schema.json` hands the contract to any JSON Schema validator. `rb schema ` prints one record's shape: `project`, `question`, `experiment`, `metric`, `criterion`, `run`, `analysis`, `resource`, `decision`, or `finding` (computed, never stored). `rb schema ir` prints the vocabulary: every kind, what it means, the write that makes one, and the provenance rb requires. **Everything `rb status`, `rb show`, `rb compare`, `rb doctor`, `rb env check` and `rb export` need is in these files.** None of it needs an account or a network. rabbitbrain.ai (optional, below) only shows what the files already say: it never holds the only copy, and nothing on it changes a finding. ### Who does what: anyone records, rb checks and computes, every write records who - You may make every write rb has: questions (and moves in the tree), experiments (and where they are filed), variants, notes, relations, metrics, settings with their sources, criteria, runs, analyses, resources and exposures, freezing, amending, retracting anything, `rb doctor --adopt`, and recording the calls (accept, reject, investigate; continue, park, stop, pivot, ship, and vouching for a setting). rb never refuses a write because of who makes it, and records each as whoever made it. A call is the user's: one you record is proposed, not made. It changes nothing until the user confirms it with one command in their own terminal (`rb confirm `, or `c` on `rb ui`), and rb lists it apart from the calls (under **Deciding**, below). - **Every write records who made it** (`human:` or `agent:`), the chat session when the runtime exports one (on every `log.jsonl` line, on decisions and on run receipts), for an agent its name, version, provider and model (`agent`, on every `log.jsonl` line and run receipt), and the reason as given (`--why`, or `-m`). When you act on the user's words from the chat, pass them verbatim with `--words ""`: every command that takes `-m`/`--why` accepts it, and so do `rb question add`, `rb experiment add` and `rb note add`. The words are kept beside the reason on decisions, freezes, amendments, retractions and criteria, and on the log line. Words given in an agent session claim the user said them, which rb cannot know, so rb never says them as the user's: its text and the user's screens say them as `words (as given): "..."`. Words read as the user's own once the user confirms the call they came with, or when the user gave them in their own terminal: rb's text says those as `your words: "..."`, and the user's own screens (`rb open`, `rb ui`) in quotes after the reason, without that tag. A page (`rb page`, `rb export`, `rb sync`), which anyone may be sent, leaves both out, but for a gate's words a session gave, which it says as given (under **The decision it gates**). Nothing is paraphrased: quote what the user wrote, or leave `--words` out. - **Write the record's text as plain text.** rb shows the text you give it (a question, a statement, a hypothesis, a note, a reason, an interpretation, a title) exactly as written, on every screen, and renders no markup. Where something a session wrote can wait for the user, every screen (rb confirm's listing and receipt, `rb status`, `rb show`, `rb context`, `rb log`, `rb open`, `rb ui`, a page) draws each character a screen would hide as its escape, so nothing the user confirms hides from them; `--json`, MCP and the SDK keep the text as written. Use Unicode symbols (≥, ×, Δ, σ, ∼) instead of LaTeX. Spell out a name that a paper defines as a macro. Leave out citation keys, and table or figure references the record cannot follow. A sentence taken from a paper keeps its words and loses its markup. Do not name the agent or tool that did the work in the text: rb records who made each write. What rb keeps exactly as given stays as given: the user's words (`--words`), a quote of a file (`--quote`) and a command as given (`--command`). In an agent session, a write whose new text holds LaTeX gets one line after it that names the field and says what to write instead: in `--json` and over MCP it is among `data.warnings`, and from Python it is a `rabbit_brain.RBWarning`. rb never refuses or changes the text for it, and a human's writes get no such line. Text that says who wrote it, or how, gets a line the same way: the writer's own runtime or model id, or the words `agent`, `session`, `memory` or `UNSUPPORTED` where they speak of the record's own writing ("the recording agent", "from session memory", "the agent inferred"), or the runtime's bare name where it says who wrote ("written by Claude"), never a research name such as GPT-2, Claude-3 or an RL agent. rb keeps the text, and `rb status` and `rb context` count it (`writer_words`), naming where the words are and never the ids (`texts` in `--json` holds them): the record already keeps who wrote; say what the text rests on, a file and line, or nothing. Text that names the user or the chat as its source ("the researcher's rule", "left to the researcher", "stated in this chat", "the account given in the session") gets a line the same way, and `rb status` counts it as `attributed_words`, naming where the words are and no writer. Both items count only text in force: a note or record you retract, or a reason a later write of its kind replaced, no longer counts, and a reason only the log keeps asks for nothing; a file's words in quotation marks, with the file beside them (`README.md:3 reads "..."`), never count; quotation marks around anything else do not hide it. Words recalled from the user go in `--words`, which rb shows as words (as given). Nor does text a write copies: settings taken with `--like`, the notes `rb analysis add --from` reads from the analysis's output file, or the title `rb reconcile --apply` reads from the README. - **Calls on results and on lines of work are the user's**: accept, reject and investigate on a criterion, an experiment, a question or a setting, and continue, park, stop, pivot and ship on an experiment or a question. When the user asks for one, record what they asked for, with their words verbatim (`--words`), and tell them it waits for one confirmation, with its command (`rb confirm `). When they have not asked, tell them what waits on them, with its command (`rb status` lists it under **Needs judgment**, below). A call made before the record is recorded as one (`--past`, under **Deciding**), never as made now. Do not set or unset `RB_ACTOR`. - `rb` reads every source it can, and it computes every finding each time it is read. No finding is stored. Nothing you write can mark a setting verified. **Never edit `.rb/` by hand.** `rb` compares each record's content with its last write; a comment or a reformat is not a change. A record changed outside `rb` (a value edited, a file deleted or moved, a file written by hand) is caught: a run in that state is not counted, an analysis or a decision in that state is not read at all (a hand-written decision is nobody's call), every finding that depends on the record lists it as an integrity problem (`edited_outside_rb`) and fails any `--fail-on`, and `rb` will not write over it (`E_STATE_EDITED`). `rb show ` of such a record says what changed (`comparisons.0.estimate: 0.023 -> 0.026`). For a field that says who made a write, how rb knew, through what, in which session or with which agent, or a branch, it says only that the field changed (`receipt.actor changed`), and so does an adoption rb refuses. `rb doctor` lists them; `rb doctor --restore` puts back what `rb` last wrote, and whoever wants the change kept runs `rb doctor --adopt --why "..."`, recorded as theirs. rb dates a write no earlier than the last write of each record it writes, so a wall clock stepped back never makes rb's own write read as edited outside rb. A log an earlier rb wrote while the clock stepped back, or one a merge or rebase put out of order, can hold a record's later write dated before an earlier one: rb says so for that record. `rb doctor --adopt` keeps it as it stands where that changes no one's authorship (it is still checked against the write dated last, which may be a later freeze or call), and `rb doctor --restore` puts back the write dated last, after which the later change is made again through rb. A file that is not Rabbit Brain YAML, or not a valid record, stops the commands that read the record (`rb status`, `rb show`, every gate) with `E_STATE_CORRUPT`, saying where it breaks; a write that reads or writes that file is refused the same way, leaving every file as it was, and any other write is made whole and succeeds. A run or an analysis recorded beside it (`rb run add` of numbers typed or read from a file, `rb analysis add`, the SDK's `record()` and `analysis()`, MCP's `run_add` and `analysis_add`) exits 0, and its reply names each record file it could not read, with doctor's fix (`data.unread_files`, each with `path`, `why` and `fix`, and among `data.warnings`; an RBWarning from Python): each its own reads stopped at, in place of the findings that read it, and each the record's index could not read, a question's or a resource's among them (a file that reads as YAML and is no valid record is named only where the reply reads it). So do `rb criterion add`, `rb criterion relate` and `rb freeze` beside an analysis's file, which none reads before it writes, `rb retract` of a note beside a criterion's file, which a note's retraction never reads, a run or an analysis already recorded (nothing written), and `rb reconcile --apply`, whether or not it records a run. A note changes no finding and is in no design or freeze: beside an experiment's note or a project note that does not read, every write leaves that note out, is made whole and succeeds, and names the file where it read the experiment or the project; only a write to that note itself (`rb retract `) is refused, naming the note's file and where it breaks. A row of a table or a database is read as a run beside a metric's or a criterion's file that does not read only where that file cannot change which of its columns are the run's numbers: where `--select` names them, each a metric the files that read declare, or where every column of the row that holds a number is one; otherwise the row is refused, naming the file and the columns rb cannot place, with nothing written. `--every-row` beside such a file reads one row as one row is read, and refuses a batch of several whole, since that file may declare the columns that tell the rows apart. Refused beside such a file, with every file as it was, since each reads it before it writes: beside an experiment's `experiment.yaml`, which declares its variants, every write to that experiment, its runs and analyses among them; beside a criterion's or a metric's file, `rb reconcile --apply`, which reads them to place what it found, a run of numbers that name no variant on an experiment that compares two or more, since rb reads the criteria and the catalogue to place them, and `rb criterion add`, `rb criterion file`, `rb criterion relate`, `rb spec set`, `rb spec vary`, `rb variant add`, `rb freeze`, `rb decide`, `rb confirm` and `rb retract` on that experiment (a note's retraction aside), with `rb metric add` beside a metric's, and, beside a criterion's, `rb experiment add`, `rb experiment gate` and `rb experiment file` in the user's own terminal, which read the designs there to say whose they are; beside an analysis's, `rb analysis add` on its experiment, and `rb decide`, `rb confirm` and `rb retract` that read its findings; beside a question's, `rb criterion add -q` and `rb criterion file` of that question, `rb question reword` and `rb question move` of it, and `rb decide` on it (`rb question add --parent` of it succeeds); beside a resource's, `rb resource expose` and `rb resource locate` of it; and any other write that reads the file before it writes. `rb doctor` still runs. A file or log line only a newer rb can read is `E_STATE_NEWER`, never corruption: the fix is an upgrade, and gates fail closed until then. A file with a field or a value rb does not know that is not what rb last wrote is said as edited by hand or written by a newer rb, and the fix offers `rb doctor --restore` beside the upgrade, where rb holds its last write; `rb doctor` says the same. So is a question call's snapshot, or a relation's declared scope, with a version or a key this rb does not know, and a declared relation of a kind, or with an orientation, this rb does not know: `rb status` lists it as `state_newer`, never as a call made on an earlier reading, a changed scope or a record that is not valid, reads the rest of the record, and rb writes nothing until the upgrade. The `state_newer` problem itself never makes a call read as made on an earlier reading, and neither does a criterion only a newer rb reads; a reading that has moved still does. An adoption can never change who wrote or decided something, or through what (`E_ADOPT_AUTHORITY`): a record's `via` beside its writer, and a run's or an analysis's receipt (its actor, `actor_via`, `via`, session and `agent`) are kept as rb wrote them, and a decision rb never wrote cannot be adopted at all. Nor can an adoption change the words of a note someone else wrote, and a gate an adoption changes is the adopter's, with no words, so a session's waits for `rb confirm` as one it sets does; with `--seen @` (repeatable) it runs only against the exact files that were reviewed (`E_STALE_REVIEW` otherwise). **Who you are to rb.** Every write records an actor. `rb` works it out in this order: 1. An agent runtime it detects: Claude Code by `CLAUDECODE=1`, `AI_AGENT`, Codex by `CODEX_SANDBOX` (and its other sandbox variables) or by its `CODEX_THREAD_ID` or `CODEX_SESSION_ID`, Cursor's agent by `CURSOR_AGENT=1`, a chat's id, entry point or process left behind with those unset (`CLAUDE_CODE_SESSION_ID`, `CLAUDE_CODE_ENTRYPOINT`, `CLAUDE_CODE_BRIDGE_SESSION_ID`, `CLAUDE_PID`, `CLAUDE_CODE_MESSAGING_SOCKET`, `CURSOR_CONVERSATION_ID` and the others `rb confirm` refuses on), or GitHub Actions. A Cursor terminal's `CURSOR_TRACE_ID` alone is no agent: a human's terminal has one too. Inside one, `RB_ACTOR=agent:` names the agent, and `RB_ACTOR=human:` is ignored (the log line keeps it as `ignored_rb_actor`), so the write remains attributed to an agent. 2. Otherwise `RB_ACTOR` (`human:` or `agent:`), when set. 3. Otherwise a human, named from git (`user.email`'s local part, else `user.name`), or from the login. Someone typing in an agent's terminal is recorded as the agent too: what rb cannot tell apart, it attributes to the session it can see. To record a call under their own identity, the user runs it in their own terminal. **Who made a write is in the data, never in the words.** rb's text and screens never say who made a write, a human or an agent: no name, session, channel (`via`), word of how rb knew, what rb keeps of an agent, or branch or worktree a write was made on, in what a command prints, the briefing, a sentence rb builds (a finding's `says`, an open item's `what`, a question's reading), or any screen: the page `rb page`, `rb export` and `rb sync` write, `rb open` (its `/state.json` and `/card.json` too, where each field that keeps who wrote holds `""`, whatever the record keeps there, so no null marks one writer's) and `rb ui`. A screen uses no word that names a role either: what is not the user's call reads as still open in the record, and a question's request in words as its Next step. `rb ui` counts what was written since the user last looked, whoever wrote it (`Since you last looked: 3 writes, 2 runs recorded · last 2026-10-10`), and nothing before their first look. Only `rb open`'s data holds who the user is recorded as (`you`), since no agent session opens it. A call reads as made, when and why (`accepted 2026-10-10: within budget`, `investigating since 2026-10-10: one more seed`). `rb log --json`, `rb show --json`, MCP and the SDK carry `actor`, `actor_via`, `via`, `session` and `agent` (and on records `created_by`, `set_by`, `by` and `declared_by`; a command's own writer is `data.actor`): look there when you need to know whether a write was a human's. A command's text says only that an `RB_ACTOR` was ignored in an agent session, and `rb doctor` says how this shell's writes are recorded. Four checks still look at who wrote, and name no one: the freeze review says each rule an agent declared is locked as the test now; a primary or gate criterion an agent retracted waits for a call (`retracted_by_agent`); a call recorded in an agent session waits for the user's confirmation (`call_proposed`, `past_call_unconfirmed`), with the words given there said as given, never as the user's; and an experiment's design a session wrote any part of reads `design not confirmed` until the user confirms it (under **Experiments and variants**). One label names a place: rb names a git worktree or a branch by its branch and path, verbatim as git gives them, only in the labels it computes for a place (the lines of **Elsewhere** on `rb status` and its notes under a finding that counts a run recorded in another worktree, the line a nested worktree's `rb status` and `rb context` say once, and, in a worktree with no record, `E_NO_PROJECT`, `rb init`'s `E_PROJECT_EXISTS` and what `rb reconcile` and `rb compile` say of the project's record; `rb remove`'s line for another checkout that holds a record, with the command to run there; on `rb open` and `rb ui`, the place they show, the places and versions they list, where a run was recorded and the notes on a finding that counts it, each in short: its branch, then its path from the project's main checkout when the place is inside it (from `./` where that path starts with `~`), from `~` under the home folder, else whole, with the whole path on hover on `rb open` and in `rb ui`'s `w` list, two places never reading the same, and every command keeping the whole path; a single checkout, live, is named there by its branch, else its folder's name, with its whole path on hover on `rb open` and in `rb ui`'s `w` list), never on a page (`rb page`, `rb export`, `rb sync`), and with no word of rb's own that ties a place to a writer. On a page, a file inside a linked worktree nested in the checkout (a run's file, a database row, a file it takes to run an experiment again, a resource's local file, the document a past call is given from) is named by its path inside that worktree, then `(in another worktree)`. On `rb open` and `rb ui` it is named by its path inside that worktree, then the worktree in short (`(in exp-b · .worktrees/b)`), said once on a run's card where the run was recorded there; the run's card keeps the whole path (`Paths as recorded`, and `inside` and `in_place` beside `path` in the data), and so does a resource's card, and its row on `rb open` on hover (`where` beside `where_short`). A file it takes to run an experiment again, named by a run recorded in a worktree with a record of its own, is named as recorded with that worktree beside it the same way. The record, `--json` and rb's text keep the path as it is. **Which session, not just which agent.** When the runtime exports a chat-session identifier, rb records it beside the actor on every log line, run receipt and decision of an agent's write. The session id is in the data (`session`), and on no screen and no line of rb's text. rb reads, in order, `RB_SESSION` (set it yourself when your runtime exports nothing), Claude Code's `CLAUDE_CODE_SESSION_ID`, Codex's session variables, Cursor's (`CURSOR_CONVERSATION_ID` is its agent's chat), Grok's, and GitHub Actions' `GITHUB_RUN_ID`. A human's write records a session only when `RB_SESSION` names one: the trace id an editor's terminal sets for whoever types in it is no chat session. Nothing set means nothing recorded; rb never invents one. **Which agent, and on which model.** An agent's write also records `agent`: on its log line and, for a run or an analysis, on its receipt. It holds these keys: - `name`: the runtime's, as its actor names it (`claude-code`, `codex`, `cursor`). Over MCP and the SDK it is the client's or the SDK's name. - `version`: from `AI_AGENT` (`claude-code_2-1-295_agent` gives `2.1.295`) or from the MCP client's `clientInfo.version`, as far as it is a version. It is absent when neither gives one. - `provider`, with `provider_from`: `said` when you gave it with `rb context --provider` or the context tool's `provider`, and `env` when `RB_PROVIDER` gives it for one command. Otherwise rb infers it from its table (`inferred`): `anthropic` for Claude Code and `openai` for Codex. It infers none for any other runtime, nor for Claude Code while it runs its models through another provider's API (`CLAUDE_CODE_USE_BEDROCK`, `CLAUDE_CODE_USE_VERTEX` or another of its `CLAUDE_CODE_USE_` provider switches is on). - `model`, with `model_from`: only what you said. rb never guesses a model. Say your model once with `rb context --model `, or the context tool's `model` (`said`). rb keeps it for your session's later writes in a file of its own, `.rb/cache/sessions/said/.json`, which an older rb in the same session leaves alone. `RB_MODEL` gives it for one command and wins for that command (`env`). Say it again when you change models. With no session id rb has nothing to keep a model under, so it keeps none, and `RB_MODEL` still works for one command. A model never carries over to another session. Over MCP, what the context tool was told also holds for the rest of the connection. rb keeps a model or provider id as you give it, but for the spaces around it, up to 200 characters; it refuses one with a control character or a line break (`E_USAGE`). A line of rb's own goes into the log when a session's agent facts are first known, and whenever they change (a model newly said). `RB_MODEL` and `RB_PROVIDER` speak for their one command only, so they change nothing here. The line has `op` `session`, `kind` `session`, `id` and `session` the session's id, and its `agent`, with no hash and no file. Each writer in a session has its own line for each channel it writes through (an agent, and a script it runs with the SDK under another name). The line records no write: `rb log` lists writes in its text and every line in `--json`. In a human's terminal `rb context --model` records nothing and says so, and a human's write carries no `agent`. ### What the repository already says: `rb reconcile` Coding agents already leave research in a repository: a results folder per experiment with a README, a manifest freezing the hashes of the code that made it, analysis and audit files, large arrays kept out of git on purpose, audit scripts and tests, a reproduction guide with its commands, a commit. rb does not ask anyone to write that again. `rb reconcile` compiles it against the record and says what it amounts to, experiment by experiment: - **Where results are.** rb looks in `out/`, `outputs/`, `results/`, `eval-out/` and `metrics/` at the top, whether or not git ignores them, and in every folder named for results (`out`, `outs`, `output`, `outputs`, `result`, `results`, `eval`, `evals`, `evaluation`, `evaluations`, `eval-out`, `eval_out`, `metrics`, `report`, `reports`, `artifact`, `artifacts`, `benchmark`, `benchmarks` or `multirun`, in any case) down to four levels from the top (`reports/stage1/eval/`, `artifacts/`) that holds a report or a structured results file within four levels of it. The search goes into no dot-folder, no link, no dependency folder (`node_modules/`, `site-packages/` and the like), no virtualenv or conda environment, no Python package (a folder with `__init__.py`), no build folder at the top (`build/`, `dist/`, `target/`), no folder of code, tests, configs or a website's static files (`src/`, `lib/`, `test/`, `tests/`, `config/`, `configs/`, `conf/`, `scripts/`, `_static/`, `static/`, `public/`), no repository or worktree of its own, and below no folder that holds more than 300 folders (a dataset's class folders); it looks for folders named for results below a folder git ignores too, since those are read even so, as `results/` is; it opens at most 5000 folders and says when it stopped there. A folder is named by its own spelling, so on a file system that ignores case (Windows, macOS) a `Results/` is named once, as itself, never also as `results/`. A folder it passes that holds .json, .jsonl, .csv or .tsv files of its own (outside what git ignores) is not read, and `rb reconcile` names it with why, unless a recorded run read a file in it, which reads it (`data/imagenet_val/ (1 .json, .jsonl, .csv or .tsv file): its name does not say it holds results`, or `imagenet/ (320 folders): it holds too many folders to look in`) and the `rb compile ` that reads it (`not_read` in `--json`, each with `folder`, `files`, `folders` and `why`). Every read says where rb looks, in the same words: folders named for results (results/, eval/, metrics/, reports/, artifacts/ and the like), down to 4 levels from the top. A results file that sits in such a folder and in no results folder (`results/ledger.jsonl`, beside no folder per run) is counted in the headline (`2 results files in results/`, never `0 results folders` while one is there) and listed: a CSV, TSV or JSON Lines table with its rows, the columns that hold numbers and the `rb run add ... --where` that reads a row; a JSON, YAML or TOML file the journal's own lines do not already name, with the `rb compile` that lists its numbers; a SQLite database, with the `rb run add ... --table --where` that reads a row; a file in a form rb does not read (Parquet, Feather, Arrow, NumPy, pickle, HDF5, Excel, a notebook), said as such; and, where nothing else is found, every other file, a folder below included (a document, which rb quotes only as a results folder's report, a log, a plot), so a folder where results land is never said to be empty (`loose_files` in `--json`, each with `path`, `kind`, `rows`, `numbers`, `why` and how many of its rows are `recorded` as runs). A file a recorded run read whole stays listed, said as read by a recorded run, and so does a table whose rows are runs, with how many (`93 of 93 rows recorded as runs`): once its runs are in, a results file is never left unsaid, and the headline never turns to `0 results folders`. The top folder's own files whose names say they hold results are listed the same way: a table (`results.tsv`, `results.csv`, `results.jsonl`, any whose name holds `result`, or one named `metrics`, `scores`, `runs`, `ledger` and the like), with the line that reads a row, and a report in prose (`RESULTS.md`, `results.md`, `report.md`, `summary.md`, `findings.md`, `comparison.md` and the like), said as prose: rb reads no run from it, and a number it states is checked by a cited rule, with the `rb criterion add ... --source --quote "..."` that declares one. `Nothing to compile` is never said while such a file is there. A CSV or TSV table whose first line holds a number with a decimal point reads as a data row, not a header (a whole number may name a column, a top-k's k): rb says the table seems to have no header line, counts every row, that line among them, and names no column (nor offers an alias for one) from it. - **Experiments.** Each folder under one of those (where rb looks, as every read says) that holds a report (`README.md`, `report.md`, `results.md`, `summary.md`) or a structured results file is a results folder: the first folder down each path, to four levels below, that holds one, so folders that only group others are passed through; a folder named for results below the top that holds them itself is one. It is read by the experiment indexed on it, by one declared with the folder's name as its id, or by each experiment whose recorded runs named a file in it (`--from`, `--config`, `--artifact`, the SDK's `config`): out/seed1/ is read by the experiment whose run was recorded from out/seed1/metrics.json. A folder no experiment reads is a new experiment, named after the folder, only when it holds a report of its own. One of results files alone is a run's output, never an experiment of its own: rb lists it as unplaced and never guesses, quoting the prose it holds (a Markdown file whose name is no report's, such as `pilot_table.md`) as it quotes a report's, at the first look as once a run reads the folder, and a run made from it is recorded with `rb run add --from `. - **Run folders.** A results folder that holds a run's own config (`config.yaml`, `config.json`, `.hydra/config.yaml`, `hparams.yaml`, `params.yaml`, `args.json`, `run.json`, `meta.json` and the like) that says something of its run, beside a results file, is a folder of one run, the common layout. For each, `rb reconcile` says what that config says of its run, as the file's words with their lines, never as what rb saw: the command, the commit, whether the code had uncommitted changes (`dirty`) and when it started; then the settings that differ between such folders side by side (`eval.crop: center (r01, r02), resize (r03)`; a value that names its own folder, an output path, is left out); and the `rb run add` that records each with all the folder gives: the experiment that reads the folder (else the one experiment whose variant the config names), `--from` its results file, `--variant` where the config's value of what the experiment varies names one variant (its own value of that setting, or, where it has none, its name: `prompt: plain` names `plain`), `--keys` with the numbers of the file the experiment's criteria and the catalogue name, each under its own name (a nested `eval.top1` is recorded as `eval.top1`, so it is no key the line gives), so a setting the file also holds is no metric, `--config` its config, `--set` for each per-run setting the config gives, `--commit`, `--produced-at` (when its results file says the run finished, else when the config says it started), and `--command` with `--command-source :` (`run_folders` and `run_folder_differences` in `--json`, each folder with its `line`); folders are compared across groups, each named from the folder they all sit in (`adamw/0`, `sgd/0`). A command the config gives as a list of words (an argv) is shown joined and never offered as `--command`, since joining loses its quoting, and `--command-source` is offered only where the config's line holds the command as `rb run add` checks it. A commit `git describe --dirty` wrote (`9ba0f86-dirty`) says both. A folder whose results file a recorded run already read says which (`recorded as `, `recorded_as` in `--json`) in place of the line. A config that says `dirty: true` is said beside it: `results/r03/config.yaml:4 says dirty: true: the code had uncommitted changes when it ran`. A run recorded this way was made before it was recorded, as every run reconcile reads is. What the folder's git line says is the folder's own state now (`folder unchanged since the last commit`), never whether the run's code was. - **Evidence and code, where they are.** The experiment points at its files by path and sha256, each with its role (report, results, manifest, audit, data, protocol, reproduction guide, code, check) and what git does with it (tracked, ignored, untracked). Data files, and anything over 64 MB, are counted by role and size rather than hashed. Nothing is copied. - **What the files say, word for word.** The report's title and opening lines, and the sections its own headings call results, conclusions or findings, limitations or caveats, and next steps or open questions, are quoted verbatim with file and line. So are the commands of the reproduction guide it links (a file named for reproduction, or one with a heading about reproducing, regenerating or a fresh clone), except commands about another results folder. rb chooses spans by the files' own headings and blocks and words nothing itself: prose is quoted, never summarised, and what no rule reaches is left out. - **What the repository claims, checked.** A manifest (a JSON file mapping paths to sha256 digests, under `sources`, `files`, `hashes` or the like) is checked against the files as they are now. An audit file (a structured file with `audit`, `check` or `verif` in its name) is read for what it says of itself: a top-level `passed`, `ok` or `success`, or a `status` of pass or fail. - **The repository's own checks.** Scripts named `audit_*`, `test_*`, `check_*`, `verify_*` or `*_test.py` are registered with the command the repository gives for them, verbatim with its file and line. A check is a folder's when the folder's report names it or its command names the folder; one the guides run on no folder in particular is the project's. Its outcome is the last run rb watched whose command names it (`rb journal run -- `), read every time, never stored. - **Ready to checkpoint.** Whether each folder holds anything untracked or changed since the last commit. `rb reconcile` prints all of it and writes nothing. `rb reconcile --apply` keeps it in the record, as whoever runs it: an experiment for each new folder with a report, each changed folder indexed again, the project's checks, and the results it places recorded as runs (below). It logs one line saying what changed in the research, and the paths of the changed files nothing in the record cites, which `rb status` prints: `Last reconcile, 2026-09-30 22:17: +1 experiment (warmup_ablation) · 1 re-indexed (warmup_cost) · 2 results quoted · 1 next step quoted · checks: 2 passed · 2 manifests, 1 source changed since frozen`. Counts, ids and paths, never a judgement; an apply that changes nothing writes nothing. A result it placed and could not record is listed under `Not recorded`, apart from the runs it recorded (`not_recorded` in `--json`); one whose run needs a per-run value nothing gives is never placed or tried: it is listed as unplaced, with the `rb run add` that records it ending `--set =`. Run it after a stretch of work: what an agent forgot to record, the repository still says. It needs no record to start: in a repository with no `.rb/`, `rb reconcile` reads it all the same, writing nothing, and `rb reconcile --apply` starts the record at the repository's root, titled as its README titles itself (the folder's name when it has none), then keeps it there. From then on every read looks at the repository as it stands (`rb show `, `rb status`, `rb context`, and the user's screens): files changed or missing since they were indexed, each by its name, quotes no longer in their file, and results folders no experiment reads yet (`reindex`: run `rb reconcile --apply` again; `unindexed_folder`: `rb reconcile` lists them, and for folders last written before the record began, `rb docs --start` says how to record that work, with `folders` in `--json` listing every one); a manifest whose sources changed since it was frozen (`manifest_changed`, and the `stale` gate); an audit file that says it failed, or a check whose last watched run failed (`audit_failed`, `check_failed`, and the `checks` gate). A decision on an indexed experiment is made on what its repository said, its result and next-step quotes and its audits' verdicts, and reads as made on an earlier reading when that changes (`decision_outdated`). A call that moves a line on (accept, continue or ship) while the experiment's own audit or check says failed is shown to the user (`call_on_failed_check`); rb refuses nothing. The next step rb suggests never adds a record nobody declared: an item's command and the Next line of `rb status`, `rb context`, `rb tour` and `rb reconcile` name `rb reconcile --apply` only while it would add no experiment; while it would, they name `rb reconcile`, which lists what it would add. What no file says is the agent's to record when it happens: the question, a hypothesis weakened (a note, verbatim, pointing at the file and line that weakened it), the next experiment with the decision it gates (`--if-holds`, `--if-not`), a call the user asks for, with their words, which waits for their confirmation. `rb reconcile` is the net under that, not a substitute for it. ### Starting the record in a project already under way: `rb docs --start` While the record holds no question and the repository has history (commits of more than what rb installs, `.rb/`, its skill, `.mcp.json`, `AGENTS.md`, its Cursor rule and the agents' hook files, and a new repository's top-level README, license and ignore files; results folders; or a database), `rb status` and `rb context` carry one open item, `no_question`, which points at `rb docs --start`; it goes once a question is recorded. The results folders last written before the record began, which no experiment reads, point at it too until then; once a question is recorded they are older work that can be backfilled later, which `rb reconcile` lists (`later` on the item) and no next step names. While the record holds no experiment either, `rb context` opens with what to do now: record where the work stands, from the files, in that turn and without asking the researcher anything, then `rb docs --start`. `E_NO_PROJECT`, and `rb compile` and `rb reconcile` with no record, point at it too. `rb docs --start` (the MCP resource `rb://start`) prints the start protocol, under 150 lines: start now, in that turn, and ask the researcher nothing up front; memory says where to look and the files decide; the scope is the current frontier (the question actively worked on, with the experiments, runs, findings and open calls around it; older work waits and can be backfilled later); infer it from the session's context and the files, or the files alone, never assert it (uncommitted plans and scripts first, then the latest activity in the store, then recent commits); with no context, the question is the files' own claim turned into a question, with every factor it names and no premise no file states, its line in a note on the experiment after the first `rb status`, and the runs at the frontier's setting that every factor of that question needs are recorded, so a rule reads each factor, and where a file states the comparison for a factor the question names, that factor gets a rule; check each claim from memory the frontier rests on, an earlier stage's included, by recording the rule rb reads over the runs it names (an ordering in every cell is one `--at-most 0 --each` rule per pair and metric, the researcher's words memory quotes in `--words` and memory's own claim in the statement, a file's line by `--source --quote ""`, never in `--why`; a claim's number is its rule, with no threshold of the session's own: `--equals` the number, `--tolerance` half its last digit; a bound ("x or less") is `--at-most x`, a spread ("differ by x or less") `--equals 0 --tolerance x`; the rule's statement words the claim it checks, never the number as a fact; a claim check takes the default role or secondary, never exploratory, so a miss waits under Needs judgment), or a note that names the file and rows where no rule reads it, never by the session's own query alone, and now, before the first `rb status`: backfill is for older work the frontier does not rest on, never for a claim it rests on; record the frontier first: its question, its experiments, its rules each linked to the question its comparison answers (no narrower question is made to hold a comparison), and only the runs they read, with the frontier's own rows; comparisons that share an arm share one experiment, so a row is read once; read runs from their stores, with per-run values and settings from the row (`@`, `@.`, `--source #
[=].`); check a batch with `--dry-run`; set the shared settings before the runs, since one set after reads them as under an earlier spec, each from a row, a config or a file at a commit before the run, since a plan's lines set only the runs it will launch, and a setting of a past run is set only from that run's row, its config or a file at a commit before the run, else left out with a note, and a summary table written after the runs (a sweep's metadata table) is not a pre-run source for a run's setting; the rules the frontier rests on are in before the first `rb status`; run `rb status` once the frontier's runs and rules are in, before the notes; criteria only from what is written (a document, or a plan the frontier rests on), each statement saying in plain words what its rule checks with the words it rests on in its quote or `--words`, a cited figure's tolerance from the rounding of the figures it is computed from, read in the table it sits under, and a threshold no document states stays unknown; a file's claim is checked only where the frontier rests on it, by the file's own comparison, never a stronger one (a statement about means is `--at-least 0` per pair, `--at-most 0` where lower is better, read on the estimate, not `--better`, which needs the interval clear of 0; its line is `--source --quote ""`, which rb checks at that line); a direction rule reads the runs already made too, so it is declared only where a document, or a plan the frontier rests on (an uncommitted script, a next step stated in the chat), states the direction for the conditions the rule reads, in its words, and never with a sentence about other conditions or another stage as its statement (one that names none is about the runs its figures come from); with no file stating the direction of the frontier's own comparison and no next step stated for when it holds, it takes no rule, and the report says rb does not compute it yet; a next step the researcher stated for when the comparison holds, earlier in the chat or in a file, is the gate of the experiment that holds the comparison (`rb experiment gate --if-holds "" --words ""`: `--if-holds` is what they would do, and the whole sentence goes in `--words`, a file's as `--why ":"` with no `--words`; nothing they did not say, no next step no one stated, and no gate from a plan's step with no such condition), and its words give the direction of the comparison the gate rests on: a direction rule on it (`--better`, no threshold, those words, as the gate takes them), so rb computes it; no call from memory; a dated decision a committed file states is recorded as a past call from that file on the experiment it concerns, and waits for the one confirmation; a dated narrowing (an arm or a range dropped while the rest went on) is `continue` on the experiment that holds the dropped arms, its quote saying what was dropped, never a park of arms that kept running; `--command` only for a launch someone saw or a log records; what no file states goes in a note that names the files read ("no file I read records this: "), a difference between memory and a file in a note that names no one and says no memory (no "as recalled", "as believed" or "remembered"), once memory's number is checked with its own `--equals` rule, since a file's rounding is not a difference, and an interpretation in a note or `--when-met` with what it rests on, which rb shows as not confirmed; a session's text calls nothing the researcher's and names no chat or session as its source; `--words` is only the researcher's words memory quotes, and memory's own claims stay in the statement; and a `--why`, where a file is the reason, cites the file and its line, a rule or gate from a next step stated in the chat takes its words in `--words` and no `--why`, and a `--why` never restates a claim as fact. It ends with the report to the researcher: what it took as the current frontier, by its question id, and why (the commits, files and runs it rests on) and the scope used, with how to change either; the calls and experiment designs (a gate waits with its design) that wait for the researcher's authority (`rb confirm` lists each with the command that confirms it, and what `rb status` lists under Needs judgment), and nothing no command confirms; as findings, only what `rb status` computes, and plainly what it does not compute yet; the researcher's words memory quotes "as you said earlier" (the gate on ``, as you said earlier: ""), memory's own claims with no speaker, a file's words with `:`; never "your words" in any form (not "your recalled words"), and never "your rule", "in your words from earlier" or "your call" for what rb does not list as the researcher's call; a miss in rb's words ("not reproduced", "not met"), never "contradicts"; when a write said rb put the code in git (`refs/rb/snapshots/`), where it is and the command that removes it; and, when `rb init` installed nothing, the commands it named for later sessions. The report ends with no question at all: what is undecided stays an open item in the record, which the report points at, and thresholds, margins, housekeeping and past calls from memory are never asked. ### When work happened and nobody told rb: the journal `rb` assumes the agent may forget to record an experiment, and recovers. rb looks at the repository when `rb status` or `rb context` runs, when `rb ui` opens and every 10 seconds while `rb open` or `rb ui` is open, on `rb journal scan` and `rb reconcile`, as an agent's session starts and as each of its turns ends (the session hook `rb init` installs, under **Install**), and before and after a command run under `rb journal run`; never where no record is yet. `rb init` (and the SDK's `init`) looks once, as the record begins, after writing what it writes: what is there then is the work the record began after, and a results file rb sees land later is what it proposes as a run. A look walks where results land (`out/`, `outputs/`, `results/`, `eval-out/`, `metrics/`, every folder below the top named for results that holds any, as **What the repository already says** has it, the project root's loose files, and every directory a run has named before, but none in a git worktree nested in the checkout, whose files are that checkout's), at most 4000 files (a file past that cap that an earlier look saw is kept as it saw it, never said gone), and asks git where HEAD is, which paths each move of HEAD changed (`git diff --name-status` from the commit seen before), which tracked files the working tree changes, and which .json, .yaml and .toml files are new and untracked. It journals facts: files that appeared or changed, git moves with those paths, a .json, .yaml or .toml file git lists whose working copy changed between two looks, and commands wrapped in `rb journal run -- `. Each entry's `by` (in `--json`) names who looked, ran the command or left an item out, never who wrote a file. rb never sees a command run outside `rb journal run` (only the files it wrote where rb looks), a file outside those places that git does not list as changed or new (one git ignores included), what code, configs, docs and database rows say when nothing in the record cites them, a change made and undone between two of its looks, or anything said only in a chat. The record and what rb found cover what rb looked at, not everything that happened, and every read says so in one quiet line: `rb status`, `rb context`, `rb open`, `rb ui` and `rb page` say where rb looks (the usual folders at the top, then folders named for results in the words `rb reconcile` says them, then the folders this repository holds beyond those), when it last looked, whether that look stopped at its cap, and what it reads only where the record points to it (`scope` in `--json`: `looked_at`, `cap_hit`, `folders`, `git_changes`, `not_watched`, `journal_aside`). The journal is a local buffer in the clone's shared git directory, by worktree (`.git/rb/journal//`; `.rb/journal/` outside git), so a worktree thrown away leaves what its commands printed for the other checkouts to find: observation, never the record, and nothing in it counts toward a finding. Where git's folder cannot be written (an agent's sandbox can keep it read-only while the project stays writable), the checkout's journal is kept in its own `.rb/journal/`, which `.rb/.gitignore` keeps out of git, and the line that says where rb looks says so (`journal_aside`); nothing fails for it, and `rb journal run` keeps what the command printed either way. The first look that can write git's folder moves it there, each entry once and in order, a linked worktree's into its own folder. A linked worktree's journal names its path and git's entry for it (`place.json`), so a later worktree git gives the same name starts a journal of its own, and the earlier one's reads as a worktree removed since; a worktree moved with `git worktree move`, or in a project folder moved and then `git worktree repair`, keeps its journal. `rb reconcile` also matches the journal against what the project declares: metric names and aliases (a criterion's `--metric` declares a name by use), experiments and their criteria. When exactly one experiment reads the numbers a new file reports, it prints the `rb run add` line that records it. A results folder's numbers go only to an experiment that reads that folder (the one indexed on it, or one whose recorded runs named a file in it), and only when its criteria read them: never to another experiment on a metric name, and where two that read it both would, the file is listed as unplaced. A new file in out/seed1/, whose run was recorded from out/seed1/metrics.json, is proposed as `rb run add --from `, never as an experiment. A file there that no watched command wrote is proposed only when rb saw it land after its first look here: one already there at that look, one a checkout, pull or merge brought with bytes rb had not seen (a commit made in this checkout brings nothing: its files were written here), one whose bytes git already kept and rb had not seen (a stash popped, a file restored or checked out from a commit), one moved from a file there at that look, or one last written before that look (moved or copied in with its time) is listed with the line that records it and never proposed, since rb cannot tell it from the work the record began after. Bytes rb saw keep the look that first saw them, wherever they come back from (rb keeps the last 4000 it saw go): a result rb saw land, stashed or left on its branch for a look and then popped or checked out again, is still proposed, and a file there at rb's first look still is not. A file that holds the same bytes as a file rb saw first, which still holds them, is a copy: listed, and never proposed, even where a watched command wrote it. A file a command run under `rb journal run` wrote, in a results folder nothing reads yet, is proposed the way what that command printed is, and so is a results file there that rb saw land after its first look here, whose numbers exactly one experiment's criteria read; one there at that look is work the start protocol records, never proposed. The line gives each per-run value the experiment declares where the run says it: in its own config beside the file (one the command that wrote the file wrote too, one a recorded run read with the values it states, or one rb saw land as it did the file; a config that states no setting the spec declares, such as a model's own config.json, is no run's), which the line passes with `--config` so rb checks it against the spec, or else on the line of the watched command that wrote it (`--seed 4`, `--seed=4`, `seed=4`, or under a nested key of the config), when that command wrote no other results file. Where neither says it, or the config and the line give it apart, the line is listed as unplaced with both, ending `--set =` for whoever records it, and `rb reconcile --apply` leaves it. Where the file holds one arm's numbers with no variant named (`{"top1": 75.4}`) and the run's config beside it, or the line of the watched command that wrote it and no other results file, states one variant's own settings and no other's, the line names that variant (`--variant int8`). A file a watched command wrote is its own even where git already kept its bytes (a rerun that makes a committed result bit for bit), unless the command runs git. A run's config is read with its run, never as found, and a file of settings a spec declares is said as settings, not results. Anything ambiguous is listed as unplaced, never guessed, and a file of only progress numbers (step, epoch, iteration) is listed as a heartbeat, never proposed as a run. The folders of results files alone that no recorded run named are one line that counts them and names every place they are, a folder alone by its path and folders side by side by the folder they sit in (`artifacts/int8_sweep/ (5 folders)`), up to 100 places, and each list in its text keeps its first 100, however many there are: `--json` lists every one. The MCP `reconcile` and `status` tools keep the first 100 of each long list too and say what they left out in `data.cut`, unless called with `all`. When it finds nothing, it says where it looked. Run the printed command to record it, as yourself, or `rb reconcile --apply`, which records each one it placed with the same `rb run add`. Each runs where rb found the result, since its paths and code are that checkout's: the record's checkout for a file, and the worktree nested in it that a watched command ran in for what that command printed. Printed in another directory, the line starts with `cd && ` (`run_in` in `--json` and the SDK), and `rb reconcile --apply` and `rb ui`'s `y` run it there. The user's screens show the same found items (`rb ui` and `rb page` under **Your call**, `rb open` under **Found in the repository**) as facts, not requests, each named by its file, or by the command as given (its start and end, where it is long) for what a command printed; two that would read the same say the path, or the first numbers printed: on `rb ui`, `y` records one with the same `rb run add`, as the user, and `n` leaves it out on that machine, with an optional reason. A session that leaves one out (the SDK's `dismiss`) leaves it out of its own briefings (`rb context`, `rb_delta`) and its own `rb reconcile` alone: `rb status` and the user's screens count it until the user leaves it out or it is recorded, and no screen says who left one out. Only a word given in the user's own name leaves an item out of their screens, and `rb journal` lists only those (`--json` keeps every one); a `dismiss` rb cannot tell the writer of (an `RB_ACTOR` it cannot read) is refused, as any write is. `rb status` says when anything is found and unrecorded, counting apart the runs whose line needs a per-run value (`found_needs` in `--json`, each with its file or command, experiment and values), the files rb cannot tell from the work before the record and those written while a watched command ran, `rb journal` prints what rb observed (an entry it cannot read is passed over and counted; `--json` gives every entry as it is), and `rb journal scan` looks once without the screen. Prefer wrapping evaluation commands, `rb journal run -- python eval.py ...`: the command, its exit code and what it wrote go on record even when nothing else is written down (what it wrote is what changed where rb looks while it ran: a file the look after it saw new or changed where the look before it reached everything, and past the 4000-file cap a file the look before it checked and the look after it saw change, or one last written after it started; another process's writes in that time are on the list too, so a file nothing the command was given or printed names, whose folder's name is not a value its line gives under that name (out/seed2102/ for `--seeds 2102 2103`), that is not in the one folder its files of numbers are in (whatever the project declares) and not beside one, is listed as `unnamed` and said as written while the command ran, never proposed as its run; of a list an earlier rb kept, which could name files the command never wrote, only a file last written while the command ran is its), and an observed exit code outranks anyone's summary of the run. The command's output passes through as it comes (with `--json`, on standard error, so standard output carries the envelope alone), and rb reads one thing from it: a last line `{"metrics": {...}}`, whose numbers it keeps with the command, so `rb reconcile` proposes the run that records them (`rb run add = ... --command "..." --produced-at `) the way it proposes a file, once the record declares those names (a metric, or a criterion's `--metric`) and an experiment reads them. `rb journal run` says after the numbers what `rb reconcile` does with them: the run it proposes, or why it proposes none (nothing declares them, no experiment does, a recorded run holds them, the run needs a per-run value, or the command failed). A command that fails keeps its last ten lines of output, and a failing check shows them. The command's code is kept at its start: the commit, and the exact tree when it had uncommitted changes (below, under the receipt). **Changed, and nothing in the record cites it.** A .json, .yaml or .toml file outside the results folders rb reads that differs from what it was where the count starts, and that no setting's source, run, analysis, criterion, call, resource (a folder it names covers what is in it), indexed folder or manifest of one cites, is listed by `rb reconcile` by path with the commit the count starts at: `run_metadata.json: changed since 1a2b3c4; nothing in the record cites it` (`new since` for a new file, `gone since` for one deleted, `, not committed` when none of the change is committed), a file with no numbers in it too. The count starts at the later of the record's first write and the last `rb reconcile --apply` in the user's own name that recorded by path all it saw, at the commit that apply saw, or else at the commit `rb init` saw while HEAD's history holds it (in a clone that got its code, the commit that put its record in place, its first after the code's) (HEAD at rb's first look here where the repository had no commit yet), else the newest commit at or before the record began. In a clone that does not hold the commit that apply saw (after a squash merge or a rewrite), the newest commit there at or before the apply stands in for it; where there is none, the count starts at HEAD, and `rb reconcile` says so (`uncited_from_head` in `--json`). rb asks git what differs from that commit; a file git does not track is compared with what rb first saw of it (one there on rb's first look here, last written before the count starts, is no change). `rb status`, `rb context` and the user's screens count them in one line with the command that lists them (`uncited_changes` in `--json`, each with its `path`, `since`, `committed`, `new`, `gone` and `key`). A file where results land that a look cannot read whole (empty, or cut mid-write) and that was written in the last minute is left out until a look reads it whole. Left out: rb's own files (and the page and export it writes beside `.rb/` by default), anything in a dot-folder or named with a dot, lockfiles, the CI, editor, lint, docs-site and test configs and coverage reports rb knows by name, a project's package and environment manifests (`pyproject.toml`, `package.json`, `Cargo.toml`, `environment.yml`), YAML in a folder named `ci`, what is in a vendored or dependency folder (`node_modules/`, `vendor/` and the like) or in a build folder at the top (`build/`, `dist/`, `target/`), the files of a results folder no experiment reads yet, whose own item says so, and a file in a folder where results land that rb already lists as found; a changed file an indexed folder lists says so on its folder's `reindex` item, by name. A file in the top folder that rb lists as found is counted in both lines, and `rb status` says how many (`1 also counted above`). rb says what changed and that nothing cites it, never whether it matters, and asks nothing. `rb reconcile --apply` keeps their paths and the start of their bytes' sha256 on its log line (`uncited_seen`, at most 1000 to a line), so they are recorded, by path, while their bytes are those; a later change shows again, and an apply that kept only some of them leaves the count's start where it was. An apply in a session keeps them on the record too, and leaves them out of that session's briefings and its `rb reconcile` alone: the user's screens count them, and `rb reconcile` lists them to the user, until an apply in the user's own name. The user leaves one out with the SDK's `dismiss` and the item's `key` (from `rb status --json` or `uncited_changes()`), and a session's word leaves it out of that session's briefings alone, as above; `rb ui` and `rb open` count them and list none, and `rb ui`'s observed lines leave out what is left out here, which `rb journal` still lists. ### Reading results that already exist: `rb compile` Research that predates the record leaves reports, write-ups, result files, databases and run scripts. `rb compile ` reads them against what the record knows and packages the rest for you. It inventories each source (a SQLite database, which rb tells by its first bytes and reads without writing, table by table: its columns, its row count and the columns that name a declared metric; a CSV, TSV or JSON Lines table, with its row count, the columns that hold numbers, the columns that name a declared metric and the `rb run add ... --where` that reads a row; a CSV or TSV table whose first line reads as a data row, said as reconcile says it, with every row counted and no column named from that line); it lists at most 200 sources, the first by path, and says how many it left out and how to read them (`sources_total` and `sources_cut` in `--json`); lists as candidates the structured files whose metric names only one experiment reads (never a lockfile or a tool's config, such as pyproject.toml or package.json, whatever numbers it holds), each with its `rb run add ... --from` line and when the file was written (`before e1 existed: likely not its run`), because a name is not provenance and a candidate is never recorded as that experiment's run on its name alone; and reconciles prose against the record's own vocabulary and numbers: terms that match known ids, metrics, aliases, variants and settings are named with their kind, and each number is grounded against stored values, derived values (means, changes against the baseline, percent changes) and the criteria's rules, with the line it sits on. What grounds nowhere is listed as unplaced, never guessed at. The command reads and never writes. With no question recorded, its next step is `rb question add`, not `rb status`: the standing comes once the frontier's question and rules are in. The rest is yours, under the contract it prints: propose structure through the ordinary commands; every value carries its source (`--source file#key`, or `--quote "..." --locator "..."` for prose, which rb reads and checks); statements travel verbatim, never reworded (their words as plain text with the markup dropped, while a `--quote` stays exact); a database's rows are read from the database itself, never from an export of it; a result that exists only in prose becomes a cited criterion that later runs reproduce or not, and a threshold a plan sets is a criterion in the plan's words with `--why ":"`, which rb checks states it; the datasets and checkpoints the runs read become resources; unknown stays unknown; everything you write is recorded as yours, and calls on results and on lines of work (`rb decide`) are the user's: record one when they ask, with their words verbatim in `--words`, and it waits for their confirmation; a call made before the record takes `--past`, and it waits for the same one confirmation whatever document it cites: `--source [:] --quote "..."` gives the document, and rb says beside the call what git shows of it. rb verifies every span on write, so a proposal the text does not state is refused, not recorded. ### The grammar: the same work, recorded the same way Two agents recording the same work should build the same record. The normalization is fixed: | What happened | How it is recorded | |---|---| | a rerun for variance | another run of the same experiment, its seed a per-run setting: `rb run add ... --set seed=` | | a changed hyperparameter, compared | a variant of the same experiment: `rb variant add --role candidate`, its value with `rb spec set . ...`, the setting declared in `--varies` (or `rb spec vary`) | | an ablation | a variant with `--role ablation` | | an invalid run, fixed and run again | a new run that replaces it: `rb run add ... --replaces --why "..."` | | a different hypothesis or approach | a new experiment that grew out of the old: `rb experiment add ...`, then `rb experiment relate follows_from ` | | a hypothesis weakened by evidence | a note on its experiment pointing at the file and line that weakens it | | a bugfix, a rename, an installed package | nothing: implementation chatter changes no research state | `bench/agent/SPEC.md` in the repository holds the agent contract: these rules and the rest of what an rb-compatible agent does, as cases with checks a benchmark runs against any agent. ### A session, start to finish ```sh rb init "INT8 ResNet-50" # creates .rb/ here, titled for the work's theme; commands then work from any subdirectory rb question add "Can we ship the INT8 model?" # → q67pn3k (each add prints the id: the kind's prefix and six random characters, or --id) rb question add "Does INT8 cost accuracy on the held-out set?" --parent q67pn3k --id q-acc rb experiment add "INT8 vs FP32 on the held-out set" --id int8 -q q-acc --hypothesis "INT8 keeps validation loss within 0.05 of FP32" -m "the calibration set matches the eval distribution" --baseline fp32 --candidate int8 --varies precision rb note add "the held-out set is not in the pretraining data" -e int8 rb metric add val_loss --unit nats --minimize --alias cross_entropy rb resource add holdout-v2 --type dataset --name "held-out images, v2" --subset "10000 images" --url https://data.example.org/holdout-v2 --at s3:s3://my-bucket/holdout-v2 rb spec set int8 --from configs/train.yaml --keys "optim.*,data.batch_size,eval.batch_size" # each key a setting, read by key path, verified rb spec set int8 fp32.precision fp32 --source configs/fp32.yaml#precision # a variant's own value rb spec set int8 int8.precision int8 --source configs/int8.yaml#precision rb spec set int8 seed --per-run # each run gives its own: --set seed=N when recording it rb criterion add "INT8 adds at most 0.05 validation loss" -e int8 -q q-acc --metric change.val_loss --at-most 0.05 --min-n 3 --noise 0.01 --when-met "INT8 stays within the accuracy budget on the held-out set" # -q: the question its finding bears on rb freeze int8 -m "before the held-out runs" # settings, variants and rules are now locked, recorded as whoever froze them rb journal run -- python eval.py --seed 1 # the command, its exit code and what it wrote go on record rb run add int8 --from out/seed1/metrics.json --set seed=1 --config out/seed1/.hydra/config.yaml --command "python eval.py --seed 1" --input holdout-v2:eval --host gpu=A100 rb status --fail-on unestablished # the CI gate: exit 1 when a primary criterion misses, is undetermined, untested or late, a finding has an integrity problem, or a question link or relation cannot be read rb show # its finding on its axes, every observation and whether it counts, the calls on it rb compare int8 # variants by metrics, with the change against the baseline rb reconcile --apply # after a stretch of work: what the repository's own files say, kept in the record rb context --model # the record as one briefing a fresh session reads first; an agent's model is kept with its writes rb ui # the user's screen, in their own terminal (bare `rb` there opens it too); w lists the places and versions it can show, y, n and c act only where it started, and escape goes back there; an agent session is refused, as when the screen is started with python -m rabbit_brain.tui; its surface is rb status rb confirm # the user, in their own terminal: a call recorded in an agent session becomes theirs, and @ an experiment's design with its gate, as listed (rb confirm alone lists what waits) ``` Declare the criteria and freeze before the runs write their outputs: evidence that already existed counts, and its finding says so on its timing (below). ### Questions `rb project title "" --why ""` retitles the work later, from any directory inside the project. The log keeps the old and new titles, who changed it and why; `--words` keeps the user's words verbatim when an agent acts on them. The project's id, uid, evidence and calls stay the same. A title already in place writes nothing. From Python, use `state.set_project_title("", why="", words="")`; over MCP, use `project_title` with `title`, `why` and optional `words`. A project's title names the work's theme (a paper's title, a model, a line of work), and the project holds as many top-level questions as the work has inquiries. `rb question add ""` adds a question; `--parent ` puts it under another, so questions form a tree, and `rb question move --parent ` (or `--top`) moves one, with the experiments filed under it and their evidence: reframing a question is not a fresh start. A question cannot sit under its own sub-question. A move accepts `--why "..."` and `--words "..."`, kept in its log entry. `rb experiment add ... -q ` files an experiment under a question (repeat `-q` for several), and `rb experiment file ` files an existing one under one more (`--remove` takes it out of one). **A question is the user's words.** Record a question in the user's own words, verbatim from the brief or their message, with `--words` keeping the sentence it came in; never formalize it, and never paraphrase it for another reader. One drafted from a file keeps that file's words. Every view shows the same one wording: views change density and annotation, never the question. `rb question reword "" --why ""` (with `--words` when the user gave the new wording) changes it later, as `rb project title` retitles the project: a revision, never an edit. The log keeps the old and new wording, who changed it and why; the question's id, its place in the tree, its experiments, evidence and calls stay. The same wording again writes nothing. A criterion linked to the question reads `question_link_outdated` until it is declared on the new wording, and a call on the question reads as made on an earlier reading. From Python, `state.reword_question("", "", why="", words="")`; over MCP, `question_reword`. **Which evidence answers which inquiry.** Filing an experiment says where to find the work. A criterion's question links say which inquiry its finding bears on: `rb criterion add ... -q --when-met ""`, or `rb criterion file --when-met "..." --why "..."` for a rule already recorded. The same experiment can test several inquiries, and one criterion can bear on several questions without duplicating the finding. Repeat `-q` when the interpretation is the same, or file each question separately when it differs. A proposed answer still lives in an experiment's hypothesis; a question link records how a test bears on that inquiry. `rb show `, `rb status`, `rb context` and the user's screens keep the question's directly linked findings, its descendants' readings and its unassigned criteria distinct. Unassigned criteria remain visible with their findings so older records lose nothing; they supply no answer by implication. Each is listed as `unassigned (filed here, no question link)`, with the command that links it, `rb criterion file --why "..."`, and the question's counts leave it out. Once any criterion in the record is linked to a question or related to another, `rb status` also gives the agent `unassigned_criterion` for each, with that command, except while the criterion, its experiment or the question reads as edited outside rb, whose repair comes first (`edited_outside_rb`); a record with no link at all is not asked to link every criterion it has. A sub-question's negative stays the sub-question's. Counts of met or missed rules never decide a question's answer. A question's state is computed, never stored: `open` until a decision on it, then `answered` (accept), `dropped` (reject), `parked`, `stopped`, `pivoted` or `shipped`; investigate and continue keep it `open`. A finding or an interpretation does not make that call. A question with nothing filed under it and no sub-question is listed under **Can be done now** (`open_question`). ### Experiments and variants An experiment is what is tested: its hypothesis (`--hypothesis ""`, with `-m ""`), or its purpose when it is not a hypothesis (`--purpose "..."`, for a benchmark or a sweep), filed under questions with `-q`. There is no separate hypothesis record: a proposed answer nobody has tested yet is an experiment with no runs, and rb shows it as planned. Its state is computed on every read: `planned` (no runs or analyses yet), `open` (evidence, and no decision on it), or what the latest decision on it made it: `accepted`, `rejected`, `investigating`, `parked`, `stopped`, `pivoted` or `shipped` (continue keeps it `open`). rb records what ran; it has no state for work in progress. An experiment nothing in the record tests yet (no criteria, runs or analyses) is listed in `rb status` (`planned`), with `--amend` in its command when the experiment is frozen. **The decision it gates.** `rb experiment add ... --if-holds "" --if-not ""` keeps the call the experiment decides: the action either way. `rb experiment gate --if-holds "..." --if-not "..."` sets it on an experiment that already exists, or revises it: the earlier words stay in the log, and since a gate says what happens next, never what the experiment reads, a frozen experiment takes it without an amendment. `rb status`, `rb show`, `rb context` and the user's screens show it beside the hypothesis, so the next experiment and what hangs on it are read together. Words given with `rb experiment gate --words` show beside it, once: in place of a part of the gate that is those words (`if it holds: words (as given): "..."`), else after it. A branch is the action taken (`--if-holds "INT8 ships"`), and the whole sentence it was said in goes in `--words`: a write whose `--if-holds` or `--if-not` holds a condition (the word "if" or "unless") keeps the gate and says so beside it, naming `if_holds` and `words` over MCP and the SDK (`data.warnings` in `--json` and over MCP, an `RBWarning` from Python). They show as words given (`words (as given): "..."`) where an agent session gave them, on every screen and on a page too, so a session's first-person words never stand bare there; the user's own show on every screen but a page. A gate an agent session set (with `rb experiment gate`, or `rb experiment add`) is part of the experiment's design and waits with it, under the design's one command (below): it ends `· not confirmed`, as a session's note does (said on two lines, after the branch a session wrote where it wrote only one), on every screen, a page and the write's own receipt, with no command beside it, where the design's own line (under it on the receipt) and the one line for the designs that wait say how it is confirmed. A gate waits with its design while a session's line wrote a branch or its words as they read now, until it reads as in a design the user owns, and reads as a session's until then, with a branch or words the user gave beside them read as given; so does a why a session's line wrote as it reads now, as its note. No line's time decides whose they are: words a session and the user both gave read as given. The rb confirm that confirms the design makes the gate and its words the user's (`gates_pending`, `gates_confirmed` in `--json`, and `gates_pending` in `rb status --json`, each with that command as `confirm`: what confirms the gate, which is its design's while the design waits with it). A gate's own key that an earlier rb listed is refused, saying the gate waits with its design now, and so is a design's key an earlier rb listed, saying rb cannot tell from it whether the design changed since, each with the design's command. The words are under `gate_words`, and who set the gate (or confirmed it) under `gate_by`, on the experiment in `rb status --json` and `rb context --json`; a later gate write with no words drops them. **An experiment's design.** Its design is what it records of how it tests its question: the questions it is filed under, its hypothesis or purpose, its why, its variants and their roles, what it varies, its settings with their sources (a fixed design factor and a cited value too), its criteria (rule and role) and what meeting each would mean for a question (`--when-met`), its gate and the words given with it, its freeze and amendments with their reasons. Its runs, analyses, notes, relations and the calls on it are not part of it. rb records a design and checks its sources; it cannot make an agent record a choice that only lived in the chat, and it cannot judge whether a method fits the question. - What a session writes of a design reads as not the user's until they confirm it. Its own views (`rb show `, its pane and row on `rb open`, its card on `rb ui`, its part of a page) say `design not confirmed` once, with the command that lists it to confirm, `rb confirm `, since none shows every part: that listing ends with the one command that confirms it as listed, `rb confirm @` (the full hash of what it listed). `rb status`, `rb context` and a page's summary say every such experiment in one line (`The design of 3 experiments is not confirmed: rb confirm lists them.`). A design written wholly in the user's own terminal carries no mark. - Where such a design has no why, or no hypothesis or purpose, its own views say `why: not recorded` or `hypothesis or purpose: not recorded`. Nothing asks for them, and no write waits on them. - Only the user confirms, in their own terminal (sessions receive `E_USAGE`). `rb confirm` lists each waiting design with every keyed field (sources in full, text escaped where a screen could hide it, every digit, the freeze's full time); `rb confirm ` lists one. `rb confirm @` confirms it as listed, with its gate, and refuses one changed since, with what it reads now and its command. After it, a session's reasons, freeze and amendments read as its notes without `· not confirmed` while they read as listed, as do the interpretation a session declared with a criterion it listed and its reason. A gate the user owns reads as theirs, and what meeting a criterion would mean reads as confirmed only while the whole design is one they own. - The confirmation is one log line, as a gate's is, and changes no file: `op` gate, `detail.owned` (the hash and the parts in rb's structured form, never its wording), and `detail.design` and `detail.parts` as an earlier rb reads them. A confirmation an earlier rb wrote kept only the wording, so it owns the design only while no session's line it did not see wrote a part of it and rb wrote every file of it as it stands; one that names no line it saw is taken to have seen none, and no change since it is said. Confirming changes standing, never a reading: findings, criterion readings and comparisons are computed as before, and a criterion a session declared counts as declared when it was declared. - How rb reads the standing, from the log alone: a design no session's line wrote any part of carries no mark; otherwise it reads confirmed only while it is exactly a design the user owns, one they confirmed or one their own write made from a design they owned at that moment (that write's line keeps it in `detail.owned`, worked out then; an adoption keeps one only where rb is sure what its files held before). So once a session touched a design it waits for one confirmation, even after the user rewrites every part the session wrote, and the user's write to a design that waits does not make it theirs; a merge, a clone, an older rb or a clock can only make rb ask again. - Whether a session's line wrote a part is what the line says it changed (`detail.changed_parts`, by experiment, compared in rb's structured form, as `detail.parts_compared` says, so a value's type or a source's term counts, and a file moved to another experiment counts for both); a line an older rb wrote, which does not say or compared the wording, wrote a part where it wrote one of the design's files (a criterion's, of every experiment a line says it was of), but for a note or its retraction, a relation, a check of a source, a reindex or a restore. Said against a design an earlier rb confirmed, its files are named among the changes, since its words could not show every change. - Where it is not a design the user owns but they own an earlier one, its own views say how it differs from the closest (fewest parts apart, then latest), part by part: `changed since you confirmed it: criterion acc-tol's rule, was change.top1 ≥ -0.005, now change.top1 ≥ -0.003` (`added` or `removed` for a part added or taken back, and what tells apart two values rb says alike: `was 3 (a number), now 3 (text)`, or `was 3` as an earlier rb listed it; a page says `changed since it was confirmed`). It says what differs, never who changed it or whether it matters; an edit outside rb is a difference too, named by its file, as is a file written since where rb can name no part. - In `--json`, over MCP and from Python each experiment carries `design`: `standing` (`yours` where no session's line wrote any part of it, `not_confirmed`, `confirmed` or `changed`), `pending`, `key` (a short name), `confirm` (the command with the full hash), `changes` (each with `part`, `change`, `was` and `now`, a gate's branches each as `rb confirm` lists them, so the two never read alike), `not_recorded`, `confirmed` (when and by whom) and `parts` (their text as written). `rb status --json` gives `designs_pending` and `counts.design_pending_count`, and `rb confirm --json` gives `designs_pending` and `designs_confirmed`. An experiment compares **variants**, and each variant has a role: `baseline`, `candidate`, `control` or `ablation`. Name them on `rb experiment add` with `--baseline` and `--candidate`, which says the variants it made, or add one later with `rb variant add --role ablation`. An experiment has one baseline. `--varies` names the settings the variants differ in on purpose, and `rb spec vary ` declares another later. While every variant has the same value of a setting it varies (none set it yet, or the same value in each), `rb status` notes it (`varies_unchanged`: `but no variant has a value for it yet`, or `but every variant has the same value`), and the write that leaves it so (`rb experiment add`, `rb spec vary`, an `rb spec set` of that setting) says it at once, in its text and `data.warnings`, with how each variant gets its own. A setting belongs either to the whole experiment (`optim.lr`) or to one variant (`int8.precision`, or `--variant int8`). A variant's own value overrides the shared one for that variant. **A setting that differs between variants and is not declared in `varies` is a confound**: an integrity problem of every finding that compares those two variants, until someone declares it (`rb spec vary`), sets it the same in every variant, or the difference is accepted: `rb decide int8/data.batch_size accept --why "..."`. `rb experiment add --like ` copies another experiment's shared settings, as provisional, what it varies, and, when you give no `-q`, the questions it is filed under. **Notes.** `rb note add "" -e ` keeps words about an experiment (an assumption, a caveat, context) verbatim, with who wrote them and when. A note reads as a note, never as a file's words or an observation, and a number in it is never read as a result; without `-e` the note is on the project, and `rb status` and the page show the project's notes. A note a session wrote, and a run's `--note` it gave, ends `· not confirmed` on every screen and in rb's text, naming no writer; so does a why a session gave an experiment, a freeze, an amendment, a question link, an analysis's `--supersedes` or a retraction, and a relation's note, a why reading as `note: · not confirmed` in place of `why: ` or `: ` (an experiment's, its freeze's and its amendments' as `note: ` once the user confirms the experiment's design), in rb's computed lines too (`superseded by an2 · note: · not confirmed`), the log, recent activity and the write's own echo (the user's own why stays as given); who wrote it, and the session, stay in `--json` (`by`), the log, MCP and the SDK. Words given with a note (`--words`) show beside it on every screen but a page, as words given (`words (as given): "..."` where an agent session gave them), and under `words` on each note in `rb status --json` and `rb context --json`. `rb experiment add --note "..."` writes an experiment's first. A note changes no finding and takes no decision: what the work takes for granted is a note, and what a number must meet is a criterion. Every screen that lists notes shows each one's short id (`note n7k2pqa: ...`, with its day where the screen gives one: `note n7k2pqa (2026-10-11): ...`), and so do the note's receipt and its log line; `rb show ` prints it, and `rb retract --why "..."` takes one back, from the SDK (`state.retract`) and MCP (`retract`) too: it stays in its file with `retracted`, under `notes` in `--json`, and leaves the screens. A note written wrong is retracted and written again, never corrected by another note. **Relations.** `rb experiment relate reproduces|extends|follows_from|compares_with ` says how an experiment stands to another: one here by its id, any experiment by its global uid (`exp_01J...`), or a published manifest's address; `--note` says more. A relation is structure: it changes no finding. `rb clone` records one on every experiment it brings: it `reproduces` the parent's. ### Settings: a value, where it comes from, and whether rb has checked it `rb spec set --source ` records a setting. When the source can be read, `rb` checks it on the spot; `--no-verify` skips the check. Each setting has one of four statuses: - **verified**: the source states this value. `rb` keeps the text it read, the file's hash, and the commit when the file is committed (an uncommitted file is said as one). - **provisional**: the value was given but not checked. A url or a note cannot be checked, and neither can a value no source has confirmed yet. - **unknown** (`--unknown`): nobody has found it yet. - **inherited**: taken from a parent project, by `rb clone`. **Sources.** | form | example | notes | |---|---|---| | `file#key.path` | `configs/train.yaml#optim.lr` | YAML, JSON or TOML, with nothing more to install. The value is optional: `rb` reads it. | | `database#table[column=value].column` | `results.db#configs[config_id=c1].width` | One column of the one row the brackets pick (as `--where` does; none when the table has one row) of a SQLite database, read where it is with rb's read-only reader, never at a `--commit`. The column must name the setting, or in a key/value table the row the brackets pick does (`meta[key=calib].value`, or `--term` with the row's own word), a NULL states nothing, and the value is optional. Only a setting reads one: a cited criterion, a reported analysis or a cited value refuses a database row, a measurement to read as a run. | | `table#[column=value].column` | `results/sweep.csv#[precision=int8].calib` | One column of the one row the brackets pick of a CSV, TSV or JSON Lines table (the file's own name may stand before the brackets, `sweep[...]`; with no brackets, the table has one row), read as `--where` reads a run's row, at `--commit` where one is given. The column must name the setting, an empty cell states nothing, the value is optional, and, as a database row, only a setting reads one. A key into a file rb reads by no key (`README.md#calib`) is refused, never kept as a path that names no file. | | `file:LINE` | `configs/train.yaml:3` | A YAML line becomes its key path when that key is the setting's own (`optim.lr` for `lr`). | | `file` with `--quote "exact text"` | | For prose. Add `--term "learning rate"` when the line calls the setting something else. | | `run:PATH#/json/pointer` | `run:out/seed1/record.json#/dataset/count` | A JSON file, read by JSON pointer; a directory means the `record.json` in it. | | `https://...` or `note:text` | | Recorded, but it stays provisional. | `--commit ` reads the file at that commit, which never goes stale. `--locator "§4.2"` says where a reader finds the value. Paths are relative to where you run the command, and must be inside the project and outside `.rb/` (`E_SOURCE_OUTSIDE` otherwise). `rb` writes every path with `/`, in the record and in what it prints, on Windows too, where a path typed with `\` is the same path. `rb spec set --from --keys "optim.*,model.depth"` records many settings at once from one config, each sourced by its key path. Choose the keys that define the experiment: a key like `log_every_n_steps` should not stop runs counting when it changes. A key whose value is not one (a section, an empty value) is skipped and named, before anything is written. **What "states" means is strict, on purpose.** - **The source must name the setting.** A key (`file#optim.lr`), a run pointer, or the line a quote is on must name it: the same last word (`lr`), or the word given with `--term "learning rate"`. A name LaTeX and Markdown escape (`seq\_len`) is that name. A line that happens to hold the value under another name does not verify it. - A comment line never verifies, a comment being what the file's type calls one: `#` in YAML, Python, or pip's and CMake's `.txt` files (requirements, constraints, CMakeLists.txt), `%` in LaTeX, `//` in C, in reStructuredText a `..` comment, and in Markdown (`.md`, `.qmd`, `.Rmd`, `.mdx`) an HTML comment that starts its line; front matter reads as YAML. Code in a document reads by the language it names: a Markdown fence (`;` in an ini fence, `/*` in a c one), a code-block directive, an ipython, testcode or doctest directive (Python), LaTeX's minted and lstlisting environments; code that names none or a language rb does not know (an indented block, a `::` block, a verbatim environment) reads as any other `.txt` file, a web page and a file with no extension do, as rb read every file before: a line that opens with `#`, `//`, `/*`, `%`, `;` or `*` is a comment. A Markdown heading, list item or `**Learning rate:** 3e-4` line is text, and so is `python train.py --lr 3e-4` in a code block. A trailing comment is not read either, by the same syntax: `lr = 1e-4 # the paper used 3e-4` states 1e-4, and `**Optimizer:** AdamW ` states AdamW; prose ends no line at a mark, so `run #3` and `#heads 12` are text. On a line with several numbers, the setting's is the one nearest its name: in `default=0.01, help="try 0.05"` it is 0.01, and in "the learning rate is 0.01 and weight decay 0.0001" weight decay's is 0.0001. A `--term` must be the words the text uses for the setting, not a word like "the". - A number is a standalone token compared exactly. `lr: 0.0001` states `1e-4`, and a sentence ending `... is 0.0001.` states it too, but `resnet50` does not state 50, and `1700000001` does not state 1700000000. Digit separators are part of the number: `48_200`, `48,200`, and LaTeX's `48{,}200` and `48\,200` state 48200. A count's suffix is too: `999M` states 999000000 and never 999, `1.5B` 1500000000, and `30K` or `30k` 30000; a letter that runs on into a word (`512MB`) is no suffix. A value given as `999M` is text: where the source states that count, the conflict names it (`says 999000000`), so give the value as the number. - Text is a whole token: `adamw` does not state `adam`. Text stays text: the version `"11.10"` is not the number 11.1. A YAML config's `1e-4` is a number. - A list states its elements in order. When the source states a *different* value, `rb` records a **conflict** on the setting: an integrity problem of every finding on its experiment, which re-verifying does not clear while the source states another value or cannot be read. Setting the value the source states, or a different source, clears it, and a check that reads the source and finds it no longer states another value replaces it with what it found. Every read re-checks every verified setting: - the same key or text found on another line is **moved**, which is fine and noted, as is a file whose bytes are those an earlier rb verified, read as that rb read comments, when this rb reads them otherwise; - a file that no longer states the value is **stale**, an integrity problem; - a file that now states another value is a **conflict**, an integrity problem. - a setting whose file has no commit from before a past run that reads it (a run made before the record began) **postdates** that run and reads **provisional**, never verified, at the write and in every count and listing, with a note (`source_postdates_run`), once per file: `max_steps, seq_len are provisional: sweep/next.sh has no commit from before run runq3k8d2 was produced (2026-05-07): the file postdates the run`. So does one verified at a commit (`--commit`) made after such a run, whatever an earlier commit held: `max_steps is provisional: sweep/train.py at 4f1c2ab was committed 2026-05-10, after run runq3k8d2 was produced (2026-05-07): the file postdates the run`. A setting read from the working tree with no `--commit`, from a file committed before the run, is as old as the commit that last changed its line: `lr is provisional: sweep/train.py:2 was last changed at 4f1c2ab, committed 2026-06-01, after run runq3k8d2 was produced (2026-05-20): the line postdates the run`, or says the line holds changes no commit has. Days are UTC, and on the same day both times are given. A finding dates a setting only against the runs it reads: a variant's own setting against that variant's runs (those that report its numbers), and of those only the runs the reading counts, so a run left out (under an earlier spec, run with a config that disagrees) never dates it; the note names one of them. The experiment's own views (`rb show `, `rb show /`, `rb status`'s setting counts, a run add's echo) date it the same way, against the runs some reading of the experiment counts, and those it leaves out only for being under an earlier spec (they count again once read anew), never a run left out for a config that disagrees or outside every rule's selectors. The write that verifies the setting says the same at once (in its text and `data.warnings`), as `rb run add` does of the run it records. A plan's line states what the runs it launches will use, not what an earlier run used; the same line at a commit from before the run (`--commit`) verifies it. A database row, and a run's own config (`--config`) or file, is read for that run itself and never gets this note. `rb show /` and `rb spec verify` say such a setting provisional too, and `verified_count` leaves it out, while the stored status and the check's `ok` keep what rb read; a vouch for its value (below) takes the note's place, as for any provisional setting. `rb status --fail-on stale` fails on either. `rb spec verify [names]` re-reads the sources. A check that fails is recorded on the setting, and `rb status` then asks for a source that states the value rather than the same check again. A setting's history (`rb show /`, `rb log /`) has every check of it, with its own outcome. `rb spec verify` exits 1 when any setting it could check did not verify, with `E_SOURCE_UNRESOLVED` per setting in `data.outcome.failures`. A url or a note is reported as not checkable (`E_SOURCE_UNVERIFIABLE`) and stays provisional without failing the command. A required setting that is unknown is an integrity problem of every finding on its experiment that reads it, and `--fail-on unknown` fails on it: a shared setting's, of every finding there; a variant's own (`p4.data`), only of the findings that read that variant, and so for every other problem or note of a variant's setting; `--optional` makes it a note instead. A provisional value is a note on those findings, never a problem: they say the value was not checked. A value nobody can check can be vouched for, recorded as whoever vouched: `rb decide int8/cuda accept --why "read it off the cluster image"` (the user's call: pass their words with `--words` when you record it for them, and it waits for their confirmation). The vouch covers that value only; a new value needs a new decision, and a setting with no value has nothing to vouch for. A rejected value is an integrity problem until it changes. **Per-run settings and cited values.** `--per-run` is for what each run chooses (a seed). Each run must then give its own with `--set seed=2`, or it is refused. `--cited 1e-4 --cited-source ` records the value a cited source used (a paper, its evaluation code), and the write says it back (`cited: 1e-4 (paper.md:12)`). A difference between the two makes a criterion that cites a number an integrity problem (`not_comparable`), and its rule reads undetermined; a criterion of the work's own, which cites nothing, is not told of it; for a per-run setting, the difference is between the cited value and the values the runs a criterion reads give (`problems 250 in the runs it reads vs 1319 in the cited source`). `verified` means the source states the value. It does not mean a run used it. To check that, record the run's own resolved config with `--config` (below). A setting's value, whether it is required or per-run, the variants and what the experiment varies make up the spec its runs are recorded under; a setting's status and source are not in it, so verifying changes nothing. After any of them changes, the runs recorded before read as under an earlier spec: they stay in the finding's history and no longer count. Declare the settings before the runs. ### Metrics `rb metric add val_loss --unit nats --minimize --alias cross_entropy` adds a metric to the catalogue: its unit, which way is better (`--minimize`, `--maximize`, or neither), and other names runs may use for it. A run that reports `cross_entropy` is then read as `val_loss`. A finding reads its result in that direction (gain or loss), `rb compare` uses it to say whether a change is better or worse, and `--better` and `--noninferior` rules need it. `rb status` names any metric the runs report that no criterion reads (`unread_metric`), and any comparison an analysis reports that no criterion reads (`unread_comparison`), under **Facts that ask for nothing**: it suggests no rule, since a threshold or a direction is a file's or the user's to state. ### Criteria `rb criterion add "" -e --metric ` declares a rule over one comparison. **Declare it before the evidence it reads.** It is attributed to whoever wrote it, at the moment they wrote it, with the user's words when an agent gives them (`--words`); an agent's criterion counts as declared, with no freeze needed. The statement says in plain words what the rule checks; words given that are the statement itself are said once, in its place, as given (`words (as given): "..."`), on a page too. Evidence that already existed counts, and the finding's timing says so (below): it is never hidden, and never read as prospective. Each criterion is a file of its own in the experiment's `criteria/`. **What a met rule means for a question.** `-q ` (repeatable) files the criterion directly to a question. `--when-met "..."` records the author's conditional interpretation, verbatim, with who declared it, when, through which interface, their reason and any `--words`. It is optional: a link can name the inquiry without claiming what its result means. The link keeps the question's text as it was when declared. `rb` computes the finding; the interpretation remains its author's. One a session declared reads as not confirmed on every screen and in rb's text (`declared when met, not confirmed: '...'`, `A recorded reading · not confirmed`, `confirmed: false` on the screens' interpretation groups), naming no writer; the user confirms it by declaring it in their own terminal, or by confirming the design of the criterion's experiment, which lists it (`interpretation_confirmed` on the question's reading). For example, a rule that detects a regression may mean "This case weakens the proposed explanation" when met. If it misses or is undetermined, rb never asserts the inverse. An interpretation is active only while its rule meets, its evidence and question have no integrity problem, and the criterion is not a gate. A call on the criterion does not change that, a reject or an investigate included: the question's reading says the latest call beside the interpretation. Its reading still carries the rule as the finding says it (`estimate meets criterion` where there is no interval), the interval, point-only or unresolved result, units, caveats and timing; across fixed design cells, how many cells meet and their range. The link has its own declaration time: adding it after seeing a result stays retrospective even if the numerical rule was declared earlier. A question's reading says the two timings once where they read alike (`rule and question link declared after the evidence existed: ...`), and each by its name where they differ (`rule declared before the evidence; ... question link declared after the evidence existed`). A gate remains a policy for spending effort, never an answer. `rb criterion file --when-met "..." --why "..."` adds or revises that scope and interpretation. Omit `--when-met` to keep an existing interpretation; `--when-met ""` clears it. Once the question has been reworded since that interpretation was declared, or a merge left two declarations of it, a filing that omits `--when-met` is refused (`E_OBJECT_INVALID`) whoever makes it, since it would carry those words into a new declaration: restate the interpretation with `--when-met` (the same words keep it), or clear it with `--when-met ""`. `--remove` withdraws the link. A revision or withdrawal keeps the earlier declaration and who took it back; refiling makes a fresh declaration. An identical filing writes nothing. A criterion has one active declaration for each question: when a merge of two branches that filed it to the same question leaves two, neither interpretation is active, the question's reading lists each one's words and reason (who declared each is in `links[].declared_by`), and `rb status` gives the agent `question_link_ambiguous` with the command that repairs it, `rb criterion file --when-met "..." --why "..."` (without `--when-met` when no declaration has words, so none is made up), since a filing or `--remove` replaces every active declaration for that question. A link to a question reworded since it was declared gives the agent `question_link_outdated`, with `rb criterion file --when-met "..." --why "..."` to declare it on the question as it reads now (without `--when-met` when the link has no words), and a link to a question that is retracted or missing gives `question_link_unavailable`, with `rb criterion file --remove --why "..."`. A changed link on a frozen experiment needs `--amend --why "..."`, just like a changed rule. These writes accept `--words`. Editing a question outside rb cannot silently retarget its links: its reading names the integrity problem and does not activate their interpretations. **What it compares.** `--metric` names a number the runs report: - `val_loss` reads a number named `val_loss`, or the only candidate's when the runs name it by variant; - `int8.val_loss` reads variant `int8`'s number, a level; - `candidate.val_loss` reads the candidate's; - `change.val_loss` is the candidate's minus the baseline's, worked out by `rb` from the same run. Record the variants' own numbers, not a change you computed; a given `change.val_loss` that disagrees with them is not read. A name no run or analysis reports and the catalogue does not hold, close to a name one of them does (`change.val_los` where runs report `val_loss`), gets one warning line naming the close name (`data.warnings`, and an RBWarning from the SDK's `criterion()`); the criterion is still declared as given, since a criterion's `--metric` declares a name by use. Declared before any run, it draws no warning; then a run whose numbers it does not read, under a close name (`val_loss`), says so with how to declare it again, and `rb status` gives that criterion the retraction as its step, never the misspelt name as the one a run should give. Or spell it out: `--metric val_loss --subject int8 --reference fp32` compares two arms, and `--subject` without `--reference` reads a level. An arm is a variant or a role, or an arm an analysis names (`new`, `old`). `--estimand pooled` (or `per_class`) names what an analysis estimated, when it reports the comparison that way; rb's own reading of the runs is a mean over units. Two names in one run that read as the same metric and disagree are not read. **Which runs it reads.** `--where width=512` selects exact values of declared shared per-run settings; repeat it to require several values. `--subject-where loops=8` and `--reference-where loops=6` select each arm within that common scope. A variant can be compared with itself at two disjoint settings, for example `--metric loss --subject model --reference model --where width=512 --subject-where loops=8 --reference-where loops=6`. Overlapping selections of the same arm are refused. Values keep their types: `true`, `1`, `1.0` and `"1"` are different selectors. Runs outside the scope stay recorded and do not contribute to its finding. Existing analyses cannot declare this scope, so a selected criterion reads runs and says when it leaves an unsliced analysis out; a custom estimand cannot yet be combined with selectors. The Python arguments are `where`, `subject_where` and `reference_where` mappings; MCP takes lists of `name=value` strings. **Whether a comparison reverses another.** `rb criterion relate reverses --orientation same --why "..."` records the relationship, with its author, time and reason. Both criteria must compare differences in the same metric and estimand. `same` declares corresponding subject/reference order; `opposite` swaps the other comparison's direction. rb keeps both original numbers and intervals visible, distinguishes a measured reversal from one with both intervals clear of zero, and never makes a question decision from it. The declaration keeps both comparison scopes; a changed scope or damaged source prevents the relationship from reading as valid. Either endpoint exposes the relationship, and a question call includes both sides when either endpoint is directly linked to it. A frozen change needs `--amend`; `--remove --why "..."` withdraws the relationship while retaining its history, and answers with the outcome `withdrawn` rather than a reading. A criterion has one active relation of each kind to another: when a merge of two branches leaves two, neither is read, and `rb status` gives the agent `relation_ambiguous`, naming each one's orientation and reason, with the command that repairs it, `rb criterion relate reverses --orientation same --why "..."` (or `opposite`), since a declaration or `--remove` replaces both. An alignment reads the same from either side: a pair declared back from the other criterion with the same orientation reads once, listing both declarations (`declarations` in the JSON, with who made each), and declaring the reversal back with the other orientation is refused (`E_OBJECT_INVALID`), naming the declaration to withdraw first; where two such declarations already disagree (two branches each made one), neither is read, and `rb status` gives the agent `relation_conflict` with the command that withdraws one, `rb criterion relate reverses --remove --why "..."`. Any other relation that cannot be read says why on `rb show `, and `rb status` gives the agent `finding_relation_invalid` with the command its problem calls for: `rb criterion relate reverses --remove --why "..."` when a side, or its experiment, was retracted or is missing, the same with `--orientation same` (or `opposite`) instead of `--remove` when a comparison's scope changed, and `rb show ` when a side's own evidence has a problem. A relation declared by a criterion of a retracted experiment is taken back with that criterion (`experiment_retracted`, under **Retracting**). Python provides `state.relate_criterion(...)` and `criterion.relate(...)`, which answer with the criterion, and `state.relations()` (or `criterion.relations()`), which reads every relation the criterion is a side of, from either side, as `rb show ` reads them (`relations` in its JSON); MCP provides `criterion_relate`. **Its rule**, exactly one: - a threshold: `--at-most X`, `--at-least X`, or `--equals X --tolerance T`. It reads the estimate, the mean over units, by default; `--each` makes every independent unit meet it; `--bound` reads the interval's bound instead (the upper for `--at-most`, the lower for `--at-least`, both for `--equals`), so it needs an interval. - `--better`: the interval clear of 0 on the better side. - `--noninferior M`: the interval's worse bound inside M. - `--equivalent M`: the interval inside ±M. The last three read the interval, so they need one (from 3 units, when rb reads the runs), and they compare two arms (a `--reference`, or `change.`). `--better` and `--noninferior` also need the metric's direction in the catalogue (`rb metric add val_loss --minimize`); without it they are refused. **Four rules of format 2** (rb 0.8; each writes its criterion's file as `rb: 2`, under **The files**). Each reads a threshold (`--at-most`, `--at-least`, `--equals --tolerance`) its own way: - `--count K` (`over: count`): at least K of the independent units meet the threshold, as `--each` judges each unit ("19 of 20 runs below 0.890": `--metric val_loss --at-most 0.890 --count 19 --of 20`). `--of N` says how many units the rule is of: where the runs hold another number, the rule reads undetermined and says both (`the rule is of 20 seeds; the runs hold 18`), and with fewer than K units it reads undetermined too. Its reading names both numbers: `criterion not met: 18 of 20 seeds at most 0.89; the rule asks for 19`, and the units outside it are its `missed_units`. - `--best` (`over: best`, with `--at-most` or `--at-least`): the unit best in the threshold's direction (the lowest for `--at-most`, the highest for `--at-least`) is read against it ("best of 5 runs reaches 0.889": `--at-most 0.889 --best --of 5`). Its reading is that unit's own value, with no interval, never a mean: `best of 5 seeds, seed 3: 0.8893 ≤ 0.889`, and its comparison's method is `best unit seed 3`. `--of N` reads as for a count. - `--relative` (`measure: relative`, comparing two arms): the relative change, (subject − reference) / reference, read from the runs, in percent, its threshold a percent ("improves val_bpb by 22.5% over base": `--metric change.val_bpb --at-most -22.5 --relative`). rb works it out as the difference of the arms' means over the reference's mean, paired where every unit holds both arms; from 3 units its interval is a delta-method t interval (`relative change of the means, paired, delta-method t interval` on rb show's reads line, `comparison.method` in `--json`). With fewer it has none, and a met rule reads `estimate meets criterion`, as any rule read on its estimate does. - `--means` (`measure: means`, with `--source` and `--quote`, comparing two arms): the cited number is a difference of the arms' means over all their units, and rb reads that estimand, with Welch's interval (`difference of means, as cited`), in place of its paired reading. Where the two readings' differing estimands were the only reason a cited rule read `not_comparable`, it no longer does. Each is said in the same words on every surface (`rb criterion add`, `rb show`, `rb status`, `rb context`, the page and `rb open`, the SDK and MCP): the rule as `19 of 20 units' int8.val_loss ≤ 0.89`, `the best of 5 units' int8.val_loss ≤ 0.889`, `change.val_bpb as a relative change ≤ -22.5%` and `change.val_loss as a difference of means = 0.0317 ± 0.00005`; on the page a count and a best are said by their units (`INT8: best of 5 seeds, seed 3: 0.8893 > 0.889`), a relative change in percent (`INT8 is 2.33% higher than the FP32 baseline`), and a difference of means with `a difference of means, as cited`. A count and a best read the runs a rule's `--where` selects, unit by unit as `--each` does (and an analysis of the comparison is said beside them, never read in their place, as for `--each`), and a met one reads `recorded values meet criterion`, whatever the interval of the units' mean, which neither reads (the tally counts it among the rules met on the estimate alone); a relative change and a difference of means read each arm's own numbers, so a run that gives only the difference gives them none, and they are not read across fixed design cells (declared with `--design-factor`), which are not independent samples: across them each cell is read on the rule's own estimand (a relative change in percent, a difference of means), its range and how many cells meet said in it, and the rule reads undetermined until a `--where` selects one cell, whose replications it reads on that estimand, with its interval. Where a reading pools a per-run setting, a best, a relative change and a difference of means are read at each value as their rule reads them (`pooled_misses`), and a count names no value: its K is of every unit it reads. Across cells, a count and a best judge the units within them, as `--each` does. A count or a best takes no `--each` or `--bound` (`E_OBJECT_INVALID`, naming both flags), and `--of` takes `--count` or `--best`. Their timing, calls, tallies and `--fail-on` gates read them as any rule. **Its role** (`--role`): `primary` (the default: what the work claims), `secondary`, `exploratory`, or `gate`. A gate is a priority policy for spending effort, not a claim: a missed gate asks for a direction call (`rb decide park --why "..." --revisit ""`), and it never refutes anything. Most gates of `rb status --fail-on` read only primary criteria (below). `--min-n 3` is how many independent units the reading must rest on; with fewer, the rule reads undetermined. `--noise 0.01` is the run-to-run spread you expect: an estimate within it of a threshold gets a borderline note (another unit may land on the other side), and the outcome stands. `rb` prints the rule with its `n` and noise everywhere it shows it (`change.val_loss ≤ 0.05 (n ≥ 3, noise 0.01)`), so whoever freezes sees all of it, each number in digits as declared, never with an exponent (`top1 = 0.999349 ± 0.0000005`, never `± 5e-07`). A criterion with `--source` cites a number someone else stated, such as a paper's table: save the text in the repository and give `--quote` and `--locator`. It takes a threshold with the stated number as its target (`--equals 5.40 --tolerance 0.05`), and a file source must state that number, or the criterion is refused. An ordering a file states ("1024 images is the best of 512 and 1024") is a threshold of 0 (`--at-least 0`, or `--at-most 0` where lower is better), cited by its words: `--quote` the line, which rb checks is there and not a comment, and `rb show ` gives the file, line and words. A stated number carries its rounding: give `--tolerance` half a unit of its last digit for each rounded figure it comes from, so a match within rounding reads `reproduced` (a paper's -0.006 worked out as 0.755 - 0.761 from two rounded means takes `--tolerance 0.001`). Its rule reads `reproduced` (`reproduced on the estimate` where the reading has no interval or one that crosses its line) or `not reproduced`. A cited difference read against rb's paired reading is compared only where every unit pairs, since only there is the mean of the paired differences the difference of the arms' means: where one arm ran units the other did not (2 seeds against 3), the paired reading leaves those runs out, and the two are different estimands. Only a number the reading could miss is compared (an `--equals`, a threshold that is not 0, or a 0 the cited words state, "at least 0 points"): where the paired reading and the difference of the means give its rule different outcomes, the rule reads `criterion undetermined` with `not_comparable`, never `not reproduced`; where they give the same outcome, the rule is judged, with the difference of the means said beside it as a note. A direction cited by its words (an ordering, `--at-least 0` with `--source --quote`) compares no number, and is read as its rule says: never `not comparable` when nothing is compared. rb says the units it paired and the runs it left out, and, where each arm is recorded in runs of its own outside design cells, the difference of the arms' means over all their units (a unit's cases averaged first, as rb's reading averages them), in the digits the rule needs to tell which side of its edges it falls (`+1.8848` beside `1.89 ± 0.005`, never `+1.885`), for the reader to set beside the cited number. A criterion a clone brought with its number (`--equals`, read as `reproduced` or not) is read the same way. A threshold a document sets, such as a plan's "ships if it loses at most 0.01", is not a cited number: keep the document's words as the statement and give its line as the reason, `--why "docs/plan.md:3"`. A reason that is a file and line alone is checked: rb refuses the criterion when the line is not there, is a comment, or does not state the rule's threshold or margin (a line with `%` may state it as a percentage). ### Runs ```sh rb run add int8.val_loss=5.64 fp32.val_loss=5.61 latency_ms=25.1 # typed numbers; recorded as typed rb run add val_loss=5.64 --variant int8 # the same as int8.val_loss=5.64 (the numbers go before the options) rb run add --from metrics.json # JSON/YAML/TOML; nested keys join with dots (--keys "summary.*" takes only those) rb run add --from results.db --table evals --where precision=int8,seed=1 # one row of a SQLite database rb run add --from results.csv --where precision=int8,seed=1 # one row of a CSV, TSV or JSON Lines table ``` A run's numbers are named for the variant they are for: a file for an experiment with two arms gives `{"fp32": {"val_loss": 0.961}, "int8": {"val_loss": 0.987}}` (or the dotted names), and `--variant int8` reads a file of one variant's numbers. On an experiment that compares two or more variants, numbers that name no variant (a flat `{"val_loss": 0.987}`, with no `--variant`) are refused unless a criterion that names no arm reads a name as given (`--metric val_loss`, no `--subject` or `--reference`): no criterion could read them as any variant's, and the refusal says how to name the variant (`E_OBJECT_INVALID`). A row picked by a value of what the experiment varies names its variant (`--where precision=int8` names `int8`, where one variant holds that value or is named by it). On an experiment of one variant, a number that names no variant is that variant's (`value=1` is `none.value` where `none` is the only variant). The refusal names the metrics the project declares among the numbers, never a per-run setting the file also holds (a seed, given with `--set`), and offers the variant the run's config (`--config`) names by its value of what the experiment varies (`prompt: plain` offers `--variant plain`); a row whose `--where` names two variants that way is refused as a row rb cannot tell the variant of. Any other run none of whose numbers a criterion of its experiment reads is recorded, and `rb run add` says in one line that it counts toward nothing, with the names its criteria read and the shape that gives them (`data.warnings`, beside `unread_metrics`); the SDK says the same as an RBWarning, with the `run.log` that gives them. `rb show ` lists the runs on its experiment that no rule counts (`counts_toward_nothing`), each with what it reports and what the rule reads, or, for a run that holds a number the rule reads outside its per-run selectors (`--subject-where`, `--reference-where`), with those selectors; and the runs outside its shared `--where`, each with its values of what the rule selects by (`run_scope`: `2 runs outside this criterion's per-run selectors are not read: run4k2xa9 (seed=2), ...`). A rule reads a number by the name it is recorded under: on an experiment of one variant, `top1` and `.top1` are the same number, so neither the write nor `rb status` says a run of it counts toward nothing, and `rb show ` names each arm's number as the rule does (`int8.eval.top1` for a rule on `change.eval.top1`); the write's line for a run of other numbers gives it as the rule names it too (`int8.top1=` for a rule on `int8.top1`, `top1=` for one on `top1`), and both spellings are read. **A row of a CSV, TSV or JSON Lines table** (`.csv`, `.tsv`, its first line naming the columns; `.jsonl` or `.ndjson`, one JSON object a line, its keys the columns, a nested object's keys joined with dots as a JSON file's are, a JSON string text whatever it spells) is read as a database's row is: `--where =` (comma-separated or repeated) must pick exactly one row, or rb says how many do, shows the first, says what those rows differ in (the columns with the fewest values first, a declared metric left out) or, where none does, what the rows hold in the columns `--where` named, and writes nothing; its numbers are the columns that name a metric the project declares (`--select` narrows them); `--set seed=@seed`, `--produced-at @finished_at` and `--commit @sha` read a value of the row (`--commit @` only where the cell holds a commit, 7 to 64 hex digits and not a number of decimal digits alone; anything else is refused, and so is `--commit @` where no row is read). A value matches as text or as the same number (`8` and `8.0`). The receipt keeps the file's hash with the run's files and the row it read (`table_row`: the path, the line the row starts on, the columns it was picked by with the row's own values, and the column each value came from), and `rb show ` says it (`row of results.csv, line 4 (precision=int8, seed=1)`), and says the run's numbers came from a row (`(row)`), as for a database's. Two rows of one table are two runs; the same row read again is the run already recorded, and the same row read as a run in a second experiment is said at the write and listed by `rb status` (`row_in_several_experiments`), as a database's row is. `--every-row` records every row `--where` picks, each as its own run through the same `rb run add`, its `@` values (`--set`, `--produced-at`, `--commit`) from its own row; a row already recorded is said so and passed. Every row is read before any is written, so a row that cannot be read writes nothing; rows told apart by their numbers alone, which rb would read as one run, are refused, and so are `--replaces` and `--repeats`, which name one run. Without it, a `--where` that picks several rows is refused and offers it. Without `--where`, rb says the table's columns and how to name a row. **A row of a SQLite database** is read from the database itself, never from an export of it. rb tells a database by its first bytes, whatever its name. `--table` and `--where =` (comma-separated or repeated; a value that holds a comma in JSON quotes, `--where 'overrides="lr=0.1,bs=32"'`; a number form too large to hold, such as the id `5310427e48210936`, is read as its text) name the row: exactly one row must have them all, and when none does, or several do, rb says how many, shows the first of them and writes nothing. The row's numbers are its columns that name a metric the project declares (a metric, an alias, or a criterion's `--metric`), under the column's own name; `--select ` narrows them. `--set seed=@seed` takes a per-run value from a column, and `--produced-at @finished_at` the time the run was made (an ISO date or time; SQLite's `2026-05-01 10:00:00` reads as UTC); without `--produced-at`, when it was made is unknown, since a database's own times are its last write's, not its row's. `@` names a value of the row, so with no row read it is refused. A value kept in another table is `@
.`, read from the one row of that table that holds what the run's row holds in the column the two tables share by name (`--set seed=@configs.seed` beside a results row with `config_id`); where they share none or several, name it: `@configs[config_id].seed`, or `@configs[config_id,step].seed` where one link column picks several rows (`--where` picks the run's row, not that one). The receipt names that row in the column (`configs[config_id=c1].seed`). The receipt keeps `row`: the database's path and sha256 when read, the table, the row's key (the `--where` columns with the row's values, and its rowid) and the column each value came from; `rb show ` prints it, and says the run's numbers came from a row (`(row)`), where `--json` keeps `basis: file`. The database is opened read-only: rb changes neither its bytes nor its schema and makes no -wal, -shm or journal file beside it. One whose latest writes are still in its -wal, or that holds an unfinished write in its journal, cannot be read without writing to it: rb says so and reads nothing. `--every-row` records every row `--where` picks of the table, each as its own run, as for a CSV table's rows (`rb run add --from results.db --table evals --where precision=int8 --set seed=@seed --every-row`), each kept by its rowid beside the `--where` key, since the rows share that key (a table with no rowid keeps a row by its primary key beside the `--where` columns, read alone or with `--every-row`, so a row written again reads as that row, changed, and a run an earlier rb read by the `--where` columns alone is still that row); without it, a `--where` that picks several rows is refused and offers it. A row is one run: read as a run in a second experiment, rb warns at the write and `rb status` lists it (`row_in_several_experiments`), naming both; reading it again records nothing, however the database has grown, and a row whose numbers or per-run values changed since it was read is refused, with `--replaces --why "..."` when that read came before the row was final and `--again --why "..."` when another execution wrote it again. After the spec changes (`rb spec vary`, a setting made per-run), the same `rb run add` with `--replaces --why "..."` reads the row, or the file, again under the spec as it is now, with the same receipt. `rb spec set` and `rb spec vary` say so when the experiment has runs; given again without `--replaces`, the same row, file or values of a run recorded under an earlier spec are answered with a warning that names it (`data.warnings`, an RBWarning in Python), and `rb status`'s `untested` item names those runs. `--again` is for a second execution only. Where the repository keeps a database (in its root or one folder down), the run `rb criterion add` and `rb status` suggest next reads a row of it (`rb run add --from results.db --table
--where =`), never numbers typed with a `--command`; elsewhere it suggests the results file a run wrote (`rb run add --from `), never a number typed in, which could be a cited rule's own number. These options add to any form: - `--set seed=2` gives the per-run settings. - `--config ` checks the run's own resolved config against the spec and the `--set` values (and where the config says the code had uncommitted changes when the run ran, `dirty: true` or `git.dirty: true`, `rb run add` and `rb show ` say so in the file's words, while the file's bytes are those the receipt kept, and what it takes to run the experiment again reads its code as partial; a config changed since is read for nothing, and `rb show ` says it changed): a Hydra `config.yaml`, a W&B `config.yaml` (its `value:` wrappers are read through), or a `hparams.yaml`. A run whose config gives a setting a different value is not counted, and says why; a setting the config does not mention is listed as absent, and a config that mentions none of them leaves the run's config unchecked. A `population.*` setting says how runs are counted, not what a run was given, so no config is compared with one, and a run an earlier rb marked as made with a config that disagreed with one counts. An `env.*` setting, the environment the work expects (`rb env check` compares it with the machine), is compared only with a config key of its own full name (an RL config's `env.name`), never one that shares its last name (`trainer.cuda` for `env.cuda`). - `--input imagenet-val:eval` names a resource the run read, and the role it played there; `--output int8-ckpt:checkpoint` names one it made. Both repeat, and each takes a resource recorded first with `rb resource add`. With each, rb stores where the resource stood at that moment and, for a local file, the sha256 of the copy here, so a later change is attributable. An input read in an evaluation role exposes the resource (see Exposures). - `--host provider=runpod --host gpu=A100` says where it ran, as you say it; rb records it and does not check it. - `--artifact ` hashes an output into the run. - `--link wandb=` records where else the run lives. - `--command` records what produced the numbers. `rb` did not run it. It reads as `command (as given)` only when the run was made when it was recorded, when `rb journal run` watched that command make these numbers, or when `--command-source :` cites a log line that holds the command (a value YAML folds onto the lines below it, each indented further, is the line it starts on), its file written no more than a week before the run was made and no more than an hour after, or first committed after the run, which says when the file was committed and not when it was written (rb refuses a line that does not hold it, and a document: a README, Markdown, reStructuredText, a notebook). A journal sighting counts only in the same window: a recipe `rb journal run` watched today is not the command of a run made in May. Otherwise rb keeps it and reads it, everywhere, as a `command given later, not observed`; `rb run add` says so when it records it, and `rb status` counts such runs (`command_later`). `command` in `rb show --json` and in `rb status`'s runs says how rb knows: `given` is `at_the_time`, `observed`, `logged` or `later`, with `how`. A command from a README, a paper or a script is a recipe: put it in a note with its file and line. - `--commit` records the commit that produced them. - `--produced-at ` says when the output existed, when that is earlier than now, and `--produced-at unknown` that nobody knows. Without it, rb takes the earliest time it can vouch for: the journal's first sighting of the file, git's first commit of the file as it is (git keeps whole seconds, so the end of that second), or the file itself, and failing those the moment it records. Numbers typed on the command line or over MCP, with no file and no row, have no sign of their own: when they were made is unknown, as with `--produced-at unknown`, unless rb's journal watched the command print them as it ran, so a criterion reads their order as unknown, never as declared before them, and the run counts among those recorded afterwards; `rb run add` says so, and `--produced-at` gives the time where it is known. That holds whichever rb recorded them: an earlier rb kept such numbers as made when they were recorded, and this rb reads those the same way, of a time nobody knows, rewriting nothing in `.rb/` (`rb status --json` gives their `produced_basis` as `unknown` and a null `produced_at`, while the file keeps what that rb wrote). A finding's timing reads it. A file written in the hour before you record it dates the run by its own time, and `rb run add` says so in one line: if it holds an older run, give `--produced-at`. - `--repeats ` records a run that re-checks another (below). - `--replaces --why "..."` records a run that replaces an invalid one of the same experiment: the invalid run is retracted with the reason, stays on record, and the two are linked (`rb show` says which replaces which); a page and `rb open` count it among the runs retracted, never among the reasons to doubt the work. It may read the same row or file as the run it replaces: a run recorded under an earlier spec is read again under this one. The **receipt** records the actor, the chat session when the runtime names one, for an agent what rb knows of it (`agent`: its name, version, provider and model, as on the log line), the time, and the repository's state *when the run is recorded*, in the checkout the command ran in: the commit, and whether the tree had uncommitted changes, with a hash covering the diff and the untracked files (`.rb/` left out, the other worktrees of the clone nested in it, which are checkouts of their own, and what `rb init` or `rb skill add` installed while it differs from the last commit by rb's own write alone, as the ownership ledger keeps it: a skill, a rule, an MCP entry, a hook, rb's section of `AGENTS.md`; and rb's own outputs while they are as rb wrote them, committed or not: the page `rb page` writes, an export folder or zip `rb export` writes, and the folder `rb tour` made; none is the code a run ran, so none makes the tree read as changed or is kept in a snapshot under `refs/rb/snapshots/`, and where they are all that is uncommitted, `rb run add` says so). It also fingerprints the environment that recorded it: python, platform, rb, and the versions of common ML packages read from installed metadata (plus CUDA when torch is already loaded, as in an SDK run block). `rb env check` compares this machine against the record's fingerprints and any `env.*` settings, and exits 0 whatever it finds: a difference is a fact to know before the first run, not an error. Declare expectations the ordinary way when they matter: `rb spec set env.torch 2.4.1 --source requirements.txt:3` (the line that pins it; a `.txt` file has no keys) verifies, conflicts and goes stale like any other setting. Record a run from the tree that produced it. When the tree had uncommitted changes, the receipt also keeps the code as it was when the run was recorded, which rb calls the exact code only where `rb journal run` watched the command make these numbers: the git tree of the files as they were (the tracked files as they are, and the untracked files git does not ignore, each up to 10 MB), a digest anyone with the same files computes the same way. A repository of its own inside the tree (an untracked one, or a submodule moved on) is not in it, and the receipt counts it (`nested`). rb writes it through a temporary index, so the user's index, branch and files are never touched (no read of rb's rewrites git's index either), and keeps it in this repository under `refs/rb/snapshots/`, a ref to the tree itself: no commit is made and nothing appears in `git log --all`. The write that makes a ref says so once, after its own text (among `data.warnings` in `--json` and over MCP, an RBWarning from Python): where it put it and the command that removes it, `git update-ref -d refs/rb/snapshots/`; `rb reconcile --apply` says it for the runs it records, and a write that finds the ref there says nothing. `rb show ` gives the `git diff` against the commit and how to restore a file from it; in a clone that lacks the tree (a clone does not carry `refs/rb/`) it says the tree is not in this clone and that the clone that recorded the run kept it. From a clean tree the receipt keeps the tree of its commit the same way, so the code a run was recorded at outlives its branch: squashed, deleted and collected, the commit is gone and the tree is still here, and the experiment still reads as able to run again. The branch the run was recorded on, and the linked worktree it was recorded in (`worktree`, its path), are context on the receipt, never part of the record's structure, and like the actor, the session and the agent they are in `--json` alone: `rb show ` prints when it was produced and when it was recorded (`produced 2026-05-07 20:07 UTC (as reported) · recorded 2026-10-10 14:51 UTC`), the repository's state when it was recorded (`not when run`, for a run made earlier), and what it read and made. `--tree ` records the exact code of a run made earlier: `rb journal run` keeps it at the start of every watched command, and the run `rb reconcile` proposes from one passes it, with the commit it ran on. **A file written after its run.** The file a run's numbers were read from is the run's own output only when it existed by the time the run was produced (its earliest sign, as above, no more than an hour later), or when the run is a row read from the database itself. A file's first commit says when it was committed, not when it was written: a file whose earliest sign is its first commit is never read as written after its run, and where the run's time is unknown it reads as `file first committed `. A file written later, an export or a transcript, reads as `file written , after the run` on `rb show`, `rb page` and `rb open` (`file_written_after` in `--json`); `rb run add` says so, the criteria that read it say so, and `rb status` counts such runs (`file_written_after`). It is never evidence of its own that the run gave those numbers: read the store they came from. When the run's time is unknown, rb cannot show its file is its own output: it reads as `file written ; when the run was made is unknown` (`file_written` in `--json` says when), `rb run add` says so, the criteria say so, and `rb status` counts such runs (`file_run_time_unknown`). **Units.** A finding reads independent units, not runs, and numbers are never merged because they are equal. By default each run is its own unit, and runs with the same per-run values (`--set seed=2` twice) are one unit, their numbers averaged in it. `population.unit` names the per-run setting whose values are the units: `rb spec set population.unit photo --no-verify` makes runs sharing a `photo` one unit, so eight noise cases from two photographs are two units. A unit that names a setting with one value for every run gives no run a unit of its own, so no run is counted: the write that makes it so (of `population.unit`, or of that setting) is told, with `rb spec set --per-run`. A run recorded with `--repeats ` re-checks that run: it is averaged with the run it repeats as one case and adds no unit, and the finding says whether the check holds (`the same number, a check that holds`) or by how much it differs. Cases are then averaged within a unit, and units averaged. `population.repeat` names the per-run settings that only number repeats (`rb spec set population.repeat epoch --no-verify`, or several: `seed,epoch`): such a setting is no condition, so no reading is said to pool it, it is never a value a rule is read at alone, it is never offered as the unit, and it links no runs: with no unit declared, runs pair by their other per-run values, a repeat being a case within its unit (epochs 0 to 2 of one model and effort are one unit, compared with the same model and effort of the other arm), and where no other per-run value tells runs apart (none but what the rule selects its arms by), each run is a unit of its own and the arms are compared unpaired. Like every `population.*` setting it reads no run as under an earlier spec. Nothing asks for it up front: the note on a reading that pools a per-run setting offers it, and so does a reading that pairs arms by one, each after the `--where` that reads one value, and only for a setting that could be a counter: its values whole numbers, which every arm holds alike. The offer says the repeat form is for a counter only (a seed, an epoch, a repeat's number), never a condition; a setting whose values are names (a model, an eval set) is never offered as a repeat, and neither is one at whose value the rule misses alone, since folding it would take that miss away. Where the rule misses, no note offers any command that changes how its runs are counted (the repeat form, a unit, a pairing's repeat): the notes keep their facts and offer none. When a per-run setting takes fewer values than there are units (within a design cell, where fixed design factors are declared, since the unit would be counted there), the finding notes it and suggests `population.unit`, never for a design factor or a setting the criterion selects by. The setting that separates a criterion's two selected arms (`--subject-where loops=8 --reference-where loops=6`) cannot be the unit, since each arm would be one unit: `rb criterion add` and `rb spec set population.unit` refuse that combination (`E_OBJECT_INVALID`). When a merge of two branches brings it about anyway (one declared the criterion, the other the unit), the criterion's finding reads it as an integrity problem (`unit_separates_arms`), and `rb status` gives the agent the repair, `rb spec set population.unit `, naming the setting whose values are the units within each arm, and asks for no more runs, since none adds a unit while each arm is one. On the reading surfaces, "criterion met" means the declared rule was met; the exact rule appears beside it. "Estimate meets criterion" keeps a point-only or threshold-crossing reading qualified. These words describe the criterion, not a decision on the question. `rule_outcome` in the JSON stays `meets`, `misses`, `undetermined` or `untested`. **Fixed design choices.** `rb spec set width --design-factor` declares a per-run setting as a fixed design factor, and makes a new setting per-run; `--sample-factor` removes that designation. On an existing setting that is not per-run (one value for every run, or one still unknown) it is refused (`E_OBJECT_INVALID`): making that setting per-run drops its value and source and reads the runs recorded so far as under an earlier spec, so run `rb spec set width --per-run` first when that is what you mean. Across several observed design cells, rb reports their individual readings, range and how many meet a threshold, with equal weight per cell. An `--each` rule judges every unit within its cell, never a cell's mean, and a cell meets it when every unit in it does. It computes no sampling interval across those cells, and their count cannot satisfy `--min-n` greater than one, so a `--better` rule across them reads undetermined, and rb status points to the rule it computes: an ordering in every cell is a threshold of 0 with `--each` (`--at-most 0 --each` where lower is better). A reason given as a line alone (`--why ":"`) must state the rule's number; an ordering states a sign, so its reason says the line in words. An incomplete comparison in any observed cell makes the rule undetermined; this does not claim that every possible cell was observed. A criterion restricted to one cell retains its ordinary interval over repeated independent units there. A declared `population.unit` that the cells share is counted within each cell, in a finding and in `rb compare`: `width=256 · seed=2` and `width=512 · seed=2` are two units, and a declared unit that is one of several factors is the cell it is in. Unsliced mean analyses are excluded when design factors are declared because they do not state which cells they describe. The designation is attributed and frozen changes need an amendment. It changes how evidence is read without changing the raw run specification or making earlier runs disappear; a late declaration remains visible as a later interpretation of those runs. **The same run twice.** A run's identity is its provenance, never its numbers. The same output files (by hash) are one run: recording them again says which run is already here and writes nothing. Typed numbers with the same values, per-run values and command are treated the same way, when given in the same checkout: given in another worktree, they are that worktree's run. A second execution is recorded with `--repeats ` when it checks that run (with the same numbers it is recorded: the check that holds), or with `--again --why "..."`. Two executions with different commands that give equal numbers are two runs. A wrong run is retracted with a reason, never deleted: `rb retract --why "..."`. When an issue touches one measurement and leaves the rest as they were (a timing bug that changed no numerical output, say), supersede only what it touched: `rb retract --why "..." --scope wall_s` stops those measurements counting while the run's other numbers still count, and every surface says both facts. A scope names a measurement by its full name or its last segment, so `--scope wall_s` covers `int8.wall_s` too. **Declaring the counting structure.** `n=3` cannot say that 8 noise cases came from 2 photographs. Declare the structure as settings named `population.*`, before the runs: `population.unit` names the unit (above), and others describe the rest (`rb spec set population.photographs 2 --source `, or `--no-verify` for a plain declaration). They are optional by default and never listed as work to do, and `rb status`, `rb page` and the screens say them beside the counts. A `population.*` setting says how the runs are counted, not what a run was given, so it is no part of the spec a run is recorded under: one declared or changed after runs are recorded changes how they are read, never which of them count (a frozen experiment still takes it with `--amend`). ### Analyses An analysis is a computation over saved outputs (predictions, per-case errors) and the comparisons it produced: a bootstrap over images, a pooled estimate, a reanalysis. It is never a run, and it adds no unit. ```sh rb analysis add --compare "int8/fp32:val_loss=0.023[0.014,0.032]" --method "paired bootstrap over images" --unit image --units 200 --draws 10000 --reads , rb analysis add --from analysis/boot.json --method "paired bootstrap over images" --unit image --units 200 --command "python analysis/boot.py" rb analysis add --compare "int8/fp32:val_loss=0.023[0.014,0.032]" --source analysis/REPORT.md --quote "" --locator "Table 2" ``` - `--compare SUBJECT[/REFERENCE]:METRIC[@ESTIMAND]=EST[LO,HI]` is one comparison (repeatable): `int8/fp32:val_loss=0.023[0.014,0.032]` is int8 against fp32 on val_loss; `new/old:score@pooled=0.12[0.07,0.17]` names the estimand; `int8:val_loss=5.64` is a level with no interval. A role name becomes the experiment's variant in that role, when it has one; any other name is an arm the analysis compares. - `--from ` reads the comparisons from the analysis's own output file (JSON, YAML or TOML: a list of objects with `subject`, `reference`, `metric`, `estimand`, `estimate`, `lo`, `hi`, `level`, `n`, `unit` and `note`, or an object whose `comparisons` holds that list). rb hashes the file, and the analysis is `computed`. - Numbers given with `--compare` are `reported`. With `--source `, rb finds the line that states each estimate and its bounds (`--quote` for the exact row, `--locator` for a reader), where no one line states the estimate and its bounds, finds the lines of one block of a section (a table or a paragraph: lines with no blank line between them) that state them between them (a statistics table, the mean on one row and its interval on another: leave `--quote` out, and give `--locator` the section's heading or the lines, `83-91`), keeping the lines it read on the receipt (`stated_lines`) and re-reading them as it re-reads one; it refuses the analysis when no line, nor lines of one section, do (a comment states nothing, as it sets no setting: a Markdown HTML comment, or a `#` line in a code block in a language rb knows; a log, a table, a text file and output in a block that names no language have no comments, so ` * Acc@1 81.8` there states its numbers), and re-checks that line on every read, as it was read while the document's bytes stand: a document that stops stating them is an integrity problem (`analysis_source_changed`), and `--fail-on stale` fails on it. Numbers with no document are recorded, and every reading of them says they were typed. - `--reads ` names what it read (repeatable or comma-separated), and `--input :` a resource it read. What it rests on is traced every step back, through the analyses it read and the inputs of the runs they read: a retracted run, analysis or resource anywhere upstream is an integrity problem (`input_retracted`), and an analysis it read that a later one superseded is `input_superseded`. - `--method`, `--unit` (what it resampled), `--units` (how many), `--draws` and `--level` (0.95 by default) say how the intervals were made. When the experiment declares `population.unit`, an analysis that counts more units than the runs it read form is an integrity problem (`units`). - `--supersedes --why "..."` replaces an earlier analysis; its readings stay as history. `--produced-at` says when it was computed, when earlier than now, or `unknown`. `--command` records what computed it; rb did not run it. The same comparisons, files and reads recorded again are not written twice. What a finding reads when an analysis and the runs both report its comparison is under **Reading a finding on its axes**. ### Resources A resource is a dataset, checkpoint, code, environment or artifact that runs read or make. It keeps what it was after its bytes are gone: ```sh rb resource add imagenet-val --type dataset --name "ImageNet-1k validation" --url https://www.image-net.org/ --at s3:s3://my-bucket/imagenet-val rb resource add int8-ckpt --type checkpoint --name "ResNet-50 INT8" --file ckpt/int8.pt # hashed now, and recorded as its first location rb resource locate int8-ckpt wandb my-team/resnet50/int8-ckpt:v3 # another place its bytes are, or were ``` - **Identity** is as exact as what was recorded, and rb says which (`identity.level` in the JSON): `exact` when `--sha256` (or `--file`, which hashes it) or `--digest` names the bytes, `version` when only `--version` names a release, and `name` when nothing does, which every surface says as name only. rb never fills in the rest: two copies of a name-only resource cannot be told apart. `--subset` says which part (`"training split, images 0..199"`). - **Locations** are any number of places, with any provider: `local`, `url`, `git`, `wandb`, `mlflow`, `s3`, `gcs`, `hf`, `runpod` or `other` (`--at PROVIDER:REF` when adding, `rb resource locate ` after). A location can vanish; the resource stays. - **How to get it again**: `--source ""`, `--url`, and `--recipe ""`. - **Availability** is computed on every read, never stored: `present` (a local copy is here, with the bytes the identity names when it names them), `changed` (a local copy is here, with other bytes), `remote` (recorded somewhere rb does not reach on its own), `reconstructible` (no copy recorded, but how to get it again is), or `lost` (none of those). Deleting a 100 GB copy never loses what the benchmark was. `rb show ` prints its identity, each location with what rb found there, how to get it again, every exposure, every run that read or made it, and the analyses that read it, directly or through what they read. `rb show ` and `rb status` say what it takes to run an experiment again: the code (the commits its runs were recorded at), the environment, the machine (from `--host`), each resource its runs read, and the files they named, each `ok`, `partial` or `missing` with the facts behind it. A run made before it was recorded (`--produced-at unknown`, or the time given with `--produced-at`, or the earliest of the journal's first sighting, git's first commit and the last write of the file its numbers were read from (`--from`), more than an hour earlier; never an `--artifact`, a file the SDK keeps with a run, nor the journal's and git's sightings of bytes a run here already recorded, which a second execution (`--again`, `--repeats`) wrote again, though they date its output) has the recorder's code and machine on its receipt: its environment is `unknown`, and its code is the commit it names (`--commit`, `--tree`, as the run `rb reconcile` proposes from a watched command names them), or `unknown` with none. ### Exposures: who has looked at the data `rb resource expose inspected|selected|evaluated|trained --why "..."` records that someone looked at a dataset in a way that can shape what they declare next: inspected its cases, selected on it, evaluated on it outside a run, or trained on it. `--at ` gives an earlier time, kept as reported, and `--at unknown` records that nobody knows when. A run that reads a resource in an evaluation role (`--input imagenet-val:eval`; a role starting with eval, test, val, bench, holdout or score) exposes it too, in any experiment. A criterion declared after its evaluation data was exposed reads as declared after the data was seen, and one whose data was exposed at an unknown time reads its order unknown. `rb show ` lists every exposure. ### Reading a finding on its axes `rb` computes a criterion's finding from its runs and analyses every time it is read. There is no single word for a finding. It reads one comparison (the subject against the reference on a metric, as its estimand says) and says what it found on each axis: - **comparison**: the current reading, with its interval, its units and where it came from (`(rb)` for rb's reading of the runs, or an analysis's id): `change.val_loss: +0.025 [+0.016, +0.034] nats · paired difference, t interval, 4 seeds, 95% (rb)`. - **result**: what the reading shows, in the metric's direction: `gain` or `loss` (the interval clear of 0 on the better or the worse side), `unresolved` (the interval includes 0), `equivalent` (inside the margin of an `--equivalent` rule), `point only` (no interval yet: rb gives one from 3 units), `measured` (a level with an interval), `higher` or `lower` (the catalogue gives the metric no direction), or `untested`. A missing interval is not a zero-width one, and an interval that includes 0 is not evidence of no difference. - **rule**: whether the declared rule is met on that reading: `criterion met` (`estimate meets criterion`, or `recorded values meet criterion` for an `--each` rule, when the reading has no interval or crosses the threshold), `criterion not met`, `criterion undetermined` (it needs an interval, more units or the metric's direction, a cited number's settings differ from the source's, or its evidence exists and is not read), or `criterion untested`; `rule_outcome` in the JSON is `meets`, `misses`, `undetermined` or `untested`. A gate reads `gate met`, `gate missed`, `gate undetermined` or `gate untested`; a cited criterion, `reproduced` or `not reproduced`, and `reproduced on the estimate` (`reproduced on the recorded values` for an `--each` rule) where it is met on a reading with no interval or one that crosses its line, as an own rule reads `estimate meets criterion` there. - **timing**: whether the criterion was declared before what it reads: `declared before the evidence`, `declared after the evidence existed`, `declared after the data was seen`, or `order unknown` (below). - **units**: how many independent units the reading rests on, what a unit is, how many runs and repeats they came from, and the declared `population.*`. - **integrity**: every problem with what the finding depends on; `ok` when there is none (below). A setting it reads that is not verified (provisional, optional and unknown, or from a line that postdates a run) is a note, not a problem, and the line then says so instead of `ok`: `settings it reads are not verified, as said above` when the note is the one said once for every criterion of its experiment. So does a run it counts whose own config says its code had uncommitted changes (`a run it counts ran on code with uncommitted changes, as said below`), and runs no config was compared for (`no run config was compared, as said above`, or several in one line); `rb status`, which hides `ok`, leaves that one to its note. `--json` keeps `ok` with the notes under `caveats`. - **review**: the latest decision on the criterion, and whether it was made on the reading as it stands (`made on an earlier reading` when the reading has moved). - **history**: earlier readings of the same comparison that the record still holds, each with why it was replaced: under an earlier spec, superseded for a measurement, or replaced by a newer analysis. A run retracted whole, or replaced with `--replaces`, leaves no earlier reading here: `rb retract` says which findings it changed, and the log keeps the retraction. The headline is the rule and the result: `criterion met · loss`, `criterion not met · unresolved`, `gate missed · point only`, `criterion untested · untested`. A reading that pools several values of a per-run setting says so after them, with each value whose runs alone would miss the rule: `reproduced · measured · pooled over 4 valset values, not reproduced at valset=imagenet-v2`; such a met rule is never drawn as a clean pass. A rule is judged on the same reading and never borrows another comparison's conclusion, so `criterion met · loss` is an ordinary finding: a loss inside what the rule allows. `rb show ` prints every axis and every observation (each run's number, its unit, and whether it counts, with the reason); `rb show --json` gives the finding in `data.finding`, with `says` (one phrase per axis, the words every surface prints, which name no writer: who declared the criterion and who made the call are `declared.by` and `review.by`), `comparison`, `result`, `rule_outcome` (and `missed_units`, the units an `--each` rule found outside it), `timing`, `units`, `integrity`, `caveats` (notes that are not problems), `review`, `history` and `observations`. **How rb reads the runs.** Each run gives the difference subject minus reference (both arms in one run, or `change.`), or the subject's level when there is no reference. A repeat is averaged with the run it repeats, cases are averaged within a unit, and units are averaged; from 3 units there is a 95% Student t interval, labelled as rb's. Arms recorded in separate runs pair up by their unit, and arms that never share a unit are compared unpaired (Welch), from 3 units in each. A reading that pairs arms recorded apart says what it pairs them by, in every view (`paired_by` among the notes, and `pairing` on the comparison in `--json`: `by`, `declared_unit`, `said`, `unpaired` and `command`): the declared unit (`paired by config, the declared unit: each config's with-skill run against the same config's without-skill run`), or, with none declared, the per-run values its pairs share that tell them apart (`paired by seed: each seed's int8 run against the same seed's fp32 run`), and the reading counts its units by that name (`3 seeds`). Beside it is the arms' reading unpaired, its estimate in the digits the rule needs, as the paired reading's is (a float's last-bit noise adds none: `-0.65`, never `-0.6500`), each arm's units apart, over all the arms' runs (those the paired reading leaves out, a unit one arm alone ran, too, and a run that gives both arms read as each arm's number: one unpaired figure for one comparison, never a second reading of the paired runs alone; where a run gives only the difference, `change.`, no arm's mean is over all its runs, and no unpaired figure is given), left out where it says exactly what the paired reading says, estimate and interval, and, with no unit declared, the command that reads each run as its own unit (`rb spec set population.repeat seed --no-verify`; where the pairs share several values, which of them only numbers repeats is the researcher's to say: declaring that one leaves it out of what pairs them, and declaring them all reads each run as its own unit), offered only for the values that could be counters (whole numbers), never for a setting the rule selects on (`--where seed=1` makes seed a condition of that rule), and said to be for a counter only: pairs that share a model or an eval set get no such command. Whether runs that share a seed or an epoch are linked is the researcher's to say: rb pairs by what the record declares and says so, and never decides it. Arms in one run (a run that gives both arms' numbers) are paired by that run, and say nothing more. A run is shown and not counted, with the reason, when it is edited outside rb, recorded under an earlier spec, run with a config that disagrees with the spec, missing the value of the declared unit, superseded for this measurement, or, where the arms pair, in a unit that has no run of the other arm, or one arm's run in a unit where a run gives the difference by itself. **When an analysis reports the comparison too.** An analysis that reports the same subject, reference, metric and estimand is read in place of the runs: the newest such analysis is the current reading, and rb's own reading of the runs stays in history (`rb's reading of the runs, replaced by a2, which re-reads these runs (no new runs)` when the analysis read every run rb counted). An analysis superseded, retracted or replaced by a newer one stays in history with why. An analysis edited outside rb is not read: what rb wrote stays in history (`as rb wrote it; edited outside rb since`), the finding lists it as an integrity problem, and the reading falls back to what else reports the comparison; when nothing else does, the rule reads undetermined, because its evidence exists and is not read. A `--each` rule is about every unit, which only runs give, and an analysis that reports a difference of means does not say that: the rule keeps reading the runs, and the analysis is said beside it, as a cited figure is (`analysis_beside`: `boot reports change.top1: -0.6 [-1.4, +0.2] ...; an --each rule reads each unit, so rb reads the runs, with the analysis beside them`). **Timing.** A criterion is declared when it is written, by whoever wrote it. Its timing compares that moment with when its evidence existed, read on its subject's runs (the arm the rule makes a claim about): each counted run's production time, or, for an analysis, its own and those of the runs it rests on, each no later than when it was recorded (a time with no offset is read as UTC). Evidence the criterion could have been fitted to counts here even when it no longer counts toward the reading: a run of the same comparison under an earlier spec, a superseded measurement, a run whose config disagreed, a run the paired reading leaves out. It also compares it with when the data the evidence evaluates on was seen: an exposure, or a run of any experiment that evaluated on the same resource. A production time of the evidence it reads, or an exposure, given as `unknown` leaves the order unknown (`timing_unknown` in `rb status`), but for a run recorded before the declaration: it was produced by then. `declared before the evidence` says what the order rests on: `by record times`, `by production times`, or both. When any of that evidence predates the declaration, the timing says how many of the reading's units were produced before it (an unpaired reading counts both arms' units, as its n does; a paired one counts a unit by its subject's run), and gives the reading of the units known to have been produced after it. A run that gives only the reference arm's number is left out of what the timing reads, so a rule declared before any run of its subject reads `declared before its runs`, even when its baseline's runs came first (`its baseline's runs came first`; `its control's runs came first` when its reference is the control, and `the runs it compares against came first` when it is another arm). **Integrity.** Each problem carries a code and a sentence, and every one is local to the finding: - `edited_outside_rb`: a record it depends on (the criterion, its experiment, its metric's catalogue entry, the runs and analyses it reads or could read, the decisions it reads) was edited, deleted or written outside rb, or `log.jsonl` was rewritten; - `shallow_clone`: a record it depends on is not what rb last wrote, and the clone is shallow, so git lacks the history that shows how its file got there (a merge may have made it): the whole history shows how (`git fetch --unshallow`; in GitHub Actions, `actions/checkout` with `fetch-depth: 0`), and a record both sides changed before a squash merge then reads as edited outside rb, since a squash leaves no merge to read; - `conflict`, `stale`, `unknown`, `rejected_setting`, `inherited_without_parent`: a setting on its experiment whose source states another value or no longer states it, a required value nobody has found, a rejected value, or a value marked inherited with nowhere it came from; - `confound`: a setting that differs between the two arms it compares, neither declared in `varies` nor accepted; - `drift`: its frozen experiment no longer matches the freeze record; - `cited_unchecked`, `cited_source_changed`, `not_comparable`: a cited number rb did not check, whose source changed, or whose settings differ from what the source used; - `analysis_source_changed`, `input_retracted`, `input_superseded`, `units`: the reading's analysis and what it rests on (above); - `arms`: a role names several variants; name the one it means; - `design_cell_unknown`: a run gives no value for a declared design factor, so rb cannot place it in its design cell; - `unit_separates_arms`: the declared `population.unit` is the setting that separates the criterion's two arms, as a merge of two branches can leave it (under **Units**); - `experiment_retracted`, `retracted`, `state_newer`: its experiment or the criterion itself was retracted, or records here only a newer rb can read. A question's reading and a declared relation carry problems of their own beside their criteria's findings, and change no finding. A question link's: `question_retracted` and `question_link_retracted` (the question, or the link, was taken back), `question_link_outdated` (the question was reworded after the link was declared), `question_link_ambiguous` (a merge left two active declarations), and `criterion_retracted` or `experiment_unavailable` (its criterion was retracted, or its experiment is missing). A relation's: `relation_retracted` (withdrawn), `relation_outdated` (a side's comparison scope changed after the alignment was declared), `relation_scope_invalid` (a side's scope cannot be read), `relation_target_missing` (the other criterion is missing), `relation_ambiguous` and `relation_conflict` (two declarations a merge left, or two that disagree), and `criterion_retracted`, `experiment_unavailable` or `experiment_retracted` for a side. `rb status` gives the agent an item with the repair wherever a live link or relation cannot be read (below). The notes (`caveats`) are not problems, and say what a reader should know: a provisional or vouched setting, numbers typed in, runs not counted and why, repeats, a borderline estimate, an interval that crosses the rule's threshold, a config never compared with the spec, a per-run setting that may be the unit, two arms read from runs that share no value of a per-run setting (`arms_differ`: `the arms differ in images: int8's runs hold 500, fp32's 250, so the comparison carries that difference too`, a variant compared with itself named by each arm's selection (`the runs of int8 at batch=64 hold 1, those of int8 at batch=32 hold 2`), or within a unit whose two arms hold different values; the unit, a design factor and a setting the rule selects by alike are no such difference, and one the arms select apart is one only where the arms are different variants too), a per-run setting whose values the reading pools (`pooled_setting`, which names `--where` first as the way to read one value, and `population.repeat` only for a setting that could be a counter at whose values the rule misses nowhere (under **Units**); two arms recorded apart, each at one value of it, pool nothing, and say only that they differ; never a declared repeat index, nor a setting the declared unit is made of, one each unit of several runs holds a single value of, as a config holds its model), each of those values whose runs alone would miss a threshold rule the pooled reading meets, where that value holds a reading of its own, at least the rule's `--min-n` units and never fewer than two (`pooled_misses`, with what those runs read: for an `--each` rule its units outside the rule and the farthest of them, `all 3 units below 75, lowest 74.21`, else the estimate there and how many units it rests on), and a run it counts whose own config says the code had uncommitted changes when it ran, in the config's words (`uncommitted_code`: `run3k8: results/r03/config.yaml:4 says dirty: true: the code had uncommitted changes when it ran`). An arm difference in a per-run setting is a note and not an integrity problem as a shared setting's undeclared difference is: a per-run value is chosen run by run, rb cannot tell a sample size meant to differ from one that should not, and no call clears a per-run setting; the note is said wherever the finding is, a page included. **The gates.** `rb status --fail-on ` exits 1 when: - `unmet`: a primary criterion misses or is undetermined; - `unresolved`: a primary criterion comparing two arms reads unresolved or point only; - `untested`: a primary criterion is untested; - `integrity`: any finding has an integrity problem, a criterion's question link cannot be read (its question was reworded, retracted, is missing or was edited outside rb, or a merge left two declarations), a declared relation cannot be read, or, for the whole project, any record was changed outside rb; - `late`: a primary criterion is not shown to precede what it reads: declared after its evidence existed, after its data was seen, or in an order nobody knows; - `undecided`: a call is still to be made (usually the user's): a primary or secondary criterion that misses (or is undetermined for another reason than its units, its interval or the metric's direction) with no decision on it, a missed gate with no direction call made on the reading as it stands, or a decision on a criterion or an experiment made on a reading that has moved (a past call is not: it was made on no reading rb holds); - `stale`: a setting's source is stale or in conflict, a cited source or an analysis's document changed, something an analysis rests on was retracted or superseded, a frozen experiment drifted from its freeze, or a source a results folder's manifest froze has changed; - `checks`: an audit file in a results folder says it failed, or a check the repository registers failed on its last run under rb's watch; - `unknown`: a required setting is unknown; - `unestablished`: any of `unmet`, `untested`, `integrity` or `late` (the name predates the axes vocabulary: not shown to meet its rule cleanly). Give several at once (`--fail-on unmet,late`, or the flag repeated). A record only a newer rb can read fails every gate, and an edit made outside rb that a finding depends on fails any `--fail-on`. In CI, check out the whole history (`fetch-depth: 0` with `actions/checkout`): rb tells a clean git merge from an edit made outside rb only through git's history, and a shallow clone cannot show it one. ### Freezing, amending, deciding, retracting **Freezing.** `rb freeze -m "..."` records a hash of everything that decides what the experiment's criteria read: - its variants and what it varies; - every setting's value, whether it is required or per-run, where its value comes from, and the value a cited source used; - every criterion's whole rule: what it compares, the rule, its role, its `n` and noise, and its active question links and interpretations; - the catalogue entries the criteria's metrics use. It prints each rule it locks, with the user's words it was declared on, and says each rule an agent declared is locked now, and that a change to it needs `--amend`; it names no writer. After that, changing any of them fails with `E_FROZEN`; the same command with `--amend --why "..."` makes the change anyway, by whoever makes it, and the change, the reason, who made it, the user's words when given (`--words`) and the hash before and after are kept as an amendment. Retracting a criterion, a setting or a variant of a frozen experiment is recorded as an amendment. Verifying a setting changes nothing. Runs recorded before the freeze are listed on it; each criterion's timing says which evidence existed before it was declared. A frozen experiment that no longer matches its record (a metric redefined through the catalogue, say) is an integrity problem (`drift`) of its findings until it is adopted as it is with `rb freeze --amend --why "..."`. Freezing a frozen experiment again fails with `E_FROZEN`, whoever asks. **Deciding.** `rb decide --why "..." [--revisit ""] [--past [--when "..."] [--source [:] --quote "..."]]` records a call together with the readings it was made on: - `accept`, `reject` and `investigate` rule on a criterion (its finding), an experiment, a question, or a setting (`/`). Accepting a setting vouches for its value, or, when it differs between variants, accepts that difference, and lapses when a value changes; rejecting it makes it an integrity problem until the value changes; a setting is accepted or rejected, never investigated. A call may accept a criterion that misses its rule: `rb decide` says the call goes against the rule and records it as made, and `rb status` shows both; History, `rb open` and a page say what was accepted: `Made a call: accepted c1 against its rule (criterion not met)`. A call on a rule rb cannot judge yet goes against nothing: rb decide says it accepts the rule unjudged, and History says `accepted c1 while rb cannot judge its rule (criterion undetermined)`. - `continue`, `park`, `stop`, `pivot` and `ship` steer a line of work, on an experiment or a question only. A missed gate is settled by such a call on its experiment, or on a question it answers or one above that: the questions the gate is linked to, or, when it has no link, the questions its experiment is filed under. The call settles the miss as the gate read then: once the gate misses on another reading, `rb status` asks for a call on it again (`gate_missed`), and `rb show ` names the gate beside the call that settled it (`settled_gates_moved` on each call in `--json`). - Anyone may record any of these, and each is recorded with who recorded it, the chat session, the reason, and the words given with it (`--words`). They are the user's: when the user makes a call in the chat, record it with their words verbatim, and do not record one they did not ask for. **Whose call it is.** rb works it out on every read, from who recorded the decision and a confirmation's log line; nothing about it is stored. - A call recorded outside an agent session is made. Whose call it is rests on whom rb wrote it for (its `log.jsonl` line), so a `by:` edited in a decision's file makes nothing the user's: that decision is listed only as edited outside rb. - A call recorded in an agent session is **proposed, not made**. It changes nothing: questions, experiments, criteria and settings read as if it were not there. rb lists it apart from the calls and never under **Your call**: `rb status`, `rb context` and `rb page` under **Proposed, not made**, each with its subject, action, the reason as given (a session's as its note, `· note: ...`) and the one command that confirms it, and, when more than one waits and no design does, the one `rb confirm ...` that confirms them all as listed (a design is read before it is confirmed: `rb confirm` lists it, and ends with the one command that confirms all it lists, as listed); `rb open` and `rb ui` in a quiet group of their own. - `--past` records a call made before it was recorded, with `--when ""` as said (unknown without it). It reads Retrospective, dated as given, and comes before every call recorded as made when it was recorded. One recorded in an agent session is a **past call mentioned, not confirmed**, listed apart as a proposed call is, whoever ran `rb init` and whatever document it cites: the session that reconstructs the past cannot attest it, so it waits for one `rb confirm`. `--source [:] --quote ""` gives the document that states it; rb refuses a quote the file does not hold. Until it is confirmed it reads in the file's voice: the document's words verbatim with its file and line (`docs/calls.md:3: "We park INT8 for now"`), and the reason a session gave only as a note on it (`· note: ...`), never as the user's call and never under **Your call**. Beside it rb says what git shows of the document, as a fact: `the quote is in docs/calls.md:3, committed 2026-05-07 (), before the record began` (adding that the file may have been written in the chat that cites it, where the record began there), or why rb cannot tell: a quote that states no call (four words at least, naming the subject: its id, or a word of its text, where a setting written as `P=4` is one word), a file git does not track, a quote first committed after the record began or outside the history it began on (rb init keeps that commit), or changed since. The user confirms it with that beside it. A past park or stop on an experiment says how many of its runs were produced after the day the call gives, and which (`runs_after` in `--json`), wherever the call shows. A record an older rb began keeps no commit; there rb goes by git's dates, which whoever commits can set. A past call recorded outside an agent session is the user's, as any call they make. - `rb confirm ` confirms one, and several ids confirm each, as listed; `rb confirm` alone lists what waits, and ends with the command that confirms everything it listed, each by its id or hash, so a call added or a design changed after the listing is not confirmed unseen. `rb confirm --all` confirms every one as it reads when it runs, after saying each (the calls, then the designs, each with its gate). Both leave a subject more than one call waits on, and `--all` names them: which is theirs is the user's to say. rb confirm says what the call is made on now, and where a reading moved since the call was recorded. A past call was made before any reading rb holds: rb decide and rb confirm say each as what it reads now (`c1 reads now: ...`), `rb show` as read when it was recorded, never as what the call was made on, and when one moves, the call says its readings have moved since it was recorded. That asks nobody to decide again: a past call raises no `decision_outdated`. A confirmation records the same call (its subject, action, reason, revisit, words, and when it was made) as whoever confirms it: an ordinary decision whose log line names what it confirms (`detail.confirms`), so the call and its words are theirs. Its reason stays the session's note on it (`· note: ...`) on every screen and in rb's text: the user confirmed the call, not the reason a session gave for it. It runs only outside an agent session; in one, or where one left its session id, it is refused (`E_USAGE`), since a confirmation an agent passes makes nothing the user's. That is what rb can check: it runs only where rb sees no agent variable, which keeps confirming out of an agent's own commands, but it does not prove the user typed it (a process an agent starts with every such variable cleared looks the same). rb confirm's listing and its receipt name the notes on the experiment that stay not confirmed (a note is not part of the design), and how the user says one in their own name (`notes_left` in `--json`). On `rb ui`, `c` confirms the highlighted one once that group has focus and its card is shown (the first `c` shows it); one a write changed since it was shown is shown again, as it reads now, before `c` confirms it. A proposed call waits no more once a call is made on its subject after it; to withdraw one, retract it. - In `--json`, over MCP and from Python, each decision carries `authority`, computed: `state` (`direct`, `confirmed`, `proposed` or `past_unconfirmed`; `documented` is no longer given), `basis` (what it rests on), `pending`, and `confirms`, `confirmed_by` or `answered_by` where they apply. `rb status` gives the calls under `decisions` and what waits under `pending` (`counts.pending_count`), and `rb show ` gives `data.authority`. An experiment's state, and a question's, is what the latest decision on it made it (above); a finding alone never changes a state, and a decision never changes a reading. Once any criterion in the record is linked to a question or related to another criterion, a new question call keeps its directly linked criteria, their interpretations, the question's text and the integrity of that reading. Descendants and criteria assigned to other inquiries stay context. Changing that scope, interpretation, relevant finding or integrity makes the call read "made on an earlier reading" (`decision_outdated`). An unrelated result in the same experiment does not, and neither does a note or a sentence rb words differently, or a move of the question in the tree. With no direct links, rb says that the call has no directly linked evidence and records it as made. In a record where no criterion is linked or related yet, a question call is made on every criterion filed under the question or a question below it, and on what their repositories say, the way earlier rb versions record it, so they still read the record. Such a call, an earlier one included, reads "made on an earlier reading" when one of those readings moves or a new one appears, and once a criterion is linked to its question. A call made while nothing was filed under its question reads that way once a reading appears, but it asks for a new call (`decision_outdated`) only once a criterion is linked to its question. Calls on experiments and criteria still keep their own findings. A run, a metric, an analysis or a resource takes no decision: to stop one counting, retract it. A decision file edited or written outside rb is nobody's call: it is not read (no vouch, no state, no settled item), every finding that would read it lists it as an integrity problem, and it cannot be adopted. **Retracting.** `rb retract --why "..."` takes anything back, whoever wrote it: a record (a decision and a note too), a setting, or a variant (`rb retract int8/fp16`); a name `--varies` declared that has no setting of its own (one mistyped there) is taken back from what the experiment varies (`rb retract int8/precison`), an amendment where the experiment is frozen. An agent session cannot take back the user's call (one they made or confirmed) or a note they wrote (`E_USAGE`): it proposes another call instead, which waits for one `rb confirm`, or adds a note. It stays on record, attributed to whoever took it back and why (with the user's words when given), and stops counting; rb says what rested on it (`data.rested_on`: for a question, the criteria linked to it; for a criterion, the questions it is linked to and the relations read through it, from either side; for an experiment, its runs, and its criteria's question links and relations, from either side) and which findings the retraction changed (`data.finding_changes`). `--scope` supersedes measurements of a run (under **Runs**). A retracted experiment is an integrity problem of its criteria and takes no more settings, criteria or runs; its criteria stay in their questions' readings with that problem. Such a criterion takes no more writes either, so `rb status` gives the agent `experiment_retracted` for each, with the one repair it takes, `rb retract --why "..."`, and no other repair of the agent's for it. A retracted criterion leaves the readings of the questions it is linked to, and its links are kept there as withdrawn context (`withdrawn` in the JSON). ### Reading the record `rb status []` prints, in order: 1. the project, and the last `rb reconcile --apply`: when, and what it changed in the research; the project by its title and its id, said once where they are the same; under the title, when anything the record holds was recorded afterwards rather than as it happened (a run made before it was recorded, an analysis, a past call, a note a clone brought), the record line (`Kept with rb since 2026-10-10. 2 runs and 1 call were recorded afterwards, not as they happened.`, each kind with its count: `1 run`, `both runs`, `all 9 runs`), and the same quiet line under the title of `rb context` and in `rb ui`'s head; a page and `rb open` say the same sentence, with the day as `10 Oct 2026`; where nothing was recorded afterwards, they say `Kept with rb since it began.`, but where the record holds work from before it began they say which (`Kept with rb since 10 Oct 2026. It began as a clone of another record.` for a clone of an address, `1 experiment reproduces an experiment of another record.` for experiments related to another record's as `rb clone` relates them, `1 experiment was read from its results folder.` for experiments `rb reconcile` read from results folders), and on a record with no experiment they say nothing; `record_start` in `--json` keeps when the record began and how many of its runs were made before it; 2. where each question stands, as a tree read from the record: its directly linked findings and attributed interpretations, descendants' readings and unassigned criteria kept distinct, each criterion on its axes, and the declared population; for an experiment nothing reads yet, its hypothesis and the decision it gates; for one read from a results folder, what its files say (results and next steps, verbatim with file and line) and where its checks, audits, manifests, reproduction guide and git stand; each experiment's latest note; and the calls made on the question's work, with the user's words when an agent acted on them; 3. the tally of the criteria by rule, which never hides a reading's outcome: of the rules met, those met on the estimate alone (no interval, or one that crosses the rule's line) are said with them (`Rules: 5 reproduced, 2 of them on the estimate alone`; `tally_on_estimate` in `--json`, beside `tally`), on `rb status`, the page's overview, each question's tally and each experiment's row alike; 4. **Needs judgment**; 5. what waits for the user's confirmation: **Proposed, not made**, then **Past calls mentioned, not confirmed**, each with the `rb confirm ` that confirms it, then one line for the experiments whose design waits, which `rb confirm` lists; 6. **Can be done now**, then **Facts that ask for nothing**: the items that ask for no command; 7. what rb found in the repository that nobody recorded, when there is any; the changed .json, .yaml and .toml files nothing in the record cites, in one line, when there are any; and the scope line: what rb looks at, when it last looked, and what it does not read or see; 8. work on this record in branches and worktrees not merged here (**Elsewhere**), when there is any, each named by its branch and, for a worktree, its path, verbatim as git gives them, and its commit (`branch`, `path`, `head` and `worktree`, the path's last part, in `--json`), with what it adds by experiment or on the project, and its decisions counted whatever they are on; a worktree removed without `git worktree prune` reads `(removed since)` with what it committed (`gone`), and one holding another project's record (another uid) reads as a separate project (`same_project` false), which the heading names apart from work on this record; 9. each criterion on its axes, with its integrity problems and notes, then the retracted criteria; a note every criterion of an experiment carries is said once, under `On every criterion:` before its first (as on `rb context` and `rb show `), and so does the question's reading (`On every criterion below: ...`, on `rb status`, `rb show `, `rb context` and `rb page`), and `--json` keeps it on each finding and sentence; 10. each experiment with its state, variants, freeze, setting counts, runs and analyses, and what it takes to run it again; 11. the runs, and whether every receipt is still what rb wrote; 12. the analyses, and which supersede which; 13. the notes on the project; 14. each resource with its availability and how exactly it is identified. `rb status --json` says how rb knows of what the record holds: `witness` on the data (when it began; of runs, analyses, calls and notes how many stand and how many were recorded afterwards), and on each run, analysis, call and finding (`seen`: `as_it_ran` or `afterwards`, with its `basis`). A long list names its first ones and counts the rest, with the command that lists every one (`... and 12 more experiments: rb show reads one, and rb status --json lists every one`): a question's reading five lines, the items for a human and each section of calls waiting ten, the criteria, experiments, analyses and resources twenty, and many results folders no experiment reads one line that names three; a criterion's notes are cut to the first few, with `rb show ` for the rest, but a note its integrity line points to (`as said below`) always stays in view. `--json` keeps every item. It ends with the next steps (`Next:`, `next` in `--json`): in the user's own terminal, while anything waits for their confirmation, `rb confirm` first (the one call's `rb confirm `, or `rb confirm`, which lists what waits), then what needs judgment, then what can be done now; an agent session's never names `rb confirm`, which only the user runs. **What changed since you last looked.** In an agent session, every command on the record ends with what changed in the research since that session last looked: in `--json` and over MCP as `rb_delta`, a list of facts each with its `kind` (`state` or `write`), `code`, `subject`, `text` and, for a state, the command that looks at it (`do`) or, for a write, who made it (`by`); in text under `Since this session last looked:`. A write's text says what it changed, with its reason and the user's words, never who made it. The facts are the ones that must not be silently forgotten: results that landed and are not in the record (`unrecorded`), a changed file nothing in the record cites (`uncited_change`), a results folder not compiled or changed since (`unindexed_folder`, `reindex`), a check or an audit that failed, a manifest whose sources moved, a decision made on a reading that has since moved, a call made while its line's own check fails, a reading that now misses or a gate missed, a record edited outside rb, a source that changed, and what anyone else (the user, another session) wrote; and last, while no model is recorded for your writes, how to say it (`model_unsaid`: its `do` is `rb context --model `, or `RB_MODEL= on each command` in a session with no id, and the briefing repeats it each time until a model is recorded). Each is told once per session (a fact that clears and comes back is told again); a session's first command is told what is outstanding, never the history before it. `rb status` and `rb context` count as told only what they print (the open items they name, and the log lines they show: rb status's last reconcile, rb context's Recent activity); what they only count is told on the session's next other command, under `Counted before, named now:` (`counted` on the fact). The open items come first, then the writes, then what rb found and the changed files nothing cites: five of each by path, and the rest on one line each with its `count` (`unrecorded_more`, `uncited_more`), told once. A command tells at most 12 facts; the rest wait for the session's next command, which tells them. A command rb runs inside another (the `rb run add` that `rb reconcile --apply` runs for each run it records) tells nothing of its own: the enclosing command tells it, and so does a write beside a note's file that does not read, which tells what it would tell were that note readable, and that the note's file changed outside rb. It says what changed and where, never what research to do. The session's seen facts live in `.rb/cache/sessions/` (local, gitignored); the user's own terminal gets none, since `rb status` is theirs. Every item in the two lists carries `who`, a code, and the exact command. `who` (`human`, `agent` or `anyone`) says who usually acts, not who may: anyone may run any of the commands but `rb confirm`, which runs only outside an agent session. Calls on results and on lines of work are the user's (`undecided`, `gate_missed`, `decision_outdated`, `investigating`, `retracted_by_agent`, `call_on_failed_check`, `call_proposed`, `past_call_unconfirmed`): tell the user, and record a call only when they ask for it, with their words. rb lists them under **Needs judgment**, but for a call that waits for the user's confirmation (`call_proposed`, `past_call_unconfirmed`), which it lists in a place of its own, with `decision` (its id) and the command `rb confirm `; those set no gate. Items on settings, and the rest of the work, are the agent's: `untested`, `inherited`, `too_few_units`, `no_interval`, `direction_unknown`, `late`, `timing_unknown`, `units`, `arms`, `input_retracted`, `input_superseded`, `cited_unchecked`, `provisional`, `unknown`, `unknown_optional`, `source_changed`, `stale`, `conflict`, `confound`, `inherited_without_parent`, `rejected_setting`, `unread_metric`, `unread_comparison`, `planned`, `open_question`, `reindex`, `unindexed_folder`, `check_failed`, `question_link_outdated`, `question_link_unavailable`, `question_link_ambiguous`, `finding_relation_invalid`, `relation_ambiguous`, `relation_conflict`, `unassigned_criterion`, `unit_separates_arms`, `experiment_retracted`, `command_later`, `file_written_after`, `file_run_time_unknown`, `writer_words`, `attributed_words`, `no_question` and `row_in_several_experiments` (one row of a database read as a run in more than one experiment, naming each run and experiment; it asks for nothing). Drift, changed cited sources, documents and manifests, audits that say they failed, edits made outside rb, records a merge changed, records a shallow clone cannot account for, an id two branches gave two records, and state from a newer rb are anyone's (`drift`, `cited_source_changed`, `analysis_source_changed`, `manifest_changed`, `audit_failed`, `edited_outside_rb`, `merged_outside_rb`, `shallow_clone`, `id_collision`, `state_newer`). **Can be done now** lists the agent's items and anyone's; work from it. An experiment's primary criteria declared after their evidence, with no reading of their own, are one `late` item (`criteria` lists them): where no more runs are expected (no gate, and no run made since the record began) it asks for nothing (`do` is empty) and is listed under **Facts that ask for nothing**; otherwise its command names both arms; a reading of its own needs its subject's runs made after the declaration (a rule's timing is read on its subject's runs). `--fail-on` takes the gates above. The other read commands: - `rb show ` prints one record in full: a criterion with its finding on its axes and its observations, an experiment with its design's standing, its spec, notes, relations and what it takes to run it again, a run with its receipt, an analysis with its comparisons and what it read, a decision with the reason, the user's words when given, what it was made on and whether that has moved (who made it and the session are in `--json`), a resource with its identity, locations, exposures and what used it, a setting (`int8/optim.lr`) with its history. For a record changed outside rb it says what changed. `rb show .yaml` reads a record file instead (above). - `rb compare ` prints the experiment's own table: variants by metrics, the mean over the independent units of the runs that count (under the current spec, repeats left out, none run with a config that disagrees with the spec), and the change against the baseline, read in the metric's direction, a change marked where it is the number a primary criterion reads, and said with the digits that criterion's rule takes. Its changes are means; the findings carry the intervals, and every primary criterion has its own reading given under the table, with its rule's digits, whether it reads a change the table shows or another number (pairing by unit, or an analysis). Where a criterion reads a variant against the baseline otherwise than the table's unpaired means (paired by unit, or over runs a `--where` selects), the cell names it and gives no change of its own (`read_by` in `--json`, with `unpaired`, what its means are over): one comparison never shows two numbers. Where one criterion reads the change the cell shows and another reads that cell otherwise (a relative change beside a difference of means), the cell keeps the change and names the other too (`also read by c32`; `read_beside` in `--json`), with its reading under the table. A per-run setting that is a design factor, or that a criterion's `--where`, `--subject-where` or `--reference-where` names, is a condition (the unit never is, nor a declared repeat index, which is never a row of its own here, as in every other reading), and so, with `population.unit` declared, is any other per-run setting that holds more than one value among one variant's runs: where the runs take more than one value of the conditions, the table has a row per variant and cell (`conditions` in `--json`, and each row's `where`), each change against the baseline in the same cell, so no mean is pooled across conditions. A change says what it rests on: `n=1` for one unit, `unpaired` where its units are not the baseline's (`paired` in `--json`), and, for a variant that differs from the baseline in more than one varied factor, which ones (`differs_in`), with no better or worse, since no one factor moved (`basis` holds the words: `+0.743 worse vs baseline · n=1, unpaired`). A criterion's reading that counts runs at more than one value of such a setting, which it neither selects nor reads by design cell, carries the note `pooled_setting`, which names the values. On the page and `rb open` the comparison is a grid: a row per variant, in plain words; a column per set of the other settings rb reads its rows under (two variants under several sets lie on their side: a row per set, then the two variants and their difference); each cell the mean with what it is over (`3 seeds`, `1 run`), and under it the change against the baseline in the same column only, in plain words (`3.60 points higher than plain`), with its interval where the units pair and else why not; a criterion that reads that comparison is linked from it. - `rb log [-n N] [id]` lists the writes: when, and what changed. Its text gives the last N writes (20 by default), each op in words ("add variant", "spec set", "spec check"), and names no writer, and says a session's reason as its note (`note: ...`); `--json` gives the log's last N lines, a session's line among them, each with who made it, how `rb` knew it, through what, in which session and with which agent. - `rb context` prints the whole record as one Markdown briefing. It is what a fresh session, or a colleague, reads first. Its long lists are cut as `rb status`'s are, each with a count and the command that lists every one: of many experiments, the twenty most recently written to (a run, an analysis, a note, a criterion, the spec, a freeze or a variant, or a call on it or on one of its criteria), then the others by id; of an experiment's runs, analyses and notes, the latest ten; of its settings, forty; of the decisions, the latest twenty. `--json` keeps every item. Like `rb status`, it looks at the repository first, and under **The repository** it counts what rb found and nobody recorded and the changed files nothing cites, as `rb status` does, with the scope line. In a session, `rb status` and `rb context` count as told only what they print: what they only count is told once, on the session's next other command. In an agent session, `--model ` (and `--provider `) keeps what you run on for the session's later writes, and the briefing says how to give it while none is recorded; in a human's terminal they record nothing and say so. Where the record cannot keep it (a read-only checkout), rb says so and still prints the briefing. `--json` gives the actor as `data.actor`, with `agent` for an agent. - `rb page [--out FILE]` writes the project as one self-contained, read-only HTML file you can send anywhere. `--out` (and `rb export --zip --out`) never writes over a file rb did not write: one that is neither in the ownership ledger as rb wrote it (under **Leaving rb**) nor, where the ledger has no line for it, an rb page or export by its own marks is refused (`E_WRITE_FAILED`) and left as it is, unless `--replace` is given (`replace` over MCP and in the SDK's `export`). So is a page or an export changed since rb wrote it, and an export folder holding a file rb did not write (a `CNAME` for hosting it). Overview shows the inquiries, their recorded interpretations, the evidence's limits and what needs a call. Tabs separate Experiments, Findings, Runs, History and Resources. A question opens its findings; a finding expands to its comparisons and provenance, every integrity problem in full (never cut to "and 1 more": a page's reader has no `rb show` to open), and every note `rb show ` gives under it, in the same words (numbers typed in, a per-run setting pooled, arms that differ, a cited value not comparable, a run whose config says its code had uncommitted changes); a reading that pools a per-run setting leads with its headline's words for it, on the question's row, the rule's row and its card (`Pooled over 3 valset values, criterion not met at valset=imagenet-v2: ...`); a reading, a rule's number, its tolerance and its margin are said to the digits the rule was written with (0.999349 ± 0.0000005, never 1.00 ± 0.000000), and one reading is written in the same digits on its card, in its list sentence and in the comparison grid; a grid's, a variant's table's and a runs table's numbers are never in fewer decimals than the rules of their experiment read the metric in, as far as the column's values need them (never a trailing 0 those digits add; never `1.00` beside a rule of 0.999349 ± 5e-07, nor `0.33 lower` beside one that reads +0.3289); a tolerance is said as the amount it is (`within 0.05 points of 49.6%`, never `within 0.05%`, which reads as relative); a metric is named as declared (`acc`, `m.acc`, `val_bpb`), never title-cased, split at `_` or `.`, nor called by its unit; a run's card, and its row in a runs table, gives its numbers in rb's own digits, as `rb show ` does (15 significant digits: a float's last-bit noise left out, `49.69`, never `49.690000000000005`, and no digit given cut, `1234.5679`, never `1234.57`); a run whose words another's share is named by where its numbers came from (`run plain · seed 0, from results/r012_plain_s0/`), its results folder where the twins' folders differ, else its file or row, and by when it was recorded only where those do not tell them apart; and a card says its checks are ok only where it has no note, and an experiment expands to its matrix and settings. The embedded navigation script changes only which view is visible, makes no network requests and performs no research calculations. Without scripts the full document remains readable; printing includes every view. It names no writer, none of the user's words, no call that waits for a confirmation and no receipt's platform (the machine's OS string), since anyone may be sent it; `rb open` shows the platform. Findings are computed at write time, so regenerate after new runs, and if the page ever disagrees with `rb status`, `rb status` is right. - `rb open [--port N]` serves the user's live catalogue in their browser, on 127.0.0.1 of their machine only, with the same tabs as `rb page` (Overview, Experiments, Findings, Runs, History, Resources), plus an experiment detail pane and run-by-run comparison, refreshed by recomputing findings. A run opens into its receipt: when it was recorded, the repository's state, what it read and made, its host and environment, with no writer or session, and no branch or worktree but in a place's label (below), and so does the data the tab draws (`/state.json`, `/card.json`); the user's words are said without their tag, as on `rb ui`; a resource says what it is, where its bytes are, and how to get it again; what rb found in the repository shows under **Found in the repository**, as a fact, and what waits for the user's confirmation in a quiet group of its own, with the `rb confirm` that confirms each. The calls waiting on the user are listed once each, as many as the count beside them, by what each asks and the title of what it is on, linked to the deepest question whose **Your call** lists it (`calls` on the home's data, each with its `at`), on `rb open` and on the page alike. It only reads: every POST gets 405, there is no launch key, and nothing is recorded or decided from the tab; the record changes through rb, in a terminal, attributed to whoever runs it. From another machine, bring it over ssh, as it prints (`ssh -N -L 8787:127.0.0.1:8787 `). It is the user's surface like `rb ui`: an agent session is refused, and an agent's surface stays `rb status` and `rb context`. A line under the title says which place it shows, by its branch, else its folder's name, and never by its path, which shows on hover, and in the **Where** list where the clone has more than one place. Where the clone has another worktree, or the record a committed version, **Where** and **Version** show another worktree's record (each worktree is listed with what it is: its own record, one that writes to this record, a separate project, no record, or removed since, with its own counts) or a past version of the record shown (each commit of its branch's own line that changed `.rb/`, a merge among them but never a merged branch's own commits, by its commit and the time, in UTC, it was committed; never its subject or author). While another place or a past version is shown, a bar at the top says so in words and stays in view as the page scrolls (`Viewing exp-c · ~/wt-c · read only` with **Back to main**, or `main · ~/p as of 4364dc8c1a2b · 2026-10-12 14:20 UTC · read only` with **Back to now**), and the tab's title names it before the project's; the place rb open started on, live, has no bar and keeps its title, and on `rb ui` the line under the head turns to the warning colour and says `read only · esc: back to main` before the place's own counts, wrapping rather than cutting. The address's `#` part keeps the tab, the place and the version shown (`#runs&place=&at=`; the place it started on, live, is the tab alone), so a reload, the browser's Back and Forward and a bookmark show them again; one that names a place or a version rb does not list now, or cannot show, shows the place rb open started on, live, and says why. On the home of the place it started on, live, where the clone has another place, **Elsewhere** lists each other place in short, with what rb says of it (its own counts and last write, never added up), what it holds that is not merged here, as `rb status` counts it (a criterion said as a rule, as on every screen), and the calls that wait there and not here, each with the one command that confirms it there (a design, which that list says in one line, with the `cd && rb confirm ` that lists it to confirm, `list` in the data beside its `confirm`); one it can show links to it, one it cannot says why (`elsewhere` in the data). `rb ui` keeps its `w` list, and a page has none. Another place and a past version are only read: nothing is written there, and the repository there is not watched. In another place, a call, gate or design that waits there shows the one command that confirms it (`cd && rb confirm `) wherever the screen gives it (the overview, an experiment's row, pane and card; on a design's row, pane and card, the one that lists it to confirm), as the line that counts the calls waiting on the user there names `cd && rb status`. A past version offers no command to copy: on `rb open` its screens draw no command box, recipe or confirm line and compose no `rb decide` line, what waited then is said in words, and so is its Next step; its data (`/state.json`) keeps the commands as the record holds them. A past version is read from a checkout of its commit that rb makes in `/rb/views/`, running none of the user's git hooks, while `rb open` (or `rb ui`) runs, at most 3 at a time and none of more than 500 MB of tracked files, and takes away when it stops, by Ctrl-C, a request to end (SIGTERM) or its terminal closing (SIGHUP); one left by a screen killed outright is taken away when the next one starts. A run recorded in another worktree says where (`recorded in exp-b · /p/.worktrees/b at 4364dc8c1a2b, with uncommitted changes`, and `(removed since)` once that worktree is gone); at a linked worktree's own record, a run recorded in the main checkout, whose receipt keeps no worktree, says the branch and commit it keeps (`recorded in main at 1cb5fd140e6e`) where that branch is not the place's. A finding that counts one says so once (`1 of 2 runs was recorded in exp-b · /p/.worktrees/b`, with `(removed since)` after a worktree gone since, as the note on code below says it too), on `rb status` too (`observed` on its findings in `--json`); it asks nothing. On `rb open`, where a run was recorded and the place in a finding's note link to that place where `rb open` can show it (listed, with a record of its own, and still on the branch the run was recorded on; `place` on `observed_in`, `observed_places` on a finding): following one shows it, live, and Back returns. One it cannot show stays plain words, and so does a run said by its branch alone: its receipt names no worktree, so rb cannot tell which checkout recorded it. Where the runs a reading counts (a finding, a criterion's reading, a comparison's cell) were recorded in more than one place and did not all run the same code, rb says so from their receipts, beside that note (`1 of 2 runs ran other code: exp-b · .worktrees/b at 4364dc8c1a2b, with uncommitted changes`): a run's code is each file its commit holds, as it was when the run ran: as rb's journal saw it when the command started, for a command it watched make the run's numbers, and otherwise as the run was recorded. `.rb/` and the files the runs name are left out, and so is a file no commit holds, which may be one a run wrote; `with uncommitted changes` means a file the commit holds had changed. Other code is code no run recorded here ran; with none recorded here, code no run of the place with the most runs ran, and where two places have as many, the runs are said by the code they ran (`3 runs did not all run the same code: 2 at 4364dc8c1a2b (1 in exp-b · .worktrees/b, 1 in exp-c · ~/wt-c); 1 in exp-d · .worktrees/d at 1cb5fd140e6e`). It needs every run's receipt to hold the code that made it: a run made before it was recorded keeps the recording checkout's code unless rb's journal watched it, and with such a run, or one whose code rb cannot read (no commit, a commit or tree this clone does not have, or uncommitted changes with no tree that keeps them all), it says nothing about code. `rb status` says it as one quiet line under the finding (`other_code` on its findings in `--json`, and on findings and comparison cells in the data), and a page by commit alone (`1 of 2 runs ran other code: commit 4364dc8c1a2b, with uncommitted changes`). The data carries `place` and `places` (and each run's `observed_in`, each finding's `observed`, with the short forms beside: `label_short` on each place and `observed_in`, `shown_short` on each version, `observed_short` on each finding), and takes `?place=&at=` only as the page lists them: anything else gets 404, and a choice not made on the page itself (sent from another site, or typed as an address) 403. - `rb export [--out DIR] [--zip] [--with-record [--url ADDR]]` writes the project's page, as `rb page` makes it, with a README and `manifest.json` (the sha256 of both), and none of the record's files: no writer, session, branch, platform or user's words travels with it. Publishing the record is a choice, made with `--with-record`, and rb says each time what the record keeps: who made each write (`human:` or `agent:`, and the channel), an agent's chat session, name, version, provider and model, the user's words passed with `--words`, each run's host, platform and branch, and, where a write was made in a linked worktree, that worktree's path (which the bundle's README says too), which anyone who can read the bundle can read. With `--with-record` it writes the project as a portable bundle: the `.rb/` record as it is, under `record/` (minus `journal/`, the lock and `cache/`, but for `cache/commits//`: each source a check reads at a commit, as it stood there, which the bundle's `.gitignore` keeps in git); `record/.rb/merges.json` when a record git merged from two branches rests on it: rb's copies of the versions it was merged from, since a bundle has no git to show the merge; the files its checks read (the sources its settings and cited criteria name); the page (computed from the bundle itself); a README; and `manifest.json` (`format: rb-bundle`, `version: 2`, and `record_format`: 2 when any file of the record is in format 2, else 1) with the sha256 of every file. It names the resources the bundle leaves behind, which a reader cannot rerun with from the bundle alone. A bundle holds at most 512 MB, zipped or unpacked, as `rb clone` takes one: an export that would hold more, or that names a file leaving less than 1 MB of it for the page, README and manifest, is refused (`E_WRITE_FAILED`), naming its largest parts, and what was at `--out` stays as it was. From a shallow clone, while a record reads `shallow_clone`, `rb export --with-record` and `rb sync --with-record` write no bundle and wait for the whole history (`E_STATE_EDITED`, fixed by `git fetch --unshallow`): a bundle has no git to show how that record got there, so it would read it as edited outside rb. The format is open and the bundle is readable without `rb` (Rabbit Brain YAML, JSON and HTML); `rb status` inside its `record/` reopens it and computes its findings from what it carries. It is how a record travels to someone who was not there: a reviewer, a manager, another machine. With `--url` (the public address it will be served from, and only beside `--with-record`: without the record, there is nothing to clone, `E_USAGE`), `page.html` opens with a Reproduce panel carrying the exact `rb clone` command, and a directory export also contains `bundle.zip`, so serving the one directory from any static host serves both the reading surface and the artifact a stranger clones. rb never publishes anything itself: hosting the export is up to the user. `--json` says which it wrote (`with_record`). `--no-repo` leaves the code repository's address out of the manifest and the page. - `rb doctor` in a project with `.rb/` checks the record: who you are recorded as, records changed outside `rb` (a comment or a reformat is not a change), files left mid-merge, files that are not Rabbit Brain YAML or not valid records, a run that names a missing resource, one id on two kinds of record, and sources outside the repository. `rb doctor --restore` puts back what `rb` last wrote, and sets aside a file rb never wrote; `rb doctor --adopt --why "..." [--seen @]` keeps the changes as they stand, recorded as whoever adopts them (above); both leave a record that reads `shallow_clone` as it is. Outside a record it says there is none (`E_NO_PROJECT`). - `rb schema ` prints one record's shape, and `rb schema all` the whole format (above). **When you report to the user from this record:** - quote each criterion's comparison with its interval, units and source, then its rule, its timing, and every integrity problem, as `rb show` prints them; - say which settings are provisional rather than verified, and which are unknown; - name what waits on the user, and every decision made on an earlier reading; - never collapse a finding into one word, and never describe a criterion as settled when its rule does not read `criterion met` (or `reproduced`) with its integrity `ok`. For example: `c1 (primary) change.val_loss: +0.021 [+0.012, +0.030] nats, 4 seeds (rb): a loss that meets its rule (≤ 0.05); declared before the evidence; integrity ok.` ### From Python, and over MCP The same record and rules are there for the code that does the work (`via: sdk` in the log): ```python import rabbit_brain as rb state = rb.open() # the .rb/ here or above; rb.init("...") starts one exp = state.experiment("int8") # or state.add_experiment("INT8 vs FP32 on the held-out set", hypothesis="INT8 keeps validation loss within 0.05 of FP32", baseline="fp32", candidates=["int8"], varies=["precision"]) exp.spec.set("optim.lr", source="configs/train.yaml#optim.lr") # read from the file and verified exp.spec.set("seed", per_run=True) # each run gives its own crit = exp.criterion("INT8 adds at most 0.05 validation loss", metric="change.val_loss", at_most=0.05, min_n=3) with exp.run(seed=2, config="outputs/2/.hydra/config.yaml") as run: ... # evaluate run.log({"fp32.val_loss": fp32_loss, "int8.val_loss": int8_loss}) f = crit.finding() # computed now, on its axes print(f.says["headline"], f.says["comparison"]) # criterion undetermined · point only change.val_loss: +0.03 · paired difference, 1 seed (rb) print(f.rule_outcome, f.result, f.timing, [p.text for p in f.integrity]) # undetermined point_only before [] ``` A run block works like this: - Its numbers are recorded as one run when the block ends without an error, with basis `logged`, dated as the block ended, which rb saw. `exp.record({...})` outside a block takes `produced_at=` as `--produced-at` does; without it, when its numbers were made is unknown (a file it keeps with them, `files=`, is no sign of their time, as an `--artifact` is none), as for numbers typed on the command line, and every reading says so (`rb show` and the finding's timing alike). A record an earlier rb wrote from the SDK keeps no mark of a block, so its time reads unknown too, with nothing in `.rb/` rewritten. - The receipt's command is the script's own command line. - A block that raises records nothing. - The last value logged under a name is the one recorded. numpy and torch scalars are fine. - `exp.run(..., inputs=["holdout-v2:eval"], outputs=[...], host={"gpu": "A100"})` names the resources it read and made (recorded first, with `state.add_resource`) and where it ran; `run.input(...)`, `run.output(...)`, `run.artifact(path)` and `run.link(name, url)` add to it inside the block, and after the block `run.record` is the run (`None`, with `run.duplicate` true, when the same recording is already here). `exp.spec.verify()`, `exp.spec.vary(...)`, `exp.record({...}, repeats=..., produced_at=...)`, `exp.record_from(path, table=..., where={...}, select=[...])` (a file or a database row, read as `rb run add --from` reads it, with no command unless one is given; over MCP, `run_add` with `from_file`, `table`, `where` and `select`), `exp.analysis([{...}], method=..., unit=..., units=..., reads=[...])`, `exp.note("...")`, `exp.relate("reproduces", "")`, `exp.compare()`, `exp.state_word`, `state.add_question(..., parent=...)`, `state.reword_question(...)`, `state.add_note(...)`, `state.add_resource(...)`, `state.locate_resource(...)`, `state.expose(...)`, `state.finding()`, `state.status()` and `state.context()` do what their commands do, and `crit.meets` says whether the rule is met (one axis, never the whole finding). The calls are recorded as whoever makes them: `exp.freeze(why=..., words=...)`, `state.decide(subject, action, why=..., revisit=..., words=..., past=..., when=..., source=..., quote=...)` and `state.retract(subject, why=..., words=...)`, where `words` is the user's words from the chat, verbatim; in an agent session a decision is proposed, not made, until the user confirms it, and an RBWarning says so; a change to a frozen experiment takes `amend=""` (`exp.spec.set`, `exp.spec.vary`, `exp.criterion`, `exp.add_variant`). `rb.open(agent="")` records every call as that agent, whatever the environment says. What a write's command warns of beside it (`data.warnings`) comes from Python as a `rabbit_brain.RBWarning` at the line that made the write: an input whose copy here is not the bytes it names, an analysis's numbers typed with no document to check them against, numbers that differ from the run already recorded with the same output files (`exp.record` then returns None and records nothing), LaTeX in an agent's new text or words in it about who wrote it, a command given later (`command_source=":"` cites the log line of its launch, as `--command-source` does), a file written after its run, and a file dated minutes ago with no `produced_at`. `exp.criterion(..., questions=["q1", "q2"], when_met="...", why="...", words="...")` declares question links with the rule. `crit.file("q1", when_met="...", why="...", amend="...", words="...")` or `state.file_criterion("c1", "q1", ...)` adds or revises one; `remove=True` withdraws it. Over MCP these are `criterion_add`, whose `question` takes the list Python calls `questions`, and `criterion_file`, with the same fields otherwise and the same attribution. `status` returns each question's current `inquiry`, including its direct findings, descendants, unassigned criteria, withdrawn links and qualifications. `rb mcp` serves the same operations to an agent over the Model Context Protocol (stdio). Each tool is the command of the same name (`context`, `status`, `show`, `compare`, `log`, `journal`, `journal_scan`, `reconcile`, `page`, `export`, `project_title`, `question_add`, `question_move`, `question_reword`, `experiment_add`, `experiment_file`, `experiment_relate`, `experiment_gate`, `note_add`, `variant_add`, `metric_add`, `spec_set`, `spec_verify`, `spec_vary`, `criterion_add`, `criterion_file`, `criterion_relate`, `analysis_add`, `run_add`, `resource_add`, `resource_locate`, `resource_expose`, `retract`, `freeze`, `decide`); every tool that takes `why`, and `question_add`, `experiment_add` and `note_add`, also takes `words`, the user's words from the chat, verbatim, and `decide` takes `past`, `when`, `source` and `quote`. A call made over MCP is proposed, not made, until the user confirms it in their own terminal; there is no tool that confirms one. It returns the same envelope as `--json`, and there are resources `rb://context`, `rb://status`, `rb://schema` (the vocabulary, as `rb schema ir` prints it), `rb://format` (`rb-v1.schema.json`, format 1, as rb 0.7 serves it), `rb://format-2` (`rb-v2.schema.json`, formats 1 and 2) and `rb://docs`. **Every call on a connection is recorded as `agent:`, whatever `RB_ACTOR` says,** with `via: mcp`, the chat session when the runtime exports one, the client's version (`clientInfo.version`) and the model the agent gives the `context` tool (`model`, with an optional `provider`), because a model is making it; nothing is refused for that. The `context` tool answers with the briefing (`data.markdown`) and the actor, with what rb keeps of the agent (`data.actor`). The server's instructions carry the same guidance: call `context` first, with your model id as `model`; record when the research state materially changes (a question or hypothesis introduced, revised or dropped; an experiment or control proposed or run; evidence that changes a conclusion; a baseline, variant or metric established; an important uncertainty; the next experiment or a decision chosen), not implementation chatter (an installed package, a renamed function); point at the repository's own files (results, reports, manifests) with sources rather than copying their prose; write text as plain text, with Unicode symbols rather than LaTeX; `reconcile` compiles what the repository's own files say against the record, and `apply` keeps it; `experiment_add` takes `if_holds` and `if_not`, the decision the experiment gates. For Claude Code, add it from the project root: `claude mcp add rabbit-brain -- rb mcp`. `--agent ` sets the name recorded, whatever the client sends. ### Cloning: starting from someone else's record `rb clone [dest] [--code
]` starts a new line of work from an exported bundle: a directory or .zip that `rb export --with-record` wrote, or the https address it is served from, which `rb` downloads first. A URL clone downloads the address the caller supplied. Login, sync, explicitly enabled usage analytics and the daily check for a newer rb (under **Updates**) also make network requests; ordinary research commands need no network to do their work. The bundle must verify against its own manifest first (`E_BUNDLE_INVALID` otherwise, as for a zip over 512 MB or one whose files unpack past that, and for an export written without `--with-record`, which carries its page alone), and a clone never lands inside an existing project. A bundle whose record holds a part only a newer rb reads is refused (`E_STATE_NEWER`) and nothing is written, since a clone would leave that part behind. What travels is the structure: the questions (open, because the decisions made on them do not transfer), the metric catalogue, the resources with what they are and where their bytes have been (a local location that was another machine's absolute path, or a file in a worktree nested in the parent's checkout, is left behind; a repository-relative one is kept), and the experiments with their purpose, hypothesis, variants, settings, notes and criteria, keeping their ids. Criteria keep their question links and the original authors of their conditional interpretations; those interpretations are inactive until new evidence meets their rules here. What does not travel is authority: a design that waits for its user's confirmation there waits here too, since each clone line keeps who wrote the parts that wait (`detail.unconfirmed_parts`); settings arrive `inherited` (the value came along, the verification did not), criteria arrive `origin: inherited`, and every inherited criterion reads `untested`, listed as `inherited` in `rb status`, until runs recorded in this repository test it; then its rule reads `reproduced` or `not reproduced`, computed like any finding. Each experiment gets a new uid here and records that it `reproduces` the parent's. The parent's runs, analyses, decisions and log stay in the parent's record; the new project records where it was cloned from (`parent` in `project.yaml`: a path as given, an address without the credentials, query or fragment it was fetched with; where an `@` follows its first `/`, `?` or `#`, rb cannot tell the host from a password or a token, and says `an address rb cannot read safely` in its place, and no git message). This is how a shared project is reproduced, challenged, or built on: clone it, run it here, and report what the findings became. **The code comes too.** The runs need the code, and a receipt keeps the code of the checkout a run is recorded in, so a clone whose bundle names its repository (the manifest's `repo`, which `rb export` writes from the `origin` remote and `HEAD` only where origin is an https address, with any credentials, query and fragment taken out; otherwise the bundle names none, and `rb export` says so in one line) lands in a checkout of that code: `rb clone` runs `git clone` of the address, checks the bundle's commit out on a new branch `rerun`, and writes the record there. `--code
` gives the address instead (a copy the reader can reach, or code the bundle does not name), at the bundle's commit. What is the parent's is taken out of that branch in the record's own commit, which changes nothing else: the record the commit holds, what the parent's runs wrote (each file they read their numbers from, in its own folder where results land), and every other results folder the commit holds that no recorded run named, so the runs made here write their own; never what a rerun may need (a resource's local copy, a database a run read, a folder that holds one of them) and never source code. git keeps all of it at the commit, and rb never reads any of it as this record's. A run recorded here then names the record's commit, whose code outside `.rb/` is the bundle's. Where the address cannot be cloned or lacks the commit, the clone lands as one with no code does: rb clone says it could not get the code from that address, then, where git failed, git's own message, without its advice, after `git said:` (for an address rb cannot read safely, why in rb's own words in its place, and no git message, since git, curl and ssh name the host they tried), then the `--code` command (`why` and `git_said` under `data.code`), with the exit status a clone with no code has. The clone's log keeps the code's address and commit, and, for each experiment, the commands the parent's runs were recorded with for the runs its first rule reads (with how rb knew each, their per-run values and the code they ran), since those runs do not travel. In a clone, `rb context` opens with **Rerun the parent**, and `rb status` says it under the title (`rerun` in `--json`): the parent; the code (here, here with changes, not in this checkout, or not named by the bundle: "The runs need the code; this bundle doesn't say where it is."), with the `rb clone ... --code
` that gets it where it is not here; each resource with where the record expects it and how to get it again; and each experiment's first command, `rb journal run -- `, which both name as their first next step until a run of it is recorded here. A clone with no code lands in a space of its own, like a fork: `rb clone` leaves the new project as a git repository with the record as its first commit. Landing inside an existing repository touches nothing there and says so; with no git identity set (`user.name` and `user.email`), the record is left staged rather than committed under one git guesses. A fork is shared back the same way: `rb export --with-record --url
` from the clone, served from that address, whose manifest then carries `parent` and whose page links the project it was cloned from when that is a web address (a path on the machine that cloned it stays in the record and the manifest), so lineage is traversable backward across plain static hosts. Planned, and not in this version: - `rb paper`: a paper's criteria and settings as a pre-filled experiment. It prints what to do today. `rb publish` fails with `E_NOT_AVAILABLE` and says what to use: rb does not publish (`rb export` writes the page, and with `--with-record` the record too, to host anywhere; `rb sync`, under rabbitbrain.ai below, keeps a private copy in the user's account). ## Leaving rb Before `rb init`, `rb compile`, `rb status`, `rb reconcile` and `rb docs --start` read the repository and write nothing, git's index included. `rb init --dry-run` lists every file and integration `rb init` would write, every file that was there it would change and how, and what later commands add (under **Previewing a write**), and `rb init` asks before it changes a file that was there (under **Install**). Every write rb makes outside `.rb/` on purpose is listed in its ownership ledger, `/rb/owned.jsonl` (outside git, `.rb/cache/owned.jsonl`, and there too while git's folder cannot be written, until a write or a look that can moves its lines into git's folder), one JSON line for each instruction file it installs, each section or entry it adds to a file that was there, each page or export it writes, each ref that keeps a run's code and each folder of its own, with its path (from the ledger's own folder for a path in the checkout, so a moved folder keeps its lines), how rb wrote it (`created`, `replaced`, `section`, `mcp_entry`, `hook_entry`, `output`, `ref` or `dir`), the sha256 of the file after the write and, where rb changed a file that was there, before it (with rb's section and what it took the place of, or the text of a file rb's own replaced), and the command. The ledger is outside the record and never travels with it. `rb remove` takes rb out of this checkout (the record its commands write to), and only what checks out as rb's. It shows the plan first, in three groups: - **Will remove**, each with how rb knows it is its own: the record's files, each as its log says rb wrote it, the log, and rb's own files and local copies; what the ownership ledger says rb wrote in this checkout, and only that: the skill and the Cursor rule, as rb wrote them (one rb put in place of a file that was there gives that file back); rb's section of `AGENTS.md`, rb's server in `.mcp.json` and rb's session hook in an agent's hook file, in today's form or an earlier rb's (every other hook stays, and a handler of rb's that rb's own took the place of in a clone, or that rb took out beside it, goes back where it was in the list, whether or not rb's own is still there), the file going back to what it was, byte for byte and with the time it had, while it is as rb left it, else rb's part alone cut out by its own bytes (and the section it took the place of put back); `rb-page.html`, `rb-export/` and `rb-export.zip` beside `.rb/`, with the bytes the ledger keeps (rb's own marks, for one an older rb wrote); the folder `rb tour` made here (`rb-tour/`, a repository of its own), while every file in it is as the tour left it; and, when no other checkout of the repository holds a record, every ref under `refs/rb/`, rb's folder inside git's (`/rb/`) and `rb ui`'s mark of what was seen (`rb-last-seen.json`). - **Will keep**, each with why: a file in `.rb/` rb did not write, or one changed since rb wrote it; an instruction file, a section or a server changed since, with the lines to delete, or one rb did not write in this checkout (a teammate's commit of rb's text); a page or an export changed since rb wrote it, or written where it was asked (`--out`) and still as rb wrote it; and the repository's refs and rb's folder while another checkout holds a record (a worktree git keeps locked while its folder is away counts as one), which is named with the command to run there (`cd && rb remove`). - **Not rb's to touch**: research files, worktrees and `.git/worktrees`, git history, git objects, config, hooks and the index. No git object is deleted: the objects only rb's refs reached are counted and named, and stay until git's own gc removes them. It names what rb wrote for this machine rather than this checkout, and removes none of it: `~/.claude/skills/rabbit-brain/` (`rb skill add`; one this checkout wrote there for itself, through a `.claude/` linked into the home folder, by `rb init` or `rb skill add --claude --project`, is removed with the rest, as the ledger says, and named with where the link goes), rb's settings folder (`rb logout`, `rb telemetry disable`) and the rb command itself (`pip uninstall rabbit-brain`). Where the record holds writes no commit keeps, the plan says so in one line (`N files of the record have uncommitted changes. To keep a copy first, run: ...`), with the command that keeps a copy beside the checkout (`rb export --with-record --zip --out /-record.zip`); it does not wait for it. A committed `.rb/` is deleted from the working tree only: `git will show N files deleted; history keeps them`. The count holds every file a commit holds that the plan deletes (a committed skill or page too), and a tracked file rb takes its part out of whose bytes then differ from git's is counted as changed (`git will show N files deleted and M changed`; `data.tracked_deleted`, `data.tracked_changed`). A file git's index holds staged with rb's writes (added with `git add` or `git add -N`, never committed) is named apart, with the one command that takes rb's writes out of the index after `rb remove` (`git restore --staged -- .rb`, `git add -- AGENTS.md` for a file staged as it stood, `git rm -r -q --cached -- .rb` before a first commit; `data.staged`, `data.unstage`): rb does not touch the index, and the next commit would add them to history. A file rb writes back whose index holds rb's part alone over what the last commit holds (staged after `rb init`, then changed again) is named with them, in the same command (with `-f` before a first commit), which takes out only rb's part and leaves the change made since. One staged in part, rb's part with other staged changes, is named with no command (`data.staged_in_part`): no one command takes out only rb's part, so the user stages it again as they want it. `rb remove --dry-run` shows the plan and removes nothing. A removal stopped midway leaves the files that say the record is rb's (its log and `project.yaml`, which go last) and rb's ledger, so `rb remove` run again finishes it. A `.rb/` that is a link is named with where it goes; rb's files in the folder it points to go, and the link and the folder stay. In the user's own terminal `rb remove` shows the plan and asks once, and No is the default; anywhere else it needs `--yes`. It is refused where `rb confirm` is (an agent session, a CI job, a chat's leftover id, or `RB_ACTOR` naming an agent), except with `--dry-run`. With `--json` the plan is in `data.remove`, `data.keep`, `data.residue`, `data.listed` and `data.others`, and what it did in `data.removed`, with the files it took its part out of (which stay) in `data.put_back`. ## Output Every command accepts `--json` and prints exactly one JSON object on stdout, the envelope; progress, logs and warnings go to stderr. `ok` is false only when the command could not do its job; a check that ran and failed is data (`ok: true`, exit 1). `errors[]` carries `code`, `message` and `fix` (and `problems` when there are several, and `report_url` for a fault in rb). `next` lists the commands that usually follow. The record's commands put the record they wrote or read in `data.object` (with its `kind` and `id`), a criterion's finding in `data.finding` (`data.findings` when there are several, as after `rb run add`), a check's result in `data.outcome` (`passed`, `failures`), counts under `*_count` names, who was recorded in `data.actor`, and anything it warns of in `data.warnings` (among it, after an agent's write whose new text holds LaTeX, the line that names the field, which rb's text prints right after the write's own). `rb schema envelope` prints the shape. The envelope's keys are `ok`, `rb_version`, `command`, `data`, `errors`, `next` and `rb_delta` (under **Reading the record**). **Previewing a write.** These commands take `--dry-run`: `rb project title`; `rb question add`, `move` and `reword`; `rb experiment add`, `file`, `relate` and `gate`; `rb note add`; `rb variant add`; `rb metric add`; `rb spec set`, `verify` and `vary`; `rb criterion add`, `file` and `relate`; `rb analysis add`; `rb run add`; `rb resource add`, `locate` and `expose`; `rb retract`; `rb freeze`; `rb decide`; and `rb confirm`. rb then makes every check the write makes, exits as the write would, with its refusal or its warnings, and writes nothing: no record file, no log line, no cache, no mark of a failed check. It prints what the write would write: a new file whole, a changed file as the lines that change, the log's new lines. With `--json`, `data.dry_run` is true and `data.would_write` holds `files` (each with its `content`), `removed` and `log`. Over MCP the same tools take `dry_run`; from Python the same writes take `dry_run=True` and return a `rabbit_brain.DryRun` (`value`, `would_write`). The write itself draws its ids and times again. A preview reads the record as it stands, so a write that rests on another (a setting of an experiment not yet added) is refused in a preview until that one is made: preview and make each write in turn. The commands that write in bulk preview in their plain form: `rb reconcile` without `--apply` says what it would keep, `rb doctor` without `--restore` or `--adopt` lists what they would put back or keep, and `rb fmt --check` names the files `rb fmt` would rewrite. `rb init --dry-run` lists every path under `.rb/` and every agent's instruction file `rb init` would write, every file that was there it would change and how (with what it asks first), and what later commands add (refs under `refs/rb/snapshots/`, the journal, past versions under `/rb/views/`, `rb ui`'s mark of what was seen, the ownership ledger, `rb-page.html`, `rb-export/` and `rb-export.zip`), writing nothing (`data.plan`, `data.later`). ## Exit codes `0` ok · `1` a record gate failed (`rb status --fail-on`), a setting did not verify (`rb spec verify`, or `rb spec set` with a checkable source), `rb doctor` found a problem in `.rb/`, `rb fmt --check` found a file not in canonical form (or `rb fmt` a file that is not Rabbit Brain YAML), `rb show .yaml` read a file that is not a valid record, or the command `rb journal run` wrapped failed; the JSON has `ok: true` because the result is data · `2` rb could not do what was asked, and says why: invalid input, a command line that does not parse (`E_USAGE`), a file it could not read or write, an address it could not reach · `3` a fault in rb itself, or an install that cannot run a part of it (`E_INTERNAL`). Every failure is said the same way: `rb : `, then a line that says what to do (a command line that does not parse shows the command's usage first, and that is its fix); with `--json`, the same in `errors[]`. Read `errors[].fix`. ## Errors and what to do | code | meaning | do this | |---|---|---| | `E_FILE_NOT_FOUND` | a path does not exist, or the file there could not be read | paths are relative to the current directory | | `E_NOT_AVAILABLE` | the command is not in this version | `rb paper` is planned; `rb rerun`, `rb serve` and `rb reproduce` are not rb commands, and `rb publish` is not rb's job; the message says what to use instead (`rb run add`, `rb open`, `rb clone`, `rb export`) | | `E_HUB_REFUSED` | the hub refused the request (`rb login`, `rb sync`) | the message carries the site's reason and fix; the record on this machine is unchanged | | `E_HUB_UNREACHABLE` | the hub could not be reached, or what answered at its address is not it | check the address (`--hub`, or `RB_HUB_URL`) and the network; nothing local was affected | | `E_BUNDLE_INVALID` | the bundle is not a valid rb export, or its files do not match its manifest; an export written without `--with-record` is a page, not a bundle | get a fresh copy from where it is served; `rb export --with-record` writes one | | `E_WRITE_FAILED` | a file could not be written; for `rb export`, a bundle that would pass 512 MB, naming its largest parts; or `--out` names a file rb did not write | the message names the file and says why; check that its directory is one you can write to; for a file rb did not write, give `--out` another path, or `--replace` to write over it | | `E_INTERNAL` | a fault in rb itself (its message starts "rb itself failed"), or an install that cannot run a part of rb (the screen without its Python) | a fault's fix line carries a report link (`report_url` under `--json`): rb's version, the platform, the command's name in rb's command tree, where in rb's own code it failed (when rb knows it) and a fingerprint of the message, never its arguments, a path, the message or anything from the record. Show the link to the user, and never open or fetch it yourself: the page shows what it would send, and the user sends it. For an install, the message says what to install | | `E_NO_PROJECT` | no `.rb/` here or in any directory above; in a worktree, the message names the worktree holding the project's record | `rb init ""` in the project root; in a project already under way, `rb docs --start`; in a worktree, commit `.rb/` there, then `git merge` its branch here | | `E_PROJECT_EXISTS` | a project (`.rb/`) already exists here or above, or in another worktree of this repository | use it: `rb status`; one `.rb/` per line of work; in a worktree outside the checkout, bring the record here as the fix says, or `rb init --new-project` for a separate project | | `E_YAML` | the file is not Rabbit Brain YAML | the message gives the line, the column and what to write instead: no anchors, tags or dates, no yes/no; `rb fmt ` rewrites a file that reads in its canonical form | | `E_OBJECT_NOT_FOUND` | no record with that id, or no such setting | `rb status --json` lists every record; `rb show ` its variants, settings, criteria and runs; a setting is `/` | | `E_OBJECT_INVALID` | the values do not make a valid record (a criterion with two rules, `--equals` without `--tolerance`, a `--better` or `--noninferior` rule on a metric the catalogue gives no direction, a taken id, a second baseline, `--amend` with nothing to amend) | do what the message says; `rb schema ` | | `E_FROZEN` | the experiment is frozen and this changes a setting, a variant, what it varies, or a criterion; or it is frozen already | the same command with `--amend --why ""`, by whoever makes the change; the amendment is kept with who made it | | `E_ACTOR_INVALID` | `RB_ACTOR` is not `human:` or `agent:` | set it correctly, or unset it | | `E_SOURCE_UNRESOLVED` | the source was read and does not state the value (a different value is recorded as a conflict), or could not be read | fix the path, key, line, quote or commit, or set the value the source states | | `E_SOURCE_UNVERIFIABLE` | a url, a note, or a file source with no key, line or quote | point at a config key, a line, a quote in a saved copy, or a run's JSON; or vouch for it (usually the user's call): `rb decide / accept --why "..."` | | `E_SOURCE_OUTSIDE` | the source is outside the project, so nobody else can check it | copy it into the repository, commit it, and point `--source` at the copy | | `E_STATE_EDITED` | a record in `.rb/` was changed outside rb (a value edited, a file moved or deleted, or written by hand), and rb will not write over it; comments and formatting are not changes | `rb doctor` lists it; `rb doctor --restore` puts back what rb last wrote; if the change is right, `rb doctor --adopt --why "..."` keeps it, recorded as whoever adopts it | | `E_STATE_CORRUPT` | a file under `.rb/` is not a valid record (not Rabbit Brain YAML, or not the shape its kind has), usually a hand edit or a merge left half done | `rb doctor` lists it and says where it breaks; `rb doctor --restore` puts back what rb last wrote | | `E_STATE_NEWER` | part of this `.rb/` was written by a newer rb than the one reading it, or holds a field rb does not know | upgrade: `pip install --upgrade rabbit-brain`; until then gates fail closed with this reason. A file that is not what rb last wrote was most likely edited by hand: the message says so, and `rb doctor --restore` puts back what rb last wrote | | `E_ADOPT_AUTHORITY` | the edit changes who wrote or decided something, or through what (a run's receipt, a decision rb never wrote, or a second copy of a record, included), which an adoption must never make true | `rb doctor --restore` puts the file back, or sets aside one rb never wrote; if something is really wrong, make the change through rb, where it is logged as whoever makes it | | `E_STALE_REVIEW` | the files changed between the review and the adoption (`rb doctor --adopt --seen @`), so what was reviewed is not what would be adopted | look again: `rb doctor` lists every file changed outside rb as it is now | | `E_USAGE` | the command line did not parse | `rb --help`; negative numbers are written as they are (`--at-most -0.001`) | ## Worked examples ### A question handed to an agent Prompt from the user: *"Find out whether INT8 costs us accuracy on the held-out set. The eval script is scripts/eval.py. Set up the criteria and the runs yourself; stay under 4 GPU-hours."* ```sh rb context --model # if .rb/ exists: read it first, and work from "Can be done now" rb init "INT8 ResNet-50" # otherwise, start it, titled for the work's theme rb question add "Does INT8 cost accuracy on the held-out set?" --id q-int8 --words "Find out whether INT8 costs us accuracy on the held-out set." rb experiment add "INT8 vs FP32 on the held-out set" --id int8 -q q-int8 --hypothesis "INT8 keeps validation loss within 0.05 of FP32" --baseline fp32 --candidate int8 --varies precision rb note add "Budget: 4 GPU-hours." -e int8 --words "stay under 4 GPU-hours" rb spec set int8 --from configs/eval.yaml --keys "model.*,data.*,eval.batch_size" rb spec set int8 seed --per-run rb metric add val_loss --unit nats --minimize rb resource add holdout-v2 --type dataset --name "held-out images, v2" --url https://data.example.org/holdout-v2 rb criterion add "INT8 adds at most 0.05 validation loss" -e int8 -q q-int8 --metric change.val_loss --at-most 0.05 --min-n 3 rb freeze int8 -m "before the runs" --words "Set up the criteria and the runs yourself" # recorded as yours, with their words rb journal run -- python scripts/eval.py --precision int8 --seed 1 # then the runs, after the declaration and the freeze rb run add int8 --from out/seed1/metrics.json --set seed=1 --config out/seed1/config.yaml --input holdout-v2:eval --command "python scripts/eval.py --precision int8 --seed 1" rb show ``` Each `--words` keeps the user's sentence, verbatim, beside what you wrote: on the log line, and on the freeze, where rb's text and the user's screens say it as `words (as given): "..."`, since rb cannot know the user said it, and a page leaves it out. The budget is a note: rb records it and enforces nothing, and keeping to it is yours. Then report to the user, in this order: 1. each criterion's headline and comparison, exactly as `rb show` prints them (for example `criterion undetermined · point only` on `change.val_loss: +0.03 nats · paired difference, 2 seeds (rb)`, because `2 units; the criterion needs 3`); 2. its timing and every integrity problem; 3. the settings that are provisional or unknown; 4. what waits on them: each item under **Needs judgment**, and each call that waits for their confirmation, with its command. Do not round `criterion undetermined · point only` up to "INT8 is fine": two seeds, and the rule asks for three. When the user answers with a call, record it as they made it, with their words: ```sh rb decide int8 park --why "two seeds do not settle it" --revisit "when the third seed is in" --words "Park it until the third seed is in." ``` The decision is recorded as yours, with the chat session, and their words sit beside the reason. It is proposed, not made: int8 reads as it did, and `rb status` lists the call apart, with `rb confirm `. Tell the user it waits for one confirmation in their own terminal (or `c` on `rb ui`); once they confirm it, the call and their words are theirs. When the user says they made a call before the record began, record it as a past call, dated as they said it: `rb decide int8 park --why "..." --past --when "last Tuesday" --words "..."`. It waits for their confirmation too. A call a committed document states is recorded from that document: `--past --source notes/calls.md:12 --quote ""`. It waits for the same one confirmation, whoever began the record, and rb says beside it what git shows of the file (the quote is there, committed before the record began, or not); point the user at it. ## rabbitbrain.ai (optional): `rb login`, `rb sync` The hosted service is open from 0.6.1: `rb login` and `rb sync` call rabbitbrain.ai. A hub named with `--hub` or `RB_HUB_URL` is called as below. `rb` and rabbitbrain.ai are like git and a code host: the record lives in the repository, `rb` never publishes, and the site is an optional place to read it. `rb login` signs this machine in with a device flow: the terminal shows a short code, the user approves it at `/link` on any device where they are signed in, and the token arrives once and is stored with mode 0600 in `~/.config/rabbit-brain/credentials.json` (or under `$RB_CONFIG_DIR`), outside the repository and outside the environment. Whoever starts `rb login`, the user approves the code on the site, so the credential is theirs. `RB_HUB_TOKEN` overrides the file for CI, and `rb logout` revokes the credential on the site and removes it here. `rb sync` puts a private copy of the record's page in the user's account, for their eyes only, signed in, from anywhere: it exports the page as `rb export` does and uploads it, and the site verifies every file against the export's manifest before showing it. `rb sync --with-record` sends the record too, as `rb export --with-record` bundles it with the address it will have, and says what the record keeps, as `rb export --with-record` does. With `--with-record` the bundle names the code as `rb export` does, where origin is an https address (with any credentials, query and fragment taken out), so a share link's readers who rerun it get the code with the record; otherwise it names none, and `rb sync` says so in `rb export`'s line. Plain `rb sync` names no repository. Only the user can download that record, unless they let a share link's readers rerun it, on the site; plain `rb sync` says that readers can rerun only a sync made with `--with-record`. The copy is the record as computed at that sync, so sync again after new runs; the repository stays the store. `rb sync --watch` keeps that copy current while the work runs: every 30 s it looks at the record and the page it would send, and syncs again, privately, when either changed, at most once a minute (a time the clock alone wrote on the page, when it was made or when rb last looked, is no change), saying each sync on standard error; a failure is said in one line and tried again after 30 s, then 1, 2, 5 and 10 min, while a refusal that waiting cannot change (the credential, or the copy itself) ends it as `rb sync` would; it has no `--json`, and Ctrl-C ends it, with nothing left behind. `rb sync --json` says which it sent (`with_record`). Making a record public is done on the site, from the record's page, never by rb. `--hub` (or `RB_HUB_URL`) names another address. You never need the token: never set or unset `RB_HUB_TOKEN`, and never read the credentials file. ## Updates Updates come without asking anyone. Installed as a tool (`uv tool install rabbit-brain`, `pipx install rabbit-brain`), at most once a day a background rb asks PyPI's public JSON for the newest release and, when it is newer, upgrades through the same installer (`uv tool upgrade rabbit-brain`, `pipx upgrade rabbit-brain`); the next command runs the new rb, and `rb ui` says `✔ Update installed · Restart to update` in a quiet line of its own. Installed with pip (a project's environment, conda), rb never touches that environment: at most once a day it says one line on standard error, `rb 0.8.0 is out: pip install -U rabbit-brain`. Neither runs in CI, in an agent session (a session's commands keep one rb), under `DO_NOT_TRACK`, after `rb config auto-update off` (`rb config auto-update` says the setting, and `on`, in the user's own terminal, turns it back on), or with `RB_AUTO_UPDATE=0` in the environment. The check carries nothing about the user, and a failed check or upgrade is silent and changes no command's result. The setting and what the last check found are kept in `update.json` in rb's settings folder, never inside a record. After rb is upgraded, a project that is set up to sync re-syncs once on its own: the first command the new rb runs there starts one `rb sync` in the background, to the hub and with the `--with-record` of the last sync, which `.rb/cache/sync.json` keeps with the version of rb that wrote the page. It never delays the command, and one that fails is not tried again until the next upgrade. ## Optional usage analytics Usage analytics is off by default and independent of the research record and `rb sync`. The user enables it with `rb telemetry enable` in their own terminal; a build with no configured PostHog destination keeps events locally until `--project-key ` and `--host ` configure delivery. `rb telemetry status` shows the setting, `rb telemetry preview` shows pending events, `rb telemetry flush` sends them and waits for the answer, and `rb telemetry disable` turns it off. `DO_NOT_TRACK=1` suppresses analytics, and so does `RB_TELEMETRY` set to any value other than `1`, `true`, `on` or `yes`; enabling while either is set says so, and the consent event waits until it is gone. The installation identifier, its usage cohort and a bounded event queue live in the user's configuration directory, outside `.rb/`. The cohort is internal for a source or editable install, which has no destination built in, unless `RB_TELEMETRY_COHORT` says otherwise when analytics is enabled. Events contain fixed usage categories, not research text, command arguments, file paths, git remotes, actor names, chat-session identifiers, or an agent's version, provider or model. CI and tutorial activity is excluded. Delivery happens in a short-lived background `rb telemetry flush` after a command and never delays it. Telemetry failure must not change a research command's result. Every part of rb works with analytics disabled. An agent cannot turn analytics on: in an agent session `rb telemetry enable` answers `E_USAGE`, and the user runs it in their own terminal. Anyone can turn it off with `rb telemetry disable`. Never send the research record, command output, credentials, or errors with their raw text to an analytics service. See [PRIVACY.md](PRIVACY.md) for the full description. ## Boundaries `rb` does not run experiments: it records what the caller says produced the numbers, who recorded them, the repository's state at that moment, and what the numbers support. A verified setting is what a file states, not proof a run used it; a receipt is what `rb` saw when the run was recorded, not proof of what produced it; a resource's availability is what `rb` can see from here, and a remote location is recorded, never fetched or checked. `rb` does not train, does not modify model code, and does not certify a model. It cannot make an agent record a choice that only lived in the chat, and it cannot judge whether a method fits the question. What it does: nothing a session wrote reads as the user's until they confirm it, every setting says where its value came from, and every finding is computed from the files, not from the agent's account. Everything `rb status`, `rb show`, `rb compare`, `rb doctor`, `rb env check` and `rb export` need is in the files; none of it needs an account or a network, and rabbitbrain.ai only shows what the files already say. `rb` never publishes. Uploading a research record requires `rb sync` (a private copy in the user's rabbitbrain.ai account); separately, explicitly enabled analytics can send the limited usage facts described above. `rb` never refuses a write because of who makes it: what it guarantees is that every write says who made it, in which session and why, with the words given with it, and that a call recorded in an agent session changes nothing until the user confirms it. ## Verify what your agent did `rb status` and `rb show ` are computed from `.rb/`, not from what anyone wrote about it. The files are plain YAML you can read, and annotate with comments: a comment is not a change. `rb log --json` lists every write with its actor and how `rb` knew it (inside an agent session your name in `RB_ACTOR` is ignored, and the entry keeps it as `ignored_rb_actor`), and `git log -p .rb/` shows every change. Every call your agent recorded is the agent's, with its session, its name, version, provider and model, its reason, and the words it gave as yours, so what it did in your name is on the record as its own; and it changes nothing until you confirm it (`rb confirm `, or `c` on `rb ui`). A setting is verified only with the text `rb` read, which `rb show /` prints. `rb doctor` lists anything changed outside `rb`, and `rb doctor --restore` puts back what `rb` wrote. The log only grows: a line that was committed and is now gone or changed fails every gate. What `rb` cannot catch on its own is a file edited and a matching line appended since the last commit. The witness for that is outside the agent's reach: commit `.rb/` and push it to a branch the agent cannot rewrite. If what the agent told you differs from `rb status`, `rb status` is right.