| name | bringup |
| description | Drive and observe the BottleShip emulator to bring up a game, using the AI-agent harness (window.__BS__.harness + bun tools/harness.ts). Use when loading a game, making it reach a menu/level, diagnosing why it crashes/hangs/renders black, or writing a repeatable bring-up/regression script. Operationalizes CLAUDE.md's debugging workflow on top of the harness verbs. Use the project's CDP harness, not a browser MCP (chrome-devtools MCP is disabled for this project). |
BottleShip Bring-up
The harness turns bring-up from "poke dbg.* by hand, grep logs, guess from
screenshots" into fluent, self-judging automation. It is headless (CLI/CDP)
and in-page (window.__BS__.harness). Logic lives in the worker
HarnessService; the page facade and CLI are thin transports over one
harness_rpc {id,cmd,args}→{id,ok,result|error} contract.
1. Preconditions — harness up
bun tools/harness.ts up
Launches/attaches Chrome with --autoplay-policy=no-user-gesture-required (so
audio unlocks with no gesture — no canvas click needed in automation), opens
http://localhost:5174/?game=dev, arms log streaming, and probes Vite(:5174
/health) + log-server(:3001 /health) + Chrome(:9333). Bring the dev servers
up first: bun run dev and bun run dev:logs (start the log
server BEFORE streaming). bun tools/harness.ts health re-probes.
2. Drive
A fluent chain (bun tools/harness.ts run <script.harness.ts>, or in the browser
console await window.__BS__.harness.chain()....run()):
import { harness } from "../harness";
await harness()
.streamLogs(["SYSTEM","DDRAW"])
.openWgb("blade-of-darkness")
.waitForEvent("dialogShow")
.click("Play Game")
.tickFrames(120)
.waitUntil(() => read32(0x6b7bf8) === 0)
.expectSurfaceNonBlack("primary")
.state(["surfaces","threads"])
.run();
Skip intros with the bundle's skipVideo. audioGesture() exists only for a
manually-opened browser; automation uses the autoplay flag.
3. Observe
state([...]) — windows/surfaces/memory/threads/rings/audio/video/modules/cpu/screen as one POJO.
shot({save}) — PNG of the on-screen frame (the canvas is an OffscreenCanvas —
a screenshot is ground truth; the main thread can't read it).
textures() + dumpSurface(ptr|'primary') — gallery + per-surface PNG to logs/debug/.
surfacePixels(sel) / expectSurfaceNonBlack(sel) — cheap liveness from a subsampled readback.
4. Diagnose
- Prefer API breakpoints (
breakOnApi("d3d9.*")) — JS layer, no JIT-off,
resolves on first hit with args + caller. Best for "where does it first touch X".
breakOnExport("d3d9.dll!Direct3DCreate9"), breakOnSymbol("core!UInput::ReadInput")
(needs loadSymbols(module, {name:rva}) from the RE layer first), watchMem(addr),
breakOn(eip) — all require JIT OFF (auto-enabled; perf collapses while
armed — clearBreaks() to restore). Addresses inside the async-park spin loop
are refused (CLAUDE.md §3.5).
- Read the streamed log;
events(n) shows recent harness events; on a WASM trap a
fault event carries the fault-grade snapshot.
5. Hypothesis from DATA, not reasoning
Confirm with a dump / a logged value (a dumpSurface PNG, a state field, a
breakOnApi snapshot) before theorizing about DC topology / vtable layout — the
canvas-vs-selected-bitmap distinction and multi-DC composites are easy to mis-model
(CLAUDE.md diagnostic discipline).
6. Fix → re-run → keep tools, drop probes
Every .run() writes a re-runnable logs/harness/run-N.harness.ts (journal). Turn
the winning chain into a checked-in *.harness.ts regression script. Remove
one-off probes; keep only reusable harness verbs.
Hard rules (don't relearn these)
- Quality gate order (CLAUDE.md):
bun tools/generate-index.ts →
bun tools/validate-signatures.ts → bun tools/validate-struct-offsets.ts →
bun run typecheck.
- Reload, not HMR, after editing
src/worker (HMR doesn't reload the worker
entry and hangs the game). The harness ships in the worker bundle — iterate via
a page reload.
- JIT-off collapses perf — only EIP/export/symbol/watch breakpoints need it;
prefer API breakpoints.
clearBreaks() when done.
- Audio needs the autoplay flag (
harness up) or a real transport click; a
synthetic event won't satisfy the browser autoplay policy.
Division of labor
The skill = workflow/checklist; the harness (src/worker/harness/,
src/harness/, tools/harness.ts) = capability/verbs; CLAUDE.md = invariants.
Bundled examples: tools/examples/bringup.harness.ts (template),
tools/examples/diagnose-eip.harness.ts (API-breakpoint + waitUntil).