Skip to content

Bootstrap phase v0.2: LLM-driven Docker env generation - #2

Merged
adithya-s-k merged 9 commits into
mainfrom
feat/bootstrap-v0.2
May 11, 2026
Merged

Bootstrap phase v0.2: LLM-driven Docker env generation#2
adithya-s-k merged 9 commits into
mainfrom
feat/bootstrap-v0.2

Conversation

@adithya-s-k

Copy link
Copy Markdown
Collaborator

Summary

  • Adds src/repo2rlenv/bootstrap/ — an LLM agent that iterates shell commands inside a long-lived Docker container until the repo builds and tests can run, then commits the container to an image and caches the digest. The prerequisite for every sandbox-required pipeline (pr_mining, mutation, commit_mining, oss_instruct, …).
  • Wires LiteLLM completion_cost(response) into per-call cost tracking; accumulates in BootstrapResult.llm_cost_estimate_usd.
  • New Rich-based Modal-style live UI for repo2rlenv bootstrap (auto-disables in non-TTY, --no-ui opt-out).
  • Renames default model from claude-sonnet-4-5claude-sonnet-4-6 across docs / tests / sample configs.

Closes #1

Live verification

Bootstrapped pallets/click on macOS + Docker Desktop:

Metric Value
Iterations 6
Wall time 88s
LLM cost $0.119
Smoke test pytest --collect-only passes

Then integrated with Harbor: harbor init --task scaffolded a Harbor task whose environment/Dockerfile is FROM our bootstrap image, harbor run -a oracle returned Mean reward 1.000 in 9s. Confirms our images are first-class Harbor environments with zero adapter code.

What's in scope

  • 9 new files in src/repo2rlenv/bootstrap/ (spec, language, docker, prompts, agent, cache, runner, ui, init)
  • BootstrapSpec wired into GenerationInput
  • New repo2rlenv bootstrap subcommand
  • 4 new test files (19 new tests; 57/57 total pass)
  • .gitignore: envs/, envs-*/, .r2e_cache/
  • rich added as a runtime dep; e2b removed (was speculative)

Test plan

  • uv run pytest -q → 57/57 pass
  • Live bootstrap against pallets/click end-to-end → image committed + cached
  • Fresh docker run against the committed image → /workspace populated, pytest --collect-only collects 1518 tests
  • Harbor integration: harbor run --path ... -a oracle → reward 1.0
  • Cost tracking returns non-zero ($0.119 per click bootstrap)
  • Cache hit on second run is idempotent

Out of scope (follow-up issues / PRs)

  • Bump Harbor emitter schema_version to "1.2"
  • Enforce BootstrapSpec.max_llm_spend_usd (currently declared, not checked)
  • Live GHCR push test (needs gh auth refresh -s write:packages first)
  • Wire bootstrap into pr_mining (full sandbox-required pipeline)

Implements the bootstrap prerequisite for every sandbox-required
pipeline. An LLM agent iterates shell commands inside a long-lived
Docker container until the repo builds and tests can run, then commits
the container to an image and caches its digest for reuse.

Verified end-to-end on pallets/click (6 iters, $0.12, 88s) and
integrated with Harbor: harbor run -a oracle against a task that
FROMs the bootstrap image returns reward 1.0.

Closes #1
@adithya-s-k

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8bdaf36e47

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/repo2rlenv/bootstrap/runner.py Outdated
Comment on lines +46 to +49
def _shallow_clone(repo_url: str, token: str | None, dest: Path, *, depth: int = 1) -> None:
url = auth_clone_url(repo_url, token)
args = ["git", "clone", "--depth", str(depth), url, str(dest)]
r = subprocess.run(args, capture_output=True, text=True, timeout=300, check=False)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fetch the requested ref before bootstrapping

When repo.ref is any branch, tag, or commit that is not present in the default branch's depth-1 clone, this clone step does not fetch it, so the following git rev-parse repo.ref fails before cache lookup or the agent can run. The CLI advertises --ref branch/tag/commit, and task generation commonly needs exact base SHAs, so bootstrap should clone/fetch the requested ref rather than always cloning the default HEAD.

Useful? React with 👍 / 👎.

Comment on lines +196 to +200
image_digest = sandbox.commit(tag, message=f"r2e bootstrap {owner_name}@{ref_sha[:12]}")

pushed = False
if spec.image_registry and "/" in spec.image_registry:
pushed = sandbox.push(tag)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Record a pullable digest after pushing images

When --image-registry is set, image_digest is captured immediately after docker commit, before the push, so a new image usually stores only the local image ID (sha256:...) rather than a registry-qualified repo@sha256:... digest. The bootstrap result describes image_digest as the load-bearing value that other sandboxes can pull, but cached/published metadata from pushed runs will not be pullable by downstream Harbor jobs unless the digest is resolved after docker push.

