---
description: Build a reusable library of save states for documenting a retro ROM, using the romdev MCP. A human plays each notable moment in a playtest window and the agent records the state to disk, paired with a per-state analysis task for later sessions. Trigger on /rom-debug:record-states, or when the user asks to record/capture save states for reverse-engineering a game, set up a state library so future sessions can analyze specific moments, or "park the game at the interesting bits so we can document them later". The capture loop is run interactively — the orchestrator drives it directly by default, optionally delegating to a sonnet subagent; the analysis itself is left as a checklist.
allowed-tools: Read Write Edit Task AskUserQuestion Bash(echo *) Bash(basename *) Bash(git rev-parse --show-toplevel) Bash(mkdir *) Bash(ls *) Bash(test *) Bash(cp *) mcp__romdev__loadMedia mcp__romdev__state mcp__romdev__playtest mcp__romdev__frame mcp__romdev__memory mcp__romdev__sprites mcp__romdev__input mcp__romdev__host mcp__romdev__cpu mcp__romdev__catalog mcp__romdev__platform
---

# record-states

Build a **library of save states** that lets future sessions document a ROM
cheaply. A `state(op:'load')` that drops you one input away from a behavior —
or onto a screen full of varied live objects — turns each documentation task
from minutes of re-navigation into a single call. The most valuable state in a
project routinely unlocks *many* analyses (in one real project a single
"one-input-from-the-event" capture unlocked 6+ separate tasks, and doubled as a
reusable multi-object RAM snapshot).

This skill is **capture-only**. It drives a human to record the states and
writes a paired **CAPTURE / ANALYSIS** checklist; it does **not** run the
analysis. The unchecked ANALYSIS items are left for later or scheduled sessions
(any model) to pick up — the same shape as a project's `TODO.md`, so those
sessions recognize them.

The capture loop is long, interactive, and token-heavy — Claude can't navigate
an unfamiliar game, so a human drives while the agent watches and records. The
session that invokes the skill (the orchestrator) runs that loop **directly** by
default: it is the agent that can prompt the human (`AskUserQuestion`) and record
each state. You *may* delegate the loop to a **sonnet** subagent to save
orchestrator tokens — but only if your harness lets a subagent reach the human
(many do not; see Step 2). Otherwise the orchestrator owns the loop.

This skill assumes a romdev disassembly project already exists (what later
ANALYSIS sessions will annotate). If there's no disassembly yet, use
`disasm({target:'project'})` first; that's out of scope here.

(Tool names here use the `mcp__romdev__` prefix, which assumes the server was
registered as `romdev`. If yours is registered under another name, match the
prefix — in `allowed-tools` and when calling — to that name. A plugin install,
for example, registers them as `mcp__plugin_rom-debug_romdev__…`. The romdev
tools may also be **deferred** — registered by name but with their schemas not
loaded — so a call can fail with the tool "not available"; load its schema first
(e.g. via `ToolSearch`) and retry. A spawned subagent inherits the same deferral
and must load the romdev tool schemas before its first call too.)

## Step 0 — Set up the data folder

All state and progress live under one folder, so sessions can find them:

```bash
PROJECT="$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")"
# ROM name = the loaded ROM's filename without extension, e.g. "rygar"
DATA="${CLAUDE_PLUGIN_DATA}/${PROJECT}-${ROM}"
mkdir -p "$DATA/states" "$DATA/tasks"
```

Use `<project name>-<rom name>` (e.g. `rygar-nes-guts-rygar`) so two ROMs in
one project don't collide. Confirm a ROM is loaded with `catalog(op:'status')`;
if not, `loadMedia` it first.

## Step 1 — Ensure `start_gameplay.state`

Every capture starts from a known mid-gameplay moment. If
`$DATA/states/start_gameplay.state` doesn't exist, it's just the first capture
target (the human plays past the title/intro into ordinary gameplay — a flat,
safe, enemy-free spot — and the agent records it via the loop below). Every
later capture loads it as the starting point.

> The state library is the single most valuable artifact here: it turns every
> future session's "get to the interesting moment" from minutes of navigation
> into one `state(op:'load')`. Record once, reuse forever.

## Step 2 — Run the capture loop

**By default the orchestrator runs the capture loop directly** — the Steps under
"The capture loop" below, once per target state, interviewing the human with
`AskUserQuestion` and recording each state itself. This is the reliable path: the
orchestrator is the agent the human can actually answer.

**Optional delegation (token-saving, harness-permitting).** You may hand the
whole loop to a **sonnet** subagent and stay a thin orchestrator — but first
confirm the subagent can actually drive it. In many harnesses a subagent has
**no `AskUserQuestion` and its plain-text prompts are not surfaced to the human**,
so it cannot run an interactive loop at all; it also inherits the deferred romdev
tools (it must load their schemas before its first call). If either is true, do
**not** delegate — run the loop yourself. When delegation does work, spawn:

