pr_diff¶
SWE-RL-inspired PR mining with a Harbor-runnable multi-component verifier. Each emitted task is a real merged GitHub PR; the agent edits the repo and the verifier scores the diff with five deterministic components plus an LLM-as-judge.
| Status | implemented |
| Sandbox required at gen | No (text-only generation; verifier runs in a thin python:3.12-slim container) |
| LLM required at gen | No |
| LLM required at verify | Optional — falls back to deterministic-only when no API key |
| Reward kinds emitted | diff_similarity (scored via the 6-component verifier — see below) |
| Inspiration | SWE-RL (Meta, NeurIPS '25) |
| Implementation | src/repo2rlenv/pipelines/pr_diff.py, src/repo2rlenv/pipelines/_pr_diff_verifier.py |
| Options model | PRDiffOptions |
| Reference dataset | AdithyaSK/repo2rlenv-pr-diff on HF Hub |
What it does¶
flowchart TD
A[Repo URL<br/>e.g. pallets/click] --> B[gh pr list<br/>--state merged]
B --> C{For each PR}
C --> D{Filters<br/>drafts? file count?<br/>test-only? docs-only?<br/>revert? min LOC?}
D -- skip --> Z[Count skip reason<br/>continue loop]
D -- pass --> E[gh pr diff #N]
E --> F[Strip info-leak from<br/>instruction title + body]
F --> G[Compute baseline reward<br/>+ difficulty bucket]
G --> H[Build Harbor task]
H --> I[task.toml<br/>+ instruction.md<br/>+ solution/patch.diff<br/>+ solution/solve.sh<br/>+ environment/Dockerfile<br/>+ tests/test.sh] For each merged PR within scope:
- List PRs via
gh pr list, apply structural filters (date, draft, file count) + quality filters (drop test-only, docs-only, reverts, trivially-small diffs). - Fetch the unified diff via
gh pr diff. - Strip leakage patterns from the PR title + body (eight pattern families — see Instruction info-leak strip below).
- Compute the calibration baseline (the score an empty patch would get against this oracle) and the difficulty bucket by LOC changed.
- Emit a Harbor-spec task:
instruction.md,solution/{patch.diff, solve.sh},environment/Dockerfile,tests/test.sh,task.toml.
The environment is a thin, agent-agnostic python:3.12-slim image with git + the repo checked out at base_commit — no agent CLI is pre-installed. Harbor's agent adapter (-a claude-code, -a openhands, -a codex, -a aider, …) drops in the runtime its agent needs when the container starts. The verifier (tests/test.sh) runs after the agent and computes the multi-component reward.
Private repos work the same way — the Dockerfile clones via an optional GITHUB_TOKEN build arg. Public repos need nothing; for a private source, the consumer passes --build-arg GITHUB_TOKEN=$GITHUB_TOKEN to harbor run. The token is used only for the clone and the remote is scrubbed afterward, so it never persists in the image. See reference/AUTH.md.
Multi-component reward¶
The verifier captures the agent's edits as a unified diff against base_commit, then scores it against the oracle (gold) diff using six components, weighted sum:
| Component | Range | Default weight | What it captures |
|---|---|---|---|
format_valid | 0 or 1 | 0.00 | Does the predicted text parse as a unified diff? Always 1 for the claude-code adapter — kept as a guard, weight 0 because it carries no discriminative signal. |
size_sanity | [0, 1] | 0.08 | min(oracle_loc, predicted_loc) / max(...). Catches "rampage through the codebase" and "no-op" failure modes. |
file_targeting | [0, 1] | 0.12 | F1 over the changed-file sets (not Jaccard — missing an oracle file is worse than touching one extra). |
region_overlap | [0, 1] | 0.20 | For each oracle hunk, did the predicted diff edit a line within 5 lines of that hunk in the same file? Strongest spatial-localization signal. |
similarity | [0, 1] | 0.10 | difflib.SequenceMatcher ratio over +/- lines only (no free credit for unchanged context). |
llm_judge | [0, 1] or null | 0.50 | Haiku rates "does this patch logically address the issue described?" Most informative semantic signal. Null on missing API key / network error → remaining weights are re-normalized. |
Final reward is clipped to [0, 1]. A catastrophic-size hard cap clamps the final to ≤ 0.40 when size_sanity < 0.10 — stops a charitable judge from inflating scores on patches that are wildly the wrong size.
The verifier writes both /logs/verifier/reward.txt (single float, Harbor reads this) and /logs/verifier/reward.json (full breakdown for downstream inspection / re-weighting).
Weights are overridable per-task via task.toml.metadata or per-run via R2E_W_{FORMAT,SIZE,FILE,REGION,SIM,JUDGE} env vars passed to harbor run --ve.
Calibration baseline¶
Each task carries task.toml.metadata.repo2env.reward_calibration.baseline_reward — the reward an empty predicted diff would get against this oracle. Consumers can normalize:
calibrated < 0 means the agent did worse than no-op. Useful for cross-task comparability since trivial 5-line fixes and 200-line refactors are no longer the same number.
Difficulty bucketing¶
Each task carries task.toml.metadata.difficulty ∈ {easy, small, medium, large} based on oracle LOC changed (≤ 5, 6 – 20, 21 – 80, > 80 respectively), plus a raw loc_changed int. Lets training scripts weight or filter by difficulty.
Instruction info-leak strip¶
PR descriptions often contain pointers to the answer. The pipeline strips eight pattern families from instructions before they reach the agent:
| Pattern | Example |
|---|---|
| Multi-issue closes | Closes #42, Fixes #1, #2, #3 |
See / refs / follow-up to linkbacks | See #99, Follow-up to PR #42 |
| Markdown issue links | [#1234](https://github.com/x/y/issues/1234) |
| Closes with markdown-link refs | Closes [#1234](url) |
| Descriptive markdown links to GH URLs | [my analysis](https://github.com/x/y/pull/1234) |
| Bare GitHub URLs | https://github.com/foo/bar/pull/42, also redirect.github.com |
| Commit trailers | Co-authored-by:, Signed-off-by: |
| Title squash suffix | Fix the bug (#1234), (fixes #1800) |
Composite patterns are stripped before piece-wise patterns so we don't leave orphaned Closes keywords or empty [text]() markdown brackets behind.
Options¶
class PRDiffOptions(BaseModel):
limit: int = 50
since: date | None = None
until: date | None = None
state: Literal["merged", "all"] = "merged"
diff_format: Literal["unified", "search_replace"] = "unified"
max_files_per_pr: int = 5
skip_drafts: bool = True
emit_harbor_env: bool = True
min_loc_changed: int = 3
| Field | Default | Notes |
|---|---|---|
limit | 50 | Max tasks emitted (over-fetched ~3× client-side to allow filtering). |
since / until | None | ISO date bounds applied to mergedAt. |
state | "merged" | Currently only merged is supported. |
max_files_per_pr | 5 | Drops sweeping refactors. |
skip_drafts | True | Drops draft PRs. |
emit_harbor_env | True | Emits environment/Dockerfile + tests/test.sh so harbor run works directly. Set False for the v0.1-style text-only output. |
min_loc_changed | 3 | Reject PRs whose oracle diff has fewer +/- lines than this — too trivial to be a meaningful task. |
Skip reasons¶
A PR may not become a task. The pipeline records counts per reason:
| Reason | Meaning |
|---|---|
draft | pr.is_draft and skip_drafts=True |
no_files / too_many_files | Outside max_files_per_pr |
not_merged | No mergedAt timestamp |
empty_diff | gh pr diff returned an empty string |
diff_fetch_failed | gh pr diff raised |
test_only_diff | 100% of touched files are test files |
docs_only_diff | 100% of touched files are .md / .rst / docs/ |
revert_pr | Title starts with Revert |
diff_too_small | Fewer than min_loc_changed +/- lines |
instruction_too_thin | Empty body + short title after info-leak strip |
Yield¶
Yield = emitted tasks ÷ merged PRs examined. pr_diff is the highest-yield pipeline (~80–95%) because there is no execution gate — any merged PR with a non-trivial diff and a usable problem statement becomes a task. Nothing is dropped for failing to build or run.
| Knob | Default | Effect on yield |
|---|---|---|
min_loc_changed | 3 | ↑ drops more trivial one-liner/typo PRs (lower yield, higher signal) |
max_files_per_pr | 5 | ↓ excludes sprawling PRs (lower yield) |
state | merged | all admits unmerged PRs (higher yield, noisier oracle) |
skip_drafts | True | drafts excluded |
The only structural drops are the buckets in the table above (test/docs-only, reverts, too-small, too-thin) — typically 5–20% combined on a healthy repo.
Worked example: to ship 100 tasks at ~90% yield you need ~110 merged PRs — one mid-sized repo (or two small ones) clears it in a single pass. This is why the reference dataset (AdithyaSK/repo2rlenv-pr-diff, 100 envs) was built from just a handful of repos.
Example invocations¶
CLI¶
# Generate one env from a single repo
repo2rlenv generate \
--repo pallets/click \
--pipeline pr_diff \
--pipeline-opt limit=5 \
--out ./datasets/click-prdiff
# Run it through harbor with the oracle adapter (must score reward=1.0)
harbor run -p ./datasets/click-prdiff -a oracle --env docker -n 1
# Run it through harbor with a real agent.
# The verifier's LLM judge also needs an API key — pass via --ve so it
# reaches the verifier container (the --ae key only reaches the agent).
# Example 1: claude-code + Sonnet 4.6 (what we used to verify the
# reference dataset).
harbor run \
-p ./datasets/click-prdiff \
-a claude-code -m anthropic/claude-sonnet-4-6 \
--ak max_budget_usd=2.00 --ak max_turns=30 \
--ae ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \
--ve ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \
--env docker -n 1
# Example 2: same env, different agent — openhands + GPT-4o.
harbor run \
-p ./datasets/click-prdiff \
-a openhands -m openai/gpt-4o \
--ae OPENAI_API_KEY=$OPENAI_API_KEY \
--ve ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \
--env docker -n 1
# Harbor ships 25+ agent harnesses you can swap in here:
# claude-code · openhands / openhands-sdk · codex · aider · gemini-cli
# copilot-cli · opencode · cursor-cli · qwen-coder · kimi-cli · goose
# mini-swe-agent · swe-agent · nemo-agent · terminus-2 · trae-agent · ...
# Each one expects its own provider env var via `--ae`. Run
# `harbor run --help` to see the full list.
# Generate locally, then push to HF Hub
repo2rlenv push ./datasets/click-prdiff <your-org>/<dataset-name>
Python¶
from pathlib import Path
from repo2rlenv.spec.input import (
GenerationInput, RepoSpec, PipelineSpec, OutputSpec, PipelineName,
)
from repo2rlenv.spec.options import PRDiffOptions
from repo2rlenv.pipelines.pr_diff import PRDiffPipeline
g = GenerationInput(
repo=RepoSpec(url="pallets/click", access="auto"),
pipeline=PipelineSpec(name=PipelineName.PR_DIFF, options={}),
output=OutputSpec(destination="./out", org="myorg", dataset_name="click-prdiff"),
)
options = PRDiffOptions(limit=5, max_files_per_pr=10)
pipeline = PRDiffPipeline(g, options)
result = pipeline.run(Path("./out"))
print(result.candidates, result.emitted, result.skip_reasons)
Pulling the reference dataset¶
A verified reference dataset is published on HF Hub:
https://huggingface.co/datasets/AdithyaSK/repo2rlenv-pr-diff
repo2rlenv pull AdithyaSK/repo2rlenv-pr-diff /tmp/pr_diff-ref
repo2rlenv validate /tmp/pr_diff-ref
# Smoke-check with the oracle (must score reward=1.0)
harbor run -p /tmp/pr_diff-ref -a oracle --env docker -n 1
You can also browse it interactively via the Harbor Visualiser badge on the dataset card.
[metadata.repo2env] schema¶
Each emitted task carries:
[metadata.repo2env]
pipeline = "pr_diff"
pipeline_version = "0.3.0"
repo = "pallets/click"
ref = "<base_commit_sha>"
reference = "https://github.com/pallets/click/pull/3508"
built_at = "2026-05-26T..."
reward_kinds = ["diff_similarity"]
[metadata.repo2env.pr_diff]
pr_merged_at = "2026-05-23T..."
diff_format = "unified"
context_files = ["src/click/shell_completion.py", "tests/test_shell_completion.py"]
[metadata.repo2env.reward_calibration]
baseline_reward = 0.0
loc_changed = 95
difficulty = "large"
Consuming the reward at training time¶
Two paths:
(a) Per-task reward via Harbor. Use harbor run against the emitted task directory. The verifier writes reward.txt (single float) and reward.json (full breakdown). This is the production path for evals.
(b) Pure-text reward for SWE-RL-style training rollouts. Score a candidate prediction against the oracle directly:
from repo2rlenv.reward import calculate_diff_similarity_reward
oracle = (task_dir / "solution" / "patch.diff").read_text()
reward, meta = calculate_diff_similarity_reward(oracle, prediction_diff)
This is the (single-component, dense) signal SWE-RL trained on. For higher fidelity, run the predicted diff through the same six-component verifier — it's a pure-stdlib module at _pr_diff_verifier.py that can be imported directly.
Acknowledgments¶
The text-only PR-as-task formulation + the diff-similarity reward shape are inspired by SWE-RL (Wei et al., NeurIPS '25, arXiv:2502.18449). The LLM-as-judge component follows the constitutional-AI / RLAIF pattern. No code is copied from SWE-RL; the reward function is an independent reimplementation against the Python standard library and is released under Apache-2.0.