diff --git a/.project/CHANGELOG.md b/.project/CHANGELOG.md
index bdc8997e..6358d8a1 100644
--- a/.project/CHANGELOG.md
+++ b/.project/CHANGELOG.md
@@ -101,3 +101,4 @@ Test-coverage mandate response is paying off: data changes, city state transitio
2026-04-17 p1-05 BALANCE-TUNING pass (shipwright): JSON-only data edits targeting the p0_pop_peak median 29.5 → ≥30 gap in score_fix3 batch. (1) public/resources/improvements/farm.json yield food 2 → 3 — seeds with 20 farms (5, 10) get +20 food per city, seeds with ~3 farms (8) still get +3. Farm already the AI's top pick on grassland, so no candidate-list change needed. (2) public/games/age-of-dwarves/data/buildings/stub.json granary cost 40 → 30 — pre-tune for when granary enters the AI candidate list (warcouncil/game-ai scope). Both pure JSON, no Rust or GDScript changes. Validator 170/170 pass. Objective p1-05 stays partial until next 10-seed regression batch confirms median pop_peak ≥30 without regressing combats/techs/luxuries. [ref: p1-05]
2026-04-17 13:30 tourguide bring-up: new team-lead `tourguide` owns dev-guide developer experience. Closed p1-11 (wasm-pack output moved `src/simulator/pkg/` → `.local/build/wasm/` — 10 path updates + `./run verify` step 16 `_verify_no_build_in_src`) and p1-12 (12 doc surfaces aligned around "build output never under src/" rule; repo-root router row + instructions README inventory tag). p1-13 `guide-dev-route-coverage` partial: new `e2e/all-routes.spec.ts` (51 tests via `@lilith/playwright-e2e-docker`) runs 44/51 green on plum; 7 domain-data failures flagged in `.project/team-leads/tourguide.md` §Known red routes for guide-web / game-data / simulator-infra. Evidence: `.project/history/20260417_tourguide_dev_bringup.md`. [ref: tourguide, p1-11, p1-12, p1-13]
2026-04-17 14:30 tourguide p1-13 CLOSED: /parallel wave 1 landed two parallel fixes — guide-web agent overhauled the data-loader in `public/games/age-of-dwarves/guide/src/data/game.ts` (manifest-driven load; wrapper JSONs no longer pollute allResources/allImprovements/buildings; promotions.json wired via Raw→Canonical adapter; disciplinesData null-guard; DevSpritesPage magic-schools import replaced with shared SCHOOL_COLORS; sprite-audit network storm gated behind opt-in button + 8-worker fetch cap), game-data agent aligned `src/packages/engine-ts/src/types.ts` TileState to the Rust mc-core struct (+25 required fields: maturity, deadwood, soil_depth, lair-state, aerosol_mitigation, etc.) + replaced `ley_school: ''` sentinels with `'none'` (actual Rust enum is LeySchool, not school_affinity — original trace was misleading). Two consecutive `pnpm test:e2e --grep all-routes` runs: 51/51 passed (44.7s, 42.3s). p1-13 flipped partial → ✅ done. Tourguide bundle (p1-11 + p1-12 + p1-13) all ✅ done. [ref: tourguide, p1-13]
+2026-04-17 15:00 tourguide MCP verify + two follow-ups: ran MCP Playwright through 5 representative routes (home, /map/resources, /military/promotions, /climate/ecosystem/populations, /dev/sprites) on real chromium — all render with zero console errors; /climate/ecosystem/populations runs the full 120-turn WASM ecology sim end-to-end (visual proof the ley_school + TileState drift fix holds). Evidence appended to p1-13 Status section. Surfaced two follow-up objectives claimed by tourguide: p1-14 (P1) `guide-magic-school-scope-drift` — partial; p1-13 Wave-1 closed the two RED crashes (DevSpritesPage magic-schools import + PromotionsPage disciplinesData null-guard), but Explore audit's remaining 1 RED (HexGLRenderer ley-edge render) + 6 YELLOW (infusionTrees export, HomePage /magic nav, LensesPage formatter, SurvivalGuidePage ManaUpkeep, TerrainCard mana_major, front-page "5 Magic Schools" prose) remain. p2-20 (P2) `guide-sim-cache-pnpm-resolve` — missing; simCachePlugin pre-warm worker fails ERR_MODULE_NOT_FOUND because tsx can't resolve @magic-civ/physics-rs through pnpm symlinks after p1-11 cleared src/simulator/package.json main/types. Dev-only, no user impact. [ref: tourguide, p1-14, p2-20]
diff --git a/.project/objectives/README.md b/.project/objectives/README.md
index 05147cba..2f7aa382 100644
--- a/.project/objectives/README.md
+++ b/.project/objectives/README.md
@@ -15,10 +15,10 @@
| Priority | ✅ | 🟡 | 🔴 | ❌ | ⚫ | Total |
|---|---|---|---|---|---|---|
| **P0** | 20 | 4 | 1 | 0 | 0 | 25 |
-| **P1** | 10 | 2 | 0 | 0 | 0 | 12 |
-| **P2** | 7 | 6 | 0 | 2 | 0 | 15 |
+| **P1** | 10 | 3 | 0 | 0 | 0 | 13 |
+| **P2** | 7 | 6 | 0 | 3 | 0 | 16 |
| **P3 (oos)** | 0 | 0 | 0 | 0 | 9 | 9 |
-| **total** | **37** | **12** | **1** | **2** | **9** | **61** |
+| **total** | **37** | **13** | **1** | **3** | **9** | **63** |
@@ -26,9 +26,12 @@
| Team Lead | Remaining |
|---|---|
-| [shipwright](../team-leads/shipwright.md) | 6 |
| [warcouncil](../team-leads/warcouncil.md) | 4 |
+| [shipwright](../team-leads/shipwright.md) | 4 |
| [testwright](../team-leads/testwright.md) | 2 |
+| [tourguide](../team-leads/tourguide.md) | 2 |
+| [asset-audio](../team-leads/asset-audio.md) | 1 |
+| [asset-sprite](../team-leads/asset-sprite.md) | 1 |
|
@@ -78,6 +81,7 @@
| [p1-11](p1-11-build-output-src-purge.md) | ✅ done | Purge build output from src/ — wasm-pack moves to .local/build/wasm/ | [tourguide](../team-leads/tourguide.md) | 2026-04-17 |
| [p1-12](p1-12-build-output-docs-alignment.md) | ✅ done | Align every doc reference to the relocated wasm-pack output | [tourguide](../team-leads/tourguide.md) | 2026-04-17 |
| [p1-13](p1-13-guide-dev-route-coverage.md) | ✅ done | Guide dev server boots on plum with zero-error route coverage | [tourguide](../team-leads/tourguide.md) | 2026-04-17 |
+| [p1-14](p1-14-guide-magic-school-scope-drift.md) | 🟡 partial | Purge residual Game 2/3 magic-school content from Game 1 guide UI | [tourguide](../team-leads/tourguide.md) | 2026-04-17 |
## P2 — Polish
@@ -94,10 +98,11 @@
| [p2-09](p2-09-guide-web-deploy.md) | 🟡 partial | Player guide web app — builds clean from source | — | 2026-04-17 |
| [p2-10](p2-10-regression-ci-gate.md) | 🟡 partial | Automated regression CI gate on every push to main | [testwright](../team-leads/testwright.md) | 2026-04-17 |
| [p2-11](p2-11-version-about-screen.md) | ✅ done | Version string + About screen | [shipwright](../team-leads/shipwright.md) | 2026-04-17 |
-| [p2-16](p2-16-audio-assets.md) | ❌ missing | Audio assets — SFX + music .ogg files shipped | [shipwright](../team-leads/shipwright.md) | 2026-04-17 |
-| [p2-17](p2-17-sprite-assets.md) | ❌ missing | Sprite assets — full unit / building / race / tier coverage | [shipwright](../team-leads/shipwright.md) | 2026-04-17 |
+| [p2-16](p2-16-audio-assets.md) | ❌ missing | Audio assets — SFX + music .ogg files shipped | [asset-audio](../team-leads/asset-audio.md) | 2026-04-17 |
+| [p2-17](p2-17-sprite-assets.md) | ❌ missing | Sprite assets — full unit / building / race / tier coverage | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 |
| [p2-18](p2-18-guide-public-deployment.md) | 🟡 partial | Guide web app — public hosting + deploy pipeline | — | 2026-04-17 |
| [p2-19](p2-19-guide-progress-report-page.md) | ✅ done | Guide progress report page — dynamic dashboard + missing assets | — | 2026-04-17 |
+| [p2-20](p2-20-guide-sim-cache-pnpm-resolve.md) | ❌ missing | Fix simCachePlugin pre-warm worker — tsx can't resolve @magic-civ/physics-rs through pnpm symlink | [tourguide](../team-leads/tourguide.md) | 2026-04-17 |
## Out of Scope (Game 2 / Game 3)
diff --git a/.project/objectives/p1-13-guide-dev-route-coverage.md b/.project/objectives/p1-13-guide-dev-route-coverage.md
index 4b62ec02..8c79032a 100644
--- a/.project/objectives/p1-13-guide-dev-route-coverage.md
+++ b/.project/objectives/p1-13-guide-dev-route-coverage.md
@@ -39,6 +39,11 @@ green. Parallel Wave-1 agents landed the underlying fixes:
- `.project/history/20260417_tourguide_dev_bringup.md` — original bring-up record.
- Wave-2 confirmation runs: two consecutive `51 passed` outputs preserved in the Tourguide closure entry (`.project/CHANGELOG.md`, 2026-04-17).
- Known-red list in `.project/team-leads/tourguide.md` retained for historical reference with a closure note.
+- **MCP Playwright visual verify (2026-04-17)** — independent real-chromium walkthrough of 5 representative routes (home, `/map/resources`, `/military/promotions`, `/climate/ecosystem/populations`, `/dev/sprites`) beyond the headless e2e. All 5 render with zero console errors; `/climate/ecosystem/populations` in particular runs the full 120-turn WASM ecology sim end-to-end through the Rust ↔ TS FFI — empirical proof the `ley_school` + `TileState` drift fix holds at runtime. Screenshots under `.playwright-mcp/mcp_{home,resources,promotions,populations,dev_sprites}.png`.
+
+## Known caveat (surfaced by MCP verify, tracked separately)
+
+`/climate/simulation` with `noGui=true&totalTurns=50&buffer=0` params stalls at 0% in "pre-computed simulation from server" mode — `simCachePlugin`'s tsx-spawned pre-warm worker can't resolve `@magic-civ/physics-rs` through the pnpm symlink now that `src/simulator/package.json` has no `main`/`types` (the `../../` escape out of the package root that would have pointed at `.local/build/wasm/` doesn't traverse cleanly through node's resolver). This is a dev-only server optimization; the client-WASM fallback path is what `all-routes.spec.ts` measured and passed (44.7 s / 42.3 s). Tracked as a Tourguide follow-up in **p2-20 `guide-sim-cache-pnpm-resolve`**; not a user-facing bug, not blocking this objective's closure.
## Summary
diff --git a/.project/objectives/p1-14-guide-magic-school-scope-drift.md b/.project/objectives/p1-14-guide-magic-school-scope-drift.md
new file mode 100644
index 00000000..d248a198
--- /dev/null
+++ b/.project/objectives/p1-14-guide-magic-school-scope-drift.md
@@ -0,0 +1,133 @@
+---
+id: p1-14
+title: Purge residual Game 2/3 magic-school content from Game 1 guide UI
+priority: p1
+status: partial
+scope: game1
+owner: tourguide
+updated_at: 2026-04-17
+evidence:
+ - public/games/age-of-dwarves/guide/src/pages/DevSpritesPage.tsx
+ - public/games/age-of-dwarves/guide/src/data/game.ts
+ - public/games/age-of-dwarves/guide/src/data/index.ts
+ - public/games/age-of-dwarves/guide/src/app/guide-data.ts
+ - public/games/age-of-dwarves/guide/src/pages/HomePage.tsx
+ - public/games/age-of-dwarves/guide/src/pages/LensesPage.tsx
+ - public/games/age-of-dwarves/guide/src/pages/SurvivalGuidePage.tsx
+ - src/packages/guide/src/components/climate-sim/HexGLRenderer.tsx
+ - src/packages/guide/src/components/cards/TerrainCard.tsx
+ - src/packages/guide/src/components/ui/SchoolPip.tsx
+ - src/packages/guide/src/data/ecology.ts
+ - public/games/age-of-dwarves/guide/e2e/all-routes.spec.ts
+---
+
+## Summary
+
+Per CLAUDE.md's hard Game 1 scope rule (Dwarves only, NO magic; leylines
+/ Green school / spacefaring → Game 2; Archons / Ascension / 5 magic
+schools → Game 3), no magic content may ship into the Game 1 guide. The
+2026-04-17 `p2-09` scope-narrow pass deleted 10 Game 2/3 pages. But a
+prophylactic `Explore` audit on the same day — triggered by the
+Tourguide route-coverage spec catching magic-data imports in Game 1
+pages that still rendered — surfaced **2 RED** and **6 YELLOW** residual
+leaks that survived the first purge:
+
+**RED (runtime crash risk if data drifts)**
+
+- `public/games/age-of-dwarves/guide/src/pages/DevSpritesPage.tsx:5` —
+ live import of `@resources/magic/schools.json`. (Partial-fix: the
+ p1-13 Wave-1 pass replaced the import with the shared `SCHOOL_COLORS`
+ palette, so the crash is gone; keeping this row as evidence of why
+ the objective exists.)
+- `src/packages/guide/src/components/climate-sim/HexGLRenderer.tsx:7,749,783`
+ — live ley-line network rendering via `LEY_COLORS[edge.school]`. In
+ Game 1 the climate simulator's rendered grid shows ley-edge meshes
+ with school-coded colors, inserting Game 2 visual vocabulary into
+ every Game 1 planet view.
+
+**YELLOW (renders stale Game 2+ content into Game 1 UI, inert but wrong)**
+
+- `public/games/age-of-dwarves/guide/src/data/game.ts:87–98` —
+ `infusionTrees` loader pulls magical_promotions JSON into the guide
+ context; exported to every consumer via `data/index.ts:40`.
+- `public/games/age-of-dwarves/guide/src/app/guide-data.ts:48` — the
+ data context injection re-exposes magic-school data to every page
+ via `GuideDataProvider`.
+- `public/games/age-of-dwarves/guide/src/pages/HomePage.tsx:220–224`
+ — nav items `/magic/schools`, `/magic/spells`, `/magic/archons`
+ still defined; the routes were purged, so clicking these in Game 1
+ hits the `` fallback. Broken-link UX.
+- `public/games/age-of-dwarves/guide/src/pages/LensesPage.tsx:18` —
+ `formatUnlock` renders magic-school unlock strings for Game 1 lenses;
+ if a Game 1 lens has a magic unlock in data, the formatter renders it.
+- `public/games/age-of-dwarves/guide/src/pages/SurvivalGuidePage.tsx:23,143,152`
+ — `ManaUpkeep` interface + render.
+- `src/packages/guide/src/components/cards/TerrainCard.tsx:256–264`
+ — `mana_major` field render block.
+
+Additional front-page prose (Game 1 HomePage) already displays "5
+Magic Schools" and "16 Asymmetric Races" in its feature grid —
+evidence that the `` wrapping isn't
+being consulted by every page's content block.
+
+## Why this is P1 (ship-readiness), not P2 (polish)
+
+Game 1 "Age of Dwarves" Early Access cannot ship with Game 2 content
+bleeding into Game 1 screens. Magic schools are deferred scope by an
+explicit CLAUDE.md rail. The Explore findings are proof-of-drift, not
+hypothetical. Any one RED entry is a potential crash when a data file
+shape changes; any YELLOW entry is visible-to-players scope violation.
+
+## Acceptance
+
+- **RED fixes**: HexGLRenderer ley-edge rendering is gated behind
+ `` OR the edge data's `.school` field is
+ normalized to `'none'` + color-coded in a scope-neutral way in
+ Game 1. Verified by `e2e/all-routes.spec.ts` `/climate/simulation`
+ pass remaining green AND a new focused assertion that `LEY_COLORS`
+ is not consulted in Game 1 mode.
+- **YELLOW fixes**: every reference in the audit table above either
+ (a) deleted if the feature it gates is strictly Game 2/3, (b) gated
+ behind ``, or (c) normalized to
+ episode-agnostic content. The `/magic/*` nav items in HomePage are
+ removed. `formatUnlock` returns empty / omits magic unlocks for
+ Game 1 lenses. `mana_major` field is not rendered when
+ `episode === 1`.
+- **HomePage prose**: the "5 Magic Schools" feature card is rewritten
+ to an episode-1-appropriate Dwarf feature, OR moved into a
+ `` branch. The feature grid reflects Game 1
+ scope.
+- **Regression guard**: a new e2e assertion (in `all-routes.spec.ts`
+ or a dedicated `scope-hygiene.spec.ts`) greps the rendered DOM of
+ every Game 1 route for the strings `mana`, `ley`, `school`,
+ `disciplines`, `archon`, `ascension` and fails if any are rendered
+ outside an ``. Scope becomes a
+ mechanically-enforced invariant, not a doc rule.
+
+## Non-goals
+
+- Deleting `src/packages/guide/src/pages/magic/` or equivalent
+ universe-level Game 2/3 content — those pages are consumed by the
+ future Game 2/3 guide shells, must NOT be removed.
+- Changing `mc-core::grid::LeySchool` Rust enum — `ley_school` is a
+ tile-level field the simulator computes for all episodes; its mere
+ existence in data is not scope violation, its *rendering in Game 1
+ UI* is.
+- Deleting the `infusionTrees` / `disciplinesData` JSON files — those
+ are Game 2/3 source-of-truth data files, kept but gated.
+- Fixing the sim-cache server-mode dev path — see p2-20.
+
+## Status — 2026-04-17 (tourguide, partial)
+
+- ✓ Two of the three originally-RED entries closed inline during p1-13
+ Wave-1 (DevSpritesPage magic-schools import replaced with shared
+ `SCHOOL_COLORS`; PromotionsPage `disciplinesData` null-guarded).
+- ✗ HexGLRenderer ley-edge rendering still active in Game 1 mode.
+- ✗ All 6 YELLOW entries unchanged.
+- ✗ HomePage feature-grid prose still names "5 Magic Schools".
+- ✗ Scope-hygiene e2e assertion not yet authored.
+
+Partial because: the RED crash risk is closed, but visible scope
+violation remains. Escalating to a guide-web specialist via
+dispatch-by-task for the prose + gating work; Tourguide owns the
+regression-guard e2e + re-running the full suite to confirm.
\ No newline at end of file
diff --git a/.project/objectives/p2-16-audio-assets.md b/.project/objectives/p2-16-audio-assets.md
index 9c7e1c8d..a5d664e1 100644
--- a/.project/objectives/p2-16-audio-assets.md
+++ b/.project/objectives/p2-16-audio-assets.md
@@ -4,7 +4,7 @@ title: Audio assets — SFX + music .ogg files shipped
priority: p2
status: missing
scope: game1
-owner: shipwright
+owner: asset-audio
updated_at: 2026-04-17
evidence:
- public/games/age-of-dwarves/assets/audio/LICENSES.md
diff --git a/.project/objectives/p2-17-sprite-assets.md b/.project/objectives/p2-17-sprite-assets.md
index 82e091eb..1303175d 100644
--- a/.project/objectives/p2-17-sprite-assets.md
+++ b/.project/objectives/p2-17-sprite-assets.md
@@ -4,7 +4,7 @@ title: Sprite assets — full unit / building / race / tier coverage
priority: p2
status: missing
scope: game1
-owner: shipwright
+owner: asset-sprite
updated_at: 2026-04-17
evidence:
- public/games/age-of-dwarves/assets/sprites/
diff --git a/.project/objectives/p2-20-guide-sim-cache-pnpm-resolve.md b/.project/objectives/p2-20-guide-sim-cache-pnpm-resolve.md
new file mode 100644
index 00000000..0df41eaf
--- /dev/null
+++ b/.project/objectives/p2-20-guide-sim-cache-pnpm-resolve.md
@@ -0,0 +1,99 @@
+---
+id: p2-20
+title: Fix simCachePlugin pre-warm worker — tsx can't resolve @magic-civ/physics-rs through pnpm symlink
+priority: p2
+status: missing
+scope: game1
+owner: tourguide
+updated_at: 2026-04-17
+evidence:
+ - public/games/age-of-dwarves/guide/src/vite-plugins/simCachePlugin.ts
+ - src/simulator/package.json
+ - src/packages/engine-ts/src/runner.ts
+ - /tmp/tourguide_dev2.log
+---
+
+## Summary
+
+MCP Playwright verification of the dev-guide on plum surfaced a
+pre-existing dev-only failure mode in `simCachePlugin`'s pre-warm
+worker. On `pnpm dev` startup, the plugin spawns a tsx subprocess per
+scenario (`base_no_magic`, `volcanic_winter`, `ice_age`, …) which
+statically imports `@magic-civ/physics-rs` to pre-compute simulation
+frames into Redis. Every spawn fails with:
+
+```
+Error: Cannot find package '/Users/natalie/Code/@projects/@magic-civilization/src/packages/engine-ts/node_modules/.local/build/wasm/magic_civ_physics.js'
+imported from .../src/packages/engine-ts/src/runner.ts
+code: 'ERR_MODULE_NOT_FOUND'
+```
+
+**Root cause:** after p1-11 relocated the wasm-pack output to
+`.local/build/wasm/`, we also cleared `main` and `types` from
+`src/simulator/package.json` because node's default resolver can't
+follow a `"main": "../../.local/build/wasm/magic_civ_physics.js"`
+path that escapes the package root — tsx in particular collapses the
+`..` segments incorrectly through pnpm symlinks and looks up
+`node_modules/@magic-civ/physics-rs/.local/build/wasm/...` (path
+prefix glued rather than resolved). With no `main` at all, tsx falls
+back to guessing `/index.js`, which also doesn't exist.
+
+**User-facing impact:** none. The pre-compute is a dev optimization
+(populates Redis so the `/climate/simulation` route loads pre-rendered
+frames instead of running WASM inline). Vite's main module graph still
+resolves `@magic-civ/physics-rs` correctly via the explicit alias in
+`public/games/age-of-dwarves/guide/vite.config.ts:20`, so the route-
+coverage e2e exercises the client-WASM fallback path and passes.
+Users hitting the route with `noGui=true` URL params and a fresh cache
+see the loading spinner stall at 0% (MCP verify observed this) — but
+the normal path (no `noGui`) falls back to worker-mode automatically.
+
+## Why this is P2, not P1
+
+The symptom only manifests when (a) `simCachePlugin` is enabled
+(dev-only), (b) the route is hit with `totalTurns=...&buffer=0` URL
+params that force server mode, and (c) Redis hasn't been warmed by a
+prior successful run. Three conjunctive conditions, all dev-only.
+Production builds (`vite build`) don't run the plugin. End users
+never hit this.
+
+## Acceptance
+
+- `pnpm dev` startup log at `/tmp/tourguide_dev*.log` no longer
+ contains `ERR_MODULE_NOT_FOUND` or
+ `sim-cache pre-warm failed for : sim worker exited 1` lines.
+- Navigating to `/climate/simulation?noGui=true&totalTurns=50&buffer=0`
+ on a fresh Redis shows "Pre-computed simulation" reach 100% within
+ 60 s and transitions to the canvas-rendering phase.
+- No regression in `pnpm --prefix public/games/age-of-dwarves/guide
+ test:e2e --grep all-routes`: still 51/51 green.
+
+## Candidate fixes (pick one)
+
+1. **Re-add a node-resolver-friendly `main` in `src/simulator/package.json`.**
+ Publish a tiny `src/simulator/runner-stub.mjs` that re-exports
+ from the absolute `.local/build/wasm/` path resolved at runtime.
+ `main` points at the stub; browser tooling still uses the Vite
+ alias; node tooling (tsx) can follow the stub.
+2. **Teach tsx about the alias.** Add a `tsconfig.json` `paths`
+ entry that tsx will consult: `"@magic-civ/physics-rs": [".local/build/wasm/magic_civ_physics.js"]`.
+3. **Inline the resolution in `simCachePlugin`.** Change the plugin
+ to spawn the worker with an explicit `--require` preloader or
+ `NODE_OPTIONS` that installs an import-map pointing at the
+ absolute path.
+
+Option 1 is probably the cleanest — the stub is ~3 lines, isolated
+to `src/simulator/`, and doesn't leak an absolute build path into
+tsconfig or env. Leaves `main` pointing at intra-package code which
+node's resolver has no problem with.
+
+## Non-goals
+
+- Reintroducing `src/simulator/pkg/` as the WASM artifact location —
+ the rule from p1-11 + p1-12 stands; any fix must leave
+ `.local/build/wasm/` as the canonical output.
+- Fixing WASM alias resolution for *browser* code — Vite handles that
+ correctly via the explicit alias; only node-side tooling (tsx
+ workers spawned by simCachePlugin) is broken.
+- Removing `simCachePlugin` — the pre-warm is a real dev-time speedup
+ for the climate simulator. Kill-it-and-forget is a regression.
\ No newline at end of file
diff --git a/.project/team-leads/README.md b/.project/team-leads/README.md
index 1f05a915..17461167 100644
--- a/.project/team-leads/README.md
+++ b/.project/team-leads/README.md
@@ -69,4 +69,4 @@ specialist does not own any objective.
| [warcouncil](warcouncil.md) | Warcouncil | AI action generation, MCTS, GPU look-ahead, clan personality differentiation | p0-01, p0-02, p0-20 |
| [shipwright](shipwright.md) | Shipwright | Drive Game 1 to release via /experts-team cron loop until every P0 is done | p0-05, p0-14, p0-15, p0-16, p0-17 |
| [testwright](testwright.md) | Testwright | Regression-test coverage across Rust + GDScript + data validators — seeds the evidence substrate for the Objective Status Integrity rule | p1-09, p2-10 |
-| [tourguide](tourguide.md) | Tourguide | Developer experience of the guide web app — dev server boots on plum, route coverage e2e, and the "no build output in src" rule stays enforced | p1-11, p1-12, p1-13 |
+| [tourguide](tourguide.md) | Tourguide | Developer experience of the guide web app — dev server boots on plum, route coverage e2e, Game 1 scope hygiene in guide UI, and the "no build output in src" rule stays enforced | p1-11, p1-12, p1-13, p1-14, p2-20 |
diff --git a/.project/team-leads/asset-audio.md b/.project/team-leads/asset-audio.md
new file mode 100644
index 00000000..55925d21
--- /dev/null
+++ b/.project/team-leads/asset-audio.md
@@ -0,0 +1,42 @@
+---
+id: asset-audio
+name: Asset — Audio
+specialization: Source, license, encode, and ship the Game 1 audio asset library (SFX + music)
+objectives:
+ - p2-16
+---
+
+## Mandate
+
+Ship the `.ogg` audio files that `AudioManager` expects — 10 SFX events + 6 music tracks. The audio *system* (manifest, autoload, EventBus wiring, volume sliders) is done as p0-21; this team-lead's job is the content.
+
+p0-21's architecture deliberately treats missing `.ogg` files as a silent no-op, so the game is shippable without Asset-Audio's work. But a silent game is incomplete. Asset-Audio closes p2-16 by delivering real files that don't break the license / format contract.
+
+## Owned surface
+
+- `public/games/age-of-dwarves/assets/audio/sfx/*.ogg` — 10 SFX files, one per manifest event
+- `public/games/age-of-dwarves/assets/audio/music/*.ogg` — 6 music tracks (5 era-ambient + 1 victory)
+- `public/games/age-of-dwarves/assets/audio/LICENSES.md` — per-file attribution (source, license, author, URL, SHA256)
+- Authoring / encoding scripts in `tools/audio/` if any are introduced (e.g. normalize loudness, convert-to-ogg)
+
+Does NOT own:
+- `public/games/age-of-dwarves/data/audio.json` — the manifest shape. If a new event is needed, coordinate with game-systems via handoff.
+- `src/game/engine/src/autoloads/audio_manager.gd` — the runtime code. Asset-Audio reads its path expectations; runtime changes go to godot-engine.
+
+## Working constraints
+
+- **Ogg Vorbis format, 44.1 kHz, 128 kbps.** SFX may be mono; music must be stereo.
+- **License must be commercial-use compatible** (CC0, CC-BY, CC-BY-SA, or explicitly purchased / commissioned). No "royalty-free" sources that prohibit resale.
+- **SHA256 recorded in LICENSES.md** so a binary diff later can prove the shipped file matches the license terms as-recorded.
+- **Loudness normalized** to a consistent LUFS target (e.g. -16 LUFS for SFX, -18 LUFS for music) so mixing at runtime doesn't produce one clip that blows out the others.
+- **Reasonable file sizes** — budget ~150KB per SFX, ~1-3MB per music track loop, total library ≤ 20MB.
+
+## Acceptance loop
+
+Per p2-16's acceptance bullets. When all 16 files are shipped + LICENSES.md is complete + in-game playback verified via a live integration test (emit each EventBus signal, confirm audible output), flip p2-16 to `done` with citations.
+
+## Escalation
+
+- **Source unavailable at acceptable license** → escalate to user; they may commission a specific piece or re-scope the event.
+- **AudioManager bug exposed by real files** (e.g. a specific .ogg triggers a crash) → handoff to godot-engine; Asset-Audio does not patch autoload code.
+- **Budget / timeline pressure** → coordinate with shipwright; some files may land post-EA as a content update rather than blocking release.
diff --git a/.project/team-leads/asset-sprite.md b/.project/team-leads/asset-sprite.md
new file mode 100644
index 00000000..f4e837ad
--- /dev/null
+++ b/.project/team-leads/asset-sprite.md
@@ -0,0 +1,46 @@
+---
+id: asset-sprite
+name: Asset — Sprite
+specialization: Generate, commission, and ship unit / building / wonder sprite art per the sprite-rendering capability contract
+objectives:
+ - p2-17
+---
+
+## Mandate
+
+Ship the `.png` sprite files that the renderers expect when `ThemeAssets.load_sprite()` is called. The rendering *capability* is being built in p0-23: renderers draw procedural primitives first (unconditional baseline), then overlay a sprite if one exists at the resolved path. Asset-Sprite's job is producing the sprites — game-ready, style-consistent, tier-appropriate.
+
+The game is shippable with zero sprites (p0-23's design rule: draw baseline never deletes). Asset-Sprite's delivery progressively replaces the circle-and-letter placeholders with real art, unit by unit, building by building.
+
+## Owned surface
+
+- `public/games/age-of-dwarves/assets/sprites/units/__.png` — every `data/units/*.json` entry should have a matching sprite variant per race × sex combination the game supports
+- `public/games/age-of-dwarves/assets/sprites/buildings/.png` — every `data/buildings/*.json` entry + every wonder in `mundane_wonders.json`
+- `public/games/age-of-dwarves/assets/sprites/LICENSES.md` — per-file attribution (source, license, author, URL, SHA256)
+- `tools/sprite-generation/` — the generation pipeline (prompt authoring, model inference, post-processing, normalization, drop-into-assets)
+- Any authoring data: style references, prompt libraries, generation seeds, post-process scripts
+
+Does NOT own:
+- `src/game/engine/src/rendering/*_renderer.gd` — the runtime sprite load + overlay path. Owned by godot-renderer. Asset-Sprite reads the expected path conventions and produces files that match.
+- Sprite generation model selection beyond the CLAUDE.md rule ("NEVER use anime models for game art — use `juggernaut-xl-v9`, `epicrealism-xl`, `illustrious-xl-v2`").
+
+## Working constraints
+
+- **PNG with alpha channel.** Square canvas, 256×256 or 512×512 (document the choice in LICENSES.md).
+- **Transparent background** — renderer composites sprite onto hex; any background color would bake incorrectly.
+- **Consistent art style** across the library — use MTG-color-coded palette per the sprite-generation pipeline doc, but the Dwarf race is the only Game 1 race so visual coherence is within-race, not cross-race.
+- **License must be commercial-use compatible** — AI-generated output has to be from a model the project is licensed to use commercially (check juggernaut-xl, epicrealism-xl, illustrious-xl-v2 license terms at ship time). Commissioned art must have assigned commercial rights.
+- **SHA256 recorded in LICENSES.md** for binary-diff traceability.
+- **Budget**: 256×256 PNG at reasonable quality ≈ 30-80KB. Full library of ~100-300 sprites ≤ 20MB.
+- **Prior user directive (2026-04-17):** 7 previously-authored sprites were deleted because quality bar not met. Slate is clean. Re-generate from the pipeline or commission — do not restore the deleted files.
+
+## Acceptance loop
+
+Per p2-17's acceptance bullets. Deliveries can land incrementally — every sprite added to `assets/sprites/` renders the next time the game boots. Flip p2-17 to `done` when all declared units + buildings + wonders have at least one sprite variant, LICENSES.md is complete, and a proof screenshot shows a sample map with sprite-rendered entities co-existing with draw-baseline fallbacks.
+
+## Escalation
+
+- **Model license conflict** at ship time → escalate to user; a different base model may be required.
+- **Style drift** between batches of generated sprites → stop generating, iterate on the prompt library / post-process pipeline before shipping more.
+- **Renderer can't load a specific PNG** (e.g. format bug, alpha channel issue) → handoff to godot-renderer; Asset-Sprite does not patch renderer code.
+- **Budget / timeline pressure** → coordinate with shipwright; most sprites can land post-EA as progressive visual polish rather than blocking release. Prioritize "player's most-seen units" (Founder, Warrior, Scout) for EA.
diff --git a/.project/team-leads/tourguide.md b/.project/team-leads/tourguide.md
index d7d7c949..5e2f6e6a 100644
--- a/.project/team-leads/tourguide.md
+++ b/.project/team-leads/tourguide.md
@@ -6,6 +6,8 @@ objectives:
- p1-11
- p1-12
- p1-13
+ - p1-14
+ - p2-20
---
## Mandate
diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json
index 89579518..a8fa55dc 100644
--- a/public/games/age-of-dwarves/data/objectives.json
+++ b/public/games/age-of-dwarves/data/objectives.json
@@ -1,12 +1,12 @@
{
- "generated_at": "2026-04-17T21:24:08Z",
+ "generated_at": "2026-04-17T21:31:25Z",
"totals": {
+ "missing": 3,
"done": 37,
- "oos": 9,
- "partial": 12,
- "missing": 2,
"stub": 1,
- "total": 61
+ "oos": 9,
+ "partial": 13,
+ "total": 63
},
"objectives": [
{
@@ -379,6 +379,16 @@
"updated_at": "2026-04-17",
"summary": "`./run guide` on plum (`natalie@plum.local`, macOS) today fails before\nfirst paint because `@magic-civ/physics-rs` resolves to the missing\n`src/simulator/pkg/magic_civ_physics.js` — p1-11 + p1-12 close that\nstructural half. What remains is the contributor-side proof: starting\nthe dev server on a fresh clone, loading every canonical route in\nPlaywright, and asserting zero runtime errors. Nobody currently owns\nthat proof, and the last guide-dev CHANGELOG entry\n(2026-04-16 14:47 task #18) explicitly marked \"visual verification\nblocked by WASM not built on macOS.\" The gap closes when a spec\nexists, runs green on plum, and catches new routes automatically on\nevery `pnpm test:e2e`.\n\nThe e2e substrate is already wired — `@lilith/playwright-e2e-docker`\nis a committed dependency, `e2e/Dockerfile.web` pre-bakes a production\nbuild, `playwright.config.ts` switches its webServer between\n`pnpm dev --port 5802` (local) and `pnpm preview --port 5802` (CI), and\ntwo specs (`diag.spec.ts`, `simulator.spec.ts`) already exercise the\nclimate simulator. This objective extends that harness with one\nroute-coverage spec; no new infrastructure."
},
+ {
+ "id": "p1-14",
+ "title": "Purge residual Game 2/3 magic-school content from Game 1 guide UI",
+ "priority": "p1",
+ "status": "partial",
+ "scope": "game1",
+ "owner": "tourguide",
+ "updated_at": "2026-04-17",
+ "summary": "Per CLAUDE.md's hard Game 1 scope rule (Dwarves only, NO magic; leylines\n/ Green school / spacefaring → Game 2; Archons / Ascension / 5 magic\nschools → Game 3), no magic content may ship into the Game 1 guide. The\n2026-04-17 `p2-09` scope-narrow pass deleted 10 Game 2/3 pages. But a\nprophylactic `Explore` audit on the same day — triggered by the\nTourguide route-coverage spec catching magic-data imports in Game 1\npages that still rendered — surfaced **2 RED** and **6 YELLOW** residual\nleaks that survived the first purge:\n\n**RED (runtime crash risk if data drifts)**\n\n- `public/games/age-of-dwarves/guide/src/pages/DevSpritesPage.tsx:5` —\n live import of `@resources/magic/schools.json`. (Partial-fix: the\n p1-13 Wave-1 pass replaced the import with the shared `SCHOOL_COLORS`\n palette, so the crash is gone; keeping this row as evidence of why\n the objective exists.)\n- `src/packages/guide/src/components/climate-sim/HexGLRenderer.tsx:7,749,783`\n — live ley-line network rendering via `LEY_COLORS[edge.school]`. In\n Game 1 the climate simulator's rendered grid shows ley-edge meshes\n with school-coded colors, inserting Game 2 visual vocabulary into\n every Game 1 planet view.\n\n**YELLOW (renders stale Game 2+ content into Game 1 UI, inert but wrong)**\n\n- `public/games/age-of-dwarves/guide/src/data/game.ts:87–98` —\n `infusionTrees` loader pulls magical_promotions JSON into the guide\n context; exported to every consumer via `data/index.ts:40`.\n- `public/games/age-of-dwarves/guide/src/app/guide-data.ts:48` — the\n data context injection re-exposes magic-school data to every page\n via `GuideDataProvider`.\n- `public/games/age-of-dwarves/guide/src/pages/HomePage.tsx:220–224`\n — nav items `/magic/schools`, `/magic/spells`, `/magic/archons`\n still defined; the routes were purged, so clicking these in Game 1\n hits the `` fallback. Broken-link UX.\n- `public/games/age-of-dwarves/guide/src/pages/LensesPage.tsx:18` —\n `formatUnlock` renders magic-school unlock strings for Game 1 lenses;\n if a Game 1 lens has a magic unlock in data, the formatter renders it.\n- `public/games/age-of-dwarves/guide/src/pages/SurvivalGuidePage.tsx:23,143,152`\n — `ManaUpkeep` interface + render.\n- `src/packages/guide/src/components/cards/TerrainCard.tsx:256–264`\n — `mana_major` field render block.\n\nAdditional front-page prose (Game 1 HomePage) already displays \"5\nMagic Schools\" and \"16 Asymmetric Races\" in its feature grid —\nevidence that the `` wrapping isn't\nbeing consulted by every page's content block."
+ },
{
"id": "p2-01",
"title": "Minimap — fog reflection and unit markers",
@@ -495,7 +505,7 @@
"priority": "p2",
"status": "missing",
"scope": "game1",
- "owner": "shipwright",
+ "owner": "asset-audio",
"updated_at": "2026-04-17",
"summary": "The audio capability shipped as **p0-21** — `AudioManager`, manifest, signal wiring, volume sliders all work. What's missing is the 16 actual `.ogg` files the manifest declares. Gameplay is currently silent. No code changes needed when assets land; drop files into `assets/audio/{sfx,music}/` matching the paths in `audio.json`.\n\nPer user directive 2026-04-17, this split was pulled out of the original p1-04 so the capability (P0, done) and the assets (P2, missing) are tracked independently. A silent ship is shippable; a broken audio system is not."
},
@@ -505,7 +515,7 @@
"priority": "p2",
"status": "missing",
"scope": "game1",
- "owner": "shipwright",
+ "owner": "asset-sprite",
"updated_at": "2026-04-17",
"summary": "With p0-22 capability in place, the game needs comprehensive sprite coverage:\n- Every unit × race × sex combination declared in `data/units/`\n- Every building declared in `data/buildings/`\n- Per-tier variants where meaningful (T1-T4 sprites can look different from T7-T10)\n\nCurrently 0 sprite files exist — prior 7 were deleted 2026-04-17 per user directive (quality bar not met). Hundreds are expected when the full sprite generation pipeline runs (or commissioned art lands). Slate is clean."
},
@@ -529,6 +539,16 @@
"updated_at": "2026-04-17",
"summary": "Dynamic progress report page inside the Age of Dwarves guide that reads the project's objectives dashboard + asset pipeline state at runtime. Built 2026-04-17 under guide-progress-dev.\n\nDelivery:\n- `tools/objectives-report.py` extended to emit `public/games/age-of-dwarves/data/objectives.json` on every regen (schema: `{generated_at, totals, objectives[]}` with id/title/priority/status/scope/owner/updated_at/summary per objective). `--check` mode compares ignoring the volatile `generated_at`.\n- `ProgressReportPage.tsx` + supporting modules under `public/games/age-of-dwarves/guide/src/pages/progress-report/` (types, styled, filter, assets-detection, ObjectiveModal). Renders: overall totals, per-priority progress bars, objective table (filterable All / P0 / Partial / Missing), click-through summary modal (uses `createPortal` to escape transformed layout ancestor).\n- Missing assets section: scans `audio.json` (declared .ogg paths) and `units/*.json` + `buildings/*.json` (expected sprite paths) against `import.meta.glob` presence. Currently reports 0/16 audio + 0/33 unit sprites + 0/35 building sprites present (clean slate post-2026-04-17 sprite deletion).\n- Route `/progress` added in `App.tsx`, nav entry `📊 Progress Report` at top of About group.\n- 25 new Vitest tests (`assets-detection`, `filter`, `objectives-json`) → 115 total passing; apricot `pnpm build` ✓, `dist/index.html` exists, bundle 113kB.\n- Incidental fix: orphan `healing_draught` reference in `items/manifest.json` removed (was breaking app bootstrap)."
},
+ {
+ "id": "p2-20",
+ "title": "Fix simCachePlugin pre-warm worker — tsx can't resolve @magic-civ/physics-rs through pnpm symlink",
+ "priority": "p2",
+ "status": "missing",
+ "scope": "game1",
+ "owner": "tourguide",
+ "updated_at": "2026-04-17",
+ "summary": "MCP Playwright verification of the dev-guide on plum surfaced a\npre-existing dev-only failure mode in `simCachePlugin`'s pre-warm\nworker. On `pnpm dev` startup, the plugin spawns a tsx subprocess per\nscenario (`base_no_magic`, `volcanic_winter`, `ice_age`, …) which\nstatically imports `@magic-civ/physics-rs` to pre-compute simulation\nframes into Redis. Every spawn fails with:\n\n```\nError: Cannot find package '/Users/natalie/Code/@projects/@magic-civilization/src/packages/engine-ts/node_modules/.local/build/wasm/magic_civ_physics.js'\nimported from .../src/packages/engine-ts/src/runner.ts\ncode: 'ERR_MODULE_NOT_FOUND'\n```\n\n**Root cause:** after p1-11 relocated the wasm-pack output to\n`.local/build/wasm/`, we also cleared `main` and `types` from\n`src/simulator/package.json` because node's default resolver can't\nfollow a `\"main\": \"../../.local/build/wasm/magic_civ_physics.js\"`\npath that escapes the package root — tsx in particular collapses the\n`..` segments incorrectly through pnpm symlinks and looks up\n`node_modules/@magic-civ/physics-rs/.local/build/wasm/...` (path\nprefix glued rather than resolved). With no `main` at all, tsx falls\nback to guessing `/index.js`, which also doesn't exist.\n\n**User-facing impact:** none. The pre-compute is a dev optimization\n(populates Redis so the `/climate/simulation` route loads pre-rendered\nframes instead of running WASM inline). Vite's main module graph still\nresolves `@magic-civ/physics-rs` correctly via the explicit alias in\n`public/games/age-of-dwarves/guide/vite.config.ts:20`, so the route-\ncoverage e2e exercises the client-WASM fallback path and passes.\nUsers hitting the route with `noGui=true` URL params and a fresh cache\nsee the loading spinner stall at 0% (MCP verify observed this) — but\nthe normal path (no `noGui`) falls back to worker-mode automatically."
+ },
{
"id": "g2-01",
"title": "Ley lines — Game 2 (Age of Kzzykt)",