```
Task(subagent_type="general-purpose", model="sonnet",
     description="Guide human through recording save states",
     prompt="""
You guide a HUMAN through recording a library of save states for ROM
reverse-engineering, using the romdev MCP. DATA=<$DATA>. REPO=<git root>.
Checklist=<$DATA/tasks/record_states.md>. start_gameplay exists=<yes/no>.

A) PLAN THE SET. Present the recommended-menu categories (below) and use
   AskUserQuestion to interview the human for game-specific moments worth a
   state. Produce an ordered list; each item = {name (kebab-case),
   moment (one line), setup-from-start_gameplay (exact steps), analysis-brief
   (what a later session should document from it)}. If start_gameplay is
   missing, make it the FIRST item.

B) FOR EACH item, run the capture loop (below), then write its paired CAPTURE
   (checked) + ANALYSIS (unchecked) entries to the checklist immediately, in
   the format given. Use today's date.

C) RETURN a summary: states written (paths), the checklist path, and anything
   the human skipped or deferred.

Follow the capture loop, recommended categories, checklist format, and gotchas
exactly as written in this skill. You OWN the single playtest window for the
whole run — never open a second interactive window.
""")
```

When you do delegate, `model="sonnet"` keeps the heavy interactive work off the
orchestrator, which then does only Step 0/1, the spawn, and the post-return
verification below. If you ran the loop yourself, you already did the per-capture
work inline — the same verification still applies.

After the loop finishes (the subagent returns, or the orchestrator's own run
ends), the orchestrator:

1. Confirms each promised `$DATA/states/<name>.state` exists (`ls`/`test`).
2. Confirms each is **already** backed up in the **repo's** `states/` dir (the
   capture loop copies it per-capture); re-copies any that's missing:
   `mkdir -p "$REPO/states" && cp "$DATA/states/<name>.state" "$REPO/states/"`.
3. Sanity-checks `record_states.md` (every CAPTURE that's `[x]` has a real
   file; every CAPTURE has a paired ANALYSIS).
4. **Reports** (see below).

## The capture loop (run by whoever owns the loop — the orchestrator by default, once per target state)

1. **Open the window:**
   `playtest(op:"open", scale:3, title:"Record <name> — play to <moment>, pause, and let me know")`.
2. **Brief the human:** load the starting point first
   (`state(op:"load", path=".../start_gameplay.state")`, except when recording
   start_gameplay itself), tell them the exact setup, and ask them to **play to
   the moment, pause, and signal** — e.g. answer an `AskUserQuestion` "Ready?"
   prompt. The pause may be the **game's own pause** OR the **emulator's
   pause** — either is fine. The human does **not** touch any save-state hotkey;
   the agent records the state.
3. **Record it yourself:** once the human signals,
   `host(op:"pause")` (idempotent — a no-op if they used the emulator pause,
   and it freezes the core if they only used the in-game pause), then
   `state(op:"save", path:".../states/<name>.state")` writes the blob straight
   to disk. Saving needs no stepping, so the window's real-time loop doesn't
   fight it. `host(op:"resume")` afterward in case they want to keep playing
   toward the next target.
4. **Verify the capture:** `playtest(op:"stop")`, then `state(op:"load")` the
   file you just wrote and inspect it — `frame(op:"screenshot")` plus
   `memory(op:"read")` / `sprites(op:"inspect")` — to confirm it froze the
   intended moment. Note a few concrete live RAM/slot values (player state byte,
   populated object slots, on-screen entity count) to enrich the analysis brief;
   those observations are what make the ANALYSIS item actionable later.
   **For a one-input-from-event capture, prove it actually triggers.** The
   on-disk file stays frozen at the cusp, but the loaded copy is a throwaway:
   resume if paused, `frame(op:"step", frames:N)`, and assert the event really
   fires (lives 2→1, a weapon-index byte 00→04, a boss-HP tick) — a frame that
   merely *looks* right may not. Record the before→after byte delta in the
   ANALYSIS brief: it confirms the state is genuine and hands the later session
   the exact byte to set a watchpoint on. Re-`load` the file afterward to return
   the host to the cusp.
5. **Back up to the repo immediately:** `mkdir -p "$REPO/states"` then
   `cp "$DATA/states/<name>.state" "$REPO/states/"`. Do this per-capture, not in
   a batch at the end — a long interactive run is routinely interrupted, and an
   in-loop copy means every state already captured survives the interruption.
6. **Write the checklist entries** (CAPTURE checked, ANALYSIS unchecked) in the
   format below, then move to the next target.

