Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/repo2rlenv/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,35 @@ def cmd_validate(args: argparse.Namespace) -> int:
return 0 if failures == 0 else 1


def cmd_migrate(args: argparse.Namespace) -> int:
from repo2rlenv.pipelines.pr_diff import migrate_pr_diff_task

dataset_dir = Path(args.path).expanduser().resolve()
task_dirs = sorted({tf.parent for tf in dataset_dir.rglob("task.toml")})
if not task_dirs:
console.error(f"no task.toml files found under {dataset_dir}")
return 1

apply = getattr(args, "apply", False)
label = "Migrating" if apply else "Auditing (dry run — pass --apply to write)"
counts: dict[str, int] = {}
errors: list[dict[str, str]] = []
with console.section(f"{label} pr_diff tasks under {dataset_dir}"):
for task_dir in task_dirs:
result = migrate_pr_diff_task(task_dir, dry_run=not apply)
action = result["action"]
counts[action] = counts.get(action, 0) + 1
if action == "error":
errors.append(result)
elif action in ("migrated", "would_migrate"):
console.success(f"{result['task']}: {action}")
for err in errors:
console.error(f"{err['task']}: {err['detail']}")

console.kv(counts, title="pr_diff migration" if apply else "pr_diff migration (dry run)")
return 0 if not errors else 1


class _Backend:
"""Which source/destination a URI points at."""

Expand Down Expand Up @@ -1101,6 +1130,22 @@ def _dispatch(argv: list[str]) -> int:
)
v.set_defaults(func=cmd_validate)

# migrate
mg = sub.add_parser("migrate", help="Repair already-emitted tasks after a generator fix")
mg_sub = mg.add_subparsers(dest="migration", required=True)
mg_pr_diff = mg_sub.add_parser(
"pr-diff",
help=(
"repair pr_diff tasks emitted before #145's oracle-isolation fix "
"(oracle baked into the agent's own image)"
),
)
mg_pr_diff.add_argument("path", help="dataset or task directory")
mg_pr_diff.add_argument(
"--apply", action="store_true", help="write the fix (default: dry-run report only)"
)
mg_pr_diff.set_defaults(func=cmd_migrate)

# push
p = sub.add_parser("push", help="Push a local dataset directory to HF Hub")
p.add_argument(
Expand Down
132 changes: 132 additions & 0 deletions src/repo2rlenv/pipelines/pr_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,138 @@ def _pr_diff_aux_files(*, oracle_diff: str, instruction: str) -> dict[str, str]:
}


# ---------------------------------------------------------------------------
# Migration: repair tasks emitted before #145's oracle-isolation fix
# ---------------------------------------------------------------------------

_BAKED_ORACLE_MARKER = "base64 -d > /verifier/oracle.patch"


def _harbor_content_hash(instruction: str, oracle_diff: str) -> str:
"""Mirror ``emitter/harbor.py:_content_hash`` without needing a full HarborTask.

``content_hash`` only ever covers ``instruction.md`` + ``solution/patch.diff``
(see that function) — the migration below never touches either file, so this
is purely a sanity check that we're reading the same pair the task was
originally hashed against, not a value the migration recomputes and writes.
"""
import hashlib

h = hashlib.sha256()
h.update(instruction.encode("utf-8"))
h.update(b"\0")
h.update(oracle_diff.encode("utf-8"))
return f"sha256:{h.hexdigest()}"