Useful? React with 👍 / 👎.

max_iterations: int = 20
max_seconds: int = 1800 # 30-minute timeout per bootstrap
base_image: str | None = None # override per-language default
user_dockerfile: Path | None = None # bypass agent iteration entirely

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor the user Dockerfile override

If a config sets bootstrap.user_dockerfile, this field promises to bypass agent iteration, but ensure_bootstrap never reads it and always clones, starts the sandbox, and runs the LLM loop. Users who provide a known-good Dockerfile will still spend LLM budget and may get an unrelated auto-generated image instead of the deterministic Dockerfile they configured.

Useful? React with 👍 / 👎.

…over

Each test_cmds entry was run in its own bash -lc, so an 'export PATH=...'
prefix never carried over to the actual test invocation. Affects Go (PATH
not pre-set in golang:1.23) and any language whose test setup needs env
vars established by an earlier command in the list. Now joined with && and
run as one shell.
1. (P1) Fetch the requested ref before bootstrapping
   _shallow_clone always cloned the default branch shallow, so any non-HEAD
   --ref (branch/tag/commit) failed at git rev-parse before the agent could
   run. New _shallow_clone_at_ref tries `git clone --branch <ref>` (handles
   branches and tags), and falls back to clone + `git fetch origin <ref>` +
   `git checkout` for commit SHAs.

2. (P1) Record a pullable digest after pushing images
   image_digest was captured immediately after `docker commit`, before push,
   so it stored the local image Id rather than the registry-qualified
   repo@sha256:... that downstream Harbor jobs need. Now: after a successful
   push, re-inspect the image via `docker image inspect --format
   {{json .RepoDigests}}` and upgrade image_digest to the registry digest.

3. (P2) Honor the user_dockerfile override
   BootstrapSpec.user_dockerfile was declared but ignored — users supplying a
   known-good Dockerfile still ran the LLM agent. New _bootstrap_from_user_dockerfile
   builds the Dockerfile directly via `docker build`, skipping the agent loop
   entirely, with the same caching + image-push behavior.

Also adds tests/test_bootstrap_runner.py (9 new tests) covering token
scrubbing, ref-resolution paths, RepoDigests parsing, and the
user_dockerfile-missing error.

Refs review: #2
@gitguardian

gitguardian Bot commented May 11, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
32723129 Triggered Basic Auth String cc2327c tests/test_bootstrap_runner.py View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@adithya-s-k

Copy link
Copy Markdown
Collaborator Author

Addressed all three Codex review comments in cc2327c:

# Severity Fix
1 P1 — fetch the requested ref New _shallow_clone_at_ref tries git clone --branch <ref> for branches/tags, falls back to clone + git fetch origin <ref> + git checkout for commit SHAs
2 P1 — pullable digest after push After sandbox.push() succeeds, re-inspect via docker image inspect --format {{json .RepoDigests}} and upgrade image_digest to the registry-qualified repo@sha256:...
3 P2 — honor user_dockerfile New _bootstrap_from_user_dockerfile builds the user's Dockerfile via docker build, skipping the LLM agent loop entirely; cached identically to the agent path

Added tests/test_bootstrap_runner.py with 9 new tests covering token scrubbing, the three ref-resolution paths, RepoDigests parsing, and the user_dockerfile-missing error. 66/66 tests pass.

@codex review

The previous test value 'https://x-access-token:[email protected]/...'
matched GitGuardian's Basic Auth String pattern even though it was an
obvious test placeholder. Function under test (_scrub_token) is content-
agnostic, so we test it with a non-URL token string instead.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cc2327c405

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

