RFC 0007: pr_to_env¶
Status: draft Author: @adithya-s-k Created: 2026-07-15 Implemented by: (pending) Reference dataset: (pending)
Summary¶
Take a single GitHub / GitLab PR URL (or a curated list) and produce one Harbor RL environment per PR, verified end-to-end. Same task shape and verifier as pr_runtime; the difference is the input surface — a specific PR the user hands you, not a mining sweep over a repo's history. First "import-shape" pipeline in the set.
Motivation¶
The pattern this pipeline breaks¶
Everything we ship today is a mining pipeline: --repo <owner/name> → walk PRs / commits / CVEs → filter → keep whatever passes the yield gate. Mining is the right shape when you want to build a 100-env reference dataset from a repo you care about. It's the wrong shape for three real workflows we hit repeatedly:
- Reproducing a reported bug as an eval. Someone (Slack, GitHub issue, a paper) says "this specific PR is a good regression to test agents on." Today the only way to consume it is
pr_runtime --repo <owner/name> --pipeline-opt limit=1000and hope your target PR is in the surviving set. A single-URL input is a mining-shape hack. - Curated hand-picked benchmarks. A researcher hands you 50 PR URLs across 20 repos ("these are the ones we want in the benchmark"). Today: 20 separate
pr_runtimeinvocations with narrow limits, then union the outputs. Mining machinery working against you when you already know the answer. - Ad-hoc RL training on user-provided regressions. A team wants to fine-tune their agent on the PRs their own users filed against their infra. They know the URLs; they don't want the mining framework.
Mining vs. import — a naming distinction worth making¶
pr_to_env isn't just a mining pipeline with a smaller input; the assumption underneath it is inverted. Mining says "here's a corpus, tell me what qualifies." Import says "here's what qualifies, tell me if you can build it." The consequences ripple through the design:
Mining (pr_runtime, commit_runtime, cve_patches, ...) | Import (pr_to_env) | |
|---|---|---|
| Input | one repo, N candidates | N specific artifacts (PRs), no filtering promise |
| Yield semantics | emitted ÷ candidates_examined, expected < 100% | per-URL success/skip — user expects an env or a specific reason for each URL |
limit / since / lite_filter | central | absent (user did the filtering) |
| Failure semantics | filtered → dropped silently | filtered → surface the reason per URL |
| CLI shape | --repo | --pipeline-opt url=… or --pipeline-opt urls_file=… |
The counter-argument: "just add a --pr-urls option to pr_runtime." That works up to a point but muddies the pipeline contract. pr_runtime's options are all mining knobs (limit, since, require_fail_to_pass, lite_filter, min_problem_statement_words); the mental model is "walk history, apply gates, emit surviving." A URL-list input needs an entirely different failure-reporting story (per-URL, not aggregate) and forbids most of the mining options as meaningless.
pr_to_env is pr_runtime's consumption-side sibling — same output, same verifier, different input assumption. If import turns out to be a useful shape, later RFCs can add commit_to_env (one commit URL → one env), issue_to_env (one issue URL → resolve it), etc. — same category, same "per-URL result" contract.
Design¶
Input¶
- Source — GitHub · GitLab (whatever URLs are given; the input-source abstraction routes per-URL).
- Trigger — two forms:
# single URL
repo2rlenv generate --pipeline pr_to_env \
--pipeline-opt url=https://github.com/pallets/click/pull/3434 \
--llm anthropic/claude-sonnet-4-6 \
--out ./datasets/click-3434
# curated file — one URL per line, comments ok
repo2rlenv generate --pipeline pr_to_env \
--pipeline-opt urls_file=./curated.txt \
--llm anthropic/claude-sonnet-4-6 \
--out ./datasets/curated
- Options model —
PrToEnvOptions:
url: str | None = None # exactly one of url/urls_file must be set
urls_file: Path | None = None
strict: bool = True # if True, ANY per-URL failure is fatal (fail-fast)
# if False, log + skip failures, emit the rest
# Verification knobs inherited from pr_runtime — reused verbatim
require_new_test_funcs: bool = True
min_problem_statement_words: int = 0
synthesize_with_llm: bool = True # inherit commit_runtime's LLM synthesis for
# leak-free instructions (same rationale)
No mining knobs (limit, since, lite_filter) — deliberately. If the user hands over 100 URLs, they get 100 attempts. strict controls what happens when a URL can't produce an env.
Algorithm¶
flowchart LR
U[URL or urls_file] --> P[Parse: extract owner/repo/N per URL]
P --> M{Group by repo}
M --> B[Ensure bootstrap per repo<br/>(reuses pr_runtime cache)]
B --> F[Fetch PR data via github.fetch_pr]
F --> V[Validate inside sandbox<br/>(reuses pr_runtime.validate_pr)]
V --> I[Synthesize instruction<br/>(reuses commit_runtime.synthesize_with_llm)]
I --> E[Emit Harbor task] - Parse URLs into
(host, owner, name, pr_number)tuples. Reject anything that isn't a PR URL with a clear error naming the URL. - Group by repo so bootstrap runs once per unique
(owner, name, ref)rather than once per PR — same optimizationpr_runtimeuses internally. - For each repo: ensure bootstrap (cache-hit path is instant); for each PR in that repo, fetch the PR metadata and diff via the existing
github.fetch_pr/gitlab.fetch_mr(route through the input-source abstraction). - Reuse
pr_runtime.validate_prverbatim for the F2P/P2P validation inside the bootstrap container. No new verifier logic. - Synthesize a leak-free instruction the same way
commit_runtimev0.8.4 does — the raw PR body has the same leakage patterns as commit messages.synthesize_with_llm=Truedefault. - Emit a Harbor task with the shared shape from
pr_runtime.
Output¶
- Task shape — identical to
pr_runtime:environment/Dockerfile,environment/docker-compose.yaml(egress guard),tests/test.sh,tests/verifier.py,tests/f2p.json,tests/p2p.json,solution/patch.diff,instruction.md,task.toml. - Task ID —
<owner>__<repo>-<pr_number>(same aspr_runtime). [metadata.repo2env]provenance — same fields aspr_runtime, plus:pipeline = "pr_to_env"(notpr_runtime, so consumers can distinguish curated tasks from mined ones)source_url = "<the URL that produced this task>"— new field, so a consumer can trace back to what the user handed in.
Verification¶
- Reward kind(s) —
test_execution+diff_similarity(same aspr_runtime). - Reward formula — graded F2P/P2P:
reward = f2p_rate × p2p_rate. Uses_pr_runtime_verifier.pyverbatim, no fork. - Oracle invariant — gold PR patch flips all F2P from FAIL→PASS while keeping all P2P green.
resolved=True,reward=1.0. Same invariant aspr_runtime. - Non-tamper — reuse
pr_runtime.build_eval_script's existing test-file reset + heredoc'd test-patch reapply. Nothing new.
Anti-contamination¶
Same leak surface as pr_runtime — the PR is on GitHub, the fix is public. The existing _env_guard.py defenses apply verbatim:
- Git-history scrub — strip repo to
base_commit, removeorigin, prune future refs. Applies out of the box. - Egress guard — blackhole
pypi.org/github.com/ their CDNs. Applies out of the box. - Instruction leak-strip — reuse
commit_runtime's LLM synthesis path (synthesize_with_llm=Truedefault) so the PR body's typical leaks (fix-commit SHA, "reverts b0e5..." lines,Closes #Ntrailers,(#NNNN)squash trailers) don't reach the prompt.
Pipeline-specific leak concern: the URL itself is a fix-pointer. Do NOT stamp source_url into instruction.md. It goes only into task.toml's [metadata.repo2env] block for provenance; the agent never sees it. Confirmed: the emitter builds instruction.md from the PR body / linked issue only.
LLM use¶
at bootstrap(cached) — one-time per (repo, ref). Reusespr_runtime's bootstrap; a curated list that reuses repos already in cache costs zero LLM.at synthesis(per emitted task) — one call to rewrite the PR body into a leak-free problem statement. Same cost ascommit_runtimev0.8.4 (single Sonnet call, ~$0.01–0.03 per task).
Cost estimate for a 100-env curated dataset: ~$1–3 of Sonnet calls if all bootstraps are cached; $3–8/uncached-repo for fresh bootstraps.
Yield & repo suitability¶
Yield is fundamentally different from mining pipelines. The user chose the URLs, so there's no candidate-vs-emitted denominator in the usual sense. The relevant number is per-URL success rate:
- ~85–95% expected on well-chosen PRs (has tests, tests are runnable, repo bootstraps clean).
- Failure modes worth surfacing to the user with distinct exit codes / skip reasons:
no_test_patch— PR doesn't add / modify any test file. Can't verify.bootstrap_failed— the repo doesn't build in a slim container.no_fail_to_pass— validation ran but no test flipped fail→pass with the fix applied.apply_failed— the merged diff doesn't apply cleanly atbase_commit(rare, but a rebased/squashed PR can hit this).network_error— URL fetch failed.
When strict=True, any of these aborts the whole run with a clear per-URL error. When strict=False, log + skip, emit whatever succeeded, report the per-URL outcomes in PipelineResult.skip_reasons.
Dependencies¶
Reused pipeline machinery (imported, not copied): - pr_runtime.validate_pr — validation harness. - pr_runtime.build_eval_script — graded test.sh emitter (with fail_to_pass + pass_to_pass args — the Arc 3 lesson). - pr_runtime.build_environment_dockerfile — env Dockerfile. - pr_runtime._runtime_aux_files — plain-artifact bakery. - pr_runtime._strip_info_leak, _reflow_pr_body, _linked_issue_number — instruction hygiene. - commit_runtime's synthesize_with_llm path if we make it shared (currently in commit_runtime.py; may want to lift to _synthesis.py). - _env_guard.py — anti-contamination guards. - github.fetch_pr / gitlab.fetch_mr — via input-source abstraction.
New code: URL parser (owner/repo/number extraction, host detection), URL-list loader (urls_file reader), grouping-by-repo logic. Probably 100–150 LOC total.
No new external deps.
Alternatives considered¶
- Add
--pr-urlstopr_runtime— rejected. Overloadspr_runtime's contract (mining vs. curated). See Motivation. - Make it a Python API function only (
from repo2rlenv import pr_from_url) — rejected. CLI parity matters for users who consume everything throughrepo2rlenv generate .... Nothing prevents adding a Python entry point too. - Batch endpoint that takes a YAML manifest of PRs + per-PR overrides — deferred.
urls_filecovers the common case; if users want per-URL overrides (differentrequire_new_test_funcs, etc.) that's a follow-up option and a bigger design conversation.
Rollout plan¶
- Smoke — 5–10 individual PR URLs across
pallets/click,urfave/cli,gorilla/mux(bootstraps cached from earlier arcs). Inspect emitted tasks; oracle-gate each; confirmreward=1.0. - Scale — a curated 100-PR list. The natural source: pick 100 top-scoring PRs from the existing
AdithyaSK/repo2rlenv-pr-runtimedataset and re-import them viapr_to_env. Same envs should come out — validates the pipeline against a known-good baseline. - Oracle gate — every URL that produced an env must score
reward=1.0. Any that don't get dropped or investigated. Track resolved / command_resolved / eval_grade the same way. - Real-agent eval — stratified sample of 15 tasks with claude-code + Sonnet at
n=2. - Publish — HF Hub dataset if we want a public reference; more likely
pr_to_envis used ad-hoc without a public dataset. - Docs —
docs/pipelines/pr_to_env.mdwith the "here's a URL, here's a task" walkthrough as the headline. - Ship experimental —
experimental = Trueat merge. Not a mining pipeline, so no reference dataset requirement to promote — promotion criterion is just "quality holds up on a diverse enough set of URLs."
Open questions¶
- Do we want
pipeline = "pr_to_env"in the task.toml, orpipeline = "pr_runtime"with a[metadata.repo2env.pr_to_env]sub-block signaling the provenance? The first makes the CLI + registry surfaces distinguish curated vs. mined, at the cost of two pipelines sharing a task shape. The second keeps everything aspr_runtimeoutputs but hides the difference. Leaning toward option 1 for clarity; open to being talked out of it. - Should we support commit URLs too? A commit URL points at a specific commit (which may or may not be a merged PR). Would be
commit_import— same design, separate pipeline for the same reason. Leave for RFC 0002 if there's demand. - How do we handle a URL to a merged PR whose
base_commitno longer exists on the repo (force-pushed / branch deleted)? Options: fail fast with a clear error, or fall back to the PR's storedmerge_basefrom the API. Prefer the fallback. urls_fileformat — plain lines vs. TOML/YAML. Plain lines is simpler; TOML/YAML admits per-URL overrides down the road. Start with plain lines +#comments; upgrade if needed.
References¶
docs/pipelines/pr_runtime.md— the pipeline whose output shape + verifierpr_to_envreuses.docs/pipelines/commit_runtime.md— the pipeline whose LLM-synthesis instruction pathpr_to_envinherits.docs/reference/RELATED_WORK.md— where the shipped provenance table will get itspr_to_envrow on merge.- SWE-bench (arXiv:2310.06770) — the canonical "PR → RL env" mapping;
pr_to_envdiffers frompr_runtimein input surface, not in the verifier.