Docs · 07
Publishing Runs
Runs are published to the Hugging Face dataset
hesamation/crusader-arena-runs,
not to this repository: they contain game footage, and a recorded 25-game-minute series takes
about 230 MB. This repository holds the tools that prepare and upload
them (tools/dataset/) and the dataset card (tools/dataset/card.md). Bundles are staged in
harness/runtime/publish/, which git ignores.
Nothing is sent anywhere until npm run upload runs with --yes.
Steps
pip install datasets # Parquet tables and the Hugging Face client
npm run episodes -- --record ... # a series of episodes, recorded (see The benchmark)
npm run package -- <series id> --submitter <hugging face user> [--tier official]
npm run upload -- harness/runtime/publish/v1.0/<benchmark>/<model>/<series id> # dry run
npm run upload -- harness/runtime/publish/v1.0/<benchmark>/<model>/<series id> --yes # upload
npm run upload -- --evaluation --yes # after editing tools/dataset/evaluation.json- Package.
npm run packagetakes learning series ids (harness/runtime/series/<id>) or run folders, checks them, and stages each one with its tables and amanifest.json. A bundle with problems is still staged so you can look at it; the problems are printed and listed in the manifest. - Review. Watch the videos and read
manifest.json: what the scrub changed, and the problems. - Upload.
npm run uploadprints what it would send. With--yesit makes one commit per bundle; with--prit opens a pull request instead, which is how anyone without write access to the dataset submits runs.--tier officialis for runs made by the maintainers; everyone else's arecommunity.--replacereplaces a bundle already in the dataset, for one packaged again after a packaging change. - Count it. A published series is not ranked until its id is in
tools/dataset/evaluation.json, which--evaluationuploads to the dataset's root. The upload refuses a list that names a series the dataset does not have. Leave a series out to publish it without ranking it: an early run, a test, or one set aside.
The token is HF_TOKEN (environment or .env), else the login from hf auth login. The
dataset is --repo ORG/NAME or HF_DATASET_REPO.
What Can Be Published
Packaging records a problem, and the upload refuses the bundle, unless:
- the series is completed and every episode has a result (below);
- every episode's result is a full-budget score (
validin itsepisode.json: completed, ended by the game-time budget, final pause confirmed, scored from the final reading;invalidsays why not) or a run the model itself ended (outcomemodel_failure, published withvalidfalse); - every episode has
video.mp4(record with--record); - the runs' code was committed (
harness.dirtyis false) and every commit is onorigin/mainas last fetched, so anyone can see the code that ran; - the runs carry a benchmark version (below);
- all episodes of a series share the benchmark minor version and preparation guide, the prompt and tool hashes, the model and its settings, and the run settings;
- the model endpoint is not a local address, and
--submittergives a Hugging Face user name; - the scrub (below) found nothing to report.
The upload also refuses a bundle without a version, and one whose files changed after
packaging: every file's size and SHA-256 must match manifest.json. It replaces a bundle
already in the dataset only with --replace, which deletes the old folder in the same commit,
so files the new bundle no longer has go too. The old files stay in the dataset's history.
Versions and Attempts
The host stamps every run with the benchmark version its code belongs to (run.json
benchmark.version, from benchmark-versions.json; see
Versions), MAJOR.MINOR.PATCH. It is null when the code's
fingerprint is not a listed version; such runs are staged under unversioned/ and cannot be
uploaded. Runs compare within one minor version, so bundles are staged under
v<major>.<minor>/ and the exact version is in the dataset_version column; the episodes of a
series may differ only in the patch.
An episode of a series can run more than once: a stop or an infrastructure failure is run
again. The series keeps an episode's last attempt that is a full-budget score or a model
failure as its result (harness/server/series.ts), and that is the run published; earlier
attempts are not published and are not problems. An episode without a result yet, or a series
that is running or paused, is. The series folder's runner.log is not published.
Scrubbing
Every text file a bundle publishes (.json, .jsonl, .md) is scrubbed by
tools/dataset/scrub.py:
- Replaced with placeholders: the game machine's SSH target and address (
<game-host>) and source path (<game-root>) from.env, and this machine's home directory (<home>) and host name (<hostname>). Only values with a/,.,@or:are replaced; a plain word such as a short host name could be ordinary text, so it is reported instead. - Dropped:
pidandwindowId(the game's process and window on the game machine),systemandgraphicsin memory samples (machine-wide memory, the graphics driver and PCI device), anddirectoryfields holding an absolute path (the recorder logs where it kept its frames). - Reported, which blocks the upload: the API keys in
.envand saved model profiles, other key-shaped strings (sk-,hf_, GitHub and AWS keys, bearer tokens, private keys), the local and game-machine user names, home and user runtime paths, IP addresses other than loopback, and email addresses. Findings name the file, line and kind, never the value.
Screenshots are base64 inside the logs; strings of 512 or more base64 characters, and
data:image URLs, are skipped, so letters inside an image are never a finding. Lines that
need no change are copied byte for byte. If text the video is drawn from (run.json,
episode.json, events.jsonl) was replaced, the video is rendered again from the scrubbed
files and the original recording/ (JPEGs, or the frames.mp4 they were compacted into);
without recording/ that is a problem.
Smaller Than the Run
After the scrub, packaging makes each episode smaller (tools/dataset/media.py). The run
folder keeps the originals.
- Images leave
events.jsonl. Every image the log holds as base64 (the screenshots, the preparation message's reference images and the final frames) is written once toimages/<hash>.webp: WebP at quality 70 and full size, or the original bytes when those are smaller. An image block{"type": "image", "data": …}becomes{"type": "image", "mimeType": …, "path": "images/…"}, and any other field that held an image (the final frames'frame.image) holds the path. A tool's screenshot appears in a run's ownevents.jsonlunder the tool's result and again at the start and end of the message that carries it, but the host keeps the image data in the result only; the other two already hold a small{"type": "image", "mimeType": …, "omitted": "see tool_execution_end"}in its place, so there is only one copy to turn into a file (runs recorded before this, with all three in full, still end up as one file: the same bytes hash the same). - Token deltas are dropped.
message_updateevents stream a reply as it is written; the reply'smessage_endholds all of it. - The video is 720p, scaled from the run's 1080p render (after re-rendering it, when the scrub changed the logs it is drawn from) with the renderer's encoder settings.
manifest.json counts what this did under slimmed.
The Dataset
v<major>.<minor>/<benchmark>/<model ID with "/" as "--">/<series id>/
series.parquet series.json playbook-after-episode-<n>.md manifest.json
episode-<n>/
episode.parquet timeline.parquet final-overview.jpg video.mp4
run.json inputs.json episode.json events.jsonl logs.jsonl notifications.jsonl
memory.json notebook.md playbook.md interview.md interview.json results.json results.md
analysis.md
images/<hash>.webpA single run that is not part of a series is staged as run-<start time>-<id>/episode-1/.
Not published: recording/ (the raw frames, or the video they were compacted into; the
rendered video replaces either), checkpoint.json (the conversation again) and
controls-guide.html.
The dataset card defines three tables, read by the Hub's viewer and by load_dataset:
| Table | Rows | Holds |
|---|---|---|
series | One per learning series | Net worth per episode, the score (last episode), change from episode 1, totals of cost, tokens, time, turns and tool calls, the final playbook, the last final image |
episodes | One per episode | Model, settings, benchmark version and harness version; the attempt and its outcome; how the run ended and whether it is a full-budget score; the scorecard and every good; game, wall and inference time; tokens by kind, cost and whether it was billed or worked out from list prices; turns, compactions, tool calls in total and by tool, errors, building and anchor-tool results; the final image; paths of the video and files |
timeline | One per game minute of each episode | Gold, net worth, population, housing, popularity, food, structures, troops, date and every good, with the tokens, cost, turns, tool calls and tool errors spent by then |
Column lists are in the dataset card. The numbers in series and
episodes come from the same code as npm run report (harness/server/run-report.ts), so
the two always agree.
The timeline is read from events.jsonl: the stats in each host observation (stamped
with the budget clock), reader samples in tool results (stamped with a game tick, placed on
the budget clock from the final reading) and the final reading (from episode.json when the
log's final reading came back empty). The grid follows the budget. Row m is the latest reading
at or before m game minutes; row 0 is the first reading and the last row the final one.
Readings come from the agent's turns, so a minute without a fresh reading repeats the one
before, and sample_game_seconds shows when it was taken. Net worth uses the marketplace
sell prices in harness/server/market-prices.ts.
The Website
The project website is a static site built from this dataset and from this repository's
ROADMAP.md, CHANGELOG.md and docs/, each read at its newest commit when the site is
built. Its leaderboards and charts use only the series in evaluation.json, and of a model's
series on one map only those on the newest patch; the others keep their run pages. A Vercel
deploy hook rebuilds it. These call it:
- This repository.
.github/workflows/notify-website.ymlcalls the hook on a push tomainthat changesdocs/,ROADMAP.mdorCHANGELOG.md, and by hand from the Actions tab. It reads the hook's URL from the Actions secretWEBSITE_DEPLOY_HOOKand does nothing in a fork. - The upload.
npm run upload -- --yestohesamation/crusader-arena-runscalls it once, after its last commit, so an upload appears on the site a minute or two later and a build never sees half of one. It usesWEBSITE_DEPLOY_HOOKfrom.envwhen it is set, and otherwise runs the workflow above with the GitHub CLI (gh, signed in with theworkflowscope). A dry run, a pull request (--pr) and an upload to another dataset do not. The dataset has no webhook: one fires on every commit and again on each of the Hub's own Parquet conversions, about ten builds an upload. - The website's own repository, on every push.
The hook can only rebuild the website's main; it cannot change its code or content. If its
URL leaks, make a new hook in Vercel and put it in the secret, and in .env where it is set.
Checks
python3 -m unittest discover -s tools/datasetThe tests build a small series and check the layout, the tables, the timeline, the scrub, the
images and token deltas leaving the logs, the video scaled to 720p, the problems that block an
upload, and the upload itself, replacing a bundle included, against a fake Hub client. One test runs
npm run report's code on a made-up run to check the fields packaging reads.
This page is docs/publishing.md in the benchmark’s repository, at commit dd89b61 (13 October 2026). View it on GitHub