一键导入
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 职业分类
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.
| 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.