def migrate_pr_diff_task(task_dir: Path, *, dry_run: bool = False) -> dict[str, str]:
"""Repair a single ``pr_diff`` task emitted before #145's oracle-isolation fix.

Pre-#145, ``environment/Dockerfile`` baked the oracle patch, instruction, and
verifier into the agent's own image — readable and ``git apply``-able for a
perfect score. This detects that shape and rewrites ``environment/Dockerfile``
+ ``tests/{test.sh,verifier.py,oracle.patch,instruction.md}`` using the exact
builder functions fresh ``generate`` calls today, so a migrated task is
byte-identical to one emitted now — not a hand-maintained parallel
implementation that could drift from the real fix.

``instruction.md`` and ``solution/patch.diff`` (the oracle diff itself) are
never modified, so ``content_hash`` — which only covers those two files
(``emitter/harbor.py:_content_hash``) — is unaffected; a migrated task keeps
its original identity. ``repo_url`` and ``base_commit`` are recovered from
the existing Dockerfile's ``remote set-url origin`` line and ``task.toml``'s
``metadata.repo2env.ref`` respectively — both fields the pre-#145 builder
already wrote unchanged, so nothing about them needs to be guessed or
re-derived from an external source.

With ``dry_run=True``, every check runs identically (including the
``content_hash`` sanity check) but nothing is written — ``action`` reports
what *would* happen (``"would_migrate"`` in place of ``"migrated"``).

Returns ``{"task": <dir name>, "action": ..., "detail": ...}`` where
``action`` is one of:

- ``"migrated"`` / ``"would_migrate"`` — rewritten in place / would be
- ``"already_safe"`` — no baked oracle found; nothing to do
- ``"skipped"`` — not a pr_diff task (different pipeline, or malformed)
- ``"error"`` — needs manual review; nothing was written
"""
import tomllib

name = task_dir.name
toml_path = task_dir / "task.toml"
if not toml_path.exists():
return {"task": name, "action": "error", "detail": "no task.toml"}
try:
config = tomllib.loads(toml_path.read_text(encoding="utf-8"))
except tomllib.TOMLDecodeError as exc:
return {"task": name, "action": "error", "detail": f"cannot parse task.toml: {exc}"}
repo2env = config.get("metadata", {}).get("repo2env", {})
if repo2env.get("pipeline") != "pr_diff":
return {"task": name, "action": "skipped", "detail": "not a pr_diff task"}

dockerfile_path = task_dir / "environment" / "Dockerfile"
if not dockerfile_path.exists():
return {"task": name, "action": "skipped", "detail": "text-only task, no environment/"}
dockerfile = dockerfile_path.read_text(encoding="utf-8")
if _BAKED_ORACLE_MARKER not in dockerfile:
return {"task": name, "action": "already_safe", "detail": ""}

base_commit = repo2env.get("ref")
origin_match = re.search(r"remote set-url origin (\S+)\n", dockerfile)
if not base_commit or not origin_match:
return {
"task": name,
"action": "error",
"detail": "cannot recover repo_url/base_commit from task.toml/Dockerfile",
}
repo_url = origin_match.group(1).strip("'\"")

instruction_path = task_dir / "instruction.md"
oracle_path = task_dir / "solution" / "patch.diff"
if not instruction_path.exists() or not oracle_path.exists():
return {
"task": name,
"action": "error",
"detail": "missing instruction.md or solution/patch.diff",
}
instruction = instruction_path.read_text(encoding="utf-8")
oracle_diff = oracle_path.read_text(encoding="utf-8")

claimed_hash = repo2env.get("content_hash")
recomputed_hash = _harbor_content_hash(instruction, oracle_diff)
if claimed_hash and claimed_hash != recomputed_hash:
return {
"task": name,
"action": "error",
"detail": (
f"content_hash mismatch: task.toml claims {claimed_hash}, "
f"instruction.md + solution/patch.diff hash to {recomputed_hash} — "
"needs manual review, not auto-migrated"
),
}

if dry_run:
return {"task": name, "action": "would_migrate", "detail": ""}

dockerfile_path.write_text(
build_pr_diff_environment_dockerfile(repo_url=repo_url, base_commit=base_commit),
encoding="utf-8",
)
tests_dir = task_dir / "tests"
tests_dir.mkdir(exist_ok=True)
(tests_dir / "test.sh").write_text(
build_pr_diff_eval_script(base_commit=base_commit), encoding="utf-8"
)
(tests_dir / "test.sh").chmod(0o755)
for relative, content in _pr_diff_aux_files(
oracle_diff=oracle_diff, instruction=instruction
).items():
(task_dir / relative).write_text(content, encoding="utf-8")

return {"task": name, "action": "migrated", "detail": ""}


# ---------------------------------------------------------------------------
# Gen-time helpers: quality filter, baseline calibration, difficulty bucket
# ---------------------------------------------------------------------------
Expand Down
Loading
Loading