--- phase: 02 plan: 06 type: execute wave: 0 depends_on: [02-01, 02-02, 02-03, 02-04, 02-05] gap_closure: true files_modified: - src/index.css - src/main.tsx - src/index.css.test.ts - src/store/session-slice.ts - src/content/schemas/ui-strings.ts - content/seasons/01-soil/ui-strings.yaml - src/ui/first-run/FirstRunHint.tsx - src/ui/first-run/FirstRunHint.test.tsx - src/ui/first-run/index.ts - src/ui/index.ts - src/App.tsx - src/render/garden/tile-renderer.ts - src/render/garden/tile-renderer.test.ts - src/render/garden/gate-renderer.ts - src/render/garden/gate-renderer.test.ts - tests/e2e/season1-loop.spec.ts autonomous: true requirements: [GARD-01, AEST-07, UX-01] tags: [gap-closure, uat, css, first-run-hint, tile-contrast, gate-context, mvp] must_haves: truths: - "G1 closed: has background #1a1a1a + serif color #e8e0d0 + zero margin from frame one — no white halo around the dark canvas (ROADMAP SC1 supplemental — UX-01 lived experience)" - "G2 closed: after BeginScreen dismisses on first run, a single bible-voice line is visible (e.g. 'Begin where the soil is bare.') from `season1.ui_strings.first_run_hint` in ui-strings.yaml — never hardcoded (CLAUDE.md tone constraint, GARD-01 lived experience)" - "G2 closed: FirstRunHint auto-dismisses on the first successful plantSeed dispatch and stays dismissed for the session; reload shows it again until first plant (A-Dark-Room first-run-of-this-tab UX, NOT save-state)" - "G2 closed: `firstRunHintDismissed` lives in src/store/session-slice.ts (NOT in V1Payload — no migrations[2]); resets on hard reload by design" - "G3 closed: empty-tile outline is brighter than 0x4d4d52 (final value ~0x5a5a60) AND the hover state contrasts the resting state (~0x7a7a82 outline + slight fill alpha bump) so the 4×4 grid reads as legible interactive surfaces against #1a1a1a — no painted assets (Phase 3 deferral preserved, GARD-01 lived experience)" - "G4 closed: gate-renderer.ts adds a faint vertical wall band (Phaser primitive — alpha 0.15-0.20 against #1a1a1a) at the gate's column connecting top-to-bottom of the canvas, so the gate reads as part of a wall rather than a floating rectangle (Bible 'walled garden', AEST-07 supplemental coverage)" - "Phase 3 watercolor + cello deferral preserved: every fix uses Phaser primitives or one CSS file. NO painted assets. NO new image loads. NO new npm dependencies." - "Tests landed for all 4 gaps: src/index.css.test.ts (G1), src/ui/first-run/FirstRunHint.test.tsx (G2), src/render/garden/tile-renderer.test.ts (G3), src/render/garden/gate-renderer.test.ts (G4)." - "Playwright tests/e2e/season1-loop.spec.ts extended with two assertions: (a) document.body computed style backgroundColor is rgb(26, 26, 26) after navigation, (b) the FirstRunHint line is visible after Begin dismiss, (c) the FirstRunHint is gone after the first plantSeed dispatches." - "After execution: gsd-verifier handoff is unblocked. The 4 gaps in 02-VERIFICATION.md frontmatter `gaps:` block clear (status gaps_found → verified). The 6 HUMAN-UAT.md tone items remain pending (out of scope for this plan)." - "All 24 Phase-2 REQ-IDs remain structurally PASS — none of these gap-fix changes regress any existing test (full `npm run ci` exits 0 + Playwright e2e exits 0)." artifacts: - path: src/index.css provides: "Global page styles — body bg #1a1a1a, color #e8e0d0, zero margin, full viewport height, serif family, #game-container centered. Imported once from src/main.tsx so Vite bundles it into the entry chunk. ~15 lines." - path: src/index.css.test.ts provides: "Vitest smoke test asserting the CSS rules are present in the source file (file-read assertion; sufficient for a single-file static stylesheet)." - path: src/ui/first-run/FirstRunHint.tsx provides: "FirstRunHint component — renders a single bible-voice line when session.firstRunHintDismissed is false AND session.beginGateDismissed is true; null otherwise. Reads the line from uiStrings[1].first_run_hint (externalized per STRY-09)." exports: ["FirstRunHint"] - path: src/ui/first-run/FirstRunHint.test.tsx provides: "Vitest cases: hidden when beginGateDismissed=false; visible after Begin dismiss; hidden when firstRunHintDismissed=true; renders the externalized string (not hardcoded); auto-dismisses when a plantSeed command commits (selectFirstPlantHasOccurred subscription)." - path: content/seasons/01-soil/ui-strings.yaml provides: "first_run_hint: added under the season-1 UI strings tree. Candidate copy ranked per Step 1: 'Begin where the soil is bare.' (Recommended — bible-voice) / 'The soil is waiting.' (alternative — quieter) / 'Click a tile to plant.' (functional fallback)." - path: src/store/session-slice.ts provides: "Adds `firstRunHintDismissed: boolean` + `dismissFirstRunHint()` action. Session-state only — NEVER added to V1Payload (per scope_constraint #3 — no migrations[2])." - path: src/render/garden/tile-renderer.ts provides: "OUTLINE_COLOR brightened to ~0x5a5a60; OUTLINE_HOVER brightened to ~0x7a7a82; hover adds a slight fill alpha bump on the hit rectangle (no animation noise — pointer-driven, reduced-motion-friendly)." - path: src/render/garden/tile-renderer.test.ts provides: "Vitest cases: drawTiles produces 16 tile groups; outline draw call uses OUTLINE_COLOR=0x5a5a60 (assert via mocked Phaser.Scene.add.graphics call args); pointerover handler uses OUTLINE_HOVER=0x7a7a82 (assert via stubbed event)." - path: src/render/garden/gate-renderer.ts provides: "drawGate() additionally adds a faint vertical wall band Phaser primitive at the gate's column (x ≈ GATE_X) spanning the canvas height (y=0 → y=768), color GATE_COLOR with alpha ~0.18. Stored on GateGameObjects as `wall: Phaser.GameObjects.Rectangle`." - path: src/render/garden/gate-renderer.test.ts provides: "Vitest case: drawGate adds the wall primitive at the expected x with low alpha (assert via mocked Phaser.Scene.add.rectangle call args matching the wall band signature: x near GATE_X, full canvas height, GATE_COLOR, alpha ~0.18)." - path: tests/e2e/season1-loop.spec.ts provides: "Three new assertions threaded into the existing PIPE-07 spec: (a) body computed bg is rgb(26, 26, 26) on first nav, (b) FirstRunHint visible after Begin click, (c) FirstRunHint gone after the first plantSeed enqueue." key_links: - from: src/main.tsx to: src/index.css via: "import './index.css'; — Vite bundles the CSS into the entry chunk so body styles apply before React mounts" pattern: "import.*index\\.css" - from: src/App.tsx to: src/ui/first-run/FirstRunHint.tsx via: " mounted alongside Letter/Settings/etc." pattern: " **Gap-closure plan. Depends on Plans 02-01..02-05 (all already executed).** This plan closes the 4 first-impression UX gaps surfaced by the 2026-05-09 live UAT walkthrough on the dev server. All 24 Phase-2 REQ-IDs are structurally PASS; the test suite cannot detect "what does a new player see on frame one?" — the gaps are minimum-viable functional UX, NOT Phase-3 aesthetic deferrals. **Scope discipline (from 02-VERIFICATION.md frontmatter `gaps:` block):** - G1 BLOCKING — no global page CSS — fix shape: src/index.css ~15 lines. - G2 BLOCKING — no first-run prompt after Begin — fix shape: tiny FirstRunHint component + session flag. - G3 HIGH — tile outlines too dim — fix shape: brighten OUTLINE_COLOR + clearer hover in tile-renderer.ts. - G4 MEDIUM — gate visual stands alone — fix shape: faint vertical wall band primitive in gate-renderer.ts. **What this plan must NOT do:** - No painted assets (Phase 3 watercolor deferral preserved). - No new npm dependencies (CSS is plain CSS imported via Vite native). - No V1Payload changes / no migrations[2] (firstRunHintDismissed is session state, not save state). - No re-litigation of typographic Begin screen tone (HUMAN-UAT.md item 4 covers that — out of scope here). - No tone judgment on Lura's voice or letter cadence (HUMAN-UAT.md items 1-2 — out of scope). **Wave structure: single wave (Wave 0) since all 4 gap-fix tasks are file-disjoint and parallel-safe within the plan.** G1, G2, G3, G4 do not overlap on `files_modified`: - G1 owns: src/index.css, src/main.tsx, src/index.css.test.ts. - G2 owns: content/seasons/01-soil/ui-strings.yaml, src/store/session-slice.ts, src/ui/first-run/* (new files), src/ui/index.ts, src/App.tsx. - G3 owns: src/render/garden/tile-renderer.ts, src/render/garden/tile-renderer.test.ts. - G4 owns: src/render/garden/gate-renderer.ts, src/render/garden/gate-renderer.test.ts. - The Playwright e2e extension touches a shared file (tests/e2e/season1-loop.spec.ts) and runs LAST as Task 5 — the integration/verification task. **Tasks: 5** (one per gap + a final integration task that extends the e2e and runs full ci). Estimated context cost ~30-40% (well within budget for a gap-closure plan). Close the 4 first-impression UX gaps that the 2026-05-09 live UAT surfaced. Each fix uses Phaser primitives or a single CSS file — no painted assets, no new dependencies, no V1Payload changes. After execution: the dark canvas no longer floats in a white viewport (G1), a first-time player sees one bible-voice instructional line after Begin dismisses (G2), the 4×4 tile grid reads as legible interactive surfaces with a clearer hover state (G3), and the gate reads as part of a wall via a faint vertical primitive band (G4). Purpose: Unblocks `/gsd-verify-work` Phase 2 sign-off. The 4 gaps in 02-VERIFICATION.md frontmatter `gaps:` block clear; the 6 HUMAN-UAT.md tone items remain pending (separate workflow). The Phase-2 vertical slice that "could plausibly ship as a free standalone Season-1 prologue" actually feels like one to a brand-new player — the loop is intuitive on frame one. Output: 4 gap fixes + 4 unit tests + 3 e2e assertions threaded into tests/e2e/season1-loop.spec.ts. `npm run ci && npm run test:e2e` exits 0. 02-VERIFICATION.md ready for re-verifier pass to flip status from gaps_found → verified. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @CLAUDE.md @.planning/anti-fomo-doctrine.md @.planning/phases/02-season-1-vertical-slice-soil/02-CONTEXT.md @.planning/phases/02-season-1-vertical-slice-soil/02-PATTERNS.md @.planning/phases/02-season-1-vertical-slice-soil/02-VERIFICATION.md @.planning/phases/02-season-1-vertical-slice-soil/02-05-letter-settings-e2e-SUMMARY.md From src/main.tsx (currently 14 lines, no CSS import): ```typescript import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './App.tsx'; const rootEl = document.getElementById('root'); if (!rootEl) { throw new Error('Root element #root not found in index.html'); } createRoot(rootEl).render( ); ``` From index.html (the body has only `
` — Phaser parents to `#game-container` rendered by `` which renders `
` inside `
` from App.tsx): ```html
``` From src/game/main.ts (Phaser config — backgroundColor #1a1a1a; parent 'game-container'): ```typescript const config: Phaser.Types.Core.GameConfig = { type: Phaser.AUTO, width: 1024, height: 768, parent: 'game-container', backgroundColor: '#1a1a1a', scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH }, scene: [Boot, Garden], }; ``` From src/store/session-slice.ts (Plan 02-01 + 02-05 — current SessionSlice): ```typescript export interface SessionSlice { beginGateDismissed: boolean; persistenceToastShown: boolean; showPersistenceToast: boolean; letterOverlayOpen: boolean; pendingLetterEventBlock: unknown | null; compostBeatTick: number; // ... actions dismissBeginGate: () => void; setPersistenceToastShown: (v: boolean) => void; setShowPersistenceToast: (v: boolean) => void; openLetter: (block: unknown) => void; dismissLetter: () => void; bumpCompostBeat: () => void; } // Default state: beginGateDismissed=false, ... ``` From src/App.tsx (Plan 02-05 — currently mounts BeginScreen, SeedPicker, FragmentRevealModal, JournalIcon, LuraDialogue, Letter, Settings, PersistenceToast, CompostToast): ```typescript return (
setSettingsOpen(false)} />
); ``` From src/ui/begin/BeginScreen.tsx (Plan 02-02 — single fixed-position dialog covering canvas; dismissed via session.beginGateDismissed; existing pattern to copy for FirstRunHint shape): ```typescript export function BeginScreen(): JSX.Element | null { const dismissed = useAppStore((s) => s.beginGateDismissed); const dismissBeginGate = useAppStore((s) => s.dismissBeginGate); if (dismissed) return null; // ... fixed-position dialog with title/subtitle/Begin button } ``` From src/render/garden/tile-renderer.ts (Plan 02-02 — current dim values that G3 fixes): ```typescript const OUTLINE_COLOR = 0x4d4d52; // ← G3: brighten to 0x5a5a60 const OUTLINE_HOVER = 0x6e6e75; // ← G3: brighten to 0x7a7a82 const OUTLINE_ALPHA = 0.6; // drawTiles creates 16 tile graphics + hit rectangles; pointerover/pointerout // swap the outline color via drawOutline(). ``` From src/render/garden/gate-renderer.ts (Plan 02-04 — current gate primitives; G4 adds a wall band): ```typescript const GATE_X = 880; const GATE_Y = 384; const GATE_COLOR = 0x6e6e75; const GATE_HIT_W = 80; const GATE_HIT_H = 120; // drawGate adds: body (rectangle), glow (rectangle, alpha 0), hit (transparent // interactive rectangle). G4 adds a 4th: wall (vertical band at GATE_X, // full canvas height 768, low alpha ~0.18). ``` From content/seasons/01-soil/ui-strings.yaml (Plan 02-02 + 02-05 current shape — G2 adds first_run_hint): ```yaml season: 1 begin: title: "The Last Garden" subtitle: "tend" cta: "Begin" seed_picker: title: "Sow" cancel: "Not yet" post_harvest_beat: - "The earth remembers." - "Something stayed." - "It rests where it grew." journal: empty_state: "Nothing yet. Plant something." back: "Close" settings: title: "Settings" export: "Save to a copy" import: "Restore from a copy" restore_snapshot: "Earlier garden" persistence_denied_toast: "The garden may forget, if your browser asks it to." plants: rosemary: "Rosemary" yarrow: "Yarrow" winter-rose: "Winter-rose" # ← G2 ADDS: # first_run_hint: "" ``` From src/content/schemas/* (Plan 01-04 — UiStringsSchema validates ui-strings.yaml at build time): The Zod schema is currently typed for the structure above. Task 2 may need to add `first_run_hint: z.string()` to the schema if strict-typed parsing rejects unknown keys. Read `src/content/schemas/index.ts` to confirm whether the schema is strict (z.object().strict()) or lenient (default — extra keys ignored). Almost certainly lenient, in which case no schema change is needed; if strict, Task 2 must extend the schema. From src/store/index.ts (Plan 02-01 + 02-05 barrel — actions exposed to React via useAppStore): useAppStore is a vanilla zustand store; subscribing to a single field (e.g. `useAppStore(s => s.beginGateDismissed)`) is the canonical pattern. The Garden scene also subscribes via `appStore.subscribe(selector, callback)` for non-React tick-driven repaints. From src/sim/garden/commands.ts (Plan 02-02/02-03/02-04 — plantSeed dispatch path): The store enqueues a command via `enqueueCommand({kind: 'plantSeed', tileIdx, plantTypeId})`. The Garden scene's update loop drains the queue once per tick. Detection of "first plant has occurred" can be done by subscribing to a derived selector — simplest reliable signal: subscribe to `harvestedFragmentIds.length OR tiles.some(t => t?.plant !== null)`. The cleanest path: subscribe to `tiles` and dismiss FirstRunHint the first time any tile has `plant !== null`. (Subscribing to plantSeed enqueues directly is brittle; the queue may apply asynchronously.) From tests/e2e/season1-loop.spec.ts (Plan 02-05 — current full-loop spec; Task 5 extends with 3 assertions): ```typescript test('load → begin → plant → fast-forward → harvest → reveal → journal → reload → persist', async ({ page }) => { await page.goto('/?devtime=fake'); await ensureFreshSave(page); await page.goto('/?devtime=fake'); // Begin button visible, click it → bootstrap audio → tiles drain. // ... existing 16-step assertion chain. }); ``` Task 5 threads: - After step 1 (initial nav): assert document.body computed background-color is `rgb(26, 26, 26)`. - After step 3 (Begin dismissed): assert the FirstRunHint element is visible. - After step 5/6 (plantSeed enqueued + applied to tile 0): assert the FirstRunHint is gone. From .planning/phases/02-season-1-vertical-slice-soil/02-VERIFICATION.md frontmatter `gaps:` block: - G1: src/main.tsx + no src/index.css; fix CSS body bg #1a1a1a + zero margin + serif color #e8e0d0. - G2: post-BeginScreen state — no instruction; fix tiny FirstRunHint with copy from ui-strings.yaml; auto-dismiss on first plant; session-state only (no migrations[2]). - G3: tile outlines too dim — brighten OUTLINE_COLOR (~0x4d4d52 → ~0x5a5a60) + add clearer hover (~0x7a7a82 + slight fill alpha bump). - G4: gate stands alone — add faint vertical band Phaser primitive (alpha 0.15-0.20 against #1a1a1a) at the gate's column connecting top-to-bottom of canvas. Task 1 (G1 — BLOCKING): Add src/index.css imported from main.tsx — close the white-halo gap - .planning/phases/02-season-1-vertical-slice-soil/02-VERIFICATION.md (frontmatter `gaps:` G1 entry — surface, evidence, fix_shape verbatim) - src/main.tsx (current 14-line entry — needs an `import './index.css';` line added after the StrictMode/createRoot imports) - src/game/main.ts (Phaser canvas backgroundColor #1a1a1a — body must match for tonal coherence) - index.html (currently no `