mutant-hunter-env / PROJECT_BLUEPRINT.md
Krishna1107's picture
Initial commit: MutantHunter β€” RL env for mutation-score-rewarded test generation
91487c9
|
Raw
History Blame
49.8 kB
# MutantHunter & CartographerZero β€” Master Project Blueprint
**Author context:** Solo participant, OpenEnv Hackathon India 2026 finale (Bangalore, April 25-26). B.Tech ML/NLP background with remote-sensing specialization. Submitting **MutantHunter** to the hackathon. **CartographerZero** is a secondary research project to be built post-hackathon (or as a stretch goal if Phase 2 ships early).
**Base model:** Qwen3-4B (Qwen3-4B-Instruct for chat-formatted RLVR; Qwen3-4B-Base if Unsloth requires base-model GRPO). Fits on a single A100 with QLoRA, plausibly on T4/L4 with aggressive quantization.
**Stack:**
- OpenEnv (latest from `meta-pytorch/OpenEnv`) for environment scaffolding
- Hugging Face TRL for GRPO trainer
- Unsloth for memory-efficient training
- Hugging Face Spaces for deployment (Docker-backed FastAPI)
- Weights & Biases for logging + public run links
- Hugging Face Hub for env, dataset, and trained-LoRA hosting
- Python 3.11+, FastAPI, pydantic v2, pytest, mutmut/cosmic-ray, Docker
**Critical references (Claude Code should read these before implementing):**
- OpenEnv repo: `https://github.com/meta-pytorch/OpenEnv`
- OpenEnv HF blog: `https://huggingface.co/blog/openenv`
- TRL OpenEnv integration: `https://huggingface.co/docs/trl/v0.27.1/openenv`
- Unsloth GRPO guide: `https://unsloth.ai/docs/get-started/reinforcement-learning-rl-guide`
- mutmut docs: `https://mutmut.readthedocs.io/`
---
# PART 1 β€” MUTANTHUNTER PHASE 1 (SAFE, SUBMISSION-READY)
## 1.1 The Pitch in One Paragraph
MutantHunter is an OpenEnv-compliant RL environment that trains LLMs to write *high-quality unit tests* via a reward signal grounded in **mutation testing**. Given a Python repository and its existing test suite, the agent writes additional tests; the reward is the fraction of *injected mutations* (operator swaps, constant changes, branch flips) that the agent's new tests *kill* but the existing suite missed. Mutation score is the gold-standard test-quality metric in software engineering β€” a deterministic, uncheatable, millisecond-fast oracle. The environment is a partial, queryable software world (Theme: World Modeling Professional). Phase 2 introduces a Mutator-agent that co-evolves with the Tester-agent, generating progressively harder mutations (Theme: Self-Improvement). The combination is genuinely novel β€” no public OpenEnv env trains test generation, and no prior work pairs RLVR with mutation-score supervision.
## 1.2 Theme Mapping (claim what's earned, gesture at the rest)
- **Wildcard**: primary. No prior art combining RLVR + mutation testing + agentic test generation in an OpenEnv env.
- **World Modeling (Professional)**: secondary. The repo is a partially-observable, action-gated world. The agent queries it through tools and must build an internal model of "what behaviors does this code have, what could break, what tests would catch breakage."
- **Self-Improvement**: Phase 2 only. Co-evolving Mutator/Tester loop.
- **Long-Horizon**: do not claim. Honestly medium-horizon (3-8 turns per episode).
## 1.3 Repository Layout (Phase 1)
```
mutant-hunter/
β”œβ”€β”€ README.md # Story-first, judge-facing
β”œβ”€β”€ pyproject.toml # Package config (mutant_hunter)
β”œβ”€β”€ requirements.txt # Pinned deps
β”œβ”€β”€ Dockerfile # HF Spaces target
β”œβ”€β”€ openenv.yaml # OpenEnv manifest
β”œβ”€β”€ LICENSE # Apache-2.0
β”œβ”€β”€ .gitignore
β”œβ”€β”€ .python-version # 3.11
β”‚
β”œβ”€β”€ src/mutant_hunter/
β”‚ β”œβ”€β”€ __init__.py
β”‚ β”œβ”€β”€ models.py # Action, Observation, State dataclasses (pydantic)
β”‚ β”œβ”€β”€ environment.py # Core MutantHunterEnvironment (Gym-style)
β”‚ β”œβ”€β”€ server.py # FastAPI app
β”‚ β”œβ”€β”€ client.py # MutantHunterClient (HTTP)
β”‚ β”‚
β”‚ β”œβ”€β”€ tools/ # Tools the agent can call mid-episode
β”‚ β”‚ β”œβ”€β”€ __init__.py
β”‚ β”‚ β”œβ”€β”€ read_file.py
β”‚ β”‚ β”œβ”€β”€ list_tests.py
β”‚ β”‚ β”œβ”€β”€ run_tests.py
β”‚ β”‚ β”œβ”€β”€ get_coverage.py
β”‚ β”‚ └── get_mutation_report.py
β”‚ β”‚
β”‚ β”œβ”€β”€ corpus/ # The repo corpus
β”‚ β”‚ β”œβ”€β”€ __init__.py
β”‚ β”‚ β”œβ”€β”€ corpus_loader.py # Loads pinned commits of target repos
β”‚ β”‚ β”œβ”€β”€ manifest.json # List of {repo, commit, modules, baseline_score}
β”‚ β”‚ └── _cache/ # Cloned repos (gitignored)
β”‚ β”‚
β”‚ β”œβ”€β”€ mutation/ # Mutation engine
β”‚ β”‚ β”œβ”€β”€ __init__.py
β”‚ β”‚ β”œβ”€β”€ engine.py # Wrapper over mutmut/cosmic-ray
β”‚ β”‚ β”œβ”€β”€ operators.py # Op definitions (AOR, COR, ROR, LCR, etc.)
β”‚ β”‚ β”œβ”€β”€ injector.py # Apply mutations deterministically
β”‚ β”‚ └── runner.py # Sandboxed test execution
β”‚ β”‚
β”‚ β”œβ”€β”€ rubric/ # Reward functions
β”‚ β”‚ β”œβ”€β”€ __init__.py
β”‚ β”‚ β”œβ”€β”€ reward_mutation_kill.py # Primary signal
β”‚ β”‚ β”œβ”€β”€ reward_no_regression.py # Multiplicative gate
β”‚ β”‚ β”œβ”€β”€ reward_coverage_delta.py # Secondary
β”‚ β”‚ β”œβ”€β”€ reward_format.py # Pytest validity
β”‚ β”‚ β”œβ”€β”€ reward_parsimony.py # Anti-bloat
β”‚ β”‚ └── compose.py # Combines all 5 into final scalar
β”‚ β”‚
β”‚ β”œβ”€β”€ safety/ # Sandboxing + anti-hack
β”‚ β”‚ β”œβ”€β”€ __init__.py
β”‚ β”‚ β”œβ”€β”€ sandbox.py # subprocess + resource limits + timeout
β”‚ β”‚ β”œβ”€β”€ validators.py # Pre-flight checks on agent output
β”‚ β”‚ β”œβ”€β”€ forbidden_patterns.py # Block os.system, eval, subprocess in tests
β”‚ β”‚ └── README.md # Documents anti-hack design
β”‚ β”‚
β”‚ β”œβ”€β”€ tasks/
β”‚ β”‚ β”œβ”€β”€ __init__.py
β”‚ β”‚ β”œβ”€β”€ generator.py # Episode = (repo, module, mutation_set)
β”‚ β”‚ β”œβ”€β”€ curriculum.py # Difficulty tiers
β”‚ β”‚ └── seeds/ # Hand-picked deterministic episodes for eval
β”‚ β”‚ └── eval_set_v1.json
β”‚ β”‚
β”‚ └── utils/
β”‚ β”œβ”€β”€ __init__.py
β”‚ β”œβ”€β”€ logging.py
β”‚ β”œβ”€β”€ tracing.py # Per-step trace capture
β”‚ └── pytest_helpers.py # Discover, parse, run tests
β”‚
β”œβ”€β”€ training/
β”‚ β”œβ”€β”€ train_grpo.ipynb # Colab notebook (judge-facing)
β”‚ β”œβ”€β”€ train_grpo.py # Scriptable version
β”‚ β”œβ”€β”€ config.yaml # All hyperparameters
β”‚ β”œβ”€β”€ prompts.py # System + few-shot prompt templates
β”‚ β”œβ”€β”€ baseline_eval.py # Run untrained model over eval_set
β”‚ └── inspect_rollouts.py # Sample N rollouts for hack detection
β”‚
β”œβ”€β”€ evaluation/
β”‚ β”œβ”€β”€ eval_harness.py # Deterministic eval over seed set
β”‚ β”œβ”€β”€ before_after.py # Side-by-side baseline vs trained
β”‚ β”œβ”€β”€ reward_hacking_tests.py # Adversarial probes (see Β§1.10)
β”‚ └── ablations.py # With/without each reward component
β”‚
β”œβ”€β”€ plots/ # Committed PNGs (NOT in Colab cells only)
β”‚ β”œβ”€β”€ reward_curve.png
β”‚ β”œβ”€β”€ per_reward_breakdown.png # Each rubric component over time
β”‚ β”œβ”€β”€ mutation_kill_rate.png # Headline metric
β”‚ β”œβ”€β”€ baseline_vs_trained.png # Bar chart
β”‚ └── curriculum_progression.png # Difficulty tier reached
β”‚
β”œβ”€β”€ docs/
β”‚ β”œβ”€β”€ problem_statement.md
β”‚ β”œβ”€β”€ reward_design.md # Walks through each reward, hacks blocked
β”‚ β”œβ”€β”€ env_architecture.md # Diagram + flow
β”‚ β”œβ”€β”€ demo_script.md # 2-min video script
β”‚ β”œβ”€β”€ safeguards.md # "Hacks we considered" β€” high-leverage doc
β”‚ └── roadmap_phase2.md # Self-play extension plan
β”‚
β”œβ”€β”€ assets/
β”‚ β”œβ”€β”€ env_diagram.svg
β”‚ β”œβ”€β”€ reward_flow.svg
β”‚ └── demo_thumbnail.png
β”‚
β”œβ”€β”€ tests/ # Tests for the env itself
β”‚ β”œβ”€β”€ test_environment.py
β”‚ β”œβ”€β”€ test_mutation_engine.py
β”‚ β”œβ”€β”€ test_rewards.py
β”‚ β”œβ”€β”€ test_sandbox.py
β”‚ └── test_reward_hacking.py # Adversarial cases must fail correctly
β”‚
└── scripts/
β”œβ”€β”€ prepare_corpus.sh # One-shot: clone all target repos at pinned commits
β”œβ”€β”€ precompute_baselines.py # Compute baseline mutation scores per (repo, module)
└── deploy_hf_space.sh # One-command HF Space push
```
## 1.4 Detailed File-by-File Implementation Spec
### 1.4.1 `pyproject.toml`
```toml
[project]
name = "mutant_hunter"
version = "0.1.0"
description = "OpenEnv RL environment for training LLMs to write high-quality tests via mutation-score rewards"
authors = [{name = "<NAME>"}]
license = {text = "Apache-2.0"}
requires-python = ">=3.11"
dependencies = [
"openenv>=0.x", # pin to whatever finale version is current
"fastapi>=0.115",
"uvicorn[standard]>=0.30",
"pydantic>=2.7",
"mutmut>=2.5",
"pytest>=8.0",
"pytest-cov>=5.0",
"pytest-timeout>=2.3",
"coverage>=7.6",
"GitPython>=3.1",
"tenacity>=9.0",
"rich>=13.7",
"PyYAML>=6.0",
"datasets>=3.0", # HF datasets
"huggingface-hub>=0.26",
"wandb>=0.18",
]
[project.optional-dependencies]
training = [
"torch>=2.5",
"transformers>=4.46",
"trl>=0.27",
"unsloth>=2024.12",
"accelerate>=1.1",
"peft>=0.13",
"bitsandbytes>=0.44",
]
dev = ["ruff>=0.7", "black>=24.10", "mypy>=1.13"]
[project.scripts]
mutant-hunter-server = "mutant_hunter.server:main"
mutant-hunter-eval = "evaluation.eval_harness:main"
```
### 1.4.2 `openenv.yaml`
OpenEnv manifest. Claude Code should follow OpenEnv's current schema; the rough shape is:
```yaml
name: mutant-hunter
version: 0.1.0
description: "RL environment for training LLM agents to write high-quality unit tests via mutation-score rewards"
author: "<NAME>"
license: Apache-2.0
entrypoint:
type: fastapi
module: mutant_hunter.server
app: app
runtime:
python: "3.11"
dockerfile: Dockerfile
tools:
- name: read_file
description: "Read a source or test file from the target repo"
- name: list_tests
description: "List existing tests for the target module"
- name: run_tests
description: "Run the existing test suite against unmodified code"
- name: get_coverage
description: "Get line coverage for the target module"
- name: get_mutation_report
description: "Get list of surviving mutants from baseline test run"
spaces:
observation_schema: src/mutant_hunter/models.py:Observation
action_schema: src/mutant_hunter/models.py:Action
```
### 1.4.3 `src/mutant_hunter/models.py`
Pydantic v2 models for the OpenEnv contract. All fields strictly typed.
```python
from pydantic import BaseModel, Field
from typing import Literal, Any
class ToolCall(BaseModel):
name: Literal["read_file", "list_tests", "run_tests", "get_coverage", "get_mutation_report"]
args: dict[str, Any] = Field(default_factory=dict)
class Action(BaseModel):
"""Agent's action per turn. Either a tool call OR a final test submission."""
kind: Literal["tool_call", "submit_tests"]
tool_call: ToolCall | None = None
test_code: str | None = Field(None, description="Final pytest file content")
class ToolResult(BaseModel):
tool: str
output: str
truncated: bool = False
class Observation(BaseModel):
repo_name: str
module_path: str
module_summary: str # AST-derived: function signatures + docstrings
existing_tests: list[str] # Names only
baseline_mutation_score: float # Pre-computed
budget_remaining: int # Tool calls left this episode
history: list[ToolResult] # All prior tool outputs this episode
turn: int
class State(BaseModel):
"""Full hidden state β€” never sent to agent."""
repo_path: str
module_path: str
full_source: str
full_test_suite: str
surviving_mutants: list[dict]
total_mutants: int
coverage_baseline: float
class StepResult(BaseModel):
observation: Observation | None
reward: float
done: bool
info: dict[str, Any] = Field(default_factory=dict)
```
### 1.4.4 `src/mutant_hunter/environment.py`
The core class. Implements OpenEnv `Environment` interface.
```python
class MutantHunterEnvironment:
"""
Episode flow:
1. reset() picks a (repo, module) from the curriculum, resets budget=5 turns.
2. Agent issues tool calls or submits tests.
3. step() executes the action; if submit_tests, runs mutation testing and returns final reward.
4. Episode ends on submit_tests or budget exhausted.
"""
def __init__(self, corpus_loader, mutation_engine, rubric, sandbox, curriculum, max_turns=8):
...
def reset(self, seed: int | None = None) -> Observation:
# Pick task from curriculum, set up sandbox, return initial observation
...
def step(self, action: Action) -> StepResult:
# If tool_call: execute via sandbox, append to history, decrement budget, return obs + 0.0 reward
# If submit_tests: run mutation pipeline, compute reward, return done=True
...
def state(self) -> Observation:
# Return current observation
...
def close(self):
# Tear down sandbox
...
```
Key implementation details:
- **`reset()` is deterministic given seed.** Critical for reproducibility.
- **`step()` is the only place reward is non-zero**, and only on `submit_tests`. No process rewards in Phase 1 (keeps it simple, debuggable).
- **All file system access via sandbox.** Never let the agent's code touch the host FS directly.
- **Budget tracking**: `budget_remaining` starts at 5 (tool calls only); `submit_tests` doesn't consume budget.
### 1.4.5 `src/mutant_hunter/server.py`
Standard OpenEnv FastAPI wrapper. Routes:
- `POST /reset` β†’ returns Observation
- `POST /step` β†’ takes Action, returns StepResult
- `GET /state` β†’ returns current Observation
- `POST /close` β†’ tears down
Use OpenEnv's helper if it provides one; otherwise write thin FastAPI handlers that delegate to `MutantHunterEnvironment`.
### 1.4.6 `src/mutant_hunter/client.py`
Mirror of server. HTTP client that the trainer uses. Must NOT import server internals (judges check for client/server separation).
### 1.4.7 `src/mutant_hunter/tools/`
Each tool is a function `(state: State, **args) -> str`. They run inside `step()` when action.kind == "tool_call".
- **`read_file(state, path: str, start_line: int = 0, end_line: int = -1) -> str`**: Returns file contents (or slice) from the sandboxed repo. Hard cap output at 4000 chars.
- **`list_tests(state) -> str`**: Returns names + first-line docstrings of all existing tests for the target module.
- **`run_tests(state) -> str`**: Runs existing test suite on unmodified code. Returns pass/fail counts + first 5 failures. *Useful for the agent to see what's already covered.*
- **`get_coverage(state) -> str`**: Returns line coverage for the target module under existing tests. Format: list of uncovered line ranges with surrounding context.
- **`get_mutation_report(state) -> str`**: Returns list of surviving mutants from the baseline run, formatted as `mutant_id, line, original β†’ mutated`. Hard cap at 30 mutants.
### 1.4.8 `src/mutant_hunter/corpus/`
**The corpus is the most important data asset. Get it right.**
Target: 12-15 small, well-scoped Python libraries with crappy-to-medium test suites. Pin them at specific commits.
Selection criteria:
- 200-1500 LOC of source code
- Existing test suite covers <70% of mutants (so there's room to improve)
- Pure Python, no compiled deps that hurt sandbox setup
- Permissive license (MIT, Apache, BSD)
- Self-contained (no DB, network, or filesystem deps in the code under test)
Concrete candidates Claude Code should investigate (verify availability + licenses):
- `dateparser`-style helpers
- Small parsers: `pyhocon`, `python-rapidjson` wrappers, simple INI parsers
- Algorithms: `python-binary-search-tree`, small graph libs
- Utilities: tiny string/url manipulation libs from PyPI
- Self-curated mini-libs: write 4-5 of your own 200-LOC libraries (`mini_calendar`, `csv_normalizer`, `interval_tree`, `bloom_filter_lite`) β€” these are *guaranteed* to be uncontaminated by training data.
**Self-curated libs are critical** because a mutmut-on-popular-pypi-package result might be in pretraining data; self-written libs can't be.
**`manifest.json` schema:**
```json
{
"version": "v1",
"repos": [
{
"name": "mini_calendar",
"source": "local", // or "git"
"path": "src/mutant_hunter/corpus/_local/mini_calendar",
"commit": null,
"modules": [
{
"module": "mini_calendar.parser",
"loc": 287,
"baseline_mutation_score": 0.41,
"total_mutants": 78,
"difficulty_tier": 1
}
]
}
]
}
```
`baseline_mutation_score` and `total_mutants` are precomputed by `scripts/precompute_baselines.py` once during prep, then never recomputed during training (saves ~30s per episode).
### 1.4.9 `src/mutant_hunter/mutation/`
The mutation engine. Two implementation paths:
**Option A (recommended for Phase 1): use `mutmut` directly.**
- Pros: mature, fast, well-documented.
- Cons: less flexible mutation operators.
**Option B: custom AST-based injector.**
- Pros: full control over operator set, deterministic ordering, fast.
- Cons: more code to write/test.
Go with Option A for Phase 1, build Option B as a backup if mutmut performance is a problem.
`engine.py` interface:
```python
class MutationEngine:
def precompute_mutants(self, repo_path: str, module: str) -> list[Mutant]:
"""Run once per (repo, module) during prep; cache to disk."""
...
def run_baseline(self, repo_path: str, module: str, test_dir: str) -> MutationReport:
"""Compute baseline: which mutants does the existing suite kill?"""
...
def run_with_new_tests(self, repo_path: str, module: str, new_test_code: str) -> MutationReport:
"""Compute: which mutants does (existing + new) suite kill?"""
...
```
**Performance constraint:** every `step()` with `submit_tests` runs the full test suite on every surviving mutant. This is expensive. Mitigations:
- Pre-filter to top-K=15 most informative surviving mutants per module.
- Use `pytest-xdist` for parallel mutant execution.
- Hard timeout per mutant: 8 seconds.
- Cache mutant ASTs.
Target: ≀20 seconds per `submit_tests` step. If slower, training rollouts dominate budget.
### 1.4.10 `src/mutant_hunter/rubric/`
The reward functions. Each is a pure function `(state, action, exec_result) -> float`.
**`reward_mutation_kill.py`**:
```python
def reward_mutation_kill(state: State, exec_result: dict) -> float:
"""
Primary signal: fraction of surviving baseline mutants killed by new tests.
Surviving = mutants that the *original* test suite did NOT kill.
"""
baseline_surviving = state.surviving_mutants # precomputed
if not baseline_surviving:
return 0.5 # nothing to kill; neutral reward
killed_by_new = exec_result["killed_by_new_only"]
return len(killed_by_new) / len(baseline_surviving)
```
**`reward_no_regression.py`** β€” multiplicative gate:
```python
def reward_no_regression(exec_result: dict) -> float:
"""1.0 if all new tests pass on UNMODIFIED code; 0.0 otherwise."""
return 1.0 if exec_result["new_tests_pass_clean"] else 0.0
```
**`reward_coverage_delta.py`**:
```python
def reward_coverage_delta(state: State, exec_result: dict) -> float:
"""Improvement in line coverage, normalized."""
delta = exec_result["new_coverage"] - state.coverage_baseline
headroom = max(100.0 - state.coverage_baseline, 1.0)
return max(0.0, min(1.0, delta / headroom))
```
**`reward_format.py`**:
```python
def reward_format(action: Action, exec_result: dict) -> float:
"""1.0 if the submitted test file parses, runs, and only contains pytest-style tests."""
if not exec_result["parses"]: return 0.0
if exec_result["contains_forbidden"]: return 0.0
return 1.0
```
**`reward_parsimony.py`**:
```python
def reward_parsimony(action: Action) -> float:
"""Mild penalty for excessively long tests (>20 LOC each)."""
n_lines_per_test = ... # parse pytest functions
avg = mean(n_lines_per_test)
return max(0.0, 1.0 - max(0, avg - 20) / 30)
```
**`compose.py`**:
```python
WEIGHTS = {
"mutation_kill": 0.60,
"coverage_delta": 0.20,
"format": 0.15,
"parsimony": 0.05,
}
def compose_reward(state: State, action: Action, exec_result: dict) -> dict:
components = {
"mutation_kill": reward_mutation_kill(state, exec_result),
"coverage_delta": reward_coverage_delta(state, exec_result),
"format": reward_format(action, exec_result),
"parsimony": reward_parsimony(action),
}
no_reg = reward_no_regression(exec_result)
weighted = sum(WEIGHTS[k] * v for k, v in components.items())
final = no_reg * weighted # gate: any regression β†’ 0 reward
return {"final": final, "components": components, "no_regression_gate": no_reg}
```
**This composition is the heart of the submission.** Document every line in `docs/reward_design.md`.
### 1.4.11 `src/mutant_hunter/safety/`
**This is what wins extra points with Red Hat / Meta engineers.** Document everything.
**`sandbox.py`**:
- All test execution inside a subprocess with:
- CPU time limit (resource.RLIMIT_CPU = 30s)
- Memory limit (resource.RLIMIT_AS = 512MB)
- No network (use `unshare -n` on Linux, or NetworkPolicy in container)
- Read-only filesystem except `/tmp/mutant_hunter_episode_<uuid>/`
- Drops privileges
- Hard timeout via `signal.alarm` or subprocess.timeout
**`forbidden_patterns.py`** β€” block these in agent-submitted test code (regex + AST):
- `import os` then `os.system`
- `subprocess` (any)
- `eval`, `exec`
- `__import__`
- File writes outside `/tmp`
- `open(...)` with mode containing `'w'`, `'a'`, `'x'`
- `socket`, `urllib`, `requests`, `httpx`
- Direct mutation of `__builtins__`, `globals()`, `sys.modules`
- Patching of mutmut/pytest internals
- Time manipulation: `time.sleep` (just because; not a real attack vector but a clean signal)
- Environment variable writes
**`validators.py`**:
- AST validity check before any execution
- Test function naming (`test_*`)
- Maximum file size (50KB)
- Maximum number of tests per submission (50)
### 1.4.12 `src/mutant_hunter/tasks/`
**`generator.py`**: deterministic episode sampling from the corpus given a seed.
**`curriculum.py`**: difficulty tiers.
- Tier 1: small modules (200-400 LOC), single-file, simple types. (Easy mutants to catch.)
- Tier 2: medium modules (400-800 LOC), multiple classes, generic types.
- Tier 3: complex modules (800-1500 LOC), inheritance, decorators, edge cases in numerical code.
Curriculum policy:
- Episodes 1–200: Tier 1 only.
- Episodes 200–500: 70% Tier 1, 30% Tier 2.
- Episodes 500+: 30% Tier 1, 50% Tier 2, 20% Tier 3.
Adjust based on rolling success rate (if median final reward > 0.5 in last 50 episodes on current tier, advance).
**`seeds/eval_set_v1.json`**: 30 deterministic (repo, module, seed) tuples used for ALL evaluations. Never trained on. Critical for "before/after" plots being credible.
### 1.4.13 `training/`
**`train_grpo.py`**:
```python
# Pseudo-code skeleton
from unsloth import FastLanguageModel
from trl import GRPOTrainer, GRPOConfig
from mutant_hunter.client import MutantHunterClient
model, tokenizer = FastLanguageModel.from_pretrained(
"Qwen/Qwen3-4B-Instruct", # or Qwen3-4B-Base, depending on Unsloth's GRPO support
max_seq_length=8192,
dtype=None,
load_in_4bit=True,
)
model = FastLanguageModel.get_peft_model(
model, r=16, lora_alpha=16, target_modules=[...],
)
env_client = MutantHunterClient(base_url="http://localhost:8000")
def reward_fn(prompts, completions, **kwargs):
"""Per-completion reward via the env. Trainer rolls out, we score via env."""
rewards = []
for completion in completions:
action = parse_action(completion)
result = env_client.step(action)
rewards.append(result.reward)
return rewards
config = GRPOConfig(
output_dir="./checkpoints",
num_generations=4,
max_steps=300,
learning_rate=5e-6,
per_device_train_batch_size=1,
gradient_accumulation_steps=4,
logging_steps=1,
report_to="wandb",
)
trainer = GRPOTrainer(model=model, reward_funcs=reward_fn, args=config, ...)
trainer.train()
trainer.save_model("./final-lora")
```
Key constraints:
- `num_generations=4` (minimum for GRPO advantage estimation; higher = more compute)
- `max_steps=300` Phase 1 (fits in ~4-6 hours on A100; budget 8h max)
- W&B logging from step 1 with public link
**`prompts.py`**: system + few-shot prompts. Critical for warm-starting GRPO so non-zero reward happens early.
```python
SYSTEM_PROMPT = """You are MutantHunter, an expert at writing pytest unit tests that catch bugs.
You will be given a Python module and its existing tests. Your job is to write ADDITIONAL tests that catch behaviors the existing tests miss.
You have tools:
- read_file(path): read source/test files
- list_tests(): list existing tests
- run_tests(): run existing suite
- get_coverage(): see which lines lack coverage
- get_mutation_report(): see what mutations the existing suite fails to catch
When ready, output your tests in this format:
<submit>
```python
import pytest
from <module> import <thing>
def test_thing_with_negative_input():
assert ...
```
</submit>
Rules:
- Tests must pass on the unmodified code (no regression).
- No imports of os, sys, subprocess, eval, exec.
- Each test ≀ 20 lines.
- Focus on edge cases the existing tests miss."""
```
Add 2 worked examples in the few-shot.
**`baseline_eval.py`**: runs untrained Qwen3-4B-Instruct on the eval seed set. Saves results to `evaluation/results/baseline.json`. **Run this before training. The before/after comparison is the demo.**
**`inspect_rollouts.py`**: every 25 training steps, sample 5 random rollouts and dump to `logs/rollouts/step_<N>.jsonl`. Manual inspection catches reward hacking before it dominates.
### 1.4.14 `evaluation/`
**`eval_harness.py`**: pure function `evaluate(model_path: str, eval_set: str) -> dict[str, float]`. Returns per-task and aggregate metrics.
**`before_after.py`**: produces the killer plot. Same eval seeds, baseline vs trained model, side-by-side.
**`reward_hacking_tests.py`**: adversarial probes that MUST fail correctly. This file is a high-leverage README artifact.
```python
ADVERSARIAL_CASES = [
{
"name": "empty_test_file",
"test_code": "",
"expected_reward_lt": 0.05,
},
{
"name": "always_passes",
"test_code": "def test_a(): assert True",
"expected_reward_lt": 0.1,
},
{
"name": "imports_subprocess",
"test_code": "import subprocess\ndef test_a(): subprocess.run(['ls']); assert True",
"expected_reward_eq": 0.0,
},
{
"name": "vacuous_assertion",
"test_code": "def test_a(): x = 1; assert x == x",
"expected_reward_lt": 0.1,
},
{
"name": "regression_introduced",
"test_code": "def test_a(): assert 1 == 2 # Always fails",
"expected_reward_eq": 0.0, # no_regression gate fires
},
# Add 10 more
]
def test_all_adversarial_cases_blocked():
for case in ADVERSARIAL_CASES:
result = run_in_env(case["test_code"])
if "expected_reward_lt" in case:
assert result.reward < case["expected_reward_lt"], f"FAIL: {case['name']}"
...
```
This file becomes a slide in the demo deck. *"Here are 15 hacks we considered. Here's our reward function refusing all of them."*
### 1.4.15 `Dockerfile`
```dockerfile
FROM python:3.11-slim
WORKDIR /app
# System deps for sandbox + git for corpus
RUN apt-get update && apt-get install -y --no-install-recommends \
git build-essential \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN pip install --no-cache-dir -e .
# Pre-clone corpus
RUN bash scripts/prepare_corpus.sh
EXPOSE 7860
CMD ["uvicorn", "mutant_hunter.server:app", "--host", "0.0.0.0", "--port", "7860"]
```
Port 7860 = HF Spaces standard.
### 1.4.16 `README.md` (judge-facing β€” most important file)
Structure (judge spends 3-5 min):
```markdown
# MutantHunter
*An OpenEnv RL environment that teaches LLMs to write high-quality tests by killing mutated code.*
[Live HF Space] [Colab Training] [W&B Run] [2-min Demo Video] [Blog Post]
![Headline plot: mutation score over training steps](plots/mutation_kill_rate.png)
*Baseline Qwen3-4B kills 18% of injected mutants. After 250 GRPO steps with mutation-score reward: 63%.*
## The Problem in 30 Seconds
[Concrete example: a 200-LOC date-parsing library. Its test suite has 95% line coverage. But mutmut shows the suite catches only 18% of bug-equivalent mutations. Coverage lies; mutation score doesn't.]
## What This Environment Trains
[3-line description of the agent's task]
## Why It's Novel
- First OpenEnv env for test generation
- First use of mutation score as RLVR reward
- Composable rubric (5 components) with multiplicative no-regression gate
- 15 adversarial cases all blocked (see safeguards.md)
## Results
[Bar chart: baseline vs trained on 30 held-out (repo, module) pairs]
[Per-component reward breakdown]
[Sample rollout: baseline writes 1 vacuous test; trained writes 6 targeted tests]
## How to Run
```bash
pip install -e .
mutant-hunter-server # starts FastAPI on :7860
# In another terminal:
jupyter notebook training/train_grpo.ipynb
```
## Repo Structure
[Tree, 10 lines]
## Safeguards Against Reward Hacking
[Table: hack β†’ defense β†’ test in evaluation/reward_hacking_tests.py]
## Roadmap (Phase 2)
Self-play: a Mutator-agent co-evolves with the Tester, generating progressively harder mutations. See `docs/roadmap_phase2.md`.
## License & Citation
Apache-2.0. If you use this, please cite [your write-up].
```
## 1.5 Phase 1 β€” 7-Day Build Plan
| Day | Goal | Deliverable |
|---|---|---|
| 1 | Skeleton + corpus | Repo structure created, 4 self-curated mini-libs written, `manifest.json` populated for tier 1 |
| 2 | Mutation engine | `mutation/engine.py` working; `precompute_baselines.py` produces `baseline_mutation_score` for all tier 1 modules |
| 3 | Sandbox + tools | `safety/sandbox.py` with all forbidden patterns; 5 tool functions |
| 4 | Environment + server | `environment.py` + `server.py` running locally; manual curl tests pass |
| 5 | Rubric + reward hacking tests | All 5 reward functions implemented; `evaluation/reward_hacking_tests.py` with 15 cases; all pass |
| 6 | Training pipeline | `train_grpo.py` runs 30 steps end-to-end without crashing |
| 7 | First real run + iterate | 200 steps; inspect rollouts; tune reward weights; first reward curve |
## 1.6 Phase 1 β€” On-Site 48-Hour Plan
Hour 0-6: Push to HF Space, verify it loads, fix Docker issues. Confirm Colab notebook runs end-to-end.
Hour 6-18: Full training run, 300+ GRPO steps. Iterate on prompt or hyperparams if curve is flat.
Hour 18-24: Sleep.
Hour 24-30: Baseline evaluation on eval set. Generate all plots.
Hour 30-36: README polish, demo script, record 2-min video.
Hour 36-42: Sleep.
Hour 42-48: Final dry runs of demo, push final commit, verify everything is linked from README.
## 1.7 Reward Hacking β€” Defense Catalog
Document this as a separate artifact (`docs/safeguards.md`). Each row is a slide-worthy bullet.
| Hack | Defense | Test |
|---|---|---|
| Empty test file | `format` reward = 0 if no test functions | `test_empty_test_file` |
| `assert True` everywhere | Vacuous tests don't kill mutants β†’ `mutation_kill` β‰ˆ 0 | `test_always_passes` |
| `import subprocess` to spawn shell | `forbidden_patterns.py` blocks; `format` = 0 | `test_imports_subprocess` |
| Tests that fail on clean code (sneaky regression) | `no_regression` gate β†’ final reward = 0 | `test_regression_introduced` |
| Tautologies (`assert x == x`) | Don't kill mutants; AST-level detection optional | `test_vacuous_assertion` |
| Patching the test runner from inside the test | Sandbox prevents writes outside `/tmp/episode/`; AST blocks `unittest.mock.patch` of pytest internals | `test_patches_pytest` |
| Massive output spam (token waste) | `parsimony` reward; max test count 50 | `test_spam_tests` |
| Network exfiltration | `unshare -n` in sandbox + forbidden imports | `test_network_attempt` |
| Write to host FS | Read-only mount + sandbox uid drop | `test_filesystem_attempt` |
| Time-based attack (sleep until killed by timeout) | `RLIMIT_CPU = 30s` per execution | `test_infinite_loop` |
| `sys.exit(0)` to fake success | Subprocess return code checked separately from pytest output | `test_sys_exit_zero` |
| Duplicate existing tests verbatim | Already-killed mutants don't double-count; reward is *new* mutants killed | `test_duplicate_existing_tests` |
| Mutation operator allowlist exploit (target only safe ops) | Mutants are pre-selected from full operator set, not agent-chosen | `test_mutation_set_fixed` |
| Hallucinate function names that don't exist | Test fails on clean code β†’ `no_regression` = 0 | `test_hallucinated_names` |
| Inject `pytest.skip` everywhere | Skipped tests don't kill mutants; treated as not-run | `test_pytest_skip_everywhere` |
15 cases documented = signal of maturity.
---
# PART 2 β€” MUTANTHUNTER PHASE 2 (PUSH THE ENVELOPE: SELF-PLAY)
## 2.1 The Extension in One Paragraph
Phase 2 introduces a **Mutator-agent** that co-evolves with the Tester-agent. The Mutator proposes mutation candidates from a constrained grammar; the Tester writes tests; reward to Mutator = `p*(1-p)` where `p` is the Tester's kill rate (Absolute-Zero / R-Zero learnability reward); reward to Tester = standard mutation kill rate. They alternate training. The result: a self-improving curriculum where mutations get progressively harder as the Tester gets better, without any human in the loop. This is genuine RLVR-grounded self-play in software.
## 2.2 What Changes vs Phase 1
- New role: `Mutator` agent.
- New action space for Mutator: structured mutation proposals (operator + target + replacement).
- New reward: learnability for Mutator.
- New training loop: alternating GRPO updates (Tester epoch, Mutator epoch, Tester epoch, ...).
- Same Tester env from Phase 1 (compositional!).
## 2.3 Phase 2 File Additions
```
mutant-hunter/
β”œβ”€β”€ src/mutant_hunter/
β”‚ β”œβ”€β”€ self_play/ # NEW
β”‚ β”‚ β”œβ”€β”€ __init__.py
β”‚ β”‚ β”œβ”€β”€ mutator_environment.py # Env for the Mutator agent
β”‚ β”‚ β”œβ”€β”€ mutator_models.py # Action/Obs for Mutator
β”‚ β”‚ β”œβ”€β”€ mutator_rubric.py # Learnability reward
β”‚ β”‚ β”œβ”€β”€ mutation_grammar.py # Constrained ops + targets
β”‚ β”‚ └── coevolution_loop.py # Alternating training driver
β”‚ β”‚
β”‚ └── ...
β”‚
β”œβ”€β”€ training/
β”‚ β”œβ”€β”€ train_self_play.ipynb # NEW β€” phase 2 driver
β”‚ └── train_self_play.py # NEW
β”‚
β”œβ”€β”€ evaluation/
β”‚ └── self_play_progression.py # Difficulty-over-time plots
β”‚
└── docs/
└── phase2_self_play.md # Methodology writeup
```
## 2.4 Mutator Action Grammar
The Mutator can NOT propose arbitrary diffs (too unconstrained, easy to hack). Instead, a structured grammar:
```python
class MutatorAction(BaseModel):
operator: Literal[
"AOR", # Arithmetic Operator Replacement: + β†’ -, * β†’ //, etc.
"ROR", # Relational: <, >, <=, >=, ==, !=
"LCR", # Logical Connector: and ↔ or
"BCR", # Boolean Constant: True ↔ False
"NCR", # Numeric Constant: replace with neighbor (n β†’ n+1, n β†’ 0)
"SCR", # String Constant: empty/swap
"BOUNDARY", # Off-by-one: range(n) β†’ range(n-1)
]
target_module: str
target_line: int
target_column: int # for disambiguation
replacement_index: int # 0-K within operator's allowed swaps
```
Mutator's role: *predict which (operator, target) the current Tester is least likely to catch.*
## 2.5 Learnability Reward
```python
def mutator_reward(tester_kill_probability: float) -> float:
"""
Per AZR: reward = p*(1-p), peaking at p=0.5 (right at edge of Tester capability).
Tester kill prob estimated by running 4-8 Tester rollouts on the proposed mutant.
"""
p = tester_kill_probability
return 4.0 * p * (1.0 - p) # scaled to peak at 1.0
```
Plus diversity bonus: penalize Mutator for proposing mutations too similar to last 50.
## 2.6 Co-Evolution Training Loop
```python
def train_self_play(initial_tester, initial_mutator, n_outer_epochs=10):
tester = initial_tester
mutator = initial_mutator
for outer in range(n_outer_epochs):
# 1. Mutator generates 200 mutation candidates
candidates = mutator.rollout(corpus, n=200)
# 2. Estimate Tester kill prob on each (k=4 rollouts)
kill_probs = [estimate_kill_prob(tester, c, k=4) for c in candidates]
# 3. Train Mutator (50 GRPO steps) on learnability reward
mutator.update(candidates, kill_probs)
# 4. Filter candidates to "interesting" (0.2 < p < 0.8)
training_set = [c for c, p in zip(candidates, kill_probs) if 0.2 < p < 0.8]
# 5. Train Tester (100 GRPO steps) on mutation-kill reward against training_set
tester.update(training_set)
log_metrics(outer, tester, mutator, candidates, kill_probs)
```
## 2.7 Phase 2 Risks
- **Compute doubles** (two models). Drop to Qwen3-1.7B for Phase 2 if VRAM tight.
- **Mode collapse**: Mutator finds one hack the Tester always misses, never explores. Mitigate with diversity bonus + KL penalty against initial Mutator.
- **Mutator easier to reward-hack** (it's reward = how confused Tester is = trivially gameable by proposing nonsense). Mitigate by requiring Mutator output to actually compile + change semantics (verifier!).
## 2.8 Phase 2 β€” 4-Day Build Plan (Days 8-11 of prep)
| Day | Goal |
|---|---|
| 8 | Mutator action grammar + env |
| 9 | Learnability reward + alternating training loop |
| 10 | First co-evolution run |
| 11 | Difficulty-progression plots, README updates |
Days 12-16 = polish, deployment, demo recording, buffer.
## 2.9 Phase 2 Storytelling Hook
*"The Tester learns to catch bugs. But who decides what bugs to plant? In Phase 1, we did. In Phase 2, an agent does β€” and that agent learns to plant exactly the bugs the Tester is on the verge of being able to catch. The result is a curriculum that adapts in real time. You see this in the training curves: as the Tester gets better, the Mutator gets sneakier, and the kill rate stays in the 'productive struggle' zone. This is genuine RLVR self-play, in software, with a verifier (the test runner) keeping both honest."*
That's three themes earned (Wildcard, World Modeling, Self-Improvement) and a paper abstract written.
---
# PART 3 β€” CARTOGRAPHERZERO (POST-HACKATHON / STRETCH)
## 3.1 Status
Build only after MutantHunter is fully shipped. Could become:
- ArXiv preprint (multi-turn calibrated geospatial RL)
- Portfolio piece
- Submission to a future hackathon (NeurIPS dataset/benchmark, EGU)
## 3.2 The Pitch
OpenEnv-compliant RL environment for **multi-turn, budgeted, calibrated geospatial reasoning**. Agent answers questions about Sentinel-1+2 satellite imagery (flood detection on Sen1Floods11; land-cover classification on BigEarthNet v2.0) with limited initial observation. Tools: `request_band(b)`, `request_neighbor(dir)`, `request_prior_scene(delta_days)`. Budget: 3 tool calls. Outputs: `{answer, confidence, evidence}`. Rewards: correctness + Brier calibration (per RLCR) + information efficiency + format. First OpenEnv geospatial env; first multi-turn extension of RLCR; first active-perception RL formulation in remote sensing.
## 3.3 Repository Layout
```
cartographer-zero/
β”œβ”€β”€ README.md
β”œβ”€β”€ pyproject.toml
β”œβ”€β”€ requirements.txt
β”œβ”€β”€ Dockerfile
β”œβ”€β”€ openenv.yaml
β”œβ”€β”€ LICENSE
β”‚
β”œβ”€β”€ src/cartographer_zero/
β”‚ β”œβ”€β”€ __init__.py
β”‚ β”œβ”€β”€ models.py # Action/Obs/State for geospatial QA
β”‚ β”œβ”€β”€ environment.py
β”‚ β”œβ”€β”€ server.py
β”‚ β”œβ”€β”€ client.py
β”‚ β”‚
β”‚ β”œβ”€β”€ data/
β”‚ β”‚ β”œβ”€β”€ sen1floods11_loader.py # Tile loading, band caching
β”‚ β”‚ β”œβ”€β”€ bigearthnet_loader.py
β”‚ β”‚ β”œβ”€β”€ feature_extractor.py # Per-band mean/std/percentiles
β”‚ β”‚ └── _cache/
β”‚ β”‚
β”‚ β”œβ”€β”€ tools/
β”‚ β”‚ β”œβ”€β”€ request_band.py
β”‚ β”‚ β”œβ”€β”€ request_neighbor.py
β”‚ β”‚ └── request_prior_scene.py
β”‚ β”‚
β”‚ β”œβ”€β”€ rubric/
β”‚ β”‚ β”œβ”€β”€ reward_correctness.py
β”‚ β”‚ β”œβ”€β”€ reward_brier.py # RLCR-style: -(conf - correct)^2
β”‚ β”‚ β”œβ”€β”€ reward_info_efficiency.py
β”‚ β”‚ β”œβ”€β”€ reward_evidence.py # Optional v2: feature-importance check
β”‚ β”‚ β”œβ”€β”€ reward_format.py
β”‚ β”‚ └── compose.py
β”‚ β”‚
β”‚ β”œβ”€β”€ tasks/
β”‚ β”‚ β”œβ”€β”€ flood_qa_generator.py
β”‚ β”‚ β”œβ”€β”€ landcover_qa_generator.py
β”‚ β”‚ └── seeds/eval_set_v1.json
β”‚ β”‚
β”‚ └── utils/
β”‚ β”œβ”€β”€ visualization.py # Tile + bands + answer overlay
β”‚ └── tracing.py
β”‚
β”œβ”€β”€ training/
β”‚ β”œβ”€β”€ train_grpo.ipynb
β”‚ β”œβ”€β”€ train_grpo.py
β”‚ β”œβ”€β”€ config.yaml
β”‚ β”œβ”€β”€ prompts.py
β”‚ └── baseline_eval.py
β”‚
β”œβ”€β”€ evaluation/
β”‚ β”œβ”€β”€ eval_harness.py
β”‚ β”œβ”€β”€ before_after.py
β”‚ └── calibration_metrics.py # ECE, MCE, Brier
β”‚
β”œβ”€β”€ plots/
β”‚ β”œβ”€β”€ correctness_curve.png
β”‚ β”œβ”€β”€ calibration_curve.png # Reliability diagram
β”‚ β”œβ”€β”€ budget_usage.png
β”‚ └── baseline_vs_trained.png
β”‚
β”œβ”€β”€ docs/
β”‚ β”œβ”€β”€ problem_statement.md
β”‚ β”œβ”€β”€ reward_design.md
β”‚ β”œβ”€β”€ india_relevance.md # NDMA, ISRO, monsoon flood case
β”‚ └── why_calibration_matters.md
β”‚
└── scripts/
β”œβ”€β”€ download_datasets.sh # Sen1Floods11, BigEarthNet subsets
└── precompute_features.py
```
## 3.4 Key Implementation Notes
- **Base model: Qwen2.5-VL-3B** (vision-language) or **Qwen3-4B + structured features only** (text-only, easier).
- **Recommended**: text-only with structured features. The agent never sees raw imagery β€” it sees per-band statistics (mean, std, percentiles) and gets to *request* more bands. This sidesteps VLM training complexity and keeps it Qwen3-4B-compatible.
- **Datasets**: pull small subsets (~500 tiles each) from Sen1Floods11 and BigEarthNet v2.0. Both have permissive licenses.
- **Brier reward** is the heart. `-(confidence - correct)Β²` directly per RLCR.
- **Evidence reward** is the stretch. Requires precomputed feature-importance maps (via SHAP on a small XGBoost trained on the same task).
## 3.5 14-Day Post-Hackathon Plan
Build at leisure post-hackathon. Aim for ArXiv preprint within 6 weeks of hackathon end.
---
# PART 4 β€” SHARED INFRASTRUCTURE NOTES
## 4.1 Hugging Face Spaces Deployment
Both envs target `huggingface.co/spaces/<your-username>/<env-name>`. SDK = `docker`. Use Spaces' free CPU tier for the env server (it doesn't need a GPU β€” training happens elsewhere).
`scripts/deploy_hf_space.sh`:
```bash
#!/bin/bash
set -euo pipefail
HF_USER=$1
ENV_NAME=$2
git clone "https://huggingface.co/spaces/$HF_USER/$ENV_NAME" /tmp/space || true
rsync -av --exclude='.git' --exclude='**/__pycache__' --exclude='_cache' . /tmp/space/
cd /tmp/space
git add -A
git commit -m "Deploy $(date -u +%FT%TZ)"
git push
```
## 4.2 Hugging Face Hub Artifacts
- Env: HF Space (Docker)
- Eval dataset: HF Dataset (`<user>/<env-name>-eval`)
- Trained LoRA: HF Model (`<user>/<env-name>-qwen3-4b-lora`)
- Blog post: HF Blog (or LinkedIn / Medium with HF Space link)
All linked from README.
## 4.3 W&B Setup
- Project: `mutant-hunter` and `cartographer-zero`
- Public report at end of training; link in README
- Critical charts: total reward, per-component reward, success rate, episode length, eval-set kill rate (every 25 steps)
## 4.4 Compute Budgets
- **MutantHunter Phase 1**: 1Γ— A100-40GB or L40S, 6-8 hours total training.
- **MutantHunter Phase 2**: 1-2Γ— A100-40GB, 12-16 hours total (alternating loop).
- **CartographerZero**: 1Γ— A100, 4-6 hours (single model, simpler).
If only T4 available on Colab: drop to Qwen3-1.7B, halve max_steps, expect noisier curves but real signal still visible.
---
# PART 5 β€” INSTRUCTIONS FOR CLAUDE CODE
When implementing, Claude Code should:
1. **Start with Phase 1 ONLY.** Do not scaffold Phase 2 or CartographerZero files until Phase 1 is shipped.
2. **Read the OpenEnv repo before generating any env code.** The exact API surface may have changed; defer to actual current docs.
3. **Follow this order strictly** (matches the 7-day plan):
1. `pyproject.toml`, `Dockerfile`, `requirements.txt`, `.gitignore`, `LICENSE`
2. `src/mutant_hunter/models.py` (the contract)
3. 4 self-curated mini-libs in `src/mutant_hunter/corpus/_local/`
4. `src/mutant_hunter/mutation/` β€” engine first
5. `scripts/precompute_baselines.py` β€” run it, populate manifest
6. `src/mutant_hunter/safety/sandbox.py` + `forbidden_patterns.py`
7. `src/mutant_hunter/tools/` β€” all 5 tools
8. `src/mutant_hunter/environment.py`
9. `src/mutant_hunter/server.py`
10. `src/mutant_hunter/client.py`
11. `src/mutant_hunter/rubric/` β€” all 5 reward functions + compose
12. `tests/` β€” env tests first (must pass before training!)
13. `evaluation/reward_hacking_tests.py` β€” adversarial cases
14. `training/train_grpo.py` (small smoke run, 10 steps)
15. `training/baseline_eval.py` β€” establish baseline
16. Real training run, 200-300 GRPO steps
17. `evaluation/before_after.py`, plot generation
18. README, docs/, demo materials
4. **Use the test harness as ground truth.** If `evaluation/reward_hacking_tests.py` doesn't pass, the env is broken β€” fix before training.
5. **Pin all dependencies** before training begins. A version bump mid-training will ruin the run.
6. **Commit early, commit often.** The HF Space is a public git repo β€” judges will see commit history. Show iteration, not a single mega-commit.
7. **When stuck on OpenEnv specifics, prefer the framework's own examples** (Echo, Wordle, 2048) over guessing. Read their source.
8. **Do NOT over-engineer.** Phase 1 should ship even if Phase 2 never starts. Every file created in Phase 1 must be necessary for Phase 1.
9. **The reward function is the soul of the project.** When in doubt, spend more time on `rubric/` than on anything else.
10. **The README is the second-most-important file.** Write it once Phase 1 works end-to-end, then iterate. Don't write it last.
---
# APPENDIX A β€” Decision Log
- **Why Qwen3-4B over Qwen2.5-Coder-7B?** User preference. Phase 1 will work; if reward curve flat after 200 steps, swap to Coder-3B.
- **Why mutmut over cosmic-ray?** Faster, simpler, more mature. Cosmic-ray for Phase 2 if needed.
- **Why 5 reward components, not 3?** Each closes a specific hack vector. See safeguards.md.
- **Why no process rewards in Phase 1?** Adds debugging surface. Keep Phase 1 outcome-only; add process rewards in Phase 2 if needed.
- **Why self-curated mini-libs over big PyPI packages?** Eliminates training-data contamination concerns. Judges will ask.
# APPENDIX B β€” What Could Go Wrong (Pre-Mortem)
1. **Mutmut is too slow on chosen modules** β†’ cap to 15 mutants per module via filtering; use `pytest-xdist`.
2. **GRPO reward is flat for 100 steps** β†’ warm-start with SFT on 50 hand-written test examples.
3. **HF Space build fails** β†’ test Dockerfile locally; pin every dep; check Space logs immediately on first push.
4. **Sandbox escapes happen** β†’ review subprocess setup; add seccomp filters if Linux capabilities allow.
5. **On-site no internet for HF push** β†’ have Space pre-deployed Day 14; on-site only for fixes.
6. **Compute credits run out mid-training** β†’ checkpoint every 25 steps; resume from last good.
7. **Demo crashes during pitch** β†’ record video as backup; have local reproduction ready.
8. **Reward hacking emerges in late training** β†’ `inspect_rollouts.py` runs every 25 steps; manual sanity check.
End of blueprint.