# Run and Observe Shared procedure for any skill that closes a story which changes something a player can see. Referenced from the point of use in `/dev-story` (Phase 6) and `/story-done` (Phase 3). Those skills keep the one load-bearing imperative inline — **a parse check is not a run, and a story nobody looked at does not close** — and cite this file for how to look. `.claude/docs/coding-standards.md` is the authority for *what* evidence a story needs. This file is the procedure for *producing* the visual half of it. ## The rule Every story that changes something player-observable is **launched and observed before it closes**, and the observation is a **retained screenshot** in `production/qa/evidence/`. Compiling proves the code loads. Tests prove the logic. Neither proves the text fits in the box. The agent can see images: Claude Code's `Read` tool renders a PNG. The whole problem is getting a picture out of the engine unattended, and every supported engine has a native way. ## The procedure 1. **Launch windowed** at a fixed resolution — `1280x720` unless the project says otherwise — using `commands.run` from `project.yaml`. Never the headless / batch / null-RHI flag: those exist to *skip* rendering. 2. **Launch straight into the scene or map the story touched.** A feature that is only reachable through the main menu and three clicks is a feature the run will not reach. Stories should be written so their surface is reachable from a launch argument (a scene path, a map URL, a `--scene` flag). 3. **Wait, capture, quit.** Let the engine render real frames — 60 is a safe floor — capture one, and exit. Each engine's mechanism is below. 4. **Look.** `Read` the PNG. Compare what is on screen to the acceptance criteria. Clipped text, an overflowing panel, a missing element, the wrong colour — these are defects, and this is the only step that finds them. 5. **Retain.** Copy the image to `production/qa/evidence/[story-slug]/[NN]-[what-it-shows].png` and reference it from the evidence doc. A screenshot that was taken and discarded is an assertion, not evidence. 6. **Report** one `Run result:` line in the implementation summary — vocabulary below. ## Per engine | `engine.name` | Launch | Capture | Output lands in | Never use | Scaffold needed | |---|---|---|---|---|---| | Godot 4 | `godot --path . --windowed --resolution 1280x720 .tscn` | `--write-movie .png --quit-after ` | beside the given path, `00000000.png` … | `--headless` | **none** | | Unity 6 | built player `.exe -screen-width 1280 -screen-height 720 -screen-fullscreen 0 --scene ` | `ScreenCapture.CaptureScreenshot()` from a bootstrap script | the absolute path you pass | `-batchmode`, `-nographics` | one MonoBehaviour | | Unreal 5 | `MSYS_NO_PATHCONV=1 UnrealEditor.exe .uproject -game -windowed -ResX=1280 -ResY=720` | `HighResShot filename=` console command | `Saved/Screenshots/Windows/` | `-nullrhi`, `-unattended` alone | one actor or `-ExecCmds` | ### Godot 4 — zero scaffold ``` godot --path . --windowed --resolution 1280x720 --write-movie shots/shop.png --quit-after 60 res://scenes/shop.tscn ``` `--write-movie` is Movie Maker mode: with a `.png` target it writes a frame sequence and `--quit-after` sets the frame count. Filenames carry **8** zero-padded digits — `shop00000000.png` through `shop00000059.png` — so take the last. `--fixed-fps` is applied automatically. The scene path is positional; any `.tscn` can be launched directly, which is how step 2 is satisfied. Capture-on-trigger alternative, for a state a scene launch cannot reach: an autoload that on the wanted condition does `await RenderingServer.frame_post_draw`, then `get_viewport().get_texture().get_image().save_png(path)`, then `get_tree().quit()`. ### Unity 6 — one MonoBehaviour `-batchmode` displays nothing, so the run is either the **built player** or the editor in play mode. The player is the robust path: ``` Builds/Win64/Game.exe -screen-width 1280 -screen-height 720 -screen-fullscreen 0 --scene Shop --screenshot C:/abs/path/shop.png --after 60 ``` `-screen-*` are Unity's own player arguments. `--scene`, `--screenshot` and `--after` are **yours**: a `ScreenshotOnArg` MonoBehaviour in the bootstrap scene parses `System.Environment.GetCommandLineArgs()`, loads `--scene`, waits `--after` frames, calls `ScreenCapture.CaptureScreenshot(path)`, waits one more frame, then `Application.Quit()`. Pass an **absolute** path — the relative base differs between editor and player. `CaptureScreenshot` captures the last frame *presented*, so it must run after at least one rendered frame. `/test-setup` scaffolds this script under the code root. Building the player first costs 1–3 minutes on a small project; that is the price of looking at the thing you ship rather than the editor. ### Unreal 5 — one actor, or `-ExecCmds` ``` MSYS_NO_PATHCONV=1 UnrealEditor.exe Game.uproject /Game/Maps/Shop -game -windowed -ResX=1280 -ResY=720 ``` `-game` runs standalone without the editor UI. The map is a **positional URL immediately after the `.uproject`** — not a console command. **The `MSYS_NO_PATHCONV=1` prefix is required on Windows:** Claude Code runs commands in Git Bash, which rewrites any argument beginning with `/` as a Windows path, so `/Game/Maps/Shop` reaches the engine as `C:/Program Files/Git/Game/Maps/Shop` and the game stops on a "map not found" dialog nobody can see. Alternatively pass the map through `-ExecCmds="open /Game/Maps/Shop"`, which is not converted. Capture is the `HighResShot` console command, fired either from a dev actor (`Execute Console Command` on BeginPlay after a short delay, then `quit`) or from the launch line: ``` -ExecCmds="HighResShot filename=C:/abs/path/shop.png,Quit" ``` `-ExecCmds` is comma-separated. Output defaults to `Saved/Screenshots/Windows/`. **Do not `Quit` on the same frame as the request** — the file flushes late and an immediate quit can leave nothing on disk; a one-second delay before `quit` is enough. The Blueprint node `Take High Res Screenshot` is editor-only and does not work in a `-game` launch. ### Any engine — OS capture fallback On Windows, PowerShell with `System.Drawing` can `CopyFromScreen` into a bitmap and save it. It needs a visible desktop and the game window on top, and it captures whatever is there — it cannot be told "the shop panel". Use it when the engine-native path is unavailable, and say so in the run result. Linux CI with no display needs `xvfb-run` in front of the launch; `--headless` is not a substitute. ## The `Run result:` line Exactly one of three shapes, in the implementation summary: - **`Run result: OBSERVED — `** with the retained path. *"Shop panel renders, all 6 slots inside the frame, prices legible at 720p — `production/qa/evidence/shop-panel/01-shop-open.png`."* - **`Run result: NOT VERIFIED — `**. Engine binary not found, `commands.run` unset, launch crashed, screenshot empty. This is a **blocker** at the default gate level for Visual/Feel and UI stories, not a note — a story nobody looked at does not close. Under an explicit `testing.strict.visual`/`.ui: false` it is recorded and the story may close with the gap in `## Completion Notes`. - **`Run result: N/A — `**, only for a story with genuinely nothing observable — a pure data migration, a save-format change with no surface. Say why. "It's a Logic story" is not a reason: a damage formula has a number on screen somewhere. The run is **not waived at `qa.level: minimal`**. Tests are waived there; the look is not. It is the cheapest verification in the pipeline and the one whose absence produces a well-tested game that looks wrong. ## What a still can and cannot show **Can:** layout, clipping, overflow, presence, alignment, colour, legibility, that the right screen appeared at all. This is where shipped visual defects actually live. **Cannot:** timing, responsiveness, weight, animation curves, audio sync — the "feel" half of Visual/Feel. Godot's `--write-movie` gives a frame sequence; the other engines need OS video capture. Feel stays a human check in `/team-qa`. Say which half the screenshot covered. ## Reaching the state A screenshot of the title screen verifies nothing about depth 2. Either launch the scene/map directly (Godot scene arg, Unreal map URL, Unity `--scene`) or give the bootstrap a debug argument that sets state (`--gold=500 --depth=2`). Write stories so their surface is reachable one of those ways; a story that is not is a story that will be marked `NOT VERIFIED` every time. ## Finding the engine None of the three engines installs onto `PATH` on Windows. `engine.path` in `project.yaml` is where the editor lives; resolve it before concluding "no engine on this machine". Absence of a path is not absence of an engine. --- *Commands and APIs above verified against the official Godot 4.7, Unity 6.3 LTS and Unreal 5.8 references on 2026-09-14. `docs/engine-reference//` is the project's pinned authority; check it before trusting a version-qualified claim here.*