بنقرة واحدة
run-league-ab-eval
Use when running a hosted Crewrift policy A/B and judging it with Wilson confidence intervals.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Use when running a hosted Crewrift policy A/B and judging it with Wilson confidence intervals.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
| name | run-league-ab-eval |
| description | Use when running a hosted Crewrift policy A/B and judging it with Wilson confidence intervals. |
The crewrift eval runs server-side on Observatory, NOT in local docker.
Each arm is one POST /v2/experience-requests: the tested policy occupies a
single forced-role seat and the league's top players fill the other 7 seats; the
games run on k8s. There is no local A/B, no Aurora/EC2 runner queue, no docker
scrimmage — those are deleted. Competitors' policy images are private (the
round/leaderboard API exposes only a policy_version_id, never an image), so
only the server can run them against you.
The engine is league_eval.py (run_league_ab); the CLI front door is
eval.py. Both live in _harness/ in the crewrift repo (the universal copies
in this package are tools/league_eval.py and tools/eval.py).
Both candidate and baseline must be uploaded, OWNED policies resolved by name. The CLI resolves names via the coworld API; a name that isn't an owned, uploaded policy fails before any submit (there is no server cancel API, so a bad name MUST fail early). Upload with:
coworld upload-policy <image> --name <name>
(Note: current coworld upload-policy consumes the server's ECR
authorization_token response. If an older pinned install fails before
upload, upgrade first; use the manual fallback in build-and-upload-policy
only if upgrade is impossible.)
Confirm the two arms are actually different builds before launching. A prior run silently A/B'd a policy against ITSELF and produced a meaningless verdict. Docker buildx content-addresses images:
docker image inspect <img> --format '{{.Id}}').git stash → build → git stash pop, or back up the file, revert the
specific edits in place, build, restore). Do NOT reuse a days-old image as
baseline — that confounds the comparison with every other drift. This repo
got burned by a dirty tree bundling an unrelated imposter rewrite into a
"crew-only" measurement, so git status the bot repo before committing or
building. (session-derived, unverified)eval.py CLIThis is the canonical "did my change help?" command. Run a dry-run first to print the seat plan and roster without submitting, then the real run.
# Dry-run: resolve roster + seat, print the plan, submit nothing.
python eval.py \
--candidate <candidate-name:vN> \
--baseline <baseline-name:vN> \
--role crew \
--num 30 \
--dry-run
# Real run.
python eval.py \
--candidate <candidate-name:vN> \
--baseline <baseline-name:vN> \
--role crew \
--num 30
CLI flags (defaults shown):
| flag | default | meaning |
|---|---|---|
--candidate | required | candidate policy name/label (owned, uploaded) |
--baseline | required | baseline policy name/label (owned, uploaded) |
--role | crew | crew or imposter — the forced role of the tested seat |
--num | 8 | paired episodes per arm. Cap 100. Use 30 for a real verdict (see below) |
--top-n | 7 | distinct top league players filling the other seats |
--maxticks | 10000 | league-realistic length; an episode runs maxticks/24 seconds of wall time |
--league | Crewrift league (hardcoded) | league_605ff338-0a2e-4e62-aeda-559df9a9198f |
--division | None | division id |
--opponent | (none) | explicit opponent player_name or live policy_version_id, repeatable, seat order; overrides ranking |
--backend | k8s | k8s = league runner (any league policy); antfarm = staging fleet (antfarm-registered only) |
--notes | eval.py | free-text label on the request |
--dry-run | off | resolve roster + print plan without submitting |
Pick --num for the CI you need, not for wall-clock. Episodes within an arm
run fully parallel server-side and both arms now run concurrently (a
2-worker thread pool, commit 6be96c2). A/B wall-clock is roughly one arm:
~7-8 min at maxticks=10000. The server is NOT the bottleneck (~240 warm-episode
capacity, MAX_OUTSTANDING_JOBS=2000, no per-user/per-league quotas; even
num=30 × 2 arms = 540 pods ≈ 195 vCPU fits the warm pool). The only floor is
realtime sim pacing (maxticks/24 s per episode). So raising num 8→30 tightens
the Wilson CIs at ~zero extra wall-clock cost — default to --num 30.
The CLI prints, per arm: win count / n, Wilson 95% CI, mean seat score, total
kills, total vote_timeouts, the two experience-request ids, and a verdict
string derived from whether the win-rate CIs separate.
roster APIFor a crux arm (one comparator's bot in YOUR seat) or any non-paired layout that
the CLI doesn't express, build the request body directly. The
POST /v2/experience-requests body was reworked server-side on 2026-06-12 to
a unified roster array. The old requester/requester_slot/opponents/
rotate_seats shape now returns 422 ("roster" Field required).
Server: https://softmax.com/api/observatory. New body — one
V2RosterParticipant per seat:
{
"target": {
"league_id": "league_605ff338-0a2e-4e62-aeda-559df9a9198f",
"division_id": null, "coworld_id": null, "variant_id": null,
"league_name": null, "division_name": null
},
"roster": [
{"player": {"policy_ref": "daveey-notsus-tier5c:v1"}, "slot": 0},
{"player": {"policy_ref": "<raw pvid uuid>"}, "slot": 1},
{"player": {"top_n": 7}},
{"player": {"random": true}}
],
"num_episodes": 30,
"execution_backend": "k8s",
"game_config_overrides": {
"maxTicks": 10000, "seed": 679961,
"imposterCount": 2, "autoImposterCount": false,
"slots": [{"role": "crew"}]
},
"notes": "crux <id> arm A"
}
Rules the API enforces:
player is exactly ONE of three keys:
policy_ref — a label name:vN OR a raw policy_version_id UUID. Must
be an owned policy or a live league member. Stale pvids from old round
results are rejected ("did not match an active runnable policy").top_n — draw from the target division's top-N champions (Competition pool).random — draw a random live member.slot pins a seat. slot: -1 or omitting slot = round-robin into open slots.num_episodes ≤ 100.game_config_overrides.slots (per-seat {"role": "crew"|"imposter"}),
with imposterCount/autoImposterCount. Forced slots are confirmed accepted
by the live server; a future rejection is a HARD failure (no seed fallback).The roster source must be LIVE members, not stale pvids. Get each top
player's current live pvid from the live-membership list ranked by recent round
score — the harness does this via league_roster.fetch_top_players /
live_members(league_id, division_id, active_only=True). A bare player_name
with >1 live version is ambiguous and rejected; a policy_version_id lifted from
an old round result is un-runnable. The league churns versions mid-session
(e.g. crewborg v19→v21→v22 in one day) — re-fetch live members and validate every
pvid right before each launch.
Poll GET /v2/experience-requests/{xid} until the top-level status AND every
episodes[].status are in {completed, failed, cancelled}. Poll cadence ~10s.
Early 404/425/429/5xx are transient — keep polling, do not fail.
Timeout budget (the formula league_eval uses — do not use a fixed short
timeout, it falsely fails slow-but-healthy episodes):
~ num * maxticks/24 * 3 + 900 seconds.maxticks/24*3 + 600 seconds, a participant is wedged — fail LOUD.Wedged-participant stall (real failure mode). If ANY of the 8 player
containers hangs (never connects, or hangs in crewrift's lockstep), the whole
episode freezes at tick 0 and never completes; the request sits running forever
(no cancel API → it orphans). Signature: all per-episode statuses running,
0 completed, and the live spectator ws renders a byte-identical looping (frozen)
scene. This is DISTINCT from cold-start: a big submit normally spends its first
~2-4 min all-pending/submitted (dispatch fan-out + Karpenter node spin-up)
before "running" climbs — that is normal, not a stall. Localize a bad bot by
bisecting at a SHORT maxticks (1000 ≈ 42s/ep): swap a known-good candidate
against the same opponents; if it completes, the original candidate is the hanger.
Lowering maxticks does NOT fix a wedge (it's a connect/start hang,
length-independent). The harness run_arm fail-fasts at the stall grace and the
dashboard has a per-row terminate button.
Run the poll from a Monitor / persistent poller, not a backgrounded Bash shell
with a long foreground sleep — detached background pollers have died silently
mid-sleep (arms completed server-side but the logfile stayed 0-byte with no
verdict). (session-derived, unverified)
Slot assignment is not guaranteed to match request order for slot: -1 /
omitted entries. Always confirm who actually landed where from
episodes[].participants[].position + policy_version_id before reading the
verdict. Two specific traps:
Also sanity-check what you measured matches what you intended: forced role
(game_config_overrides.slots), and — if the policy carries an LLM advisor — that
the advisor actually fired (vote-accuracy / fire-rate via metrics.py), since
there is no eval-time bedrock flag; behavior is whatever the uploaded image carries.
champion label.You have a trustworthy verdict when ALL of:
{completed, failed, cancelled}, with no wedged-participant stall (no arm sat
running with 0 completed past the stall grace).episodes[].participants[].position — the tested
policy was in the intended forced-role seat in BOTH arms, and any duplicate
comparator was disambiguated by position.--num was large enough (default 30) that the Wilson CIs are tight enough to
either separate or to call the change neutral.verdict reflects whether the win-rate CIs separate.If any of these fail, the verdict is not trustworthy — re-run, don't ship the edit.
Use when improving a Coworld player through one diagnosed failure and completed comparison evidence.
Use when compiling, validating, and uploading a notsus change as a new Crewrift policy version.
Use when decoding a Crewrift replay for authoritative deaths, survival, movement, roles, tasks, or rewards.
Use when verifying that the notsus Bedrock vote advisor fires before an advisor-sensitive evaluation.
Use when classifying a stuck, failed, empty, or zero-scoring evaluation as infrastructure or policy failure.
Use when finding and statistically confirming a reproducible Crewrift crux before changing the policy.