Follow-on to #84. GM-Bench's decision loop was modelled on ZenGM Hockey; this issue closes the gaps that a read of the ZenGM engine (GameSim.hockey, developSeason.hockey.ts, ValueChangeCalculator, player/value.ts) makes visible in ours.
Why now, and only now
Every item below moves contract_fingerprint. Today that costs nothing: sota-v3 is open, no paid panel has been bought, and the seven checkpoints under data/model_checkpoints/ are already invalidated by #88. The moment a paid sota-v3 panel runs, all of this becomes unaffordable — a docstring edit to a _CONTRACT_SOURCES file is enough to invalidate the rows. This is the window.
Pre-change reference (fingerprint 8a4eb422d548317a, seeds 11-18 x 5 seasons):
| agent |
mean_score |
| pick-trader |
411.62 |
| strategic |
402.02 |
| scaffold-view |
401.95 |
| shrewd |
371.77 |
| value |
354.62 |
| win-now |
275.83 |
| conservative |
139.03 |
| rebuild |
138.75 |
| exploit |
137.13 |
| random |
96.72 |
The diagnosis
The benchmark contests exactly one valuation: hidden true_potential. Difficulty in a management benchmark comes from assets whose value is disputed between the agent's model and the environment's model, and we currently have a single such asset. Everything else is priced identically for both sides, which is a large part of why a 120-line scripted pick-trader beats every frontier model we ran.
Each item below converts an unpriced asset into a contested one.
P1 - Contract economics (revives #24, closed with #62 unmerged)
#62 was closed 2026-07-16 purely to protect the sota-v2 publication lane, not on quality. Its concepts should be reimplemented on current main.
- Dead cap on release.
simulator.py:_release currently sets salary = 0.0 with no charge. Any bad contract is erasable at will above the 18-player floor, so the cap_room score term measures no skill. ZenGM keeps released salary on the books.
- Incumbent extension rights. Expiring players drop straight into the shared pool with no advantage to the team that developed them. In ZenGM, re-sign-vs-let-walk is the central GM decision, and it is the one decision GM-Bench never asks.
- Market inflation + term premium so the 1-5 years dial has economic content.
#62 found a real exploit here — sign 1 year, immediately extend 5 at the loyalty discount — and tuned the discount 8% -> 3% to keep INC5 above FA1. Both must be preserved.
P2 - Standings-linked pick values
pick_value is 12.0 * 0.8^n regardless of whose pick it is, so a contender's pick and a cellar-dweller's are interchangeable to both the trade market and score_components.future_pick_assets. ZenGM's ValueChangeCalculator estimates the actual slot from projected records and regresses toward the mean with distance. This is the most likely single explanation for pick-trader's dominance.
P3 - In-season dynamics
simulate_season computes ratings once (simulator.py:330) and draws all 33 games from frozen numbers, so deadline moves cannot affect the standings they were made for. Recompute at the midseason break so the trade deadline is causally connected to the second leg.
P4 - morale is dead
Generated, updated by _update_morale_from_standings, published in Player.public_dict — and read by nothing. It is a pure token tax on agents inside a 4,096-token lane. Either wire it into free-agent/extension willingness (ZenGM's mood system: loyalty / money / playing time / winning) or drop it from the observation.
Constraints
- Validity canaries in
validate-contract encode baseline orderings (strategic > shrewd > value > exploit). If a change inverts one, that is a finding to reason about, not a threshold to loosen.
- The
exploit canary must not gain ground; a dead-cap rule that made hoarding cheap would be a regression.
- Frozen
sota-v2 evidence under results/, releases/, docs/blog/, web/src/data/ stays untouched. SOTA_V2_CONTRACT (558e8f35ea1d66b9) must not move.
docs/scoring_calibration.md and golden scores get updated with each change, not after all of them.
Follow-on to #84. GM-Bench's decision loop was modelled on ZenGM Hockey; this issue closes the gaps that a read of the ZenGM engine (
GameSim.hockey,developSeason.hockey.ts,ValueChangeCalculator,player/value.ts) makes visible in ours.Why now, and only now
Every item below moves
contract_fingerprint. Today that costs nothing:sota-v3is open, no paid panel has been bought, and the seven checkpoints underdata/model_checkpoints/are already invalidated by #88. The moment a paidsota-v3panel runs, all of this becomes unaffordable — a docstring edit to a_CONTRACT_SOURCESfile is enough to invalidate the rows. This is the window.Pre-change reference (fingerprint
8a4eb422d548317a, seeds 11-18 x 5 seasons):The diagnosis
The benchmark contests exactly one valuation: hidden
true_potential. Difficulty in a management benchmark comes from assets whose value is disputed between the agent's model and the environment's model, and we currently have a single such asset. Everything else is priced identically for both sides, which is a large part of why a 120-line scriptedpick-traderbeats every frontier model we ran.Each item below converts an unpriced asset into a contested one.
P1 - Contract economics (revives #24, closed with #62 unmerged)
#62 was closed 2026-07-16 purely to protect the
sota-v2publication lane, not on quality. Its concepts should be reimplemented on currentmain.simulator.py:_releasecurrently setssalary = 0.0with no charge. Any bad contract is erasable at will above the 18-player floor, so thecap_roomscore term measures no skill. ZenGM keeps released salary on the books.#62 found a real exploit here — sign 1 year, immediately extend 5 at the loyalty discount — and tuned the discount 8% -> 3% to keep
INC5aboveFA1. Both must be preserved.P2 - Standings-linked pick values
pick_valueis12.0 * 0.8^nregardless of whose pick it is, so a contender's pick and a cellar-dweller's are interchangeable to both the trade market andscore_components.future_pick_assets. ZenGM'sValueChangeCalculatorestimates the actual slot from projected records and regresses toward the mean with distance. This is the most likely single explanation forpick-trader's dominance.P3 - In-season dynamics
simulate_seasoncomputes ratings once (simulator.py:330) and draws all 33 games from frozen numbers, so deadline moves cannot affect the standings they were made for. Recompute at the midseason break so the trade deadline is causally connected to the second leg.P4 -
moraleis deadGenerated, updated by
_update_morale_from_standings, published inPlayer.public_dict— and read by nothing. It is a pure token tax on agents inside a 4,096-token lane. Either wire it into free-agent/extension willingness (ZenGM's mood system: loyalty / money / playing time / winning) or drop it from the observation.Constraints
validate-contractencode baseline orderings (strategic > shrewd > value > exploit). If a change inverts one, that is a finding to reason about, not a threshold to loosen.exploitcanary must not gain ground; a dead-cap rule that made hoarding cheap would be a regression.sota-v2evidence underresults/,releases/,docs/blog/,web/src/data/stays untouched.SOTA_V2_CONTRACT(558e8f35ea1d66b9) must not move.docs/scoring_calibration.mdand golden scores get updated with each change, not after all of them.