AutoR: A Recursive Research System
It proposes, tests, and tries to refute itself. The approval gate is the one thing it does not own — by default, that is you.
Start here: The Framework · English Guide · 中文教程 · Full Documentation
AutoR is not a chat demo, not a generic agent framework, and not a markdown-only research toy.
It is a structured research harness over a coding-agent execution layer: the agent handles execution, the human owns the direction, and every run becomes an inspectable research artifact on disk.
docs/framework.md is the single document that describes what this system is: its implementation, its modules, what is new in it, and what it contributes. This README is the overview and the operating manual.
Contents
What AutoR is · Quick start · The stage graph · The rigor dial · Self-improvement rounds · Review · The stage contract · Execution model · Run layout · Architecture · Benchmarks (ResearchClawBench · FIRE-Bench · AIRS-Bench) · Documentation · Limits · License
What AutoR is
Most autoresearch systems optimize for autonomy. AutoR takes a different position: research is too important to hand over as a blind end-to-end loop. The goal is not to remove humans from research. The goal is to give them a stronger execution system.
AutoR runs a research project as eight stages wired into a directed graph. Six of the forward
edges are guarded by artifacts on disk; thirteen backward edges let a late finding send the run
back — Stage 07 can reopen the literature survey. Hypotheses are frozen and hashed when Stage 04 is
approved, every one must be adjudicated at Stage 06 against a named result file that exists, and
every paper claim traced at Stage 07; a supported or refuted verdict resting on a single seed is
refused unless the run records why one run settles it. An adversarial reviewer attacks Stage 05's
results and Stage 06's analysis, and the stage after each must answer every finding in writing or the
gate refuses it. Drafts are scored and a refinement that does not improve is reverted. Every stage
still stops at an approval gate, and by default that gate is you.
"Recursive" is eight mechanisms, each of them a file you can open
| Move | What runs | Where |
|---|---|---|
| Propose | Five proposers work from distinct lenses — mechanism, contrarian, adjacent field, null/artifact, regime — blind to each other; two statements whose Jaccard overlap reaches 0.5 collapse into one idea | ideation_panel.py |
| Test | Every baseline declares why_competent and a tuning_budget before it runs; the hypothesis set is frozen and hashed before any result exists, and a later change is legal only as a recorded amendment | experimental_protocol.pypreregistration.py |
| Refute | An adversarial pass asks why the result is wrong across ten named failure modes — confound, leakage, metric_cherry_picking, effect_within_noise, six more; a round can close as converged, refine_design, new_hypothesis or abandon | validity_review.pyresearch_rounds.py |
| Critique | Five seats review independently, cross-examine anonymised, then converge; a blocking objection is turned into a refusal in code against the panel's own chair, and a different model family audits the approval as a veto | review_panel.pycross_reviewer.py |
| Iterate | Every valid draft is scored against a rubric read off disk; the champion is kept and a losing polish round is reverted before anyone reads it; a draft that loses on the weighted total but is non-dominated on the criterion vector is kept anyway | rubric.pyevolution.pypareto.py |
| Learn | Each finished run records its route and measured fitness; a fitness comparison is keyed on the set of stages the run actually measured, so a run cannot score well by stopping early | archive.pydecisions.py |
| Deliberate | A stage that hits a genuine crux stops, names the question, and pulls in theorist / empiricist / critic / pragmatist plus an expert brief, then continues with an answer that names its own falsifier; budgeted, and measured against what the agent already believed | deliberation.py |
| Localise | A reviewer quotes the passage it objects to instead of refusing the whole stage; the revision is told to change only those spans and is diffed against them, so "preserve the correct parts" is measured rather than hoped for | stage_comments.py |
What a default run (--rigor standard) actually uses. Test, Refute, Iterate and
Learn are on: the validity chain is unconditional at every rigor level including fast,
--evolve defaults on, and the archive records every run — though it only steers under
--archive-steer. Localise runs whenever a reviewer quotes a passage, which requires an agent
reviewer. Propose and Deliberate need --rigor thorough; Critique's panel needs
--rigor max and its cross-model veto is live on the rcb_agent.py path only. See
the rigor dial for the exact mapping.
AutoR does not run itself. Manual approval is the default: approval_mode is manual unless a
flag opts out. Seven of the eight moves above can only score, refuse, revert or re-order; none of
them can approve a stage. The eighth, the review panel, is an approval gate, and it exists only on
the runs where you hand it the gate. Recursion did not change who decides; it changed what reaches
the desk. The research unit is unchanged: one reproducible run under runs/<run_id>/, isolated,
resumable, with redo and rollback.
The one thing the docs will not claim
Approved stage summaries are the only free-text cross-stage memory. Every other cross-stage edge is
a typed artifact with a declared reader: twenty typed channels in
information_flow.py each name the exact stage slugs that consume them,
and the nine channels produced inside the walk name their producing stage as well. obligations.json and
review_policy.json cross stages without touching a summary at all — both only behind an agent
approval gate.
Many systems aim to generate research outputs that look ready. So the question is not
Does it look ready?
It is
Can you verify every part of it?
The answer is the validity chain — freeze at Stage 04, adjudicate at Stage 06, trace at Stage 07
(preregistration.py) — and the edge into writing stays shut until every
frozen hypothesis carries a verdict (_guard_validity_chain, stage_graph.py).
The shape of the system, in counts you can re-derive
Every number below comes from a named symbol in the source. Re-derive them; that is the point of naming them.
| Count | Symbol | Value |
|---|---|---|
| Stages (nodes in the walk) | STAGES, src/utils.py | 8 |
| Guarded forward edges | _ADVANCE_GUARDS, src/stage_graph.py | 6 |
| Backward edges | REVISIT_EDGES | 13 |
| Conditional terminal edges | TERMINAL_EDGES | 1 |
Edges in the default (adaptive) graph | StageGraph.adaptive() | 22 |
Edges in --stage-graph linear | StageGraph.linear() | 9 |
| Typed information channels | CHANNELS, src/information_flow.py | 20 |
validate_* functions the stage gate calls | validate_stage_artifacts, src/utils.py | 17 |
| Required stage-summary headings | REQUIRED_STAGE_HEADINGS | 7 |
| Rubric criteria (weighted, backend-free) | CRITERIA, src/rubric.py | 10 |
Flags on main.py / rcb_agent.py | parse_args | 61 / 37 |
| Python modules / lines / tests | the tree | 254 / 137 k / 4053 |
python -m unittest discover -s tests -p "test_*.py" runs 4053 tests in ~440 s across 143 test
modules, with no third-party dependency.
Quick start
Prerequisites
- Python 3.10+
- Claude CLI or Codex CLI on
PATHfor real runs - Local TeX tools only for
--output-format latex; the default markdown output needs no TeX pip install google-genaiplus a key inGOOGLE_API_KEYorGEMINI_API_KEY— needed by three paths, not only the diagram one:--web-search gemini, required where the backend's ownWebSearchtool is disabled (build_genai_client, src/web_search.py); the cross-model veto--cross-review auto|gemini(src/cross_reviewer.py); and--research-diagram, which also readsconfigs/diagram_config.yaml- The SDK is not a default dependency. Without it the diagram step prints
Diagram generation failed: No module named 'google'and the run continues; cross-review records itself unavailable rather than agreeing
Common commands
| Goal | Command |
|---|---|
| Start a run (the goal is prompted for if omitted) | python main.py --goal "Your research goal here" |
| Start with preloaded resources | python main.py --goal "..." --resources paper.pdf refs.bib data.csv |
| Run a local smoke test without a real agent backend | python main.py --fake-operator --goal "Smoke test" |
| Run with the automated reviewer gate | python main.py --full-auto --goal "..." |
| Choose how much optional machinery to run | python main.py --rigor thorough --goal "..." |
| Give the panel a researcher persona to stand in for | python main.py --review-panel --persona docs/persona-example.md --goal "..." |
| Seat the panel across different models | python main.py --review-panel --panel-models pi=opus skeptic=codex:default --goal "..." |
| Seat the optional Area Chair as a sixth reviewer | python main.py --review-panel --panel-roles pi domain method repro skeptic reader --goal "..." |
| Keep the strong model for the steps that matter | python main.py --effort-tiers --model opus --routine-model sonnet --goal "..." |
| Choose the execution backend and model | python main.py --operator claude --model opus or python main.py --operator codex --model default |
| Choose the reviewer backend separately | python main.py --full-auto --review-operator claude --review-model opus |
| Allow Codex-backed SSH / remote GPU execution | python main.py --operator codex --codex-sandbox danger-full-access --goal "..." |
| Produce a LaTeX paper package instead of a markdown report | python main.py --output-format latex --goal "..." |
| Stop once the report is written, skipping dissemination | python main.py --final-stage 07_writing --goal "..." |
| Choose a writing venue profile | python main.py --venue neurips_2025 · --venue nature · --venue jmlr |
| Resume the latest run | python main.py --resume-run latest |
| Redo a stage inside the same run | python main.py --resume-run 20260329_210252 --redo-stage 03 |
| Roll back to a stage inside the same run | python main.py --resume-run 20260329_210252 --rollback-stage 03 |
| Re-enter an existing project instead of starting over | python main.py --project-root ~/code/my-project --goal "..." |
| Seed the run from your own prior papers | python main.py --paper-corpus ~/papers --goal "..." |
| Store runs on another disk | python main.py --runs-dir /mnt/big-disk/runs --goal "..." |
| Raise the per-attempt ceiling for long training runs | python main.py --stage-timeout 43200 --goal "..." |
| Give a stubborn stage more retries | python main.py --max-attempts 10 --goal "..." |
| Let Stages 03-06 run as a repeatable round (default 1) | python main.py --max-rounds 2 --goal "..." |
| Escalate a crux to a four-voice panel | python main.py --deliberation --max-deliberations 3 --goal "..." |
| Widen Stage 02 with divergent proposers | python main.py --ideation-panel --ideas-per-proposer 3 --goal "..." |
| Skip the intake stage | python main.py --skip-intake --goal "..." |
| Add a generated method diagram to the paper | python main.py --research-diagram --goal "..." |
Search the web where the agent's own WebSearch is disabled | python main.py --web-search gemini --goal "..." |
| Tag this run as one arm of a paired trial | python main.py --trial t1 --capability review_panel --arm on --goal "..." |
| Read the paired-trial analysis and exit | python main.py --trial-report |
| Benchmark AutoR on ResearchClawBench | python rcb_agent.py --workspace <WORKSPACE> |
| Score a finished benchmark run with the reference judge | python tools/score_rcb_run.py --workspace <WORKSPACE> --bench <BENCH> |
| Rediscover a published finding on FIRE-Bench, under its own one-hour clock | python fire_agent.py --bench-root ~/FIRE-Bench --task cot_in_planning --profile pipeline · --profile direct |
| Score a FIRE-Bench conclusion with the benchmark's own claim-level judge | python tools/score_fire_run.py --bench-root ~/FIRE-Bench --log-file <log.log> --task cot_in_planning --draws 3 |
| Stage an AIRS-Bench task's data and workspace | python tools/airs_setup.py --task <TASK> --repo <AIRS_BENCH> --raw-dir <RAW> --workspace <WS> |
| Solve one AIRS-Bench task and score the submission | python airs_agent.py --task <TASK> --repo <AIRS_BENCH> --raw-dir <RAW> --workspace <WS> |
| Run one arm of an AIRS-Bench comparison — AutoR, or the same CLI with no AutoR | python tools/airs_arm.py --arm autor --tasks <TASK>... · --arm bare |
| Report finished AIRS-Bench arms in the benchmark's own three metrics | python tools/airs_report.py --arm autor=<MANIFEST> --arm bare=<MANIFEST> --figure <PNG> |
Every flag, its default, and what is preserved on resume:
docs/cli-reference.md. Stage identifiers accept 03, 3 or
03_study_design; --venue defaults to neurips_2025.
Three flags put an agent in the approval seat, not two — and a fourth removes the human without replacing them.
approval_modebecomesagentfor--approval-mode agent,--full-autoand--review-panel, andcreate_revieweris called only when it does.--unattendedon its own is the odd one:resolve_unattendedreturnsTruefor all four, but withapproval_modestillmanualthere is no reviewer to install, so the first approval menu raisesUnattendedInputErrorrather than being decided. For a run with nobody at the terminal, pass--full-auto.Because
--rigoris resolved beforeresolve_unattendedruns, a plain--rigor maxsetsreview_panel = Trueand silently converts an interactive run into an unattended agent-gated one. Under a badge reading Human approval required, the flag that looks like more review is the flag that removes the reviewer. Three headline mechanisms — obligations, the standing review policy, the cross-model veto — also run only behind that agent gate, as do anchored comments. Manual approval is the default and remains the path for work you intend to publish.
For Codex-backed runs AutoR defaults to --codex-sandbox workspace-write. If a verified remote
experiment needs SSH or external GPU access, use --codex-sandbox danger-full-access
intentionally: it grants the Codex backend unrestricted local and remote execution, so it should not
be the default for untrusted tasks.
# Self-improvement is on by default: navigate the graph, score every draft, keep the
# best, and record the route in ~/.autor/archive.
python main.py --goal "..."
python main.py --archive-report # what the archive has learned so far
python main.py --goal "..." --evolve-rounds 4 # spend more on improvement
python main.py --goal "..." --evolve-rounds 0 # measure and ratchet, no extra passes
python main.py --goal "..." --archive-steer # let the archive pick the topology
# Opt out entirely: the strict 01-through-08 sequence, last draft wins.
python main.py --goal "..." --stage-graph linear --routing off --no-evolve --no-archive
Studio (browser UI)
A local web UI over the same Claude-backed runs: create a project, watch stages execute, approve or
send feedback, read the compiled paper. It needs the Claude CLI on PATH to start a run.
python studio.py # http://127.0.0.1:8000/studio/
python studio.py --host 0.0.0.0 --port 8765 # bind externally, see the warning below
python studio.py --runs-dir /path/to/runs # override runs directory
The Studio API has no authentication. It binds to
127.0.0.1by default; anything that can reach it can start runs, approve stages, and read every file under the runs directory. For remote access prefer an SSH tunnel over--host 0.0.0.0. See SECURITY.md.
One honest limit, then the walkthrough: the Studio's lazy-resume approve path picks the next stage arithmetically — the first stage with a higher number (src/backend/studio_runner.py) — and never consults the router, so graph routing and backward moves are a CLI capability today. Page-by-page walkthrough and the full HTTP API: docs/studio.md.
How it works: the stage graph
Eight stages are the nodes; a finish node closes the walk. Stage 00 intake is not one of them — it
runs before the walk starts, and _graph_entry_stage → _select_stages_for_run
(src/manager.py) only ever yield the eight. Solid edges advance, dotted edges go
back. --stage-graph linear is the eight advance edges plus the conditional terminal — nine in all —
and the guards come off with the backward ones (_advance_edges(guarded=False)): one edge out of
each node leaves nothing to choose, so a guard there could only halt a run that the stage's own
validation is about to fail anyway.
flowchart LR
S1[01 Literature] --> S2[02 Hypotheses]
S2 -->|has_hypotheses| S3[03 Design]
S3 -->|design_artifacts| S4[04 Implementation]
S4 -->|runnable_code| S5[05 Experiments]
S5 -->|results_exist| S6[06 Analysis]
S6 -->|validity_chain| S7[07 Writing]
S7 -->|report_exists| S8[08 Dissemination]
S8 --> Z([finish])
S6 ==>|round abandoned| Z
S2 -.->|the gap it rests on is not a gap| S1
S3 -.->|a hypothesis cannot be brought to a decision| S2
S4 -.->|not executable as specified| S3
S5 -.->|implementation is at fault| S4
S5 -.->|comparison cannot distinguish| S3
S6 -.->|results insufficient to decide| S5
S6 -.->|confound the results cannot repair| S3
S6 -.->|evidence refutes, and points somewhere| S2
S6 -.->|the numbers are wrong, not disappointing| S4
S7 -.->|claim has no analysis behind it| S6
S7 -.->|needs a result never produced| S5
S7 -.->|the survey missed related work| S1
S8 -.->|deliverable is not what a reader needs| S7
Six of the eight forward edges carry a guard, one per target stage (_ADVANCE_GUARDS); 01→02 and
08→finish are unguarded. Thirteen dotted edges go back (REVISIT_EDGES) — the longest is 07→01:
writing it up showed the finding relates to work the survey missed. One conditional terminal
(TERMINAL_EDGES, carried by both topologies) lets an abandoned round finish from Stage 06. The
Stage 07 guard is the strictest: every preregistered empirical hypothesis needs a verdict and at
least one figure under workspace/figures (_guard_validity_chain).
Who decides the move. AutoR decides which moves are admissible, by evaluating each edge's guard
against the artifacts on disk. With --routing auto (the default, DEFAULT_ROUTING_MODE) the agent
chooses among them and states a reason; --routing off always takes the graph's default. An
off-menu choice — an unlisted target, or one with no stated reason — is refused, written to
evolution/routing_refusals.jsonl (src/router.py), and replaced by the forward edge.
Two design calls worth naming. Blocked moves are handed to the agent with the reason they are
blocked (StageGraph.moves) — the useful thing to say is not "you may go to 06" but "07 is closed
because H2 has no verdict", and an agent that sees why writing is closed routes to the analysis that
opens it. And a revisit whose justification repeats one already on the path is refused
(repeats_a_previous_reason): going again on the same grounds is a loop, not an iteration.
A backward move is only ever a deliberate choice. The default is always the forward edge, and when a guard has closed it the default advances anyway and lets the stage's own validation — still refusing a Stage 07 that writes up unadjudicated hypotheses — be the gate it always was. A guard is a routing preference; the gate is the gate. So a refusal, a routing failure, or a run nobody is steering all come out as the linear pipeline rather than as a stall.
A stage is a node with a visit budget, not a position in a sequence: DEFAULT_MAX_VISITS = 3
(--graph-max-visits); DEFAULT_MAX_STEPS = 20 bounds the whole walk (--graph-max-steps).
The eight stages, and what you check at each
| Stage | Role | What the human is checking |
|---|---|---|
00_intake (before the walk) | Align the goal, resources, constraints, target venue and success criteria. | Answer the clarification questions, add the missing constraints, and narrow the project until it is executable. |
01_literature_survey | Build the related-work base, organize the evidence, identify the real gap. | Reject shallow paper lists; require task framing, benchmarks, baselines, differences, and structured literature files with a cross-referenced sources.json/claims.json. |
02_hypothesis_generation | Convert the direction into typed, testable hypotheses and provisional paper claims. | A - Decision rule: line on every empirical hypothesis, stating in advance what would count as support and what would count as refutation. These are the hypotheses frozen at 04 and adjudicated at 06. |
03_study_design | Turn the hypotheses into an executable plan, a declared protocol and a committed report plan. | Datasets, metrics, ablations, budgets, failure criteria, machine-readable data artifacts, a baseline set where every entry states why_competent and its tuning_budget — and the figures the report will carry, each naming the claim it supports. |
04_implementation | Build the runnable code, configs, data preparation and sanity checks. | This is the freeze point: approving the stage hashes the hypothesis set into workspace/notes/preregistration.json. Check the set you are freezing, and do not approve skeletons. |
05_experimentation | Run the planned experiments and write machine-readable results. | The declared baselines and the seeds: a supported or refuted verdict off a single seed is refused unless the run states why one run settles it (MIN_SEEDS_FOR_A_VERDICT = 2). |
06_analysis | Interpret the results, produce figures, adjudicate every frozen hypothesis. | A verdict for each one, backed by a result file the validator can find. The forward edge stays closed until then. |
07_writing | Produce the deliverable: a markdown report with embedded figures, or a venue-aware LaTeX package with a compiled PDF. | That every claim traces, and that the report answers what the task actually asked. A confirmatory claim whose hypothesis is not in the supported set is already refused, so what is left to check is whether the exploratory ones are honestly labelled. |
08_dissemination | Package the run for review, release, reproduction or presentation. | Readiness notes, review materials, manifests and outward-facing deliverables exist. |
The rigor dial
--rigor is the single source of truth for which optional machinery a run uses. The table is
generated from _LEVEL_FEATURES in src/rigor.py; an explicit --flag / --no-flag
always beats the level, which is why those switches use BooleanOptionalAction with default=None.
--rigor | --effort-tiers | --deliberation | --ideation-panel | --review-panel |
|---|---|---|---|---|
fast | – | – | – | – |
standard (default) | on | – | – | – |
thorough | on | on | on | – |
max | on | on | on | on |
Two consequences worth stating out loud:
- Effort tiers are on by default. A default run therefore routes
04_implementation,05_experimentationand08_disseminationto a lean prompt and a single reviewer (DEFAULT_TIERS, src/effort.py). Under--rigor maxthe seated panel does not sit at those three gates unless you also pass--no-effort-tiers. --rigor maxmakes the run unattended, because it implies--review-panel. See the note under Common commands.
The scientific-validity chain is not on this dial. It is unconditional at every level, fast
included.
Self-improvement rounds
Every valid stage draft is measured against a rigour rubric read off disk — do the paths it names
resolve, do the numbers it reports appear in a results file, did it produce artifacts during this
execution, is the decision ledger four different things rather than one sentence four times. Nine
weighted criteria, RUBRIC_VERSION = "8":
| Criterion | Weight | From | What it measures |
|---|---|---|---|
grounding | 3.0 | 01 | References that resolve — every path the draft names exists on disk |
numeric_fidelity | 3.0 | 05 | Reported numbers trace to a results file |
reproducibility | 3.0 | 01 | The machine-readable validity chain is present and parses |
deliverable_coverage | 3.0 | 01 | The draft speaks to each thing the task statement asked for, with a number an artifact holds |
source_figure_coverage | 2.0 | 06 | Each panel the source published has a figure of this run's, published and referenced |
contract | 2.0 | 01 | Contract compliance in substance, not just in headings |
artifact_breadth | 2.0 | 01 | Artifacts produced this stage, in the directories this stage's prompt named |
quantification | 2.0 | 04 | Findings carrying numbers rather than adjectives |
traceability | 1.5 | 01 | The decision ledger is four different things, not one sentence four times |
commitment | 1.5 | 01 | Reports work, not intentions |
min_stage exists so a criterion that cannot apply is not scored zero: Stage 01 has no experiment
manifest to produce, and grading it as if it failed to produce one would make every early stage look
worse than every late one — which would make the ratchet prefer late drafts for a reason unconnected
to quality.
artifact_breadth is scored against STAGE_ARTIFACT_KINDS, the set of workspace directories this
stage's prompt tells the agent to write — literature/ at Stage 01, artifacts/ + reviews/ +
writing/ at Stage 08, and so on. A test refuses any expectation the stage's own prompt never
asked for, so the criterion cannot drift into demanding work the run was never told to do. AutoR's
own bookkeeping does not count towards it: neither the RECORD_ARTIFACTS the experiment manifest
already excludes, nor the ideation pool, the writing and layout triage, the comment, crux and effort
ledgers, the panel's transcripts, the run scorecard or the adversarial reviewer's findings, all of
which the workflow manager writes into those same directories — six of them inside a stage's own
window, before its draft is scored. _harness_written_records names them, each imported from the
module that writes it, and a census over src/ fails the suite on a path under one of those
directories that nobody has said who owns.
Measuring is free and always on. The rubric reads the run off disk and never calls a backend, so the property it buys costs nothing: the draft that gets promoted is the best one the run produced, not the last one. That is the half that was missing before — AutoR could iterate, but "later" was the only ordering it had, so a refinement that dropped a resolving reference was promoted on exactly the same terms as one that fixed something.
Improvement rounds are the half that costs, and they are budgeted separately from
--max-attempts, which bounds a stage that is failing rather than one being improved. Two per
stage by default, and a stage whose rubric has no shortfall worth acting on spends none of them — a
round aimed at a criterion already at full marks produces churn, so AutoR does not buy one.
--evolve-rounds 0 measures without polishing; --no-evolve restores the old behaviour entirely.
One edge of that budget is worth knowing before you resume a run. state() rehydrates the champion
and the Pareto frontier from disk and nothing else (src/evolution.py), so
--resume-run restarts rounds_spent and the patience counter at zero: the best draft survives the
resume, the spend cap does not, and a stage resumed twice can buy the two rounds twice.
A round that scores worse is reverted, so a stage can only improve. A round that changes a hypothesis
verdict is rejected outright, whatever it scored — the rubric is blind to what the run concluded,
which removes the incentive, and the verdict_drift check removes the possibility. A revision a
human asked for always stands, whatever it measures. The ratchet governs AutoR's own rounds, not
the direction it is given.
Full mechanism, and the reasoning behind each refusal, in docs/self-improvement.md.
The archive: which moves paid, across runs
Every finished run is recorded into ~/.autor/archive — the route it took, the rubric fitness it
reached, and the set of stages it actually measured (Archive.record_run, from record_into_archive
in main.py). edge_payoffs compares runs that took an edge against runs that were
offered it and declined (src/decisions.py), and propose_variant turns a
payoff that is believable — enough observations, and a delta above min_gain — into a child variant
that moves that one edge one step up or down the preference order. When no payoff is believable,
propose_exploration proposes an unexplored edge instead, so an edge nobody has taken is not
stranded forever.
A variant is only a reordering. It never opens a guarded edge, never adds one that was not declared, and never removes one: the guards are the correctness argument for letting an agent route at all, and the component that learns from outcomes is precisely the one that must not be able to weaken them. Promotion is as conservative — a challenger has to beat the incumbent within every comparability basis rather than on a pooled mean, because "runs that stopped early" is the cheapest composition for a topology to win on.
The archive records and proposes on every run; it steers only when you ask. The proposed variant
is written down and reported, but the topology a run walks comes from the archive only under
--archive-steer. Without it, resolve_graph returns the declared topology unchanged.
Paired trials
--trial ID --capability NAME --arm LABEL tags a run as one arm of a paired A/B trial in the
archive; --trial-report prints the within-pair rubric difference with a two-sided sign-flip
p-value and the smallest p-value the estimator behind it could possibly reach
(src/trials.py, src/inference.py). The p is exact by
enumeration up to MAX_EXACT_PAIRS = 18; above that it is a seeded sample of
SAMPLED_SIGN_ASSIGNMENTS = 200,000 sign assignments over the same differences, and the report
says so and prints the seed. Below
MIN_PAIRS_FOR_SIGNIFICANCE = 6 a trial is labelled underpowered rather than reported as a null.
This is the apparatus for answering "does this mechanism help?", not the answer. No paired trial has been run yet. Read docs/self-improvement.md before quoting anything from it.
Review: five kinds of critic
Only two of the five are the approval gate. The other three cannot approve anything.
| Kind | What it is | Can it approve? |
|---|---|---|
Solo reviewer (approval_agent.py) | A coding agent with file tools returning one of six choices as JSON, through a parser that re-asks once and then falls back | Yes — this is the gate under --full-auto |
Review panel (review_panel.py) | Five role-differentiated seats review blind, cross-examine anonymised peers, then a chair synthesizes | Yes — this is the gate under --review-panel |
Cross-model veto (cross_reviewer.py) | A different model family audits an approval only | No — veto only |
Adversarial validity review (validity_review.py) | Runs after Stages 05 and 06 are approved with the opposite instruction: "explain why this result is wrong" | No — it creates debts the next stage must answer |
Crux panel (deliberation.py) | The executing agent raises a question; four voices answer it while arguing against themselves | No — it is not a reviewer of a stage at all |
The panel's teeth are mechanical. If any final-round seat carries blocking: true and the chair
returned "approve", _enforce_blocking_objections() rewrites the approval into a refinement and
records chair_overridden. It is enforced in code precisely because the chair is a model that can be
argued out of a prompt-level rule. Blocking is read from the seat's own payload and only counts when
that payload's decision token is legible, so a seat whose verdict was unreadable cannot veto.
Every panel run carries its own control arm. The chair's round-1 verdict is one model, one call,
no peer input; panel_effect.json accumulates solo-vs-panel across the run and writes a verdict
sentence deliberately phrased to be unflattering — "it did not earn that cost; consider dropping the
panel" — when that is the truth. The same is true of the ideation panel, anchored comments, crux
deliberation and effort tiers; scorecard.py reads all five ledgers at the end of
every run and writes workspace/reviews/scorecard.md, keeping "could not be measured" separate from
"changed nothing".
Two ledgers run underneath. review_policy.json turns every refusal into a standing rule injected
into every later solo review, deduplicated on normalized text so a reviewer restating one complaint
cannot manufacture the appearance of learning. obligations.json lets an approving reviewer attach a
debt to a later stage — "fine, but you owe me a power analysis at design time" — which only a later
reviewer may discharge; deferral is counted and shown, never silent.
Anchored comments make a refusal local. A reviewer quotes the exact passage it objects to
(minimum 12 characters, and an unfindable quote is dropped as unanchored rather than sent), the
revision is told to leave everything else byte-identical, and the next draft is diffed against the
quotes so collateral rewriting is counted rather than assumed away.
Full seat charters, the deliberation protocol and the measurement design: docs/review-panel.md, docs/deliberation.md, docs/stage-comments.md, docs/scorecard.md.
The stage contract and what gets validated
AutoR does not consider a run successful just because it generated a plausible markdown summary.
Required stage summary shape. Seven headings, in this order — REQUIRED_STAGE_HEADINGS:
# Stage X: <name>
## Objective
## What I Did
## Key Results
## Files Produced
## Decision Ledger
## Suggestions for Refinement
## Your Options
Also required, and checked: exactly 3 numbered refinement suggestions, exactly the fixed 6 user
options, concrete file paths under Files Produced, and no [In progress], [Pending], [TODO] or
[TBD] placeholders.
Artifact gates. Most start by asking whether a file is there — the rows that say "valid", "resolving" or "matching" then parse it.
| Stage | Required non-toy output |
|---|---|
| Stage 01 | A cross-referenced evidence ledger: sources.json and claims.json, where every cited source_id resolves |
| Stage 02+ | A decision_rule on every empirical hypothesis in hypothesis_manifest.json — held here, at the stage that writes them, rather than at the Stage 05 preregistration gate, where the set is already frozen and the only repair is a rollback |
| Stage 03+ | Machine-readable data under workspace/data/, plus a report_plan.json committing to the figures and headline numbers the report will carry |
| Stage 05+ | Machine-readable results under workspace/results/, plus a valid experiment_manifest.json |
| Stage 06+ | Real figure files under workspace/figures/, and every planned figure's source_artifact resolving to a non-empty file |
| Stage 07+ (markdown) | report/report.md with resolving figure references, between min_report_figures and MAX_REPORT_FIGURES figures under report/images/, deliverables_coverage.json, citation_verification.json, self_review.json, report_review.json |
| Stage 07+ (latex) | main.tex matching the venue, sections/*.tex, a bibliography, a compiled PDF, build_log.txt, citation_verification.json, self_review.json, layout_review.json |
| Stage 08+ | Review and readiness assets under workspace/reviews/ |
Requirements are cumulative, and the stage that produces a class of artifact must produce it
during that stage's execution — a re-run is not credited with the previous attempt's files. The
cutoff is stage_execution_started_at feeding recent_in, and the rubric enforces the same rule
independently in _fresh_artifact_kinds, so a Stage 07 draft cannot score on Stage 06's figures.
min_report_figures is a run_config.json field, set by rcb_agent.py --min-report-figures, clamped to [1, MAX_REPORT_FIGURES] (15). It is 1 for
an ordinary run and 3 for a ResearchClawBench run (BENCHMARK_MIN_REPORT_FIGURES).
Validity gates. The same function — validate_stage_artifacts (src/utils.py) —
also runs the validators that ask whether a claim is warranted rather than whether output exists.
Eighteen validate_* functions are reachable from it in all:
| Fires at | Validator | Refuses when |
|---|---|---|
| 01 | validate_literature_evidence | A claim cites a source_id that is not in sources.json |
| 02+ | validate_hypothesis_decision_rules | An empirical hypothesis in hypothesis_manifest.json carries no decision_rule, or the manifest does not parse |
| 03+ | validate_report_plan | The plan has no task outputs, non-contiguous slots, a slot with no supported claim, or headline numbers without a quantity, unit and source |
| 05+ | validate_preregistration | Nothing is frozen, an empirical hypothesis has no decision rule, the frozen file disagrees with its own digest or with AutoR's stamped copy, or the manifest changed — or went missing — with no amendment on record |
| 05+ | validate_experimental_protocol | No primary metric, planned_seeds < 1, or a baseline missing why_competent / tuning_budget |
| 05+ | validate_experiment_manifest | The manifest does not parse into the declared shape |
| 06+ | validate_hypothesis_outcomes | A frozen hypothesis has no verdict, a verdict adjudicates something unpreregistered, or a supported/refuted verdict cites an evidence path that does not exist |
| 06+ | validate_outcome_statistics | A verdict has no n_seeds, a dispersion_type naming no known measure, a single seed with no justification, or dispersion_type: none with two or more seeds |
| 06+ | validate_report_plan_sources | A planned figure or headline number's source_artifact is missing or empty |
| 06, 07 | validate_validity_response | The stage did not answer every adversarial finding from the one before it, with a status, a ≥40-character explanation, and evidence when it claims addressed — or the workspace copy of that review disagrees with AutoR's stamped copy |
| 06 | validate_round_decision | A round closes as converged with no supported hypothesis and no negative_result: true |
| 07+ | validate_claim_provenance | A manuscript claim is confirmatory on a hypothesis that is not supported, or cites no evidence file that exists |
| 07 md | validate_markdown_report | The report is under 1200 characters, carries placeholder text, references an image that does not resolve, or publishes fewer than min_report_figures |
| 07 md | validate_report_plan_coverage | A planned figure was never published or never referenced, or every slot was dropped |
| 07 md | validate_deliverables_coverage | The report does not answer a demanding sentence of the task statement, or a coverage entry quotes something that is not verbatim in the task |
| 07 md | validate_report_review | The AutoR-generated triage artifact is malformed |
| 07 latex | validate_layout_review | The LaTeX build triage artifact is malformed |
| 07+ | validate_citation_verification | The self-report has no status, a non-integer citation count, or an empty claim-coverage list |
The code labels the split itself: "the scientific-validity chain, distinct from the artifact gates
around it". A run can fail because a claim is unwarranted, not only because a file is absent. The
06→07 router edge is closed on top of that, in the adaptive topology only.
The frozen preregistration is checked against a copy the stage cannot reach. A digest stored
beside the bytes it describes certifies nothing, and this one is worse than most: the agent is shown
it, because format_preregistration_for_prompt renders digest into the prompt. So AutoR keeps its
own copy of the frozen record at runs/<id>/preregistration_stamp.json, outside workspace/, and
preregistration_tamper_findings runs three comparisons rather than one — the hypotheses against
the digest the file states for them, that digest against the stamped one, and the length of the
amendment ledger. Each catches a rewrite the other two miss: an edited statement, an edited
statement with the header recomputed, and a deleted amendment row. Deleting the frozen file is not a
way around them either — freeze_preregistration restores the stamped record rather than deriving a
fresh one, and deleting both copies is refused rather than re-derived, because the first freeze is
witnessed in the run log. So a re-freeze cannot hand the run a post-results date and an empty
ledger, and the
disagreement it found is appended to the stamp's repairs list before the copy goes back.
The complete gate, including every JSON schema that is parsed rather than merely counted, is in docs/stage-contract.md.
Execution model
Context is composed per consumer, not per availability. A stage's inbound block is built by
render_inbound(ChannelContext(...), CHANNELS) from the eighteen typed channels in
src/information_flow.py. Each channel declares produced_by, a
consumed_by set of real stage slugs, and a rationale;
test_every_narrowing_is_argued_for (tests/test_information_flow.py)
fails a channel that withholds itself from a stage without saying why. Withholding has to be argued
for, not just done.
Four narrowings worth knowing, because the abstraction is not the point. Eighteen channels narrow; these four are the ones whose reason is not readable off the key:
- the artifact index skips Stages 00-02 — they produce no data, results or figures, so the index is empty noise there
- the writing manifest reaches Stage 07 alone
- the mutable Stage 02 hypotheses stop at
04_implementation, because the freeze at Stage 04's approval supersedes them. Before that edge was typed, the same H1 went into every prompt from Stage 05 on twice — one copy labelled editable, sitting next to the frozen one at exactly the stages where the freeze is the point. - the project bootstrap narrows by exactly one stage and no more.
recommend_entry_stagecan return any stage from 01 to 08, so a fixed early set of readers would withhold the description of the repository from the run that re-enters latest — the one that has seen least of it. Stage 00 is the single exclusion, becauserun()scans the repository after intake has finished, so the block is empty there every time. This block deliberately overlaps# Approved Memory:_adopt_project_bootstrap_baselinecopies each below-entry assessment into a stage summary, butappend_approved_stage_summarykeeps only the entries numbered below the stage it writes, so the first approval below the re-entry point erases them and this block becomes the only copy.
dependency_edges() returns every (producer, consumer, channel key) triple, so the information
topology can be printed and diffed rather than reconstructed from a pile of if statements.
_record_inbound_channels writes the delivered channel keys per stage into the run log.
Honest scope: twenty blocks are typed. Six more — obligations_context, intake_context_text,
web_search_context, approved_memory, handoff_context, and the # What the Task Asks For block
that build_prompt composes inline from
format_deliverables_for_prompt — are still delivered by build_prompt
itself rather than declared as channels, so each one's delivery rule lives there instead of next to a
consumed_by set. Around them, compose_stage_template
(src/prompt_fragments.py) assembles the stage's own instructions, the
accepted-extension lists generated from the validators' constants rather than hand-copied, and the
run-safety rules.
The assembled prompt is written to runs/<run_id>/prompt_cache/, per-stage session IDs to
runs/<run_id>/operator_state/, and the selected CLI backend is invoked in live streaming mode.
Alongside the prompt, AutoR installs an agent skill pack from src/skills/ into
runs/<run_id>/.claude/skills/ — the operator's working directory — so the agent can pull
long-form craft guidance when it needs it. A skill costs nothing in the prompts that do not use it.
168 skills ship today: 79 general ones and 89 field-specific ones. Forty of them were written in one pass against the twelve tasks that trailed a bare-Claude-Code control under a single judge — three or four per task, each selected by a phrase in that task's brief and in no other of the forty, and every one of the forty pinned. Most of them were written against a scored arm's per-criterion losses on the twenty-five ResearchClawBench tasks that lost, at least three per task. A run is not offered all of them. Two filters narrow the pack, and a skill has to survive both:
-
Field. A skill named
<field>-...is installed only for a run in that field, so twenty become two. A materials run does not benefit from being offered advice about observational astronomy, it just has one more description to read past. -
Shape. A skill may carry an
applies_whenregex, matched against this run's own research brief and data manifest. 51 skills are scoped this way today; measured over the forty ResearchClawBench briefs they select between 1 and 7 tasks each — forty of the forty-four RCB-shaped ones select exactly one — eighteen tasks receive none of them, and no task receives more than six. The seven added for AIRS-Bench are scoped on a different corpus and select 19 of its 20 briefs and none of the forty ResearchClawBench ones; the twentieth is a brief whose whole task paragraph is one sentence that never says what the deliverable is, so no predicate over task shape can reach it and its pin does instead.tools/skill_selectivity.pyprints the selection set for a corpus and--expectturns it into an assertion, because a predicate is a claim about a kind of research problem and it should be checkable.A skill that selects nothing over the forty is exempt by name in
tests/test_every_skill_can_be_loaded.pyrather than passed by a loosened regex, and the exemption carries the measurement that justifies it. Those seven are the whole list today: a predicate written against one harness's own task shape generalises to nothing outside that harness, which is a real cost written down rather than argued away, and it is why they reach a run through the third route below rather than through this one.
The predicate reads the brief, never the task's identifier: a table of benchmark ids would select the same tasks today and generalise to nothing.
- Pin. There is one exception, and it is deliberate.
configs/task_skill_pins.json maps a task identifier to skills
that are installed for it whatever the two filters say. A pin is not an inference about a kind of
task — it is a record that this exact identifier already ran, already scored, and lost criteria
whose subject is those skills, so it is the one routing input that cannot be derived from the
task statement and does not generalise past the name it carries. Forty-seven tasks are pinned
today — twenty-seven from ResearchClawBench and all twenty of AIRS-Bench — 420 pins between
them, at most fifteen on any one task; twenty of them are skills the two filters would have
withheld, in each case a field skill whose content applies outside its own field. The two
benchmarks' pins are derived differently and the file says so: RCB's from per-criterion losses,
AIRS-Bench's from a mechanism that is arm-wide because that benchmark has no criteria — 43% of
the median run's tool calls landed after its predictions file stopped changing. A run that matches an entry writes
skill_pinsinto itsrun_config.jsonand askills pinned_by_task_idline into its log, because a pinned arm and an unpinned arm are two configurations and a score from one is not a score from the other. - Force. A front end may set
Manager.skill_forceto a set of names installed on every run it launches, whatever the filters say and whatever the pin table holds. Neither an inference about this task nor a record of this task: a decision about a whole benchmark population, taken outside the run, on evidence the run cannot see. No front end in the tree sets it today — the adapter that did belonged to a benchmark since removed, which offered--no-forced-skillsas the control arm out of the same binary. It also closes a hole the predicate cannot:select_run_skillsfails closed on an empty brief and refuses every task-scoped skill silently. A control arm has to setManager.skill_withholdas well as clear the force, and that is not a hypothetical: the five skills that adapter forced also carried a predicate matching all sixty of that benchmark's task statements, so clearing the force alone left the arm running with the same five under a different banner — measured on a--fake-operatorrun beforeskill_withholdexisted, where the difference between the two arms came to one paragraph of prompt rather than five skills. Withholding beats every other input here, including a pin, because it is not a routing decision — it is an experimenter saying which arm this run is. A forced run writesskill_forcedandskill_forced_byinto itsrun_config.jsonand askills forced_by_front_endline into its log, saying in the same sentence that a score from it is not comparable to one from a run without them. A front end that uses it owes the same fact somewhere a trial can read — taken off the manager's installed set rather than off the flag, and folded into the arms' environment digest — so that sentence about comparability is enforced rather than only printed. Announced in the prompt under its own banner, never the pin's: the pin sentence earns its force by being precise about a scored run of this exact task, and reusing it here would be a claim that is false of every run that reads it.
Pull-based is not the same as discoverable. Measured over a 40-task arm, the pack drew 78 Skill
calls in 789 hours of agent time, 31 of them the one skill a stage prompt named imperatively —
and stage 05 launched none in any of the forty runs. So every general skill is now named at the
stage whose decision it covers, a task-scoped one is announced by the task_shaped_skills channel
for the runs that were selected for it, and
tests/test_a_skill_is_named_where_it_is_needed.py refuses a skill that nothing announces —
in either direction, since a prompt naming a skill most runs will not have is the same defect
reversed.
Claude CLI invocation
First attempt for a stage:
claude --model <model> \
--permission-mode bypassPermissions \
--dangerously-skip-permissions \
--session-id <stage_session_id> \
-p @runs/<run_id>/prompt_cache/<stage>_attempt_<nn>.prompt.md \
--output-format stream-json \
--verbose
Continuation attempt for the same stage replaces --session-id with --resume.
_build_cli_command (src/operator.py) additionally inserts
--mcp-config <run>/operator_state/mcp_config.json whenever the MCP web-search server is active, and
--tools <tools> when a tool restriction is set.
Web search where the backend has none. Some deployments disable the agent's built-in WebSearch
tool. --web-search gemini starts a stdlib JSON-RPC MCP stdio server
(src/mcp_web_search.py) that exposes one tool,
mcp__autor-search__web_search, backed by Gemini with grounded search, and passes it to the CLI via
--mcp-config. assess_search_readiness() refuses to promise a capability the environment cannot
deliver: a hard blocker (no key, no SDK) is reported before the run starts rather than discovered at
Stage 01.
Important behaviour:
- refinement attempts reuse the same stage conversation whenever possible
- streamed agent output is shown live in the terminal
- raw stream-json output is captured in
logs_raw.jsonl - if resume fails, AutoR can fall back to a fresh session
- if stage markdown is incomplete, AutoR can repair or normalize it locally before failing the stage
- a backend that is unreachable is classified by src/backend_health.py and
surfaces as
run.backend_unavailable, so "the model was down" never reads as "the research failed"
Run layout
Every run lives entirely inside its own directory. The tree is build_run_paths
(src/utils.py).
runs/<run_id>/
├── user_input.txt memory.md run_config.json
├── run_manifest.json artifact_index.json intake_context.json
├── obligations.json review_policy.json # both per-run; nothing crosses runs
├── report_plan_stamp.json preregistration_stamp.json validity_review_stamp.json
│ # AutoR's copies, outside workspace/ on purpose
├── stage_cost_ledger.json # one row per stage visit: what it spent, why each attempt failed
├── logs.txt logs_raw.jsonl
├── prompt_cache/ operator_state/ handoff/ stages/
├── .claude/skills/ # the skill pack, pulled on demand by the agent
├── evolution/ # champion drafts, improvement_ledger.jsonl, summary.json,
│ # stage_graph.json, routing_refusals.jsonl
└── workspace/
├── literature/ code/ data/ figures/ report/ writing/
├── bootstrap/ profile/
├── notes/ preregistration.json, hypothesis_manifest.json, experimental_protocol.json,
│ report_plan.json, research_rounds.json, round_decision.json,
│ deliberation_request.json
├── results/ experiment_manifest.json, hypothesis_outcomes.json
├── artifacts/ claim_provenance.json, deliverables_coverage.json, citation_verification.json,
│ self_review.json, report_review.json | layout_review.json, compiled PDFs
└── reviews/ validity_review_<stage>.json, validity_response_<stage>.json,
comment_ledger.json, deliberations.json, scorecard.md, panel/
evolution/ sits outside workspace/ on purpose, and the dataclass records the reason: it is "a
record of how the run reached its answer, not part of the answer, and a benchmark export that swept
it up would ship the losing drafts alongside the report". report_plan_stamp.json,
preregistration_stamp.json and validity_review_stamp.json are outside workspace/ for the same
class of reason: the agent must not be able to backdate its own declaration, rewrite the commitment
it is being held to, or edit the record of the objections it owes an answer to. So is
stage_cost_ledger.json, which is a run's account of what each stage visit cost and why each
attempt failed — a receipt the payer prints is worth what it cost to print.
That row carries the backend's own dollar figure and its four token fields, wired out through
OperatorResult, ReviewDecision and ValidityReviewOutcome rather than scraped back out of
logs_raw.jsonl. A field the backend did not report is absent rather than zero, so a run smoke-tested
with --fake-operator says not measured instead of $0.00. At the end of a run — completed or
cancelled — AutoR prints one summary of it to the terminal, and to nothing else: the deliverable does
not change, and logs.txt keeps the attempts and the failure census without the money. Nothing at
runtime decides on any of it. Not the supervisor, not the router, not a gate: the fields may appear
in the record, in the summary and in the formatter, and in no condition anywhere under src/, which
tests/test_cost_is_recorded_and_unread.py asserts over the syntax of every module the way
tests/test_router_budget.py asserts it of StageRouter.choose.
The only state AutoR writes outside a run directory is the cross-run archive at ~/.autor/archive
(--archive, --no-archive).
Workspace semantics. literature/ reading notes, survey tables, benchmark notes · code/
runnable code, scripts, configs · data/ machine-readable datasets, manifests, processed splits ·
results/ metrics, predictions, ablations, plus the standardized experiment_manifest.json ·
report/ the markdown deliverable, report.md and the PNGs it embeds under images/ · writing/
LaTeX sources, sections, tables, bibliography · figures/ plots and paper figures · artifacts/
review JSON, build metadata, compiled PDFs, packaged deliverables · notes/ the frozen files of the
validity chain plus supporting notes · reviews/ adversarial validity reviews, panel transcripts,
self-measurement ledgers, the run scorecard.
Outside workspace/: memory.md is the approved free-text cross-stage memory; handoff/<slug>.md
is the second free-text carrier, each approved summary trimmed to Objective / Key Results / Files
Produced and sent only on a continuation attempt or when memory is still empty. Every other
cross-stage edge is a typed channel or a JSON artifact. run_manifest.json is the lifecycle state
that resume, redo and rollback read; prompt_cache/ holds the exact prompt of every attempt,
repair, review, panel seat and crux voice.
evolution/artifact_provenance.json records which stage wrote each workspace file and every
version it has held; evolution/effects/<slug>.jsonl is that stage's accumulated inverses, moved
to <slug>.reverted.jsonl once applied, and evolution/effects/blobs/ is the content-addressed
store the rewinds read from. A rollback is not only a manifest edit: it applies those inverses in
reverse, deletes what the withdrawn stages created, rewinds what they amended back to the version
the last surviving stage left, and drops the withheld emissions in
evolution/emissions.json. Everything it moved is named in the run log under
rollback recovery, and the preview says it before the operator confirms.
Full file-by-file reference: docs/run-artifacts.md.
Architecture
flowchart LR
P[rigor.py · effort.py<br/>policy: what machinery runs] --> M
C[information_flow.py<br/>20 typed channels] --> M
M[manager.py<br/>walks the stage graph] --> W[walk<br/>stage_graph · router]
M --> G[gates<br/>utils · preregistration · experimental_protocol<br/>report_plan · deliverables · validity_review]
M --> I[improvement<br/>rubric · evolution · pareto]
M --> R[review<br/>approval_agent · review_panel · cross_reviewer<br/>obligations · review_policy · stage_comments]
M --> S[self-measurement<br/>scorecard · archive · decisions · trials · inference]
M --> X[execution<br/>operator · operator_codex · web_search · backend_health]
| Module | What it owns |
|---|---|
| src/manager.py | Walks the stage graph until it reaches finish or nothing is open — plus the router call, the evolution controller, the freeze/amend seam, the validity review, the round close, the obligation ledger, the cross-review veto, the crux settlement and the inbound-channel record |
| src/utils.py | Stage metadata, run paths, prompt assembly, markdown validation, the artifact gates and the validity-chain wiring |
| src/operator.py | The Claude CLI adapter: stage session state, live streaming, resume fallback, MCP config, skill pack install |
| src/review_panel.py | The deliberating panel; a blocking objection is enforced in code against its own chair |
| main.py | CLI entry: 61 flags, start, resume, --redo-stage, --rollback-stage, the archive record and the reports that print and exit |
| src/report_plan.py | Figures and headline numbers committed at Stage 03, stamped outside the workspace, enforced at 03, 06 and 07 |
| src/rcb.py | The ResearchClawBench adapter core: workspace layout, goal construction, report synthesis, figure publication, export |
| src/airsbench.py | The AIRS-Bench adapter core: task specifications, the scaffold-neutral brief both arms get, submission export that never writes a submission, and the benchmark's own normalized score |
| src/stage_graph.py | Stages as nodes: six guarded forward edges, thirteen backward edges, a conditional terminal, a per-stage visit budget |
| src/archive.py | Cross-run routes and edge payoffs keyed on a comparability basis; variant proposal, exploration and promotion |
| src/rubric.py | The rigour score over a draft and the artifacts it names. Never calls a backend |
| src/web_search.py | Gemini-backed search, readiness assessment, MCP config construction |
| src/deliberation.py | The crux panel: four voices, each arguing against itself, resolved into an answer that names its own falsifier |
| src/ideation_panel.py | Divergent Stage 02 proposers across five lenses, deduplicated into a candidate pool |
| src/evolution.py | The champion ratchet: budgeted polish rounds, reverted when they do not improve, rejected on verdict drift |
| src/writing_manifest.py | The Stage 07 inventory plus the AutoR-owned triage artifact for each output format |
| src/provenance.py | Which stage wrote each workspace file, every version it has held, and what a rollback withdraws or rewinds |
| rcb_tools/ | The operator scripts every benchmark arm in the lab notebook was actually run and scored by — launchers, the claim protocol, the two scorers and the gpt-5.1 judge swap. Machine-specific by design and not imported by anything under src/; kept for provenance, since two of the defects that notebook records were defects in these files |
| src/effects.py | The inverse of each write, accumulated per stage and applied in reverse on a backward edge; commutative and ordered keys |
| src/emissions.py | Acts that leave the run, withheld until the stage that asked for them is approved |
| src/approval_agent.py | The solo approval gate, its six-choice vocabulary and its unreadable-verdict fallback |
| src/preregistration.py | Freeze, amend, adjudicate, trace |
| src/information_flow.py | Twenty typed information channels, each with declared readers and a written rationale |
| src/router.py | The agent's choice among admissible moves; an off-menu choice is refused and logged |
| src/validity_review.py | The adversarial pass after Stages 05 and 06, and the response gate that follows it |
| src/research_rounds.py | Stages 03-06 as a repeatable round, bounded by --max-rounds |
| src/trials.py | Paired A/B trials over archived runs, with a sign-flip p-value — enumerated below 19 pairs, sampled above — and the attainable floor of whichever estimator ran |
| src/stage_comments.py | Anchored review comments and the collateral-change diff |
| src/effort.py | Routine vs deliberative tiering, and the concentration of the strong model |
| src/scorecard.py | Reads all five self-measurement ledgers and says which features earned their cost |
| src/obligations.py | What a later stage still owes; only a reviewer can discharge it |
| src/decisions.py | "Was offered the edge and declined" — the control arm the archive's payoffs are computed against |
| src/cross_reviewer.py | A second opinion from a different model family. Veto only, never an override |
| src/experimental_protocol.py | Declared baselines, seeds and dispersion, fixed before the result exists |
| src/deliverables.py | Did the run answer what the task statement actually demanded? |
| src/pareto.py | Non-dominated drafts kept beside the champion, and the pair worth merging |
| src/review_policy.py | Standing review rules learned from this run's own corrections |
| src/inference.py | Exact permutation tests and attainable-p floors; derives the archive's min_observations rather than asserting it |
| src/rigor.py | The one dial: which optional machinery a level turns on |
| src/backend_health.py | Distinguishes "the backend is down" from "the research failed" |
| src/prompt_fragments.py | Shared prompt blocks generated from the validators' own constants |
Supporting modules: operator_codex.py and operator_protocol.py, intake.py, manifest.py, artifact_index.py, experiment_manifest.py, evidence_ledger.py, hypothesis_manifest.py, mcp_web_search.py, diagram_gen.py, bootstrap.py and project_bootstrap.py, platform/foundry.py, run_skills.py, terminal_ui.py, prompts/, skills/, and backend/ + frontend/ for the Studio. Runnable tools: tools/score_rcb_run.py (score a benchmark run with the reference judge) and tools/archive_sample_complexity.py (how many runs the archive needs before an edge becomes believable).
The full module map, the stage attempt loop and the extension points are in docs/architecture.md. The design rationale — what is new here and why — is in docs/framework.md.
Benchmarks
AutoR is wired to three, and they measure different halves of it. ResearchClawBench hands the agent a workspace of raw data and reference papers and scores the report and figures it produces against the published paper — a test of conducting research. FIRE-Bench hands it a research question from a published empirical study, expects it to design and run its own experiments, and scores the two-sentence conclusion it writes against the authors' own — claim by claim, under a wall clock the harness enforces. AIRS-Bench hands it a prepared dataset and a metric and scores the predictions it writes. A change that moves one need not move the others.
The last one is worth having for a reason the other two cannot supply. Both of them
reach their number through a model reading what AutoR wrote, and on ResearchClawBench the choice
of reader is worth more than most of the effects being argued about — 16.2 points between two
judges on one identical artifact set, and 8.5 points between eight draws of the same judge.
AIRS-Bench runs scipy over a CSV: the same submission scores the same number every time. It is
the only one of the three where a one-task before-and-after is a measurement rather than a draw
from a distribution.
ResearchClawBench
python rcb_agent.py --workspace <WORKSPACE> runs AutoR against a
ResearchClawBench workspace with no human in
the loop and exports the benchmark's deliverables (report/report.md, report/images/, code/,
outputs/). Scoring is the benchmark's own rubric judge; tools/score_rcb_run.py defaults to the
reference judge, gpt-5.1, which is what ResearchClawBench itself scores with.
Judge choice can move a score by more than the gap between the top and the bottom third of the leaderboard. On one identical artifact set Gemini 2.5 Flash scored 37.0 where Opus scored 20.8, a spread of 16.2; on another, Opus scored 52.6 where gpt-5.1 scored 46.0, a spread of 6.6. A number carrying the wrong judge is not a smaller number, it is an incomparable one. Quote the judge with every total.
Fixing the judge does not fix the number. Eight draws of gpt-5.1 over one identical artifact
set — same workspace, same report, same five figures, nothing changed between draws — scored
41.4, 42.8, 45.5, 47.1, 49.1, 49.6, 49.8 and 49.9: a spread of 8.5 points around a mean of 46.9,
sd 3.4. The variance is worst where it costs most, on the heaviest checklist item: that one is
weighted 0.5 and spanned 32 to 55 across the eight, 11.5 points of the total by itself. So a
single-draw score on a single task carries roughly ±4 points of pure sampling noise, and any
one-task A/B comparison below about eight points is uninterpretable — including a
before-and-after on the same task, which is the shape a harness change most tempts you into. Average
draws, or compare across tasks, or say nothing.
How the number got here
Three earlier measurements of this benchmark are superseded and have been moved to the lab notebook, which is where the arm-by-arm record belongs: the first run at 14.16 where AutoR placed last behind the bare Codex CLI, the post-repair re-run at 23.57 against a 29.24 control, and the re-score that corrected both. Each was wrong in a way worth knowing about rather than wrong by accident, and the notebook keeps each one next to the correction that retired it:
- The judge was shown between 29% and 80% of the figures, depending on the arm.
gpt51_judge.pyheldMAX_IMAGES = 5for four days after upstream raised its own cap from five to fifteen (bfffc48, 2026-08-14) — a lag behind upstream, not a local deviation from it — andimage_paths[0]is the target figure, so four workspace slots were left. Arms differ in how many figures they ship, so the clip was not a constant offset: four slots showed the control 67% of its figures and the skills arm 29%. - The arms were not given the same budget. The AutoR side ran
--stage-timeout 1800and 28 of its 40 runs loggedStage timed out; the bare arm had no per-stage cap. - The draw count decides the verdict. One judge draw against three moves an arm's comparator by more than the effect being measured.
One correction from that era is a method, not an arm, so it stays here. The control was
never search-less: it was recorded as having no working web search on the evidence of 16
WebSearch calls returning an org-policy 400, when all 44 of its runs also had
ai4ai-web-search connected and called it successfully 12 times. Both that reading and the
"search parity" that replaced it came from counting tool names. Pair tool_use ids to
tool_result ids and read the body.
What has not changed: AutoR writes 36% more prose than the bare agent and covers less of what the task asked for.
Four later arms have landed, and they are ahead
The paragraph that stood here said a promising arm was being withheld until all forty of its tasks
landed, because a partly-finished arm is a subset selected the flattering way. They have landed —
and three more arms with them. Every score below is tools/score_rcb_run.py --judge reference --draws 3, the same instrument the control was measured with; a one-draw number is not
comparable to these and the reason is
in the lab notebook.
This table is generated, not maintained by hand — the version that stood here read n=35 for arms that had reached 40 and omitted nine arms run since. Regenerate it with:
python3 tools/rcb_scoreboard.py --contrasts
Snapshot 2026-08-22. One score per task, from the newest scored workspace; only
_score_gpt51.json, which is --judge reference --draws 3.
| arm | n | mean | vs bare Claude Code | 95% CI | W–L |
|---|---|---|---|---|---|
pins_on | 29 | 41.54 | +9.33 ± 1.84 ✳ | +5.61 … +13.05 | 25–4 |
full40_abl40 | 37 | 40.36 | +8.89 ± 1.52 ✳ | +5.82 … +11.97 | 29–8 |
xrev_on | 26 | 38.99 | +8.75 ± 1.77 ✳ | +5.10 … +12.39 | 21–5 |
xrev_off | 40 | 40.17 | +8.70 ± 1.58 ✳ | +5.50 … +11.90 | 31–9 |
opcalls_on | 27 | 40.38 | +8.68 ± 2.17 ✳ | +4.30 … +13.07 | 21–6 |
topo_adaptive | 25 | 38.46 | +8.40 ± 1.82 ✳ | +4.64 … +12.15 | 22–3 |
full40_skills161 | 37 | 39.36 | +7.80 ± 1.45 ✳ | +4.85 … +10.75 | 30–7 |
full40_a9c2b48 | 39 | 38.48 | +7.35 ± 1.58 ✳ | +4.17 … +10.54 | 29–10 |
opcalls_off | 26 | 40.74 | +7.32 ± 2.31 ✳ | +2.56 … +12.09 | 20–6 |
figfloor | 27 | 35.75 | +7.16 ± 1.87 ✳ | +3.38 … +10.93 | 22–5 |
base_a | 22 | 37.25 | +7.04 ± 2.00 ✳ | +2.88 … +11.20 | 17–5 |
topo_linear | 36 | 37.24 | +6.81 ± 1.68 ✳ | +3.40 … +10.22 | 27–9 |
full40_main40 | 40 | 38.28 | +6.80 ± 1.70 ✳ | +3.36 … +10.24 | 30–10 |
pins_off | 29 | 37.10 | +6.47 ± 1.77 ✳ | +2.90 … +10.05 | 24–5 |
base_b | 24 | 37.11 | +5.86 ± 2.07 ✳ | +1.67 … +10.04 | 17–7 |
full40_skills | 40 | 37.01 | +5.54 ± 1.59 ✳ | +2.31 … +8.76 | 31–9 |
noskills | 27 | 36.60 | +5.17 ± 2.82 | −0.52 … +10.87 | 20–7 |
champion_v1 | 40 | 36.38 | +4.90 ± 1.69 ✳ | +1.47 … +8.33 | 28–12 |
full40_pins | 40 | 34.47 | +2.99 ± 1.54 | −0.13 … +6.11 | 26–14 |
full40_oldpins | 13 | 34.18 | +1.61 ± 4.38 | −7.94 … +11.15 | 7–6 |
| bare Claude Code (Opus) | 40 | 31.48 | — | ||
full40_v220 | 40 | 28.77 | −2.70 ± 1.79 | −6.32 … +0.92 | 14–26 |
arm_2ffaeb4 | 40 | 28.75 | −2.73 ± 1.78 | −6.32 … +0.87 | 20–20 |
full40 | 40 | 23.07 | −8.40 ± 1.78 ✳ | −12.01 … −4.79 | 12–28 |
✳ = 95% interval excludes zero. Bold n = all forty tasks scored.
Do not read the top of that table as a ranking. Only seven arms have all forty tasks,
and the arms below forty are not missing tasks at random: the outstanding ones are the slow
and hard ones, which the control scores above its own average on, so a partial arm's
paired lead reads high. The best complete arm is xrev_off at 40.17, +8.70.
The arms are ahead; nothing inside them is proven
Every one-variable contrast available, each pair being one tree apart — and three of them are placebos that should come out at zero:
| one-variable contrast | n | difference | sd |
|---|---|---|---|
| cross-review on − off | 26 | −0.87 ± 1.39 | 7.07 |
| operator calls on − off | 18 | −1.05 ± 0.96 | 4.06 |
| task-id pins on − off | 21 | +1.82 ± 1.63 | 7.45 |
| 120-skill pack − 161 | 37 | +2.16 ± 1.06 ✳ | 6.44 |
| 120-skill pack − 161 (second pair) | 34 | +0.95 ± 1.48 | 8.64 |
| adaptive graph − linear | 24 | +0.77 ± 1.28 | 6.27 |
| figure floor 15 − 3 | 17 | −1.01 ± 1.27 | 5.24 |
| 45 low-read skills withheld | 16 | −0.59 ± 3.15 | 12.62 |
| placebo: same pack, different code | 37 | +0.33 ± 1.25 | 7.58 |
| placebo: same pack, different code | 36 | +1.34 ± 1.24 | 7.41 |
| placebo: byte-identical command | 14 | +2.05 ± 1.51 | 5.66 |
Ten contrasts, one clears two standard errors — and two of the three placebos are more than half its size. The bottom row is two arms whose command lines are identical character for character, and it reads +2.05. That is the scale at which a difference here means nothing, and it is larger than most of the rows above it.
So the defensible summary is two sentences. AutoR is clearly ahead of the bare agent it wraps, by about 8 points on the best complete arm. Which parts of AutoR are responsible is unknown, because no individual change has been shown to beat its own placebo. How to fix that ratio is how to hill-climb this benchmark.
Read against the sentence further up — "the scaffold is currently worth less than no scaffold" — that claim is retired: the scaffold is ahead of the bare agent it wraps on every recent arm, on the same judge and the same forty tasks.
The one contrast that clears its error bar says a skill set is worth negative points.
abl40 is main40 with 41 task-scoped SKILL.md files removed, otherwise the same commit
and flags, and over the 37 tasks both have finished the 120-skill pack is +2.16 ± 1.06
ahead. Both arms are well clear of the control, so they are ahead despite that skill set
rather than because of it. Two things keep it from being a verdict: a second pair of the
same contrast (abl40 − skills161) reads only +0.95 ± 1.48, and the byte-identical
placebo reads +2.05.
This is not a verdict on pinning either, and an earlier version of this paragraph said it
was. It
read "the pins are worth negative three points", which conflated two different experiments.
abl40 deletes skill files and leaves 240 pins across 25 tasks in place: pinning is still on in
both of its arms. The ablation that actually isolates pinning is a different pair, pins_on
against pins_off — same commit, all 168 skill directories present on both sides, 420 pins
against zero — and it points the other way:
| ablation | what differs | n | difference | 95% CI |
|---|---|---|---|---|
main40 − abl40 | 41 skill files deleted; pinning on in both | 35 | −2.75 ± 1.08 | −4.88 … −0.63 |
pins_on − pins_off | pinning on vs off; every skill present in both | 21 | +1.82 ± 1.63 | −1.36 … +5.01 |
Read together: a particular set of skills hurts, and the mechanism that routes skills to tasks does not — it is mildly positive and nowhere near significant at n=21. Those are complementary answers, not contradictory ones, and telling them apart required reading the two worktrees rather than the two names.
a9c2b48 is the row that answers the caveat the other three carry. Every other arm above is
35–39 of 40, so each is a subset selected on its own completions — and that selection is not
neutral: when this arm stood at 33 of 40 the seven it had not finished were ones the control scored
well on, 33.90 against 27.11 on the tasks that had paired, which is the flattering direction.
Assuming the worst for those seven would have collapsed it from +8.29 to +0.72. It was held back
from this table for that reason. All forty have now landed and the estimate moved the other way,
to +7.33 over 40 pairs with nothing dropped. That is the first complete AutoR arm to separate
from the control, and it is what makes the four incomplete rows readable as low rather than
inflated.
One caveat still travels with the rest: abl40's lead over main40 rests on 33 pairs, which
resolves about 3 points and is measuring an effect of about that size.
The framework document's §6 is the full account, including the part that is worse than the mean: the two highest scores came from runs that halted at hypothesis generation, and across 133 stage visits the graph took one backward edge.
Where to go next: for what the reported systems actually score and which of their numbers reproduce, see docs/researchclawbench-landscape.md; for the adapter, its output contract and the export rules, see docs/researchclawbench.md; for every arm, what changed between them and what the instrument was doing while we read them, see docs/researchclawbench-arms.md.
FIRE-Bench
python fire_agent.py --bench-root ~/FIRE-Bench --task cot_in_planning --profile pipeline
runs one FIRE-Bench task: a research question
from a published empirical study, a sandbox, no reference paper, and 3600 seconds of wall
clock that the harness enforces with a kill. What is scored is the two-sentence conclusion
the agent writes, decomposed into atomic claims and matched against the authors' own.
--profile direct is the paired control — the same goal, model, denied tools, sandbox and
deadline, in one agentic call instead of a stage walk.
Measured here, six tasks, one run each, opus executing and reviewing, every arm on the
benchmark's own 3600 s clock. Each cell is the median judge draw of three — one draw, so
its three numbers are consistent with each other; arms are the mean ± sd across tasks,
which is the shape FIRE-Bench's own Table 3
reports:
| task | AutoR pipeline | AutoR direct | stock Claude Code | ||||||
|---|---|---|---|---|---|---|---|---|---|
| P | R | F1 | P | R | F1 | P | R | F1 | |
cot_in_planning | 62.5 | 100.0 | 76.9 | 60.0 | 66.7 | 63.2 | — | — | no conclusion |
premise_order_effects | 0.0 | 0.0 | 0.0 | 80.0 | 80.0 | 80.0 | — | — | no conclusion |
prompt_formatting_sensitivity | 50.0 | 33.3 | 40.0 | 75.0 | 66.7 | 70.6 | — | — | no conclusion |
lifebench_length_following | 0.0 | 0.0 | 0.0 | 46.2 | 40.0 | 42.9 | — | — | no conclusion |
persona_reasoning_biases | 0.0 | 0.0 | 0.0 | 28.6 | 50.0 | 36.4 | 11.1 | 50.0 | 18.2 |
mcq_selection_bias | 30.0 | 50.0 | 37.5 | 75.0 | 66.7 | 70.6 | 25.0 | 83.3 | 38.5 |
| arm | scoreable | Prec. | Recall | F1 | median wall clock |
|---|---|---|---|---|---|
| AutoR pipeline | 6 / 6 | 23.8 ± 28.0 | 30.6 ± 40.0 | 25.7 ± 31.5 | 52 min |
| AutoR direct | 6 / 6 | 60.8 ± 20.1 | 61.7 ± 14.3 | 60.6 ± 17.2 | 11 min |
| stock Claude Code | 2 / 6 | 18.1 ± 9.8 | 66.7 ± 23.5 | 28.4 ± 14.4 | 61 min, killed |
| stock, counting an unscoreable run as 0 | 6 / 6 | 6.0 ± 10.3 | 22.2 ± 36.0 | 9.4 ± 16.0 |
The stock arm's two rows are both reported because neither is obviously the right one and the choice moves its F1 from 28.4 to 9.4. Which is comparable to a published table depends on how that table handled a run that produced nothing, and FIRE-Bench's paper does not say.
How much of this is the judge. These six logs were scored twice, with nothing changing
but the judge's sampling. The arm means moved by 4 to 5 F1 points — pipeline 30.2 → 25.7,
direct 55.9 → 60.6, stock 22.9 → 28.4 — and one task moved by 13.7 (cot_in_planning,
pipeline arm, 100.0 → 76.9). The ordering of the arms did not move. Treat the ordering as
the result and the individual numbers as one draw of a noisy instrument; on a single
unchanged log the measured range is 43 F1 points.
Three things this says, in decreasing order of how much the sample supports them.
1. The clock is the benchmark's main filter, and telling the agent about it is what gets
past it. Four of six stock Claude Code runs produced nothing scoreable: given the raw
instruction.txt, which asks for a full report and says nothing about a deadline, they were
still building figures and prose when the harness killed them at 61 minutes. Both AutoR arms
were scoreable 6 times out of 6. The difference is not the pipeline — the direct arm has none
— it is a goal contract that states the wall clock, states that the conclusion is the only
artifact read, and says to write it early and rewrite it, plus a watcher that republishes the
scored line every time the file on disk improves.
2. Under this clock, the pipeline loses to one call of the same model on the same contract. Direct wins 5 of 6 tasks, median +34.8 F1, and it wins on precision and recall alike rather than by trading one for the other. Every pipeline run hit the reserve boundary at 52 minutes having approved one to three of its four stages; the direct arm finished in a median of 11. This is the same direction as ResearchClawBench and as the sixty-task written-answer trial of 2026-08-19, on a benchmark since removed from this repository, and it is the third benchmark to say it.
3. Three of the pipeline's four low scores are honest nulls, not empty runs — and the
metric cannot tell the difference. On premise_order_effects the pipeline ran 112 problems
across nine orderings on two models, 694 billed calls per arm, with a corrupted-premise
control at 0.15 accuracy confirming the task was real, and concluded that premise order did
not move accuracy. That is correct about what it measured: its strong model scored 1.000 in
every ordering — at ceiling — and it said so. It scored 0.0, because the reference
conclusion says premise order matters. The direct arm, on the same task in a fifth of the
time, ran a pilot, saw the ceiling, generated a harder pool with chains up to sixteen steps,
found the effect at sign-test p = 0.0001, and scored 80.0 / 80.0 / 80.0. The lesson is about
iteration, not about honesty: what the pipeline lacked was a second pass at its own
instance difficulty, and its budget went to preregistration, a reproduction table and a gate
ledger instead.
What this is not. Six of thirty-five tasks, one run per cell, and a judge whose measured range on a single unchanged log is 43 F1 points. It is not comparable to FIRE-Bench's published table (best row: Claude Code with Sonnet-4 at 46.7 ± 23.4 F1): different executing model, and — decisively — none of the models the tasks name is served on this deployment, so every arm substituted from the same catalogue and every arm is answering a slightly different question from the one the papers answered. That substitution is also the likeliest reason for finding 2's ceilings: these papers' effects were measured on gpt-3.5 and Llama-2-era models.
The adapter, the deadline design, the six exit clauses, the judge's noise and the six holes in the benchmark's own harness that had to be routed around are in docs/firebench.md.
AIRS-Bench
python airs_agent.py --task <TASK> --repo <AIRS_BENCH> --raw-dir <RAW> --workspace <WS>
runs AutoR against one of AIRS-Bench's
twenty machine-learning research tasks and exports the one thing it scores,
submission.csv. Scoring is the task's own evaluate.py — scipy over a CSV — so unlike
the other two benchmarks the same artifact scores the same number every time.
tools/airs_arm.py runs one arm of a comparison and its control: both arms are handed
build_task_brief's output byte for byte, the same CLI, model, permission mode, denied
tools, workspace and wall-clock cap, and --compare refuses to print a delta between two
manifests that disagree on any of them.
Nineteen tasks (the twentieth cannot be staged), one seed, opus executing in both arms,
4 h of wall clock each, no web search, one (arm, task) per slurm array element on
CPU-only nodes. Reported in the benchmark's own three units, computed by its own
create_summary_plots.ipynb rules — a run with no scoreable submission is a 0 in the
mean, not an omission. 1.000 is human SOTA.
| arm | valid submission | mean | median | IQM | Elo* |
|---|---|---|---|---|---|
| bare Claude Code (opus) | 100.0 % | 1.560 | 0.932 | 0.899 | 951 |
| AutoR (opus) | 94.7 % | 1.134 | 0.844 | 0.838 | 895 |
| SOTA | — | 1.000 | 1.000 | 1.000 | 1154 |
Paired over the nineteen tasks: +0.426 mean, +0.069 median to the bare CLI, which wins
16 of 19. The mean and the median disagree by a factor of six because one task carries
it: CodeGenerationAPPSPassAt5's normalized score has a denominator eleven times smaller
than a typical task's, and the two arms' Pass@5 of 0.783 and 0.947 land at 7.37 and 14.15.
Drop that task and the same nineteen-task run reads 0.860 against 0.787, +0.073 paired,
bare winning 15 of 18 — which is why all three aggregates are printed and the median is
the one to quote. A five-task pilot on a GPU node gave the same direction.
* Elo is over a three-entity pool including SOTA and is not comparable to a rating from the published fifteen-entity pool; with two agents it re-expresses the head-to-head count. There are no error bars: the published ones bootstrap 10–20 seeds per task and these arms have one seed each, so drawing an interval over tasks instead would be a different quantity wearing the same mark.
The mechanism is legible rather than inferred, and it is the same one the pilot showed: every AutoR run hit the cap and not one finished the walk — six of nineteen never left Stage 01, at 13 to 22 attempts on a literature survey for tasks whose whole specification is "predict this column" — while the bare arm hit the cap zero times at a median of 3 h 14 m. AutoR's single invalid submission is the failure the brief warns about: 1,137 rows where the split has 1,147, a whole task lost to ten rows, and under the benchmark's own convention that task is a zero in its mean rather than a gap in it.
This is the same direction as every other benchmark AutoR has been run on, and the fourth to say it — the other three being the two above and the sixty-task written-answer trial of 2026-08-19, on a benchmark since removed from this repository (§6.8) — this time through an instrument with no judge in it, which is the one thing the reading could not previously be blamed on.
These are not leaderboard numbers. The published table is twenty tasks at ten to twenty
seeds, scored in a container with no network, while an agent with a shell here can
snapshot_download a model and run inference — seven of the bare arm's nineteen runs did.
One deviation from the notebook is deliberate and forced: it anchors each task on the worst
score observed in the analysis, which for a two-arm pool that both beat SOTA sends the
denominator negative, so the published estimated_worst_score is used instead. Tool-call
audit hits for the held-out labels across all 38 runs: zero.
The adapter, the arm harness, the five defects running it surfaced in the benchmark itself, and the one it surfaced in this adapter are in docs/airsbench.md.
Documentation
The docs/ directory is the reference documentation. This README is the overview; everything below is the detail behind it.
| The Framework | What AutoR is as a system: the implementation, every module and what it owns, what is new here, and what it contributes. Start here if you want the design, not the commands. |
| English Guide · 中文教程 | Install, run your first project end to end, review each stage, and write feedback that actually improves output. |
| CLI Reference | Every flag on main.py, rcb_agent.py and studio.py, defaults, what is preserved on resume, exit codes. |
| Configuration | run_config.json, the venue registry, diagram setup, environment variables, hard-coded limits. |
| Run Artifacts | The run directory, file by file, and the schema of every machine-readable artifact. |
| Stage Contract | Exactly what a stage must produce to be accepted, as validate_stage_artifacts enforces it. |
| Recursive Self-Improvement | The stage graph, routing, the rigour rubric and the champion ratchet, the cross-run archive — and the constraints that stop a scored loop from optimising toward a nicer answer. |
| Rigor Levels | The one dial, what each level turns on, and how an explicit flag overrides it. |
| Effort Tiers | Routine vs deliberative stages, tier promotion, and concentrating the strong model. |
| Review Panel | The five seats, the independent round and the cross-examination that only runs on disagreement, blocking objections, --panel-models, --persona, and the solo baseline every panel run measures itself against. |
| Ideation Panel | The five proposer lenses, Jaccard deduplication, scoring into a candidate pool, and the adoption measurement taken after the stage is approved. |
| Crux Deliberation | When a stage may stop and escalate, the four voices, and the falsifier the resolution must name. |
| Anchored Comments | Quoting a passage instead of refusing a stage, and the collateral-change diff. |
| Run Scorecard | The five self-measurement ledgers and the end-of-run verdict on which flags earned their cost. |
| Backend Health | Telling "the model was unreachable" apart from "the research failed". |
| Studio Guide & API | The browser workspace and its complete HTTP API. |
| ResearchClawBench | Running with no human in the loop: unattended execution, the benchmark adapter and its output contract, and Gemini-backed web search. |
| Skill-routing arm record | One arm end to end: the corrected baseline, why the scoring pass is part of the result, the four defects the run exposed, the analysis written before the numbers arrived — and the result, which is the first arm ahead of the bare agent and which attributes almost all of it to one lookup table. |
| ResearchClawBench Landscape | How EvoScientist, ARIS Codex and MIRA actually score on the benchmark, which reported numbers reproduce, and the baseline any result must be quoted against. |
| AIRS-Bench Run Log | The experimental record behind those numbers: provenance, the exact commands, every task's value and wall clock, the six things that went wrong while it ran, and the integrity audit. |
| AIRS-Bench | Twenty ML research tasks scored by a deterministic metric over submission.csv. The adapter, the arm harness, three defects running it surfaced in the benchmark itself, and what AutoR scores. |
| Architecture | Layers, the module map, the stage walk, prompt assembly by typed channel, recovery, extension points. |
| Development | Dev setup, tests, CI, conventions, and recipes for adding a stage, venue, or backend. |
| Troubleshooting | Symptom-to-fix for the errors AutoR actually raises. |
| Contributing · Security · Code of Conduct | How to land a change; the security model, the sandbox trade-offs and how to report a vulnerability; community expectations. |
Showcase
runs/20260330_101222 is the full example run the docs work from. Run directories are gitignored, so
what ships in the repository is the artifacts lifted out of it, under assets/.
| What the run produced | What it demonstrates |
|---|---|
| example_paper.pdf | A compiled manuscript inside a larger research package |
| Executable research code | The run is not a writing pipeline |
| Machine-readable datasets and result files | Claims are backed by inspectable experiment outputs |
| Real figures used in the package | Publication-style visuals, not placeholders |
| Review and dissemination materials | The run continues past writing into release readiness |
AGSNv2 reached 36.21 ± 1.08 on Actor, and the run preserved the full human approval trail.
Accuracy Comparison![]() | Ablation + Actor Results![]() |

