Architecture¶
repogym is a small library (about 4,000 lines of Python, four runtime dependencies) organised around one idea: a coding task is data, and a reward is a pure function of a task and a patch. Everything else (workspaces, sandboxes, environments, agents, miners) exists to produce patches and evaluate them reproducibly.
System overview¶
flowchart TB
subgraph Authoring
A1[task.yaml] --- A2[repo/ snapshot<br/>or git url+commit]
A2 --- A3[hidden/ tests]
A3 --- A4[solution.patch]
M1[repogym mine<br/>git history] --> A1
M2[repogym mutate<br/>fault injection] --> A1
end
A1 --> T[Task]
T --> V[validate_task]
T --> W[Workspace<br/>git baseline commit]
subgraph Episode
E[RepoEnv<br/>reset / step / state] --> W
AG[Agent<br/>golden · shell · claude · yours] -->|Action| E
E -->|Observation, reward| AG
W --> S[Sandbox<br/>local · docker]
end
E -->|submit| G[CompositeGrader]
G --> G1[ConstraintGrader]
G --> G2[TestGrader<br/>JUnit XML]
G --> G3[PerfGrader<br/>benchmark]
G --> R[GradeResult<br/>score ∈ 0..1, patch]
R --> TR[Trajectory JSON]
R --> RP[Reports · leaderboard · JSONL export]
Components¶
| Module | Responsibility | Key types |
|---|---|---|
models.py |
Pydantic schema for task.yaml and every result object. Strict (extra="forbid"), cross-field validated. |
TaskSpec, TestSpec, PerfSpec, ConstraintSpec, GradingSpec, GradeResult, Trajectory, RunResult |
task.py |
Discover and load task directories; resolve paths; filter by id/type/tag. | Task, load_tasks |
workspace.py |
Materialise a task into an isolated directory, create a git baseline commit, track changes as diffs, apply patches, inject/remove hidden tests, restore protected files, reset. | Workspace |
sandbox.py |
Execute shell commands. LocalSandbox scrubs the environment to an allowlist; DockerSandbox bind-mounts the workspace into a fresh container with network off. |
Sandbox, LocalSandbox, DockerSandbox |
junit.py |
Parse JUnit XML from any runner into {test_id: status}, registering pytest node ids, classname::name and bare names. |
parse_junit_xml |
metrics.py |
Dependency-free AST metrics: cyclomatic complexity and function length. | function_metrics |
graders/ |
Turn a workspace into a GradeComponent (objective or gate); combine components into a GradeResult. |
TestGrader, PerfGrader, QualityGrader, ConstraintGrader, CompositeGrader |
env.py |
Gym-style environment: actions, observations, step limits, command allowlist, reward shaping, trajectory recording. | RepoEnv, Action, Observation |
agents/ |
Things that drive an env to completion. | Agent, GoldenAgent, NoopAgent, ScriptedAgent, ShellAgent, ClaudeAgent |
validate.py |
Prove a task is a sound environment. | validate_task |
runner.py |
Run agents over task sets, serially or with a thread pool. | run_task, run_suite |
mine/ |
Generate tasks from git history or by mutation. | HistoryMiner, MutationMiner |
export.py, report.py |
JSONL datasets, terminal/Markdown/HTML reports, leaderboards. | |
cli.py |
Typer CLI over all of the above. |
Lifecycle of an episode¶
sequenceDiagram
participant Ag as Agent
participant Env as RepoEnv
participant WS as Workspace
participant SB as Sandbox
participant Gr as CompositeGrader
Ag->>Env: reset()
Env->>WS: materialize() → copy/clone, run setup, git commit baseline
Env-->>Ag: Observation(problem statement + file tree)
loop until submit or max_steps
Ag->>Env: step(Read/Write/Edit/Run/Test/ListFiles)
Env->>WS: read/write files (path-escape guarded)
Env->>SB: run(command) for Run/Test (allowlisted, timed out)
Env-->>Ag: Observation(text, reward=0 or shaped)
end
Ag->>Env: step(Submit)
Env->>Gr: grade_result(workspace)
Gr->>WS: diff() → patch snapshot
Gr->>WS: restore(protected tests), inject hidden tests
Gr->>SB: run tests → JUnit XML, run benchmark
Gr-->>Env: GradeResult(score, passed, components)
Env-->>Ag: Observation(done=True, reward=score)
Env->>Env: write Trajectory JSON
Design decisions¶
Git as the change-tracking substrate. Every workspace becomes a git work tree
with a baseline commit after repo.setup has run. That gives diffs, resets,
restore(files) and change statistics for free, works for any language, and means
the patch is the canonical artefact of an episode. Build artefacts produced by
setup are folded into the baseline so they never pollute diffs. The git metadata
lives outside the working tree (<workspace>.scratch/git, passed explicitly
as GIT_DIR), so an agent cannot blind the graders by deleting .git; if the
metadata is destroyed anyway, git calls fail closed and the submission scores 0.
Objectives and gates. Every grader component has a role. Objectives (target
tests, benchmarks, quality limits on refactor tasks) contribute weighted score;
gates (regression tests, scope and pattern rules, quality limits elsewhere)
contribute nothing but zero the score when they fail. This is the rule that makes
"an empty submission scores 0" true for all four task types, which validate
checks.
JUnit XML as the test contract. Instead of parsing each runner's stdout,
repogym asks the test command to write JUnit XML ({junit} placeholder). pytest,
node --test, jest, mocha, cargo-nextest, gradle, maven and go test (via
go-junit-report) all support it, so per-test fail_to_pass / pass_to_pass
rewards work everywhere.
Verification is a separate, explicit command. validate_task runs the
baseline, the noop grade, the golden grade and (optionally) repeated golden grades.
A task that is not validated cannot be trusted as a reward signal; CI runs
repogym validate tasks on every push.
Reward hacking is treated as a design constraint, not an afterthought.
| Attack | Defence |
|---|---|
| Edit the visible tests so they pass | Files referenced by fail_to_pass/pass_to_pass are restored to baseline before grading; the tampering is recorded as a constraint violation (a gate, so the score is 0) |
| Overfit to the visible tests | hidden/ tests are copied in only at grade time and removed afterwards; the test action never shows them, not even hidden pass_to_pass ids |
| Modify unrelated files / delete code | allowed_files, protected_files, max_files_changed, max_lines_changed gates |
| Fake the benchmark timing (monkeypatch the clock) | benchmarks are timed externally (wall clock of the process) by default; non-finite / zero timings score 0 |
| Fast but wrong perf solution | passed requires every grader to pass; a broken pass_to_pass test is a failed gate |
| Trivial refactor (rename only) | max_cyclomatic_complexity / max_function_length are measured on the result |
Delete or rewrite .git to blind the graders |
git metadata lives outside the working tree; missing metadata makes grading fail closed with score 0 |
| Submit nothing and collect constraint credit | gates never contribute credit; validate asserts the noop score is exactly 0 |
| Shell out to the network / destructive commands | run action is allowlisted per shell segment, quote-aware, rejects command substitution and known-dangerous tools. This is a guardrail, not a security boundary: an allowlisted interpreter runs anything. Use DockerSandbox (network off) for untrusted agents |
| Secrets leakage | LocalSandbox passes only an allowlist of environment variables |
Sparse reward by default. RL practitioners want the terminal reward to be the
graded score. Shaping (env.reward_shaping) is opt-in and bounded (at most 0.1
total, only for improvements on visible tests), so cumulative return stays
interpretable.
Pydantic everywhere. Task specs are strict (extra="forbid") with cross-field
validation (a perf task must have a perf section; every task must have some reward
signal). Results are models too, so trajectories, grades and run results serialise
to JSON without custom code.
Miners produce validated tasks by construction. Both miners execute the test-suite before and after the fix and only emit a task when the fail-to-pass set is non-empty and the failure ratio is sane. The mutation miner applies faults textually at AST-reported positions so formatting and comments survive and the golden patch is minimal.
Extension points¶
- Grader: subclass
Grader, return aGradeComponent; add it to aCompositeGraderwith a weight. - Agent: subclass
Agentand implementsolve(env, obs); or wrap a CLI withShellAgent. - Sandbox: implement
run(command, cwd, timeout, env) -> CommandResultandavailable(). - Action:
Actionis a plain Pydantic model with akinddiscriminator; new kinds need a_do_<kind>handler onRepoEnv. - Task source:
RepoSpec.sourceislocalorgit; other sources (tarballs, registries) only need a new branch inWorkspace.materialize.
Repository layout¶
src/repogym/ library
tasks/ example tasks (validated in CI)
tests/ pytest suite (125 tests, ~94% statement coverage)
docs/ + mkdocs.yml documentation site
website/index.html landing page
examples/ runnable scripts (custom agent, custom grader, RL loop)
.github/workflows/ CI (lint, mypy, tests, validate tasks), Pages, Release