---
phase: 06.1-gap-closure-d-39-d-46-uat-2026-05-11
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
  - apps/client/test/e2e/cli-08-camera.e2e.test.ts
  - apps/client/test/e2e/cli-08-tiles.e2e.test.ts
  - apps/client/test/e2e/cli-08-nameplate.e2e.test.ts
  - apps/client/test/e2e/cli-08-anim.e2e.test.ts
  - .planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-HUMAN-UAT.md
  - .planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-D40-SPIKE.md
autonomous: true
requirements: [REQ-CLI-08]
must_haves:
  truths:
    - "D-40 root cause is identified by name (atlas frame-key miss | schema discriminant mismatch | Ed25519 verify failure) and recorded with reproducible repro log"
    - "4 Playwright e2e test files exist with @rebno/client fixture imports and skeleton assertions"
    - "06.1-HUMAN-UAT.md captures the 5-test re-run procedure with operator instructions"
  artifacts:
    - path: "apps/client/test/e2e/cli-08-camera.e2e.test.ts"
      provides: "Camera-follow scrollX/Y delta assertion (skeleton, GREEN after Wave 3)"
      contains: "camera.scrollX"
    - path: "apps/client/test/e2e/cli-08-tiles.e2e.test.ts"
      provides: "Tile count + TSide1 placement assertion (skeleton)"
      contains: "tileCount"
    - path: "apps/client/test/e2e/cli-08-nameplate.e2e.test.ts"
      provides: "Cyan local / white remote color rule assertion (skeleton)"
      contains: "getComputedStyle"
    - path: "apps/client/test/e2e/cli-08-anim.e2e.test.ts"
      provides: "Local + remote anim-rate smoke assertion (skeleton)"
      contains: "localFrame"
    - path: ".planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-D40-SPIKE.md"
      provides: "D-40 root-cause spike findings (read-only investigation)"
      contains: "Root cause:"
    - path: ".planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-HUMAN-UAT.md"
      provides: "UAT re-run instructions for Tests 1-5"
      contains: "Test 2"
  key_links:
    - from: "D40 spike findings"
      to: "Wave 2 RoomRenderer fix direction"
      via: "named root cause records which of the 3 candidates (atlas key / schema / Ed25519) to fix"
      pattern: "Root cause:"
    - from: "e2e test skeletons"
      to: "Wave 4 GREEN gates"
      via: "skeleton fixtures + __rebno hooks match Phase 6 e2e analog files"
      pattern: "waitForGameReady"
---

<objective>
Two outputs in one plan, both Wave 0 / discovery work that unblocks Wave 2:

1. **D-40 root-cause spike (read-only).** Per CONTEXT D6.1-105/RESEARCH Open Question #1, run the 15-minute three-step verification on staging to identify which of the three candidate causes (atlas frame-key miss, schema dual-union mismatch, Ed25519 verify failure) is short-circuiting `RoomRenderer.renderNew`. Record findings in `06.1-D40-SPIKE.md` so Wave 2's RoomRenderer plan applies the right fix.
2. **Wave 0 Playwright e2e skeletons + UAT instructions.** Create the 4 e2e test files referenced in VALIDATION.md as RED skeletons (imports + scaffolding + assertions that will fail until Wave 2-3 lands) plus the UAT re-run script.

Purpose: D-40 spike outcome decides whether the Wave 2 RoomRenderer fix is a 1-line atlas-key change (Pitfall 1), a schema migration (Pitfall 3), or a verify softening (Pitfall 2). The e2e skeletons give Wave 4 a complete frame to make GREEN once renderers and GameScene are wired up.

Output: spike findings doc + 4 RED e2e files + HUMAN-UAT script.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@CLAUDE.md
@.planning/PROJECT.md
@.planning/STATE.md
@.planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-CONTEXT.md
@.planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-RESEARCH.md
@.planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-PATTERNS.md
@.planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-VALIDATION.md
@.planning/phases/06-client-rebuild-mvp-gate-cli-08-hard-milestone/06-HUMAN-UAT.md

<interfaces>
<!-- Existing analog files copied/adapted: -->
<!-- apps/client/test/e2e/fixtures.ts exports: loginAs, waitForGameReady, accountA, accountB, inviteSuffix -->
<!-- apps/client/test/e2e/camera-follow.e2e.test.ts lines 16-89 = camera test analog -->
<!-- apps/client/test/e2e/sprite-state.e2e.test.ts lines 16-46 = anim test analog -->
<!-- apps/client/test/e2e/sprite-state.e2e.test.ts lines 48-59 = nameplate DOM-mirror analog -->

