# MEMORY Session memory for durable decisions, gotchas, and commands. Implementation history lives in `DONE.md`, user-facing changes in `CHANGELOG.md`, the live task list in `TODO.md`, and the canonical implemented-features list in `docs/development-status.md`. ## Decisions - **Local-only reference override (2026-11-03, owner):** development-only photo research ignores the licence gate. Downloads live in `tmp/research/` (gitignored): DDG image search, street-view frames, neighbouring buildings — never committed, never shipped. Promotion into `docs/screens/refs/` always passes the sidecar licence + privacy gate and `CREDITS.md`. - **AI-assisted asset review loop (2026-11-04):** head/face shape decisions use a concept + judge loop — generate a target concept (`generate_textures.generate_image`, Seedream) and use `tools/describe_image.py` as the vision judge, iterating until the judge's specific fixes converge. Review pages: `docs/screens/head-review/README.md`, `docs/3d-model-review.md` §AI-driven head rework. - **Project agent skills (2026-11-03):** two tracked skills live in `.agents/skills/`: `gig-line-blender-asset-contract` and `gig-line-release-qa`. They are guidance only, add no runtime dependency and are referenced from `tools/README.md`. The third-party `blender-skills` pack remains gitignored. - **TV Studio shares the Hollywood location (owner):** the Late Night TV Studio is the same El Capitan Entertainment Centre block at 6840 Hollywood Blvd (which houses *Jimmy Kimmel Live!*), restaged for a late-night arrival. `VENUE_SPECS["tv_studio"]` is a 30.0 m variant of the 29.6 m Hollywood facade. One reference board serves both venues; never create a separate TV Studio board. - **Jimmy Kimmel Live = starting point:** the Hollywood first shift and the TV Studio arrival both begin at the El Capitan block. Stated in `docs/venues.md#hollywood-boulevard` and `docs/gameplay.md`. - **Street-view limitation:** Google Street View panoramas need a Google API key or manual browser capture, so the local reference sets use DDG street-level photos instead. - **Image-recitation filter:** Gemini image models reject well-known branded facades (Comedy Store wall) as generation anchors (`IMAGE_RECITATION`). Workaround: anchor on a less-branded frame of the same block, or generate text-only when the reference adds no geometry. - **OpenRouter image-to-image:** `/v1/images` is text-to-image only; the working path is `POST /v1/chat/completions` with a `content` array carrying a base64 `image_url`. Response images arrive as `message.images[].image_url.url` (fallback: a data URI inside the message text). Implemented in `generate_textures.generate_image(reference_path=...)` and `scene_concepts.py generate/anchor`; provenance sidecars are `.refgen.json`. - **Facade/venue material picks (2026-11-03):** the Masonic/El Capitan facade uses the photo-anchored `limestone_ashlar` field (warm tint `Color(0.85, 0.81, 0.74)`) with `stone_steps` for trim; the Joke Shop uses `dark_stucco`; the marquee canopy/sign box stay `black_metal` by design (fictional "GIG LINE" marquee, not the red El Capitan Theatre marquee next door). The TV Studio reuses `masonic_facade`. Committed Comedy Store refs were removed in favour of the local-only `tmp/research/comedy-store/` set; the three-up board's photo panel is a historical public-domain frame. - **Godot 4.7.2 `--import` gotcha:** with a stale `.godot/` cache, new image files never import (no `.import` created) and `preload()` of the new texture fails every dependent script with the misleading `Could not resolve external class member "spec"`. Fix: import in a scratch copy with `.godot/` removed, then copy back the `.png.import` and `.godot/imported/*.ctex/.md5`. - **Blender skills pack (local aid):** MIT `blender-skills` at `.agents/skills/blender-skills/` (94 skills + shared `references/`), gitignored, kept out of Godot's scanner by `.agents/.gdignore`. Keep the pack flat when updating (`cp -a /.claude/skills/. ...`). Pi gates project `.agents/skills` behind project trust. - **Gaussian splatting (unversioned):** offline/development-only until an ADR proves licence, privacy, Compatibility/Web cost and mesh collision. Accepted photo channels: team captures, permissioned user/volunteer photos, clearly licensed downloads; every set needs a provenance/consent sidecar. No runtime splat dependency in 0.10.0. - **0.10.0 balance protocol:** `docs/release-qa.md#balance-acceptance` defines targets/denominators for the 36 venue/difficulty/phase cells, seeds 1–100, and access/emergency error rates. Gotcha: `_read_npc_truth()` is type-derived, not behaviour-derived; the emergency-fairness review must account for that oracle mismatch. - **Blender MCP is optional (2026-11-04):** setup and safe part workflow are in `docs/asset-tooling.md#optional-interactive-blender-mcp`. The headless flatpak scripts remain authoritative; MCP is never a build/CI/test/runtime dependency, and anything MCP touched must pass the CLI audit/fit gates. - **Character mesh cap (2026-11-05):** all characters use 24,000 tris / 24,000 glTF vertices (`CHAR_MAX_TRIS`/`CHAR_MAX_VERTS` in `tools/blender_assets.py`; `npc_pipeline.MAX_TRIANGLES`/ `MAX_VERTICES`). The required 13 core bones stay, optional extra bones are allowed, and the low-poly `npc_base` fallback remains the shipped fallback. The raise funds the Quaternius base bake and the 12-variant clothing detail. Measure the 24-NPC/Web budget after any geometry change; never silently lower the cap without a new decision. - **Quaternius is the NPC base (2026-11-05, owner):** ADR-0017 supersedes the generator-first sourcing decision. The CC0 Quaternius Ultimate Modular character pack is the primary NPC mesh and animation source. Stage 1 bakes a proportion-warped, 13-core-contract `npc_base` so `NpcAvatar`, gear and animations keep working; the 62-joint/24-clip source rig is authoring input, not a runtime dependency. If the warp fails the silhouette gate, Stage 2 needs a new ADR for a 62-joint runtime, not a hybrid. The MakeHuman head and procedural `npc_base` remain tested fallbacks until the Quaternius rollout ships. Quaternius source: CC0, attribution in `CREDITS.md`; the three researched GLBs and licence notes are kept in `tmp/npc-research/quaternius/`. MPFB/MakeHuman body work is closed; do not add new work there. - **Quaternius bake keeps source detail (2026-11-05, owner):** during the contract bake (§2), keep the Quaternius mesh as close to source as possible — no unnecessary decimation or LOD reduction. The canonical base is only 3,199 verts / 6,206 tris, far under the 24,000 cap, so there is no budget reason to decimate before the silhouette gate. Do not reuse the old 7,000-triangle or MPFB decimation targets. - **Quaternius runtime rig (2026-11-05, owner, ADR-0018 option 2):** the Stage 1 proportion warp failed the silhouette gate (arm ×1.63 / leg ×0.74 / torso ×1.61; vision judge: paddle hands, lost forearms, stretched shins). The owner chose to **adopt the Quaternius 62-joint rig at runtime** instead of warping: `NpcAvatar` maps the Quaternius joint names, gear attach points are re-derived from the Quaternius skeleton (hip ≈0.87, shoulder ≈0.148/1.39, wrist ≈0.99, knee ≈0.52) and the mesh keeps source LOD (arm-down rest pose baked). **The 24 upstream clips are not played**: `_animate()` drives the proxies and `_sync_skeleton()` writes the bone poses, which is what the FSM already needed for sitting, hand-raising and head turns. Playing the clips would replace that state machine, not supplement it, so all 24 stay unused and the clips are authoring reference only. - **Engine baseline:** Godot 4.7.2 stable (official editor `~/.local/bin/godot`). The system's `godot-runner` cannot load arbitrary project paths (compiled without path overrides); use the official editor binary. - **GDScript/architecture:** cross-file references use explicit `preload()` constants (not `class_name`) so scripts compile in headless `--script` runs; the test runner is a custom minimal harness (not GUT, which needs the editor plugin); no autoload signal bus — `ShiftCoordinator` owns the `EventBus` instance passed to subsystems. - **Skinned NPCs (v0.6.0):** `NpcAvatar._build_skinned_body()` loads `npc_base.glb`; procedural `_build_body()` is the fallback. Bone pose rotation is the **absolute local orientation**, not an offset — compose with `get_bone_rest(i).basis` or the body snaps to a broken pose. Five tint slots via `SurfaceMaterials.npc_slot()`; keep `_rig.scale` uniform on the skinned path. - **NPC facing:** the avatar's visual front is **+Z**; Godot `look_at()` aims **-Z** (made NPCs walk backwards). Use `NpcAvatar.yaw_towards(dir)` and set `rotation.y`; never `look_at()` on avatars. Head-turn offsets use `+basis.z` for the same reason. - **Release blockers (still open):** macOS unsigned, AI image provider terms/commercial rights not cleared, storefront/rating approval pending, external playtests and full hardware matrix pending. Recorded in `CREDITS.md`, `PRIVACY.md`, `docs/accessibility.md`, `docs/known-limitations.md`. - **Release roadmap (2026-11-05, owner):** `0.9.9` Quaternius pilot base (rig/export fix, rest-relative pose math, owner face/silhouette gate), `0.9.10` 12-role roster, `0.9.11` scheduled venue/prop/lighting polish, `0.9.12` performance/Web/accessibility verification, `0.10.0` public-readiness minor, then `0.10.1`–`0.10.3` fixes, `0.11.0` replay + fourth venue + localization foundation, `0.12.0` signed/storefront builds + save v2, `0.13.x` content complete and release candidates, `1.0.0` stable. The full gap analysis and roadmap live in `PLAN.md`; the character track is versioned now, so a blocked rig cannot block the release. Planning only — none of these versions is built yet. - **Quaternius face/readability gate (2026-11-05):** the base is 6.2k tris total and replaces a library with a sculpted realistic face, so the 0.9.9 gate is a product decision, not just an art check. Owner picks full swap / crowd-only / extra head detail. Do not lower character fidelity silently. - **Pose composition is rest-relative (resolved 2026-11-05):** see `_swing_bone()` in the gotchas — the rest-relative `set_bone_pose_rotation` form was abandoned; the global-pose write replaced it. - **Full roster swap (2026-11-05, owner):** at the `0.9.9` face gate the owner chose option (a), full swap. All twelve public character names are baked from the three CC0 Quaternius sources (`casual`, `hoodie`, `worker`); variety comes from the five tint slots, the outfit rules and the gear library. The procedural library + MakeHuman head stay in the tree as the tested fallback (`build --source procedural`), not in the shipped roster. Silhouette variety is the known regression — three meshes for twelve roles — and the modular part library is the `0.9.10` fix. - **No hats except the firefighter helmet (2026-11-06, owner):** the iconography is not worth the cost — a cap or hood on this head hides the face, and every hat needed a lift+scale hack to clear the 0.228 m hair shell. `ACCESSORIES` maps no role to `cap`/`hood`, the guard/vest per-character caps are gone, the performer cone was removed, and the primitive cap/hood fallbacks were deleted with their `GEAR_OFFSETS`/`GEAR_SCALE` compensation. `firefighter_helmet` is the only head item. Do not re-add head gear without an owner decision and a fit measurement. - **Mouths are authored, not upstream (2026-11-06):** none of the three Quaternius heads has a mouth material, so the faces read as brow-and-eye masks. `_add_quaternius_mouth()` places a small lip block on the measured skin surface (front-most skin vertex in the 1.600–1.632 m band, ±35 mm of centre) at 1.616 m. One rule covers all three bases; it runs before the slot map, so match the material as `_base_name(m.name).lower() == "skin"`. - **One character source, one fallback:** no new NPC source without an ADR. MPFB body work stays closed, the Stage-1 proportion warp is not reopened, and the procedural `npc_base` + MakeHuman head stay tested until the Quaternius base passes its gate. ## Gotchas / Known Issues - **Blender material-list edits corrupt polygons silently.** Two separate bugs, same root: `obj.data.materials.clear()` resets **every polygon to slot 0**, so the per-face indices must be captured *before* the clear and reassigned after; and `IDMaterials.pop(index=)` repoints the polygons that used the slot, which once painted a whole face with the eye material. The safe pattern is `_purge_unused_materials()`: read the per-face material **names**, rebuild the slot list from the names, then write the indices back by name. - **A zero-user material still owns its name.** Removing a slot leaves the datablock behind, so a donor material renamed to the freed name lands as `Eye.001` and every `m.name == "Eye"` rule silently misses it. Purge the datablocks (`bpy.data.materials.remove(m)` when `m.users == 0`) or match on `_base_name()`. - **A duplicated detail face has to be offset along its own normal.** Copying the eye quad into a pupil and nudging it in world `-Y` vanished: the eye plane is tilted, so the "front" nudge went backwards and the pupil sat inside the sclera. Offsetting along `face.normal * 0.0032` fixed it. Also scale the depth axis when shrinking a copy, or it flattens onto the parent plane. - **Blender node colours are linear; Godot tonemaps them.** A mouth lip at `0.16` linear rendered as salmon/mauve. Divide by roughly 3–4 versus the intended sRGB value: the hair uses 0.027 linear, the lip 0.055/0.018/0.02. - **The modular Quaternius Worker has no torso under its vest and no crown under its hard hat.** Deleting either material leaves real holes (that is what shipped in the first 0.9.9 build: guard and firefighter showed the inside of the head and torso). Remap instead of deleting; the only geometry the build removes is the Worker brow bar, which is replaced by a donor part. - **Head gear authored for the 0.315 m procedural head is wrong on the 0.20 m Quaternius head.** Gear is authored around the head *centre* (1.62) while the attach node is the Head *bone* (1.587), and the Quaternius centre is 1.69 — a 7 cm error put every cap brim across the brows, which I spent several passes misreading as a mesh hole (the band was pure black and no material had faces there). `HEAD_GEAR_LIFT = 0.103` fixes the common case. With the hats removed this only affects the firefighter helmet. - **Two character builders — do not confuse them.** `tools/blender_assets.py` is the real builder for the **eight library characters, 18 gear pieces and props** (it has the 0.9.4 face/hand sculpting in `_face()`/`_hands_and_shoes()`); `tools/npc_pipeline.py` builds **only** `assets/models/generated/npc_base.glb`, the frozen plain-primitive fallback. Reading `npc_pipeline.py` to judge the library's face/hand detail is wrong. - **Pose capture fixed (2026-11-04):** the harness was missing `set_physics_process(false)`/ `set_process(false)` on the avatar, so the FSM (`_advance_fsm` + `_move_toward_target`) ran every tick, reset `arrived`/velocity and made `theater_pose()` revert to idle/rest. Disabling both fixes it (sit vs idle diff 5.63, walk vs idle 2.29). The earlier "moving the `Rig` child does not render" probe was a red herring — it set the position inside `_shoot()` in the same frame as `root.get_texture().get_image()`, so it never rendered a frame. Lesson: pose/portrait harnesses must freeze the avatar's process/physics before driving `_animate()`. - Capture/pose harness gotchas: scripts that read captured-mouse input must run windowed (`smoke_props.gd`); the NPC FSM rewrites `rotation.y` every physics tick, so pose/portrait captures set the yaw after `set_physics_process(false)`; `visible=false` does not keep the crowd out of a portrait frame (park the rest of the crowd away instead). - Blender: `bisect_plane` and thin bevels leave zero-area triangles (Godot "non-finite normal in LOD generation"); `_clean()` + a weld after bisect fixes them and the audit rejects them. - Quaternius arm-down bake (working recipe, `tmp/npc-research/bake_armdown_v5.py`): pose-rotate `UpperArm.L/R` −90° local X → read the pose matrices → apply the Armature modifier → **reset the pose to identity** → set each arm bone's rest matrix from its pose matrix → re-add the modifier → export `export_apply=False`. Missing the pose reset double-applies the arm-down (mesh ±0.53 instead of ±0.23). `armature_apply` in Blender 5.2 is broken (deforms the mesh but does not bake the rest). The canonical base must be **join-only** (`build_base_minimal.py`: no `remove_doubles` weld, no scale/ground transform, keep the Armature modifiers) — the weld and the scale/ground transform corrupt the skin weights (arm poses to ±0.51 instead of −0.23). - Quaternius `NpcAvatar` wiring (commits `2a6f29f`, `cd285b3`): `RIG_BONES` + `NpcAvatar.bone_name(joint, side)` hold the **only** side mapping — the Quaternius labels `L`/`R` opposite the project, so proxy side 0 (`-x`) drives `UpperLeg.R`/`UpperArm.R` while side 1 drives the `.L` bones. The arm/elbow swing axis is `Vector3.LEFT` (legs stay `RIGHT`). The hand proxy is `global_wrist − global_elbow` (subtracting the parent-relative `elbow.position` misplaces handheld gear). Old 13-core names resolve through the same table. - **Bone posing must write the global pose** (`NpcAvatar._swing_bone`): `reset_bone_pose()` then `set_bone_global_pose(Transform3D(Basis(swing * global_rot), current_origin))`, conjugated with `get_bone_global_rest()`. Both rejected alternatives fail: the rest-relative `set_bone_pose_rotation(rest * Quaternion(rest⁻¹·axis, angle))` swings about the parent-relative rest (the glTF importer already folded the axis conversion into it) and splayed the Quaternius arms sideways; the plain `rest⁻¹ * swing * rest` conjugation flips the limbs 180°. Verified by measuring `get_bone_global_pose()` after the call — posed bone axis matches the proxy pivot at dot 1.000 on the Quaternius and the procedural rigs render unchanged. Two caveats: children need the parent's swing passed in (the write is absolute), and `set_bone_global_pose()` is a **no-op outside the scene tree**, so the headless runner (tests run in `_initialize()`) cannot assert on it. - **The Quaternius source UV map is degenerate** — every loop at (0,0), flat-colour materials, no atlas. The AO/texture bake silently produces a 100 % black map, which tints every AO'd slot (skin, hair, accessory) black in Godot: a black leg is the symptom. `build_base_minimal.py` now Smart-UV-unwraps and repacks the layout (texel density 0 → 73–74 px/m on 256²), and the base pipeline is two explicit steps with distinct files: `build_base_minimal.py` (`quaternius/Casual_Hoodie.glb` → `quaternius_tpose.glb`, joins without welding, renames the materials to the five tint slots, unwraps) then `bake_armdown_v5.py` (`quaternius_tpose.glb` → `quaternius_base.glb` + `assets/models/blender_src/npc_casual.blend`). Re-running v5 on its own output double-bakes the rest. - Hand gear must be self-contained: the camera carried a 0.42 m neck-strap loop that floats when the item is parented to the wrist. Hand items get a short wrist strap, not a neck strap. The backpack straps were authored for the wider procedural chest and floated ~5 cm proud of the Quaternius torso; the chest points now sit at x 0.12 / Godot z 0.15, which fits both. - Image models have per-model size floors/caps (Seedream needs 1920+; `GEN_SIZE=2048`, do not lower; gpt-5-image-mini caps lower). `generate_image` applies model-specific sizing; concept generation defaults to `--request-size 1536`. - Local photo research tooling: DDG rate-limits by IP (HTTP 202) — the `refcollect.py` token bucket (0.5 req/s, burst 3) plus retry/backoff handles it; some DDG frames are AVIF mislabeled `.png`/`.jpg` (convert with ffmpeg; `refcollect.py sheet` skips unreadable). Commons API needs a descriptive User-Agent and rate-limits thumbnail downloads (fetch all metadata in one `titles=A|B|C` query). - Frozen decisions: do not re-open `override_access`; do not bind wand to `W`; do not remove the radio easter egg. - GDScript: no static signals; lambdas capture locals **by value** (mutating a captured local does not affect the outer variable — use member functions for accumulators). - Reference cycles: `FSM` holds its owner via `WeakRef`, `ShiftCoordinator` is `RefCounted`; if a new subsystem introduces a cycle, repeat the WeakRef pattern. - Headless (dummy renderer) logs `Parameter m is null` from `mesh_get_surface_count` — benign, does not occur with a real GL driver. - macOS export requires `rendering/textures/vram_compression/import_etc2_astc=true` (universal build demands ETC2 ASTC). - Typed spawn signal: handlers must accept the 4th `event: Dictionary` arg or Godot will not call them. - GDScript inference trap: a `var` typed as a base class (`radial: CanvasLayer`) exposes script members as `Variant`; type it as the script type (`ActionRadial`). `--check-only --script` cannot see the `Save` autoload — use a real boot (`--quit-after 180`) to verify full-scene compile. - Do not snapshot right after changing a transform in the same frame — `capture_screenshots.gd` needs a state/frame gap or the capture uses the old camera orientation. - `EventScheduler` probabilities are per simulated second, not per call; never poll it per frame. - Light falloff: `omni_attenuation = 0.0` is **constant**, not physical, falloff in Godot 4.7 — all local lights use `1.0`. - glTF `baseColorFactor` is LINEAR — writing sRGB makes props pastel; `srgb_to_linear()` is applied in `build_glb`. A new/changed `.glb` needs `godot --headless --import` before `load()` sees it. - **Rebuilding a character GLB also requires re-baking its AO** (`tools/bake_ao.py -- --only `). `blender_assets.py build` writes the GLB only; the old `_ao.png` then maps onto the new geometry and produces large dark cheek/jaw blotches. Bake the AO, re-import, and refresh the stale `.godot` cache (scratch-copy import) before judging a face/body change. - `Save.SETTINGS_FILE` is declared and unused; settings persist through `load_settings()`/`save_settings()`. - Crowd reactions emit `event_bus.crowd_changed`; `GameManager._connect_crowd()` must reconnect after every `coordinator.setup()` (same class of bug as the radio-dispatch reconnect). - Packaging: NSIS uses `-D` defines (never `/D`) and resolves relative defines against the script directory (pass `$PWD`-absolute paths); installed exe must be space-free; ICO must be classic BMP entries (PIL emits PNG-compressed ICOs NSIS can't load). appimagetool runs system `appstreamcli` — pass `--pedantic`. - Credentials: `~/.git-credentials` holds the Gitea creds; `.env` holds only `OPENROUTER_API_KEY`. - `assets/textures/game_ready/` is not imported (no `.import`) — use the imported `assets/textures/skyline.png` path. - Godot 4.7 writes `*.gd.uid` files — kept out of `git status` via `.git/info/exclude`. - Collaborators occasionally leave uncommitted work in the shared tree; check `git status` before committing and leave foreign changes untouched. ## Commands - Tests: `godot --headless --path . --script res://src/test/test_runner.gd` - Boot check: `godot --headless --path . --quit-after 120` - Single screenshot: `godot --path . -s res://src/test/take_screenshot.gd` - Staged gallery: `tools/check_screenshots.sh` (re-run the whole script, never a partial capture) - Blender build: `flatpak run org.blender.Blender --background --python tools/npc_pipeline.py -- build` (flatpak cwd is `/app/blender` and cannot read `/tmp` — use absolute repo paths; `.gdignore` markers keep Godot from importing `.blend`/`tmp`) - Textures: `python3 tools/generate_textures.py` (then `godot --headless --import`); AI mode `python3 tools/generate_textures.py --ai`; general pipeline `python3 tools/asset_pipeline.py` - Character checks: `python3 tools/blender_assets.py inspect|audit|fit`, `python3 tools/npc_proportions.py` - Docs: `python3 tools/check_docs.py` (every tracked image must be linked from its folder README) - Head/part pipeline: `python3 tools/npc_parts.py build|extract|assemble|inspect|sheet|compare` (Blender subcommands run under flatpak). Head sources: `build_character(head="face"|"proto"|"mh")`. - Reference research: `python3 tools/refcollect.py search --slug --limit N` (local-only), `tools/refboard.py` (Commons), `tools/three_up.py compose/rows`, `tools/scene_concepts.py generate/anchor/realism` ## Open Questions - Whether the responder avatar should be playable/controllable or purely scripted. - Quaternius scope: full 12-role swap, crowd-only, or extra head detail (owner decision at 0.9.9). - Localization target languages beyond English (0.11.0/0.12.0). - macOS notarization approach for broad distribution (0.12.0). - Fourth-venue choice for 0.11.0 (candidate: TCL Chinese Theatre forecourt). ## Current State v0.9.8 is published. The build covers the three venues, the shift loop, tutorial/campaign/ checkpoints, the 0.9.3–0.9.8 polish/texture/integration/character passes and the Blender + realism asset pipeline (see `DONE.md` and `CHANGELOG.md`). The 12-character procedural library (realistic face + four crowd variants) and the Comedy Store map rebuild are complete. **Planning pivot (2026-11-05, owner):** the character and map work was re-sequenced into patches (`0.9.9`–`0.9.12`) with `0.10.0` as the public-readiness minor. The gap analysis and the three release roadmaps now live in `PLAN.md`; `TODO.md` is the executable task list. ADR-0017 Stage 1 (proportion warp) is dead; ADR-0018 option 2 (62-joint runtime) is accepted. **Full roster (0.9.9, commits `a6ccd86` … `9fd4824`):** all twelve characters run on the Quaternius bases (62 joints, 73–74 px/m texel density), built by the shipped `tools/blender_assets.py` two-pass pipeline (`_quaternius_tpose_glb` → `_bake_quaternius_rest`). The owner signed the roster off **with follow-ups** and rejected three review rows; the Worker body defects (holes, unibrow, missing eyes) and the head-gear seating are fixed, every hat except the firefighter helmet is removed, and every head has an authored mouth. Gates: `audit` 12/12, `inspect` clean, `fit` OK, 24-NPC budget 174,976 tris / 0.85 ms, Godot 278/278, Python 167/167. The owner **accepted the roster on 2026-11-06** and the review page plus its sheets were then retired; durable evidence lives in `docs/3d-model-review.md` and `docs/blender-assets.md`. Release packaging was fixed the same day: excluding `tmp/`, `build/`, `docs/`, `tools/`, `src/test/` and `.agents/` from the export presets cut the Linux binary from 426 MB to 273 MB, and `tools/build_releases.sh` writes a SHA256SUMS file. **Open after the swap:** hand-held gear and role props are still authored for the old body (wrist y 0.78 → 1.017, arm shorter), so the clipboard and umbrella are oversized; the eyes have no whites; the arms graze the torso on the `worker` base; roster variety is three meshes for twelve roles. The prop/gear fit pass is queued in `TODO.md` under 0.9.10. **Quaternius pilot (2026-11-05, commits `cd285b3`, `fb3e75a`, `90f189a`):** `npc_casual` ships as the Quaternius base. It renders arm-down, carries the five tint slots, plays the Idle/Walk/Sit pose code on the 62-joint rig, and the procedural rigs are visually unchanged. Verified: `inspect` (no issues), `audit` OK (6,206 tris / 12,988 verts, 62 bones), `fit` head 0.201 m, texel density 73–74 px/m, `npc_proportions` measured, 24-NPC budget smoke OK (168 meshes, 162,304 tris, 0.82 ms avg frame). The face/silhouette gate sheet and the three options are in the face-gate decision (owner chose the full swap). Test counts as of 2026-11-05: Python 167/167, Godot 278/278, `check_docs` OK.