> Optional: a human who'd rather use the **emulator's own save-state hotkey**
> may — then persist that slot with `state(op:"export", fromSlot:<slot>,
> path:".../states/<name>.state")` (find the slot with `state(op:"list")`).
> But the default is the agent saving it, so the human only has to play + pause.

> **Capture opportunistically — the planned list is a menu, not a script.** The
> human sees the game live; when they call out that something interesting is one
> input away ("an enemy's about to hit me", "a pickup is drifting in"), pause and
> save it on the spot, then name it after the fact. Some of the best
> one-input-from-event states are ones nobody planned — don't make the human
> re-navigate to a named target when the moment is already on screen.

## Recommended-menu categories

Game-agnostic starting points (the human adds specifics). The best state puts a
later session as close as possible to the code of interest — ideally one known
input or one RAM poke away from the behavior — and freezes many objects in
varied states so one capture serves many analyses.

1. **Base `start_gameplay`** — past the intro, ordinary gameplay, calm spot.
   The reusable foundation every other capture (and session) loads first.
2. **One-input-from-a-complex-event** *(highest ROI)* — parked on the cusp of a
   rare event so a later session replays one input + sets a watchpoint: a
   special/charged move, item or weapon use, a pickup, taking damage, dying.
3. **Rich live-RAM snapshot** — a frame with many entity/object slots populated
   with *varied* values (a busy screen), reusable across many analyses with no
   new capture.
4. **Just-before-a-transition** — right before a level/area/mode change or a
   cutscene boundary, for the advance condition and the transition handler.
5. **Encounter-in-progress** — a boss/miniboss live on screen, for
   `/rom-debug:locate-value` HP hunts and the defeated branch.

## Checklist format (`$DATA/tasks/record_states.md`)

One section per state. A CAPTURE must be `[x]` before its ANALYSIS can start;
the ANALYSIS items are deliberately left unchecked for later/scheduled sessions
(any model) to pick up.

```markdown
# Record states — <project>-<rom>

CAPTURE tasks (human at the playtest window) + paired ANALYSIS tasks (any
model, later/scheduled). A CAPTURE must be [x] before its ANALYSIS can start.

## <moment name>  (category: one-input-from-event)
- [x] CAPTURE `states/<name>.state` (<date>) — in $DATA/states AND backed up at
      states/<name>.state in the repo. Setup from start_gameplay: <exact steps>.
      Live snapshot: <key RAM/slot values confirming the moment>.
- [ ] ANALYSIS (needs <name>.state): <subroutines / RAM addresses to document,
      and the technique — replay+write-watchpoint, /rom-debug:locate-value, etc.>.
```

## Resuming across sessions

There is no in-memory progress — disk is the source of truth:

1. Re-derive `$DATA` (Step 0).
2. Read `record_states.md`. The first `- [ ]` **CAPTURE** is where this skill
   continues. (Unchecked **ANALYSIS** items are not this skill's job — they're
   for later/scheduled documentation sessions.)
3. A CAPTURE that's already `[x]` with a state file on disk is done; don't
   re-record it.

## Gotchas (romdev)

- **Close the playtest window before deterministic stepping.** Its real-time
  loop overshoots `state(op:'load')` and races `frame(op:'step')` — so do the
  verify step (Step 4) only after `playtest(op:'stop')`. Saving while the window
  is open is fine: you `host(op:'pause')` first and the human has stopped
  pressing.
- **The window only fights you while a human is actively pressing** (~2s).
  `frame`/`input` responses carry a `humanCoDriveWarning`, and
  `playtest({op:'status'})` / `catalog({op:'status'})` expose `humanInputActive`
  — wait for the human to settle before you pause + save.
- **`state(op:'load')` needs the media loaded first and clears active cheats.**
  `loadMedia` before any `state(op:'load')`, and re-apply cheats after if you
  rely on them. A load also rewrites all of RAM — expected here, since loading
  is exactly how you reset to `start_gameplay`.
- **A `.state` is ROM-content-independent** (RAM/CPU/PPU/APU, not the ROM), so a
  state recorded on the stock ROM reloads cleanly into a rebuilt/patched ROM of
  the same core — which is what makes these states durable across the project.
- Call `catalog({op:'categories'})` for the tool set and `platform({op:'doc'})`
  for the platform's memory map before interpreting any address in a snapshot.

## Report

When you pause or finish, summarize: which states were recorded this session
(names + paths, and that each is backed up in the repo's `states/`), which
checklist CAPTURE items are now `[x]`, the next unchecked CAPTURE a future
session should pick up, and a reminder that the ANALYSIS items are queued for
later documentation sessions.
