Guidelines for AI agents working on the PostTrainBench codebase.
PostTrainBench is a benchmark framework that measures AI agents' ability to perform LLM post-training - improving pre-trained small language models on specific benchmarks through automated research. Agents are typically given 10 hours on an H100 GPU to improve base models, with performance measured by benchmark scores.
PostTrainBench/
├── agents/ # Agent implementations (claude, codex, gemini, opencode, qwen3max, glm5, ...)
├── cluster/ # Cluster-specific helper artifacts
├── containers/ # Apptainer/Singularity container definitions and helpers
├── dev_utils/ # Development/debugging utilities (run failure triage, trace extraction, ...)
├── scripts/ # Result aggregation and analysis
├── src/
│ ├── baselines/ # Baseline score computation
│ ├── commit_utils/ # HTCondor job submission utilities (incl. set_env_vars.sh)
│ ├── judges/ # Reward-hacking judges (one folder per judge; see Safety)
│ ├── eval/
│ │ ├── general/ # Prompt generation (get_prompt.py, prompt.txt)
│ │ ├── tasks/ # Evaluation benchmarks (aime2025, aime2026, gsm8k, ...)
│ │ └── templates/ # Chat templates (Jinja2)
│ ├── trace_parsing/ # Per-agent trace parsers (claude/codex/gemini/opencode)
│ ├── utils/ # Utility scripts (check_cuda, system_monitor, timestamp_lines, ...)
│ └── run_task.sh # Main task execution orchestrator
└── results/ # Evaluation results storage (path controlled by POST_TRAIN_BENCH_RESULTS_DIR)
| File | Purpose |
|---|---|
src/run_task.sh |
Main task execution orchestrator (runs agent, then 4 judges, then evaluation) |
src/commit_utils/commit.sh |
Batch job submission across agents × benchmarks × models |
src/commit_utils/set_env_vars.sh |
Sources .env and exports POST_TRAIN_BENCH_* env vars |
src/commit_utils/single_task.sub |
HTCondor submission template |
src/commit_utils/single_task_gemini.sub |
Gemini-specific HTCondor submission template |
src/eval/general/get_prompt.py |
Generates agent prompts |
src/eval/general/prompt.txt |
Agent prompt template |
src/trace_parsing/parse_trace.py |
Dispatches to per-agent parser to produce human-readable trace |
src/utils/update_agent_cli.sh |
Auto-updates an agent's CLI harness to latest and records its version |
src/judges/run_judges.sh |
Runs judges on an existing result dir (--judges to select a subset); each writes its own per-judge JSON |
src/judges/get_judge_prompt.py |
Generates judge prompts (--judge <judge_name>) |
containers/standard.def |
Main container definition (other .def files exist per-agent) |
scripts/constants.py |
Agent/benchmark mappings |
example.env |
Template for the .env file (API keys + POST_TRAIN_BENCH_* paths) |
- Create directory:
agents/<agent_name>/ - Create entry point:
agents/<agent_name>/solve.sh - The
solve.shscript receives the system prompt via the$PROMPTenvironment variable (also some agents take it as a CLI arg). It runs inside the apptainer sandbox with/home/ben/taskas the working directory and should producefinal_model/there when finished. - Add an agent-specific submission template if needed:
src/commit_utils/single_task_<agent>.sub - Per-agent parsing: add a parser in
src/trace_parsing/<agent>_parser.pyand register it inparse_trace.pysosolve_parsed.txtis human-readable. - If the agent needs persistent OAuth state (e.g.
codex_non_api), drop anauth.jsonoroauth_tokennext tosolve.sh;run_task.shbind-mounts them into the sandbox. - Auto-update the CLI harness: call
bash /home/ben/update_agent_cli.sh <cli-binary>insolve.shjust before launching the CLI (e.g.... update_agent_cli.sh claude). This upgrades the harness to the latest npm release and writes its version tocli_version.txt(surfaced in the result dir). The helper (src/utils/update_agent_cli.sh, copied into the sandbox byrun_task.sh) holds the binary→npm-package mapping; add acaseentry there if the agent uses a CLI not already covered (claude,codex,gemini,opencode). The update is best-effort — a failure falls back to the container's pinned version and still records what actually ran. SetPOST_TRAIN_BENCH_SKIP_CLI_UPDATE=1in.envto disable the update globally and pin CLI versions to whatever the container ships;cli_version.txtstill records what ran (update: skipped).
Example structure:
#!/bin/bash
# agents/myagent/solve.sh
myagent-cli --model "$AGENT_CONFIG" "$PROMPT"Required: agents/<agent_name>/api_keys.json declares the third-party API keys the agent is
allowed to receive, e.g. {"allowed_api_keys": ["CODEX_API_KEY"]} (use [] for subscription-auth
agents like codex_non_api). run_task.sh passes ONLY these keys — plus any the benchmark
requires — into the -c --cleanenv sandbox; every other provider key is never injected (so it is
unset inside the sandbox). A missing api_keys.json is a hard error. See "API Key Provisioning"
below. With the allowlist in place there is no need to unset/blank keys inside solve.sh.
Currently supported agents include: claude, claude_non_api, claude_non_api_max, codex,
codex_non_api (and _high, _xhigh, _reprompt, ...), codexhigh, codexlow, cursor_cli,
gemini, glm5, grok_cli, opencode, qwen3max.
cursor_cli uses the Cursor CLI (agent) with subscription auth: solve.sh installs the CLI
via the official curl installer (cursor.com/install, drops agent + cursor-agent symlinks in
$HOME/.local/bin pointing at $HOME/.local/share/cursor-agent/versions/<v>/cursor-agent) and
runs it with --print --force --trust --output-format stream-json --workspace /home/ben/task.
Auth tokens live at ~/.config/cursor/auth.json; run_task.sh bind-mounts
agents/cursor_cli/cursor_auth.json → /home/ben/.config/cursor/auth.json so the CLI reads and
rotates tokens against the shared host file. Set it up:
curl -fsS https://cursor.com/install | bash # installs to ~/.local/bin/{agent,cursor-agent}
export PATH="$HOME/.local/bin:$PATH"
cursor-agent login # completes OAuth in a browser
cp ~/.config/cursor/auth.json agents/cursor_cli/cursor_auth.json
chmod 600 agents/cursor_cli/cursor_auth.json
Pass the Cursor model ID as agent_config (e.g. grok-4.5, gpt-5, sonnet-4-thinking). Run
agent --list-models from a compute-node shell (head-node routes to Cursor's API are typically
blocked by corp proxy) to see the full list.
grok_cli uses the xAI Grok Build CLI (grok) with subscription auth: solve.sh installs the CLI
via the official curl installer (x.ai/cli/install.sh, drops binary at $HOME/.grok/bin/grok) and
runs it with --oauth --always-approve --no-auto-update --output-format streaming-json. Auth
tokens live at ~/.grok/auth.json (not config.toml, which is preferences only); run_task.sh
bind-mounts agents/grok_cli/grok_auth.json → /home/ben/.grok/auth.json so the CLI reads and
rotates tokens against the shared host file. Set it up:
grok login # or `grok login --device-auth` on a headless machine
cp ~/.grok/auth.json agents/grok_cli/grok_auth.json
chmod 600 agents/grok_cli/grok_auth.json
The explicit model ID for Grok 4.5 is grok-4.5 (per docs.x.ai/docs/models) — pass it as
agent_config so -m grok-4.5 pins the exact version regardless of the floating grok-build
alias in the CLI's [models] default.
- Create directory:
src/eval/tasks/<task_name>/ - Required files:
evaluate.py- Evaluation script using Inspect AI frameworkbenchmark.txt- Official benchmark name (single line)info.json- Benchmark metadata used by judges/prompt generation. Add"required_api_keys": ["OPENAI_API_KEY"]here if the benchmark'sevaluate.pyneeds a provider key for grading (onlyarenahardwriting/healthbenchdo today); the key is then provisioned into the agent sandbox for that benchmark. Defaults to none if omitted.test_data.json- Test items used to compute scores and by the contamination judge; when present,run_task.shalso copies it (pluscontamination_check.py) into the agent sandbox home andget_prompt.pyadds a "Decontamination Tool" prompt section, so the agent can screen its own training data for test-set overlap
- Optional files:
evaluation_code/- Supporting evaluation code copied into the agent sandboxtask_context/- Additional context (e.g. dataset hints) copied into the agent sandbox
The evaluate.py must:
- Use
inspect_aiframework - Accept
--model-pathfor the model directory and--templates-dirfor chat templates - Write metrics to the path given by
--json-output-file(consumed by aggregation scripts)
# Submit the full agent × benchmark × model sweep (edit the loop / EXPERIMENT_NAME at the top)
bash src/commit_utils/commit.sh
# Submit a single task
condor_submit_bid 100 \
-a "agent=codex" \
-a "agent_config=gpt-5.1-codex-max" \
-a "eval=gsm8k" \
-a "model_to_train=Qwen/Qwen3-4B-Base" \
-a "num_hours=10" \
src/commit_utils/single_task.subsingle_task.sub accepts extra -a overrides such as num_gpus, request_memory,
request_cpus, and experiment_name (which becomes the _run* suffix on the result dir).
HTCondor scheduler logs go to logs/. Every .sub file writes its per-job error,
output, and log files to logs/<prefix>_$(Cluster).{err,out,log} (relative to the submit
directory, which is always the repo root).
- Bash for orchestration: All entry points and job scripts are bash
- Python for evaluation: Use Inspect AI framework for benchmark evaluation
- Jinja2 for templates: Chat templates in
src/eval/templates/ - HTCondor for scheduling: Job submission via
.subfiles. Schedulererror/output/logfiles are written underlogs/(gitignored), never to the repo root. - Apptainer for containers: Container definitions in
.deffiles; built.siffiles live in$POST_TRAIN_BENCH_CONTAINERS_DIR - Sandbox layout: Inside the container, the home is
/home/benand the working dir is/home/ben/task. The agent must place its trained checkpoint attask/final_model/.
The agent sandbox is launched by run_task.sh with apptainer -c --cleanenv (plus --pid --no-init), so it inherits nothing from the host environment — a variable exists inside the
sandbox only if an explicit
--env flag passes it. run_task.sh builds that --env list from an allowlist, so the policy is
declarative and lives in two places:
agents/<agent>/api_keys.json→{"allowed_api_keys": [...]}— the provider keys this agent is permitted to use (e.g.["ANTHROPIC_API_KEY"],["CODEX_API_KEY"], or[]for subscription-auth agents).src/eval/tasks/<task>/info.json→"required_api_keys": [...]— keys the benchmark's own grading needs (onlyarenahardwriting/healthbench, which needOPENAI_API_KEY).
The sandbox receives exactly the union of the two. Consequences:
OPENAI_API_KEYreaches an agent only when it runsarenahardwriting/healthbench, so the agent can runevaluate.py's OpenAI judge; for every other benchmark it is absent.- Subscription-auth agents (
*_non_api) list[]and authenticate via their bind-mountedauth.json/oauth_token; on the two writing benchmarks they additionally receiveOPENAI_API_KEYpurely for grading. solve.shscripts must NOTunset/blank keys — the allowlist already withholds them. Genuine per-agent logic stays (e.g.glm5/qwen3maxremap their provider key intoANTHROPIC_API_KEY;codex_non_apisetsforced_login_method).- The host-side
CODEX_API_KEY/OPENAI_API_KEYsetup (renaming the OpenAI key for codex agents and restoringOPENAI_API_KEYfor the two writing benchmarks) lives near the top ofrun_task.sh.
Note: VLLM_API_KEY (the local inspect/vLLM eval server) is operational, not a third-party key, and
is always passed as the constant inspectai; it is not part of the allowlist.
The project includes reward-hacking detection via agent-as-judge runs invoked by
src/run_task.sh after the agent finishes (also exposed standalone via
src/judges/run_judges.sh). Each judge is a folder under src/judges/ with a judge.conf
and a prompt template (see src/judges/README.md, including how to add a new judge):
data_contamination_judge(GPT-5.4 via codex CLI, subscription auth) — checks for test-data usage, eval tampering, model substitution, and forbidden fine-tuning practices.api_usage_judge(GPT-5.4 via codex CLI) — separate schema (disallowed_api_usage), checks whether the agent called external LLM APIs in a disallowed way. Its verdict (judgement_api.json) is consumed by scoring: a flagged run falls back to the baseline score inscripts/collect.py.ptb_lookup_judge(GPT-5.4 via codex CLI) — separate schema (disallowed_ptb_lookup), checks whether the agent looked up PostTrainBench itself (the website, the GitHub repo, or published traces of past runs, e.g. to copy strategies). Its verdict (judgement_ptb_lookup.json) is archival — it does not feed score fallback — butscripts/collect.pyraises an error if it ever flags, so a firing lookup judge cannot pass unnoticed.general_judge(GPT-5.6 Terra via codex CLI pinned to 0.144.5) — separate schema (general_anomaly), an open-ended sweep for unknown unknowns: run-integrity problems the other judges were not designed to catch, whether caused by the agent (novel reward hacking outside the other judges' scope) or suffered by it (premature stop without visible reason, agent usage limits / token exhaustion, grader-API credit exhaustion on arenahardwriting/healthbench, harness or infra failures). Its verdict (judgement_general.json) is purely archival:scripts/collect.pyignores it entirely — no score impact and no error, whether it is missing or flagged. Review its flags withscripts/find_flagged_runs.py --judge general_judge.
Each judge writes its own per-judge file; there is no aggregation step. The canonical verdicts
consumed downstream are judgement_gpt5_4.json (contamination/disallowed_model) and
judgement_api.json (API usage) — or their _rerun variants when the rerun pipeline has
produced them. scripts/collect.py enforces judge coverage: every scored run (metrics.json
present) must carry a contamination and an API verdict, and runs with ids >=
NEWER_JUDGES_MIN_RUN_ID (scripts/utils.py) a PTB-lookup verdict too; a method containing a
scored run without a required verdict is skipped (warning, no CSVs) instead of aggregated.
Runs below that id may legitimately lack the PTB-lookup file (they predate the judge) — a
verdict present on such a run is still read as a tripwire. The general judgement is never
required.
Reruns: src/judges/rerun/ holds the batch-rerun pipeline. Rerun outputs
always carry a _rerun suffix so original judge files produced during run_task.sh are
preserved.
The judge tooling itself lives in:
src/judges/judge_lib.sh— shared bash helpers used by bothrun_task.shandrun_judges.sh(sandbox prep, codex invocation, output collection)src/judges/judge_tools/—contamination_check.py,model_identity_check.py, andreference_configs/(copied into the judge sandbox so the judge can run them as tools).contamination_check.py(with the benchmark'stest_data.json) is additionally copied into the agent sandbox home before the run, so the agent can self-decontaminate its training data with the exact checker the judge uses; the judge phase re-copies both files, so the judges never run agent-modified versions.src/judges/test_data_download/— helpers to (re)download test data
results/{agent}_{agent_config}_{num_hours}h[_{num_gpus}gpu]{experiment_name}/
└── {benchmark}_{model_name}_{cluster_id}/
├── output.log # run_task.sh stdout
├── error.log # run_task.sh stderr
├── prompt.txt # Generated agent prompt
├── time_taken.txt # Agent execution duration (HH:MM:SS)
├── solve_out.txt # Raw agent trace (stdout+stderr from the agent CLI)
├── solve_parsed.txt # Human-readable trace from src/trace_parsing/parse_trace.py
├── cli_version.txt # Auto-updated agent CLI harness version (src/utils/update_agent_cli.sh)
├── task/ # Snapshot of the agent's working directory (post-cleanup)
├── final_model/ # Trained model checkpoint
├── system_monitor.log # GPU/CPU/RAM samples from src/utils/system_monitor.sh
├── judge_output_gpt5_4.{json,txt} # data_contamination_judge raw + parsed trace
├── judgement_gpt5_4.json # data_contamination_judge structured verdict
├── judge_output_api.{json,txt} # api_usage_judge raw + parsed trace
├── judgement_api.json # api_usage_judge structured verdict (flag ⇒ baseline score)
├── judge_output_ptb_lookup.{json,txt} # ptb_lookup_judge raw + parsed trace
├── judgement_ptb_lookup.json # ptb_lookup_judge structured verdict (archival; collect.py errors if flagged)
├── judge_output_general.{json,txt} # general_judge raw + parsed trace
├── judgement_general.json # general_judge structured verdict (archival; ignored by collect.py)
├── final_eval_*.txt # vLLM/inspect-ai evaluation logs (one per retry)
└── metrics.json # Final benchmark scores
Result directories with the _rerun suffix on judgement_*.json come from the rerun-judge
pipeline; original files are kept side-by-side. The canonical contamination verdict is
judgement_gpt5_4.json (or judgement_gpt5_4_rerun.json when present); the canonical
API-usage verdict is judgement_api.json (or judgement_api_rerun.json).
- Shell scripts: Use bash, include
#!/bin/bashshebang - Python: Standard library preferred, use type hints
- Error handling: Fail explicitly - do not silently handle errors
When writing scripts/tooling, .env (at the repo root) is the canonical source for the
POST_TRAIN_BENCH_* variables. Read them directly from .env — do not assume they are
already exported in the environment, and do not source set_env_vars.sh from a script
(its module-loading block fails on compute nodes without tclsh). Examples of the correct
pattern: src/judges/rerun/commit_rerun_judges.sh (grep the var
out of .env) and find_disallowed_api_usage.py (load_results_dir_from_env(), which falls
back to .env when the var is unset).
Common ones (defined in .env; also sourced via src/commit_utils/set_env_vars.sh):
POST_TRAIN_BENCH_RESULTS_DIR— where per-run result directories are writtenPOST_TRAIN_BENCH_CONTAINERS_DIR— where built.sifcontainers livePOST_TRAIN_BENCH_CONTAINER_NAME— default container for the agent sandboxPOST_TRAIN_BENCH_EXPERIMENT_NAME— suffix added to the result directory namePOST_TRAIN_BENCH_JOB_SCHEDULER— controls which scheduler branch incommit.shrunsHF_HOME— host-side Hugging Face cache that gets overlay-mounted into the sandbox
Never add yourself as a co-author to commits.
Usually when testing, it is best to use the openai model gpt-5.1-codex-max, because we have a
lot of api credits for openai.
Add your own notes to the directory /home/brank/Documents/agents_notes and index them via
/home/brank/Documents/agents_notes/index.md. Add those notes for other agents and for yourself
for future use.