3. Otherwise (commit SHA / fallback) → bare clone + `git fetch origin <ref>`
+ `git checkout`.
"""
url = auth_clone_url(repo_url, token)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Strip credentials before baking the clone

When bootstrapping a private repo, auth_clone_url() embeds the GitHub token in the clone URL, and Git records that exact URL in .git/config. The cloned directory is then copied into the committed sandbox image and also used as the Docker build context for user_dockerfile, so any pushed bootstrap image can contain the caller's token unless the remote URL or .git metadata is scrubbed before those steps.

Useful? React with 👍 / 👎.

platform=spec.platform,
) as sandbox:
# Quick sanity: git is installed in the container (most base images include it)
outcome = run_agent_loop(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce the configured LLM spend cap

This agent loop is started without any budget check, so BootstrapSpec.max_llm_spend_usd (defaulted to 5.0 and documented as a per-bootstrap cap) is ignored. If a repository keeps failing until max_iterations or max_seconds, the loop can continue issuing paid LLM calls well past the configured cap instead of aborting once the accumulated cost exceeds it.

Useful? React with 👍 / 👎.

Comment thread src/repo2rlenv/bootstrap/runner.py Outdated
Comment on lines +236 to +240
lines = [f"# Auto-generated from r2e-bootstrap agent transcript", f"FROM {base_image}", ""]
for t in turns:
if getattr(t.action, "name", None) == "BASH":
cmd = t.action.input.replace("\n", " \\\n ")
lines.append(f"RUN {cmd}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make the reconstructed Dockerfile rebuildable

For common successful transcripts where the agent ran commands such as pip install -e . or pytest from the cloned repository, the saved Dockerfile replays those RUN commands before setting /workspace and without copying the repository into the image. That means the cached Dockerfile cannot reproduce the bootstrap image even though it is advertised as rebuild provenance; it will fail as soon as a command expects repo files to exist in the working directory.

Useful? React with 👍 / 👎.

New src/repo2rlenv/ui/ — one place for theme, console, primitives, views:
  theme.py        STYLE/GLYPH constants (single source of look-and-feel)
  console.py      R2EConsole singleton + install_logging() (RichHandler)
  primitives.py   success_panel, error_panel, kv_panel, styled_table
  live.py         live_view() ctx manager + quiet_libraries()
  views/bootstrap.py  BootstrapView with Phases + Steps + Now + Thought + Output
  views/generation.py GenerationView with progress bar + skip-reasons

CLI migration:
  generate    → GenerationView (live progress bar, per-candidate updates)
  validate    → console.success/warn/error per task
  reward      → console.kv panel
  bootstrap   → views/bootstrap.py (no more bespoke bootstrap/ui.py)
  init        → console.success

bootstrap/ui.py deleted (replaced by ui/views/bootstrap.py).
Noisy library loggers (litellm/httpx/anthropic/openai) auto-suppressed to
WARNING while any Live is active via quiet_libraries() inside live_view().
Phase emissions (fixes 'Phases panel stuck on pending' bug):
  runner.py emits clone_start/done, agent_start/done, commit_start/done,
  push_start/done|skipped|failed.
  docker.py:DockerSandbox.start now accepts on_phase and emits
  pull_start/done + sandbox_start/done.
  Cache hits emit *_skipped(detail='cache hit') for all post-clone phases
  so the UI shows the right state immediately.

Parser fix (fixes 'bash: Action:: command not found'):
  _ACTION_RE now stops at the next 'Action:' or 'Thought:' header instead
  of greedy-slurping past them. When an LLM emits two action blocks in one
  response (Sonnet 4.6 occasionally does this), we now correctly take only
  the first and let the next turn handle the rest.
  Prompt tightened with explicit 'NEVER emit multiple Action blocks.'
PRMiningLitePipeline.set_progress_callback(cb): hook for the
GenerationView to receive per-candidate emit/skip/error events. The CLI
wires this up; pipelines that don't define the method are unaffected.

CLAUDE.md: project memory file auto-loaded by Claude Code sessions in
this repo. Covers architecture (3-layer: generation/spec/consumption),
directory map, Pipeline Protocol, UI conventions (use console, never
print()), auth chain, commit/PR conventions, common-task cheatsheet, key
design decisions, status. Trust the code, not this file, on conflicts.

.gitignore: exclude demo_*.sh local scratch scripts.
- Add --base-image CLI flag; expose existing spec.base_image so users
  can override per-language defaults (e.g. ubuntu:24.04, python:3.11-slim).
- Enforce cost guardrail in agent loop: check accumulated cost against
  spec.max_llm_spend_usd before each LLM call; abort cleanly if hit.
  Configurable via --max-spend-usd (default 5.0; 0 = unlimited).
- Stream pull/clone progress so UI doesn't look frozen on large pulls:
  new pull_image_streaming() + _run_git_streaming() forward progress
  lines through phase callbacks; BootstrapView renders them via a new
  <phase>_progress event.
- Surface actionable hint when bootstrap fails with auto-detected
  language: error panel now suggests --language / --base-image overrides.
@adithya-s-k

Copy link
Copy Markdown
Collaborator Author

@codex please review this and see if the end to end implementation is proper, and will be general enough to work across repositories

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 67b26f9c18

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/repo2rlenv/bootstrap/runner.py Outdated
# Cache check (after we know the resolved SHA)
owner_name = "/".join(repo.owner_name)
if not force:
cached = cache_mod.load(owner_name, ref_sha, spec.cache_dir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include bootstrap options in the cache key

When the same repo/ref is bootstrapped again with different options, this lookup returns the old image solely by owner_name and resolved SHA. That means a prior local linux/amd64 build can be reused for a later --platform linux/arm64, a run with a different --base-image or user_dockerfile can silently get the old environment, and a later run adding --image-registry will skip the push because it hits the local cache. Include the platform/base/Dockerfile/registry-relevant config in the cache key or validate the cached result against the current spec before returning it.

Useful? React with 👍 / 👎.

Three real findings from the latest codex pass:

- P1 (security): scrub embedded GitHub token from the clone's .git/config
  before `docker cp`-ing it into the sandbox. auth_clone_url() writes
  https://x-access-token:[email protected]/... and git persists that in
  config; a `docker commit` of the sandbox would otherwise bake the token
  into the published image. New _scrub_clone_credentials() resets the
  remote to the bare URL right after clone; both clone paths use it.

- P2 (reproducibility): the auto-generated Dockerfile replayed agent RUN
  commands without COPYing the repo into the build context, so commands
  like `pip install -e .` would fail on rebuild. Added WORKDIR /workspace
  + COPY . /workspace before the RUN block.

- P2 (cache correctness): the cache slot only keyed on (owner_name, sha),
  so a prior linux/amd64 build would silently satisfy a later
  --platform linux/arm64 or --base-image ubuntu:24.04 request from the
  same SHA. Added cache_options() / _options_hash(); slot becomes
  <short_sha>__<opts8> when any image-identity-affecting field is set.
  Default-only specs keep the v0.2 slot path (backwards compatible).

Tests: +5 covering hashing, options round-trip, dockerfile rebuildability,
and token scrubbing. 71/71 passing.
@adithya-s-k
adithya-s-k merged commit 2339524 into main May 11, 2026
1 check passed
@adithya-s-k
adithya-s-k deleted the feat/bootstrap-v0.2 branch May 11, 2026 10:19
adithya-s-k added a commit that referenced this pull request May 21, 2026
PR descriptions frequently link back to the answer the agent is supposed
to produce — adding noise that lets a frontier model shortcut by fetching
the linked artifact. We now strip:

  - Closes / Fixes / Resolves #N (already; extended to multi: '#1, #2')
  - See / Refs / Follow-up to #N
  - Markdown issue links [#N](url)
  - Bare github.com /pull/, /issues/, /commit/ URLs
  - Co-authored-by / Signed-off-by / Reviewed-by / Acked-by trailer lines
  - "(#N)" squash-merge suffix on the title

12 new unit tests in tests/test_pipeline_pr_diff.py covering each pattern
+ the end-to-end _build_instruction shape. Full suite at 647 passing.

This is the v0.8.3 Arc 1 optimization landed alongside the full pr_diff
sweep.
adithya-s-k added a commit that referenced this pull request May 26, 2026
… dataset (#40)

* pr_diff: broaden instruction info-leak strip

PR descriptions frequently link back to the answer the agent is supposed
to produce — adding noise that lets a frontier model shortcut by fetching
the linked artifact. We now strip:

  - Closes / Fixes / Resolves #N (already; extended to multi: '#1, #2')
  - See / Refs / Follow-up to #N
  - Markdown issue links [#N](url)
  - Bare github.com /pull/, /issues/, /commit/ URLs
  - Co-authored-by / Signed-off-by / Reviewed-by / Acked-by trailer lines
  - "(#N)" squash-merge suffix on the title

12 new unit tests in tests/test_pipeline_pr_diff.py covering each pattern
+ the end-to-end _build_instruction shape. Full suite at 647 passing.

This is the v0.8.3 Arc 1 optimization landed alongside the full pr_diff
sweep.

* pr_diff: harden info-leak strip after sweep findings

Sweeping the 38 launch repos surfaced two leak patterns that slipped past
the v0.8.1 + first-pass strip:

  - Manual close marker in the PR title: " (fixes #N)" — caught
    stretchr/testify#1888.
  - Dependabot release notes embedding `https://redirect.github.com/...`
    URLs that point straight at the linked PR/issue — caught 6 cells
    (gin, jsonschema×2, urfave/cli, expressjs, chronotope).

Title squash-suffix regex now also matches parenthesized closes-style
markers (`(closes|fixes|resolves|see|refs) #N` with optional comma list).
GH-URL regex now matches any `[subdomain.]github.com` host so redirector
domains are stripped too.

Re-ran the sweep after the fix — zero leaks remain across all 127
emitted instructions. Findings + per-pattern verification in
docs/release_notes/v0.8.3/findings-pr_diff.md.

Adds 2 tests to tests/test_pipeline_pr_diff.py covering both real-world
patterns. Full suite at 649 passing.

* pr_diff: handle markdown-link forms of issue refs and GH URLs

Sergio's review on the PR surfaced two additional patterns that the
first-pass strip didn't handle cleanly:

1. `Closes [#1234](url)` — the markdown-link form of a Closes/Fixes/
   Resolves ref. The bare-#N regex didn't match, so the keyword was
   left orphaned in the output (only the [#N](url) part was stripped
   by the issue-link regex).

2. `[descriptive text](https://github.com/x/y/pull/N)` — markdown
   link whose URL points at a GH pull/issues/commit, with any link
   text (not just `[#N]`). The bare-URL regex stripped the URL but
   left empty `[text]()` brackets in the prose.

Two new regexes — _CLOSES_MD_RE / _REFS_MD_RE for the closes-style
markdown variants, _MD_GH_URL_RE for the descriptive-text variant —
run BEFORE the piece-wise regexes so composite patterns are stripped
whole rather than fragmented.

4 new unit tests cover both cases (single, multi-list, descriptive
text, see-with-markdown). Suite at 638 passing.

* pr_diff: emit Harbor-runnable env with SWE-RL diff-similarity verifier

The whole point of Repo2RLEnv is verifiable RL envs that LLMs can solve.
v0.8.1's pr_diff was text-only — instruction.md + solution/patch.diff,
nothing harbor-runnable. The reward was supposed to be computed by the
consumer externally. That fails the bar.

This change makes pr_diff produce a fully Harbor-runnable env where the
verifier IS the SWE-RL-style sequence-similarity score against the
oracle diff. Same reward function as repo2rlenv.reward (in fact, the
verifier's embedded Python is kept in lockstep with reward.py — pure
stdlib, ~30 lines).

New helpers in src/repo2rlenv/pipelines/pr_diff.py:
- `build_pr_diff_environment_dockerfile(repo_url, base_commit, oracle_diff)`
  - FROM python:3.12-slim + git
  - shallow clone of the repo @ base_commit
  - base64-bakes the oracle diff into /verifier/oracle.patch
  - no bootstrap LLM — ~30s build per task
- `build_pr_diff_eval_script(base_commit)`
  - captures git diff <base_commit> after the agent's edits
  - embeds the diff-similarity Python (base64) — needs only python3 + git
  - writes the score to /logs/verifier/reward.txt

`PRDiffOptions.emit_harbor_env: bool = True` default-on. Set False to
get the v0.8.1 text-only output (consumer computes reward).

End-to-end smoke on pallets/click PR #3508:
- harbor run -a oracle → reward 1.000 (28s)
- harbor run -a claude-code -m anthropic/claude-sonnet-4-6 → reward 0.710 (4m32s)

Both via harbor run on the emitted task as-is. The oracle case proves
the verifier is wired correctly; the Sonnet case proves the agent path
works end-to-end with a real partial-credit reward.

6 new unit tests on the Dockerfile + eval-script builders (base64
encoding, special-char patches, difflib in inline Python, reward.txt
path). 1 existing test updated to reflect the new instruction wording.
Suite at 644 passing.

* pr_diff: 6-component reward + LLM judge + calibration metadata

Replace the single-scalar diff-similarity reward with the SWE-RL-paper-
style multi-component approach. Lifts pr_diff from "coarse training
signal" to "evaluable single-task env" while keeping the cheap/multi-
language property.

New module: src/repo2rlenv/pipelines/_pr_diff_verifier.py
  - Pure-stdlib in-container verifier (read at gen time, base64-baked
    into tests/test.sh, decoded back to a file at run time).
  - Reviewable + unit-testable as ordinary Python — no more opaque
    base64-blob diffs in test.sh.

6 components (sum = 1.0 default weights):
  format_valid     0.05   (predicted text parses as unified diff)
  size_sanity      0.05   (min(o_loc, p_loc) / max — rampage guard)
  file_targeting   0.10   (F1, NOT Jaccard — recall matters more
                          than punishing extras; explained in module)
  region_overlap   0.20   (predicted hunks overlap oracle hunks
                          with 5-line slack — strongest spatial signal)
  similarity       0.20   (SequenceMatcher over +/- lines ONLY —
                          fixes v0.8.1 context-credit inflation)
  llm_judge        0.40   (Haiku scores semantic correctness;
                          graceful degradation on API failure,
                          remaining weights re-normalize)

Verifier outputs reward.txt (single float, harbor reads this) AND
reward.json (full breakdown for downstream inspection / re-weighting).

Gen-time additions in pr_diff.py:
  - Quality filter: drops test-only / docs-only / revert / trivially-
    small diffs / instruction-too-thin candidates before emission.
  - Calibration baseline: empty-patch reward stamped in
    task.toml.metadata.reward_calibration.baseline_reward. Consumers
    compute calibrated = (raw - baseline) / (1 - baseline) for
    cross-task comparability.
  - Difficulty bucket: trivial/small/medium/large by oracle LOC.
  - PRDiffOptions.min_loc_changed: int = 3 (filter knob).

Dockerfile now pre-installs claude-code via npm at build time (apt + npm
layers cacheable across all pr_diff tasks). This makes harbor's
claude-code agent-setup robust at 25-parallel-container scale — no more
per-container curl-install flakiness.

End-to-end smoke on pallets/click PR #3508:
  - harbor run -a oracle: reward 1.000 (8s with cached layers)
  - harbor run -a claude-code -m claude-sonnet-4-6 --ve ANTHROPIC_API_KEY=$X:
    final 0.98 (5m), all 5 deterministic = 1.0, llm_judge = 0.95

Test coverage: 37 new tests in tests/test_pr_diff_verifier.py covering
each component, judge graceful degradation, weight redistribution.
Suite at 681 passing.

* pr_diff: stage new files before git diff in verifier

`git diff <base>` only sees TRACKED files. New files added by the
oracle patch (or the agent) are untracked and silently absent from
the predicted.patch, causing oracle runs to score < 1.0 for any PR
that introduces new files.

Found by the 25-env pilot: 3 of 13 oracle runs scored 0.4-0.79
instead of 1.0 — all 3 were PRs that added new files (gradio added
3 new files in a 4-file PR, scoring 0.40 instead of 1.0).

Fix: `git add -A; git diff --cached <base>` — `-A` stages all new
files so they appear in the predicted diff just like the oracle.

* pr_diff: bake claude-code at build time, not at agent-setup

Pilot at concurrency=25 (then 12) had 100% Sonnet failure with
AgentSetupTimeoutError after 360s. Root cause: harbor's claude-code
adapter runs `curl claude.ai/install.sh | bash` in EVERY container at
agent-setup time. N parallel ~80MB downloads saturate local bandwidth
and time out.

Fix: pre-install claude-code in the Dockerfile at BUILD time using the
same install.sh route harbor uses. Docker's content-addressable layer
cache means the install runs ONCE per image-build and is shared across
all per-task images (the install RUN line is identical between tasks).
When harbor's adapter re-runs the install script per-container, it
finds the binary already in /root/.local/bin and short-circuits the
network step.

Also drops the nodejs/npm route which was a dead end — harbor's
adapter only uses npm on Alpine images; on python:3.12-slim it always
uses curl install.sh regardless of pre-installed binaries.

Single-task smoke after the fix: 0.858 reward in 3m57s (was 4m56s
before the bake; same task, all 6 components fire including
llm_judge=0.92).

* pr_diff: retune verifier weights after 23-task pilot

Sonnet pilot ran 23 tasks through the full env (oracle 21/21=1.000,
sonnet mean 0.634, range 0.16-0.98 = healthy eval distribution). Fed
the per-task component data to Sonnet for reward-engineering analysis;
its grounded recommendations are now the defaults.

Changes:
  format_valid    0.05 → 0.00   (was 1.0 on EVERY trial; pure dead weight)
  size_sanity     0.05 → 0.08   (useful outlier detector)
  file_targeting  0.10 → 0.12   (leading indicator, least correlated)
  region_overlap  0.20 → 0.20   (unchanged; strongest spatial signal)
  similarity      0.20 → 0.10   (~0.85 correlation with region_overlap;
                                 double-counts positional accuracy AND
                                 penalizes alternative implementations)
  llm_judge       0.40 → 0.50   (only semantic-independent signal;
                                 diverges informatively from text-based
                                 components on e.g. tokenizers/evaluate)
  TOTAL = 1.00

Also adds a catastrophic-size hard cap: if size_sanity < 0.10, the
final reward is clamped to ≤ 0.40. Prevents a charitable judge from
inflating the score on patches that are dramatically the wrong size
(prettier hit 0.011 — Sonnet wrote ~5 lines vs oracle's 500-line
release-notes dump; without the cap, judge=0.25 still pulled the
final to 0.23).

Two new tests pin the defaults — `_DEFAULT_WEIGHTS` must sum to 1.0,
format_valid must stay 0.0, similarity < region_overlap. Suite at 683
passing.

* docs: pr_diff now Harbor-runnable + 6-component reward

- docs/pipelines/pr_diff.md rewritten end-to-end:
  - Multi-component reward (6 components + LLM judge + catastrophic-size cap)
  - Calibration baseline + difficulty bucket metadata
  - Updated info-leak strip (8 pattern families)
  - New options table (emit_harbor_env, min_loc_changed)
  - New skip-reason list (test-only, docs-only, revert, diff-too-small, thin-instruction)
  - Consumer-side now uses harbor run with claude-code; LLM-judge env-var path documented
  - Reference dataset link placeholder (filled after HF push)

- docs/quickstart.md updated: pr_diff is now Harbor-runnable; show the full
  harbor run incantation for both oracle and Sonnet adapters.

- docs/pipelines/README.md table: pr_diff Sandbox column is now "thin¹" with
  footnote explaining the python:3.12-slim env + LLM-as-judge at verify time.

- README.md table mirrors the same.

Test counts left generic in docs per repo convention (specifics live in PR
bodies / release notes).

* pr_diff: publish 100-env reference dataset + augmented HF card

Dataset live at https://huggingface.co/datasets/AdithyaSK/repo2rlenv-pr-diff
- 100 verified environments
- 26 source repos (Tier A SWE-bench + Tier B HF ecosystem + Tier C multi-lang)
- All structurally validated (`repo2rlenv validate` passes for every task)

Augmented HF dataset card (src/repo2rlenv/hub.py:_build_dataset_card):
- Supports multi-repo datasets — renders "Source repos (N)" list when the
  push spans more than one source repo (was: single repo only).
- Detects whether tasks ship environment/Dockerfile and renders the
  harbor-runnable recipe (oracle + claude-code) accordingly.
- Adds "How it was generated" section with reproduction recipe.
- Adds the LLM-as-judge `--ve ANTHROPIC_API_KEY=$X` flag to the harbor-run
  example (was missing — verifier-side env wasn't documented).
- Tags include the pipeline name so collections can filter.

Registry-integration fast-path (src/repo2rlenv/registry/integration.py):
- When every task's Dockerfile FROM ref is publicly pullable (e.g.
  `python:3.12-slim` for pr_diff's self-contained Dockerfile), skip the
  image-distribution step entirely. Was: every dataset with
  environment/ tried to push an image to a container registry and
  failed for pr_diff (no bootstrap upstream image to push).

Docs:
- docs/pipelines/pr_diff.md gets the published HF URL.
- docs/release_notes/v0.8.3/findings-pr_diff.md rewritten with the 100-env
  numbers, the LLM-driven reward-weight retune story, and the 8-pattern
  info-leak strip.

* pr_diff: drop claude-code from Dockerfile (wrong layer)

Earlier commit (69d1631) baked claude-code into the task's
environment/Dockerfile via curl claude.ai/install.sh at build time.
That was wrong: the task spec is supposed to be agent-agnostic. The
Dockerfile defines the ENVIRONMENT the task runs in (repo, verifier,
tools) — it shouldn't pre-install a specific agent vendor's binary.

Consequences of the bake:
- ~80MB of claude-code in every image, even when the agent is
  openhands / codex / aider / etc.
- A specific Anthropic CLI on PATH could interfere with what other
  agents do (unlikely but possible).
- Cross-vendor contamination — the dataset implicitly assumes
  claude-code is the runner.

The original problem the bake was supposed to solve: at concurrency≥8,
N parallel `curl claude.ai/install.sh` calls during harbor's claude-code
agent-setup saturate local bandwidth and time out. Correct fixes:

1. Lower concurrency to ≤5 (harbor's own default is 4). Confirmed
   working in the latest smoke: 0.975 reward in 2m33s with no bake.
2. Use --max-retries 2 (already in run_pilot.py).
3. File an upstream harbor issue for install-script caching.

Single-task smoke after the revert: 0.975, 2m33s. The 100-env
reference dataset has been re-generated and re-pushed to HF Hub with
the clean Dockerfile.

* pr_diff: drop duplicated weights, import _DEFAULT_WEIGHTS directly

Addresses sergiopaniego's PR #40 review: the no-op calibration helper
had a hand-copied weights dict that drifted out of sync with the
retune in _pr_diff_verifier._DEFAULT_WEIGHTS. Import the single source
of truth instead.

The duplicate was harmless in practice (format_valid('') = 0.0 makes
the baseline 0.0 with either weight set), but it's a latent footgun
the next time the weights change.

* docs: richer pipeline table + pr_diff spotlight + multi-agent examples

README + docs/pipelines/README:
- Expanded the pipelines table with a longer "What it produces" column
  and a per-pipeline "LLM use" classification (at synthesis / at
  bootstrap / at verify) — every pipeline calls an LLM somewhere, the
  table now makes the location explicit.
- docs/pipelines/README.md adds a "Reference dataset" column linking
  the published HF dataset for `pr_diff`, plus a Spotlight section
  that walks the task layout + 6-component reward + reproduction
  recipe inline.

Multi-agent examples:
- quickstart, pr_diff doc, and Spotlight now show claude-code AND
  openhands invocations side-by-side, with the full Harbor agent
  catalogue (25+ harnesses) inlined as a comment block. The contract
  is agent-agnostic — claude-code is what we used to verify the
  reference dataset, not a requirement.

findings-pr_diff: corrected the stale claim that we bake claude-code
into the Dockerfile (we don't anymore — Harbor's agent adapter
installs whatever runtime its agent needs at run time).

* docs: drop references to gitignored launch-side scripts

findings-pr_diff used to point at plans/v083_scripts/run_pilot.py for
the generation recipe, but plans/ is gitignored — those paths can't
be opened by anyone reading the PR. Rewrote the recipe in terms of
the public `repo2rlenv generate` CLI so the published dataset is
reproducible without internal tooling.

Also dropped the analyze_pilot.py mention in
_pr_diff_verifier.py's _DEFAULT_WEIGHTS comment — the rationale is
the point, not the specific script that produced it.

* pr_diff: address external audit — fast-path safety + private-repo guard

Three correctness fixes flagged by an independent audit of PR #40:

1. Self-contained Dockerfile fast path was too broad. It skipped the
   image-push step for ANY non-local FROM ref, including private or
   unqualified images (my-bootstrap:latest, company/base:dev) that a
   consumer couldn't actually pull/rebuild. Narrowed to an explicit
   allowlist of known-public Docker Hub bases (python/node/golang/...).
   Anything else falls through to the normal push path, which surfaces
   a clear error rather than silently publishing a broken dataset.

2. The fast path left stale reproducibility metadata. The emitter seeds
   every Dockerfile task with mode=local_only, image_visibility=private;
   the fast path returned without rewriting it, so published pr_diff
   tasks advertised "private/local_only" despite being publicly
   rebuildable. Now rewrites each task.toml to mode=inline_dockerfile,
   image_visibility=public, with the Dockerfile sha256 stamped for
   traceability.

3. Private repos with emit_harbor_env=True silently produced envs that
   fail at consumer build time (the inlined `git clone` is
   unauthenticated). Now fails fast at run() with a clear message
   pointing at emit_harbor_env=False for text-only output.

Also: corrected the verifier module docstring (said "5-component" +
listed pre-retune weights) and removed four unused _NORMALIZE_RE_*
constants left over from an earlier normalization path.

Tests: +3 (public-base fast path rewrites metadata; non-allowlisted
image surfaces an error; private+emit_harbor_env fails fast). The
private e2e test now passes emit_harbor_env=False explicitly.

* pr_diff: support private repos via GITHUB_TOKEN build arg (not a guard)

Replaces the wrong "private + emit_harbor_env → error" guard from the
previous commit. Private-repo support is a first-class goal of this
repo, and emit_harbor_env=True is the default for a reason — the env
is the whole point. Forbidding it for private sources defeats that.

The actual bug the audit caught was that the emitted Dockerfile cloned
over an unauthenticated URL, so private repos failed at consumer build
time. Fix: the Dockerfile now declares `ARG GITHUB_TOKEN=` (empty
default) and clones via an x-access-token URL when it's set, then
resets the remote to the clean URL so the token never persists in the
image's git config or any layer. Public repos need no arg.

Consumers building a private-repo task pass:
    harbor run ... --build-arg GITHUB_TOKEN=$GITHUB_TOKEN

This mirrors how bootstrap already handles private repos (host-side
clone with the resolved token, never embedded). Documented in
docs/reference/AUTH.md + docs/pipelines/pr_diff.md.

* pr_diff: reconcile docs/code with the spec contracts

A compliance pass against the repo's own contract docs (SPEC.md,
CLAUDE.md, the pipeline pages) surfaced two divergences introduced by
the pr_diff upgrade:

1. reward_kinds emitted "diff_similarity_multi_component", which is not
   in SPEC.md's reward-kind set and disagreed with the README table.
   The reward IS a diff_similarity reward — the 6 components are how
   it's scored, surfaced in /logs/verifier/reward.json, not a separate
   kind. Emit the documented "diff_similarity"; describe the
   multi-component scoring in prose. Updated pr_diff.md to match.

2. Multiple docs + docstrings still called pr_diff "text-only / no
   sandbox", which is now only true of *generation*. The emitted task
   runs in Docker by default. Reconciled the prose in SPEC.md,
   CLAUDE.md, the pr_diff.py module + class docstrings, and the
   harbor emitter docstring to distinguish sandbox-free generation
   from Docker-runnable consumption.

No behavior change — verifier scoring + emitted files are unchanged;
this aligns metadata + documentation with the spec.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bootstrap phase (v0.2): LLM-driven Docker env generation for any repo

1 participant