6c6de16e60
Add replay.py: a ReplayLog records every (observation, action) pair a policy produced, keyed by monotonic call_seq, plus the EncounterSpec, seed and round_cap, and serializes the whole battle to JSONL. replay() turns a log back into an observation-only LLMPolicy that returns each recorded action, raising ReplayMismatchError on a differing observation and ReplayExhaustedError past the last call. Observations are matched by their canonical JSON (observation_key), not raw dataclass equality, because JSON collapses tuples and lists and the snapshot's inner dict[str, object] holds both. Action and the spec are dumped through Pydantic TypeAdapters (mode=json) so tuple fields survive as arrays and are coerced back on load. Move TrickCondition/ReadyTrigger out of the TYPE_CHECKING block in combat.py: TypeAdapter(Action) must resolve them at runtime to serialize the dataclass, so the Literal aliases now live at module level.