Pipelines¶
A pipeline is a synthesis method that takes a repo and emits Harbor-shaped tasks. They share the same input shape (GenerationInput) and output shape (Harbor task dirs); they differ in how they manufacture verifiable tasks.
Common shape¶
Every pipeline follows the same skeleton — only the box labelled "synthesize" varies.
flowchart LR
A[Source repo<br/>+ config] --> B[Discover<br/>candidates]
B --> C[Synthesize<br/>per pipeline]
C --> D[QA gate]
D -- pass --> E[Harbor task dir]
D -- fail --> F[Skip + log reason]
E --> G[Local dataset]
E --> H[HF Hub<br/>+ registry.json] Pipelines¶
All 6 pipelines are shipped — 3 stable (pr_diff, pr_runtime, commit_runtime), 3 experimental. See per-pipeline pages for the recipe + options + Harbor verification status.
| Pipeline | What it produces | Source | Sandbox | LLM use | GPU helpful? | Reference dataset | Inspiration |
|---|---|---|---|---|---|---|---|
pr_diff | Harbor-runnable env + 6-component diff-similarity reward (deterministic 5 + LLM judge) | GitHub · GitLab | thin¹ | at verify (judge, optional) | No | AdithyaSK/repo2rlenv-pr-diff (100) | SWE-RL |
pr_runtime | Sandbox-verified PR with F2P/P2P test oracle | GitHub · GitLab | ✅ | at bootstrap (cached) | ML repos | AdithyaSK/repo2rlenv-pr-runtime (100) | SWE-bench |
commit_runtime | Commit-level oracle (bypass PR-review filters) | GitHub · GitLab · local | ✅ | at bootstrap (cached) | ML repos | — | R2E-Gym SWE-GEN |
code_instruct | LLM-authored problem + executable verifier anchored to real source | GitHub · GitLab · local | ✅ | at synthesis (problem + verifier) | Sometimes | repo2rlenv-code-instruct — 100 tasks × 5 Python repos | Magicoder |
equivalence_tests | Extract a function; LLM writes equivalence tests vs reference_<name> | GitHub · GitLab · local | ✅ | at synthesis (tests, feedback-driven retry) | If function uses GPU | pending v0.8.8 | R2E |
cve_patches | OSV CVE → fix commit → Harbor task (reuses pr_runtime verifier) | GitHub | ✅ | at bootstrap (cached) | Rarely | AdithyaSK/repo2rlenv-cve-patches (19) | PatchSeeker / CVE-Bench |
- Source — where
--repocan point.GitHub · GitLab · local= a GitHubowner/name, agitlab.comURL, or a local path (/abs,./rel,~,file://); these need only git + source files.GitHub · GitLab= PR/MR-mining pipelines (github.com or gitlab.com, not a bare local clone).GitHub= needs the GitHub commit API + OSV CVE data (cve_patches).generateblocks an unsupported source up front with a clear error. - Sandbox ✅ = needs Docker + the bootstrap-built env.
thin¹= needs Docker but ships a lightweightpython:3.12-slimenv baked at generation time (no bootstrap LLM agent, ~30 s build).—= pure text, no execution. - LLM use: every pipeline calls an LLM at some stage.
at synthesis= the pipeline itself authors task content (problems, mutations, tests) — this is the heavy spend.at bootstrap (cached)= the pipeline doesn't call the LLM, but the per-repo env construction does — that runs once per repo, content-addressed, then cached.at verify= an LLM is invoked at reward time (onlypr_diff's LLM-judge component).
¹ pr_diff is the unusual case — it skips bootstrap entirely (no per-repo image build), ships a generic env, and only uses the LLM at verify time. The judge degrades gracefully on missing API key (status=no_api_key), and the remaining 5 components renormalize.
Reference repos are cloned shallowly under references/ (gitignored).
Yield: how many tasks you actually get¶
Yield = emitted tasks ÷ candidates examined. It is not limit: limit caps the output, but each pipeline discards candidates that fail its quality gate, so the realized count is usually lower. Plan your limit and repo list around the yield band below.
| Pipeline | Typical yield | Dominant factor | Knobs that move it |
|---|---|---|---|
pr_diff | 80–95% | almost every merged PR qualifies (text-only, no execution gate) | min_loc_changed, max_files_per_pr, skip_drafts |
pr_runtime | 15–40% | does a PR ship a new test that flips fail→pass, and does the suite run green in the container? | require_fail_to_pass, require_new_test_funcs, lite_filter, min_problem_statement_words |
commit_runtime | 10–35% | same F2P gate as pr_runtime, on commits — ~0% on squash/merge-PR repos (use pr_runtime there) | skip_merge_commits, require_new_test_funcs, min_message_words, synthesize_with_llm |
code_instruct | 60–90% (v0.8.6 gates + retries; empirically 75.8% across 5 Python libs on the reference dataset) | fraction of seed snippets where the LLM's test passes all quality gates AND fails-without / passes-with the oracle | max_attempts_per_seed (default 3), LLM quality, seed_min/max_loc |
equivalence_tests | ~50% of pure candidates (v0.8.7 gates + retries; the purity filter is the real gate — framework-heavy repos yield 0–2 pure candidates, utility-heavy repos yield 10s) | fraction of extracted pure functions where the LLM writes a test that fails-with-stub / passes-with-oracle | max_attempts_per_function (default 3), min/max_loc, LLM quality, repo shape (utility vs framework) |
cve_patches | 5–25% | does the CVE fix have a verifiable test (shipped or agent-synthesized) and does the repo's suite collect in a slim container? | synthesize_poc_test, poc_agent, require_fail_to_pass, min_severity |
The single biggest lever for every execution-gated pipeline (*_runtime, cve_patches, the synthesis pipelines) is repo health — if the suite doesn't collect and run green inside the bootstrap container, yield is ~0 regardless of options. (Real example: urllib3 → 0/16 CVEs, because its tests need network and collect nothing in a slim image; sqlparse → 2/4, because its suite runs clean.) Pick library-shaped, CPU-only, pytest-clean repos.
Numerical example — targeting 100 pr_runtime tasks¶
At ~25% yield you need ~400 candidate PRs. Spread across repos (one bootstrap each, cached after the first run):
Bump lite_filter/require_new_test_funcs and yield drops (stricter), but the surviving tasks are higher quality. For pr_diff the same 100 tasks need only ~110 candidate PRs (one or two repos). For cve_patches, budget many repos: at ~15% yield, 100 tasks ≈ 15–20 CVE-rich, test-clean repos (see plans/cve_repo_scout.py).
Spotlight: pr_diff (v0.8.3 reference dataset)¶
The first pipeline to ship a published 100-env reference dataset. Pull and run it on a fresh machine in two commands:
repo2rlenv pull AdithyaSK/repo2rlenv-pr-diff /tmp/pr-diff
harbor run -p /tmp/pr-diff -a oracle --env docker # → 1.000 on every task
What each task contains¶
default/<repo>__<pr_number>/
├── task.toml # Harbor task spec + [metadata.repo2env] (provenance,
│ # reward_calibration baseline, difficulty bucket)
├── instruction.md # PR title + description (info-leak stripped)
├── solution/
│ ├── patch.diff # the merged PR's diff = oracle
│ └── solve.sh # `git apply patch.diff` (used by harbor's oracle agent)
├── environment/
│ └── Dockerfile # python:3.12-slim + repo @ base_commit + base64-baked
│ # oracle.patch, instruction.md, verifier.py
└── tests/
└── test.sh # extract verifier from base64; run on the agent's diff
The 6-component reward¶
| Component | Weight | What it captures |
|---|---|---|
format_valid | 0.00 | Parses as a unified diff (always 1.0 for claude-code — kept as a guard, weight=0) |
size_sanity | 0.08 | min(oracle_loc, predicted_loc) / max(...) — catches over/under-generation |
file_targeting | 0.12 | F1 over the changed-file sets (not Jaccard — F1 properly credits TP) |
region_overlap | 0.20 | Predicted hunks overlap oracle hunks (5-line slack) |
similarity | 0.10 | SequenceMatcher over +/- lines only (no free credit for context) |
llm_judge | 0.50 | Haiku 4.5 rates semantic correctness; graceful degradation on missing API key |
Plus a catastrophic-size hard cap: clamps reward to ≤ 0.40 when size_sanity < 0.10, so a charitable judge can't inflate scores on patches that are wildly the wrong size.
Weights were retuned via an LLM-driven reward-engineering pass on a 23-task pilot: Sonnet 4.6 analyzed per-task component data and recommended these weights (data-grounded; the original guesses scored format_valid and similarity too high).
Reproduction recipe (the exact pipeline that produced the 100 envs)¶
# Generate
repo2rlenv generate \
--repo pallets/click --pipeline pr_diff \
--pipeline-opt limit=4 \
--out /tmp/pr-diff-click
# Validate (structural, fast)
repo2rlenv validate /tmp/pr-diff-click
# Score with oracle (sanity-check verifier)
harbor run -p /tmp/pr-diff-click -a oracle --env docker
# Score with an actual agent. We used `claude-code` + Sonnet 4.6 to
# verify the published reference dataset:
harbor run -p /tmp/pr-diff-click -a claude-code \
-m anthropic/claude-sonnet-4-6 \
--ae ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \
--ve ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \
--env docker --max-retries 2
# Same env, different agent. Harbor has 25+ harnesses — swap `-a` and
# `-m` and the corresponding `--ae <PROVIDER>_API_KEY=...`:
# -a claude-code -m anthropic/claude-sonnet-4-6
# -a openhands -m openai/gpt-4o
# -a codex -m openai/o1
# -a aider -m anthropic/claude-sonnet-4-6
# -a gemini-cli -m gemini/gemini-2.5-pro
# -a opencode -m anthropic/claude-sonnet-4-6
# -a qwen-coder -m qwen/qwen3-coder
# -a copilot-cli (uses GH_TOKEN)
# -a mini-swe-agent · swe-agent · cursor-cli · kimi-cli · goose · ...
# The verifier's LLM-judge always uses Anthropic (Haiku) — pass
# ANTHROPIC_API_KEY via `--ve` regardless of which agent you run.
# Publish
repo2rlenv push /tmp/pr-diff-click <your-org>/<dataset-name>
For the full design rationale + dataset card layout + pilot evidence, see pr_diff.md and docs/release_notes/v0.8.3/findings-pr_diff.md.
Reward kinds emitted¶
| Pipeline | diff_similarity | test_execution |
|---|---|---|
pr_diff | ✅ | — |
pr_runtime | ✅ | ✅ |
commit_runtime | ✅ | ✅ |
code_instruct | optional | ✅ |
equivalence_tests | — | ✅ |
cve_patches | ✅ | ✅ |
diff_similarity works without a sandbox; test_execution requires one.
Contamination defenses¶
A published fix lives on the package index and the code host, so an agent with open egress (or a leaky container) can fetch the gold patch for the very repo it is asked to fix. We saw this in practice: an agent blocked from the web fell back to git diff origin/main, and when that was closed it ran pip download <pkg>==<fixed> and read the fix out of the wheel. The principle we settled on: the environment enforces, the prompt never asks. Every sandbox-verified task (pr_runtime, commit_runtime, cve_patches, and pr_diff's thin env) ships three defenses, all baked in at generation time by pipelines/_env_guard.py:
- Git-history scrub — after checking out
base_commit, the env removes theoriginremote and prunes every ref/commit past the base (thengc), so the fix commit and hidden tests can't be read offline viagit diff origin/mainorgit show origin/main:<testfile>.base_commitstays reachable for the verifier's anti-tamper reset. - Egress guard — an
environment/docker-compose.yamloverlay blackholes the package index + code host (pypi.org,files.pythonhosted.org,github.com, their CDNs), sopip download/git fetch/ web fetches against them fail while the model API and the agent's installer stay reachable. This is a denylist (the realistic control at the compose layer); a default-deny egress allowlist proxy or a date-pinned package mirror is the stricter follow-up. - Instruction leak-strip — synthesized/CVE instructions drop fix-pointers (CVE/GHSA ids, PR/commit URLs, "fixed in vX.Y"), leaving only the symptom.
These reduce the attack surface but the real guarantee is network isolation; for trustworthy eval numbers, run with allow_internet=false (offline, self-contained image) or the allowlist proxy. The full investigation that drove these is in plans/reward_hacking_writeups.md.
Adding a new pipeline¶
See the cookbook for the full step-by-step walkthrough — covers the enum + Options + Pipeline class + tests + doc page, with template snippets and conventions taken from pr_diff.
TL;DR: every pipeline must satisfy the Pipeline Protocol:
class Pipeline(Protocol):
name: ClassVar[PipelineName]
def __init__(self, input: GenerationInput, options: BaseModel) -> None: ...
def run(self, out_dir: Path) -> PipelineResult: ...
tests/test_pipeline_contract.py verifies every registered pipeline conforms to the Protocol — adding a new one without finishing the registration steps will fail there.