<!-- __rebno deterministic test hook (Phase 6 D-35 pattern) — fields used by these tests: -->
window.__rebno?.localFrame: string        // frame key e.g. "NaviRunD_002"
window.__rebno?.loadedTextures: string[]  // atlas keys loaded
window.__rebno?.tileCount: number         // RoomRenderer.renderNew this.group.getLength() (Wave 2 will populate)
window.__rebno?.bgframe: number            // BackgroundRenderer (Wave 2 will populate)

<!-- Playwright pattern (S-09): -->
import { test, expect, loginAs, waitForGameReady, accountA, accountB, inviteSuffix } from './fixtures.js';
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: D-40 root-cause spike — read-only investigation against staging</name>
  <files>.planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-D40-SPIKE.md</files>
  <read_first>
    - apps/client/src/render/RoomRenderer.ts lines 88-167 (the renderNew tile loop, where frame-key lookup happens around line 117)
    - apps/client/src/scenes/GameScene.ts lines 500-553 (onRoomLayout handler — where the schema discriminant `'room_id' in layout` decision sits AND the Ed25519 verify bail-out at lines 529-537)
    - apps/client/src/net/roomLayoutVerify.ts (Ed25519 verify implementation — Plan 04-12 / 06-07 deliverable)
    - apps/server/public/atlas-mvp.json (BEFORE the Plan 06.1-01 regen — read the CURRENT version to confirm whether `0023-Tile1_000` is a present key)
    - apps/server/public/pipeline-manifest.json (verify sprite IDs)
    - .planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-RESEARCH.md §Pitfall 1/2/3 lines 481-503 (the three candidate root causes + verification order)
    - .planning/phases/06-client-rebuild-mvp-gate-cli-08-hard-milestone/06-HUMAN-UAT.md (the UAT 2026-05-11 §"Re-Run" finding D-40 verbatim)
  </read_first>
  <action>
    Read-only investigation in this order (cheapest first per CONTEXT D6.1-105):

    1. **Atlas frame-key miss check (5 min).** Open `apps/server/public/atlas-mvp.json` (the pre-06.1-01 version — read with `git show HEAD:apps/server/public/atlas-mvp.json | head -200` if needed). Confirm whether `0023-Tile1_000` exists as a frame key. Cross-reference against any `tileset_sprite_id` value the server emits in `mvp-lobby/000` room layout. Find the layout JSON via `grep -rn "tileset_sprite_id" apps/server/` and read the resulting file. If the layout references a `tileset_sprite_id` that is NOT present in `atlas-mvp.json`, the root cause is atlas-key miss.

    2. **Schema discriminant check (5 min).** Read `apps/client/src/scenes/GameScene.ts` `onRoomLayout` handler around lines 500-553. Confirm the discriminant logic (`'room_id' in layout` or similar). Read the server's `room_layout` broadcast packing in `apps/server/src/RebnoRoom.ts` (find via `grep -nR "room_layout" apps/server/src/`). Confirm whether the msgpackr-packed shape decoded on the client preserves `room_id` at the top level or nests it. If nested OR if `room_id` is decoded as undefined, the root cause is schema mismatch.

    3. **Ed25519 verify failure check (5 min).** Read `apps/client/src/net/roomLayoutVerify.ts`. Confirm whether the canonical-stringification path matches the server-side signing path (per Pitfall 2 — JS `JSON.stringify` insertion-order vs server's on-disk order). Read the 06-07-SUMMARY.md note about "approximate verify" if present. Decide whether verify is likely failing.

    Record findings in `.planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-D40-SPIKE.md` with: (a) which of the three causes is the root cause, (b) the specific file:line that exhibits the bug, (c) the recommended Wave 2 fix shape (1-line atlas-key change | discriminant logic update | verify-softening to log-and-render), (d) any secondary issues observed. The doc is consumed by the Wave 2 RoomRenderer plan as authoritative direction.

    **No code changes in this task.** Spike is read-only.
  </action>
  <verify>
    <automated>test -f .planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-D40-SPIKE.md &amp;&amp; grep -qE '^Root cause: (atlas frame-key miss|schema dual-union mismatch|Ed25519 verify failure)' .planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-D40-SPIKE.md</automated>
  </verify>
  <acceptance_criteria>
    - File `.planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-D40-SPIKE.md` exists.
    - File contains a line matching the regex `^Root cause: (atlas frame-key miss|schema dual-union mismatch|Ed25519 verify failure)` — pins the spike output to one of the three candidate causes per checker W3.
    - File cites a specific file:line for the bug location (e.g. `apps/client/src/scenes/GameScene.ts:529`).
    - File recommends a specific Wave 2 fix shape (≤ 3 sentences).
    - No source files modified in this task (verify via `git status --short apps/ packages/ tools/` shows clean).
  </acceptance_criteria>
  <done>Spike doc written with named root cause + line citation + recommended fix shape.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: 4 RED Playwright e2e skeletons + UAT re-run script</name>
  <files>apps/client/test/e2e/cli-08-camera.e2e.test.ts, apps/client/test/e2e/cli-08-tiles.e2e.test.ts, apps/client/test/e2e/cli-08-nameplate.e2e.test.ts, apps/client/test/e2e/cli-08-anim.e2e.test.ts, .planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-HUMAN-UAT.md</files>
  <read_first>
    - apps/client/test/e2e/fixtures.ts (full file — exports loginAs, waitForGameReady, accountA, accountB, inviteSuffix; Playwright test definition is re-exported as `test`/`expect`)
    - apps/client/test/e2e/camera-follow.e2e.test.ts lines 16-89 (analog — exact template for cli-08-camera)
    - apps/client/test/e2e/sprite-state.e2e.test.ts lines 16-46 (analog — frame-string assertion for cli-08-anim)
    - apps/client/test/e2e/sprite-state.e2e.test.ts lines 48-59 (analog — DOM-mirror count + computed-style assertion for cli-08-nameplate)
    - apps/client/test/e2e/cli-08.e2e.test.ts (existing two-player smoke from Phase 6)
    - .planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-PATTERNS.md §"Test patterns" lines 855-872 + §S-08 (`__rebno` hook) + §S-09 (fixtures)
    - .planning/phases/06-client-rebuild-mvp-gate-cli-08-hard-milestone/06-HUMAN-UAT.md §"Re-Run 2026-05-11" (D-39..D-46 source — copy into UAT instructions)
    - .planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-VALIDATION.md (Wave 0 e2e file list + per-task verification map)
  </read_first>
  <behavior>
    - All 4 e2e files exist with correct fixture imports and skeleton assertion bodies.
    - Each file fails (RED) when run against current main — because Wave 2/3 work hasn't landed yet (tileCount field missing, bgframe missing, color rule not implemented, camera setBounds still in place).
    - Each file tagged `// [int->REQ-CLI-08]`.
    - 06.1-HUMAN-UAT.md captures: Test 1 alpha-keyed sprites visible, Test 2 two-player CLI-08 milestone (capture `06.1-CLI-08-milestone.mp4`), Test 3 anim rate consistency, Test 4 camera follow smoothness, Test 5 walkable-edge block + background scroll/visible-past-edge.
  </behavior>
  <action>
    Create `apps/client/test/e2e/cli-08-camera.e2e.test.ts`: import from `./fixtures.js`. Test body: `loginAs(page, accountA, inviteSuffix)` → `waitForGameReady(page)` → read `await page.evaluate(() => (window as any).__rebno?.cameraScrollX ?? null)` baseline → `await page.keyboard.down('KeyD')` → `await page.waitForTimeout(200)` → read same field → `await page.keyboard.up('KeyD')` → `expect(after - before).toBeGreaterThan(15)` (RUN_SPEED 5 × 6 sim-ticks in 200ms = 30 px expected; threshold 15 for jitter). Mirror the exact shape of `camera-follow.e2e.test.ts` lines 16-89. Tag `// [int->REQ-CLI-08]`.

    Create `apps/client/test/e2e/cli-08-tiles.e2e.test.ts`: `loginAs` + `waitForGameReady` → `expect(await page.evaluate(() => (window as any).__rebno?.tileCount ?? 0)).toBeGreaterThan(0)`. Add a secondary assertion `expect((window as any).__rebno?.loadedTextures).toContain('atlas-mvp')`. Add a smoke for TSide1 presence: `expect(await page.evaluate(() => (window as any).__rebno?.tsideCount ?? 0)).toBeGreaterThan(0)`. Tag `// [int->REQ-CLI-08]`.

    Create `apps/client/test/e2e/cli-08-nameplate.e2e.test.ts`: `loginAs(page, accountA, inviteSuffix)` → `waitForGameReady(page)` → locate `[data-nameplate]` DOM mirror via `page.locator('[data-nameplate]')` → `expect(count).toBeGreaterThan(0)` → assert local nameplate's `color` via `evaluate(el => getComputedStyle(el).color)` equals `rgb(0, 255, 255)`. For remote-color assertion, spawn a second `loginAs(page2, accountB, inviteSuffix)` in a parallel context AND from page1 assert the OTHER nameplate's color is `rgb(255, 255, 255)`. Tag `// [int->REQ-CLI-08]`.

    Create `apps/client/test/e2e/cli-08-anim.e2e.test.ts`: `loginAs` + `waitForGameReady` → press `KeyD` for 1000ms → sample `(window as any).__rebno?.localFrame` every 50ms → collect distinct frame keys → `expect(unique.size).toBeGreaterThanOrEqual(3)` (at FRAMES_PER_TICK_AT_RUN=0.5 with 6 frames over 1000ms = 15 frame advances). Tag `// [int->REQ-CLI-08]`.

    Create `.planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-HUMAN-UAT.md`: 5-test re-run script following the existing `.planning/phases/06-client-rebuild-mvp-gate-cli-08-hard-milestone/06-HUMAN-UAT.md` format. Tests: (1) alpha-keyed sprites visible (no opaque magenta), (2) two-player CLI-08 milestone (capture `06.1-CLI-08-milestone.mp4`), (3) anim rate consistency local vs remote, (4) camera follow smoothness + nameplate legibility + Fixedsys font load, (5) walkable-edge block + bkdraw background scroll + visible past room edge. Each test: preconditions, exact reproduce steps, pass criteria. Tag `<!-- [doc->REQ-CLI-08] -->`.
  </action>
  <verify>
    <automated>pnpm --filter @rebno/client tsc --noEmit -p test/e2e</automated>
  </verify>
  <acceptance_criteria>
    - All 4 files exist under `apps/client/test/e2e/` with the exact filenames listed in files_modified.
    - `apps/client/test/e2e/cli-08-camera.e2e.test.ts` contains the literal string `__rebno?.cameraScrollX` AND `expect(after - before).toBeGreaterThan`.
    - `apps/client/test/e2e/cli-08-tiles.e2e.test.ts` contains `__rebno?.tileCount` AND `__rebno?.tsideCount`.
    - `apps/client/test/e2e/cli-08-nameplate.e2e.test.ts` contains `rgb(0, 255, 255)` AND `rgb(255, 255, 255)`.
    - `apps/client/test/e2e/cli-08-anim.e2e.test.ts` contains `__rebno?.localFrame` AND `toBeGreaterThanOrEqual(3)`.
    - Each file contains `// [int->REQ-CLI-08]` literal tag.
    - `pnpm --filter @rebno/client tsc --noEmit` exits 0 (type-checks pass even though assertions will fail at runtime).
    - `.planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-HUMAN-UAT.md` exists with sections `## Test 1` through `## Test 5`.
    - HUMAN-UAT.md contains `<!-- [doc->REQ-CLI-08] -->`.
    - Running these e2e tests now (before Wave 2-3) MUST exit non-zero (RED). RED status is the expected initial state — captured in the task completion note.
  </acceptance_criteria>
  <done>4 e2e RED skeletons committed + HUMAN-UAT script committed; tsc clean; RED captured.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| (none for this plan — discovery + test-scaffolding work) | No new runtime code paths introduced |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-06.1.03-01 | Information disclosure | spike doc may reference internal architecture | accept | Doc lives under `.planning/`; repo is private through Phase 7 per CLAUDE.md hard rule #8 |
</threat_model>

<verification>
- 06.1-D40-SPIKE.md contains a named root cause and a file:line citation.
- 4 e2e files compile (`pnpm --filter @rebno/client tsc --noEmit`).
- 06.1-HUMAN-UAT.md covers all 5 tests with reproduce steps.
- Spike outcome IS the input that the Wave 2 RoomRenderer plan uses to pick its fix shape.
</verification>

<success_criteria>
- D-40 root cause named and located before Wave 2 starts.
- e2e skeletons in place so Wave 4 has the GREEN gate ready.
- UAT instructions concrete enough for operator to execute without further discussion.
</success_criteria>

<output>
After completion, create `.planning/phases/06.1-gap-closure-d-39-d-46-uat-2026-05-11/06.1-03-SUMMARY.md`: link to the spike doc, list the 4 e2e files, and record the RED test runtime status as expected baseline.
</output>
