✦ the diary of a workshop ✦

Founder Worldchangelog

One page per day. What we built, in our own voice, with the wax still warm.

Saturday
·

May 16, 2026

The Pivot

SHIPPED
20:10·2 commits

Day zero — the scaffold rises

First commits. Next.js 16 on Vercel, Phaser 3 in an iframe-less canvas, Supabase for tenancy and RLS, Composio reserved for OAuth, ruflo for the agent loop.

Nothing renders that you would call beautiful yet, but the founder sprite walks where you click on a 14×14 isometric grid. Everything later stands on this one.

SHIPPED
22:56·1 commit

Habbo-Argentum, locked in

Tossed the muted slate-and-sage minimalism. The game should feel like a place, not a dashboard. Pivoted to a vivid Habbo-Argentum palette: orange #ff6b35, teal #06d6a0, walnut, mustard, navy.

Stood up the gpt-image-2 → sprite pipeline: one style-reference sheet, then 26 starter assets batch-generated against it, then a sharp-based processor that removes the off-white background and auto-crops to content. Three.js is gone — Phaser sprites win.

  • src/r3f deleted, deps uninstalled
  • scripts/genart.mjs + scripts/process-sprites.mjs
  • Four agent personas wired to Claude (Vera / Mateo / Iris / Felix)
SHIPPED
23:43–23:59·2 commits

Landing page — spec at noon, ship by midnight

A live Phaser hero up top. Four agent cards each speaking in their own voice. A four-beat "walk in" session walkthrough. A chunky orange CTA into /game and a quieter one into /whitepaper.

Spec committed at 11:43 PM. Implementation pushed at 11:59:50 PM. Ten seconds to spare.

the founder
the founder
Sunday
·

May 17, 2026

The big build

SHIPPED
00:21–01:29·3 commits

Whitepaper — thesis sharpened

Started the day with a copy pass on /whitepaper. New spec, full redesign, then a hero pivot from "scroll-tax" framing to a much sharper "play-state" thesis that lines up with how the game actually plays.

Three commits between 00:21 and 01:29. The rest of the document fell into place behind it.

SHIPPED
04:56–05:14·4 commits

Sprint 1 + 2 — the magic-moment loop ships

The hour the app started actually doing the thing. Composio Reddit OAuth + /connectors picker; Vera ingestion cron pulling pain threads from r/SaaS every six hours via Kimi K2.6, bridged into founder_memories and pain_point_npcs; NPCs spawning in the lobby, walking into the office on engage, auto-opening Mateo's chat next to his desk.

Sprint 2 #4 closed the loop: Mateo auto-drafts outreach on NPC arrival, MateoDrafts panel for Send / Edit / Discard with explicit confirm before posting publicly, pipeline cards auto-advancing spotted → contacted on send.

Plus Clerk via Vercel Marketplace (env-aware demo fallback) and five fresh migrations applied to prod.

  • Migrations: founder_memories, clerk_auth, connections, pain_point_npcs_dedupe, outreach_drafts
  • Vercel deploy triggered via the GitHub integration
  • In-game /connectors modal + prod schema repair
SHIPPED
05:35·3 commits

Startup Tower — multi-floor Habbo nav

Click "tower" in the HUD → TowerScene. A procedurally-drawn isometric building with two facades, a roof, per-floor windows (warm-glow for yours, dark for vacant) and a twinkling-star sky behind it.

GET /api/tower/floors returns five floors — your office on 2, vacant 3-5, lobby on 1. Hardcoded for v1; will populate from real tenants in v2 when federation lands. The foundation for the federated-offices vision from the whitepaper.

SHIPPED
05:41–06:34·6 commits

Onboarding wizard + agent tour + Habbo glide

First-time founders now hit a 2-step wizard before the office loads: identity (display name + company, ≥2 chars each), then character — pick one of four archetypes (hoodie / blazer / streetwear / casual), each rendered as a 64×96 isometric sprite.

Once past the wizard, the camera runs an in-world tour — pans + zooms across each of the four agent desks (Vera, Mateo, Iris, Felix) and floats a speech bubble per stop. React owns the Next / Skip HUD; Phaser owns the camera and bubbles.

And the walk got the upgrade it deserved — full 4-direction × 4-frame animation per archetype, smooth Habbo-style glide instead of the old per-tile hop.

SHIPPED
05:50–07:42·12 commits

Sit-poses — the four agents sit down

The agents were standing at their desks like they were waiting for a bus. Now they sit. New calibration sprite established the founder sit pose; the same pipeline (with per-asset reference overrides for pose/rotation variants) generated sit sprites for research, outreach, and pipeline agents.

Seated them in the default layout (sit sprites + char-before-chair). Then fixed the avatar tween that was stretching the founder's head off her body and the floating-32px issue that made her hover above the floor.

SHIPPED
06:49–07:24·8 commits

Pixel-art pipeline — Recraft V3 → pixellab

First swing: real pixel art via Recraft V3 + a few procedural tiles. ~20% of the batch came back with scenery leak — bookcases with rooms baked in, characters with desk shadows attached. The verdict was "still horrible".

Re-rolled the 21 broken sprites through pixellab.ai's create_map_object MCP — dedicated pixel-art generator with native transparent backgrounds, no scene-context drift. Pipeline runs async (30-90s per asset, 10 concurrent, ~$0.10 each) into the existing crop+resize step. No code changes, pure asset swap under the same manifest keys.

Then a true-Habbo character pass via pixellab create_character, plus directional walk sprites for all four onboarding archetypes. Later regenerated those four via the foundr-world:create-character skill once it stabilised.

  • Verified office render via Playwright (office-pixellab.png) — hard edges, consistent palette, no leak
  • foundr-world:create-character skill used as the reference implementation
SHIPPED
07:33–08:39·29 commits

Inbox + Realtime foundation

The largest piece of the sprint by far. The inbox is where the outside world enters the office — a Gmail reply, a new NPC arriving in the lobby, an outreach draft Mateo wants you to sign off on, a system tip. Everything emits to one feed.

Built from the foundation up: schema → emission helper with an 8KB payload cap → REST endpoints → realtime hook with subscription + REST fallback + dedup → action registry → InboxItem cards → HUD badge with pulse + disconnected glyph → popover (latest five) and a paginated, filtered drawer.

Three crons keep it alive without anyone watching: poll Gmail every five minutes, simulate wins hourly for founders without a Gmail connection, weekly cleanup of old events.

  • inbox_events + outreach_replies + gmail cursor tables (with realtime publication)
  • API: GET /inbox, POST /read, POST /read-all
  • Emitters: system.welcome, system.tip, outreach.drafted/sent, npc.arrived, gmail.reply
  • Tests: RLS cross-tenant fixture (Alice / Bob), end-to-end realtime channel contract
  • useSafeAuth wrapper so the hook does not blow up in demo mode
SHIPPED
07:42–07:54·11 commits

Skin drawer + edit-room mode

A bottom slide-up drawer to swap founder archetype mid-session. Card click fires onPreview() for an in-world live swap; Save persists via PATCH /api/founder/avatar; Cancel / Esc / scrim-click revert cleanly.

Underneath: extracted FOUNDER_ARCHETYPES into a shared module, added Avatar.swapTexture() for live previews, wired the avatar:swap event through OfficeScene, and bridged window-level avatar-preview events into the game bus.

Edit-room mode came along for the ride — Habbo-style room inventory you can rearrange, and a 10-carpet collection now lives in the inventory drawer.

blazer
blazer
SHIPPED
14:09·1 commit

Sign-in unblocked

Blank /sign-in page would not render. Switched to a client component and dropped the explicit routing props that Clerk now infers — the page came back.

SHIPPED
14:28–14:50·14 commits

Admin panel @ /admin

A real admin surface for granting items to founders, browsing the catalog, and inspecting any founder's layout. Email allow-list at the door, Clerk middleware on /admin and /api/admin, a server-side gate that 404s on anyone who is not on the list.

GET /api/admin/founders joins Clerk emails. POST /grant hands items to a chosen founder, indexed by kind and collection. Two-column UI: catalog on the left, target founder + their stash on the right.

admin desk
admin desk
SHIPPED
14:38–16:47·9 commits

Asset forge — furniture, dragons, skyline shipped

Five Habbo-style furniture pieces, each rendered in eight rotations plus four state variants. Four dragon statues — red, blue, green, dark — as the first proper decoration objects.

And the skyline towers actually shipped. The spec from earlier got a plan, reference images for style lock, a pixellab.ai generation script, then ten Habbo-style backdrop towers rendered as a parallax skyline behind the main building.

red
red
green
green
blue
blue
SHIPPED
15:33–16:42·22 commits

Inventory & rooms — relational redo, wired end-to-end

The first schema was flat and convenient. The second one is proper: collections (drops, sets, freebies), items_catalog as the single source of truth for every grantable thing, rooms, and room_items as the per-founder layout table — with FK indexes and enum CHECKs so the constraints aren't aspirational.

Then we wired it through everything. Helpers (items-catalog, rooms, room-items grant/pickup/place/rotate). New APIs: GET /api/me/inventory, GET+PATCH /api/rooms/[id]/items. Admin grant and /layout rewritten on top of room_items. OfficeScene now hydrates from /api/rooms/[id]/items instead of the old defaultLayout — which got deleted along with grantable-catalog and the room-layout route. The old room_layouts table got dropped.

Side effect: a Save-button-stuck bug in the skin drawer when reopening, fixed by resetting submitting state on open.

  • Primary room auto-created at signup (with a self-heal path for pre-signup founders)
  • items_catalog seeded directly from the SPRITES manifest
  • Game fetches bootstrap + room before booting Phaser to prevent flash-of-empty-office
SHIPPED
21:43–21:49·2 commits

Bootstrap self-heal

Last commit of the day. Some founders existed before the primary-room-at-signup rule landed and had no room — the game would crash for them. Added a self-heal path that creates the room on bootstrap if it's missing, and made the game fetch bootstrap + room before instantiating Phaser so nothing races.

SHIPPED
22:02–23:11·5 commits

/changelog diary + the skill that keeps it honest

We needed a place to remember what we shipped each day, in a voice that reads like a diary instead of a release-notes feed. Built /changelog as a parchment-styled long-scroll with wax-sealed feature cards, locked to the same cream/wood/sage palette as /whitepaper and / so it reads as part of the site, not a one-off.

Wrote a `foundr-world:update-changelog` skill alongside it. State lives in a `processed.json` file under the skill — every SHA narrated in the diary is recorded there, so the next run only sees what is genuinely new. A small `new-commits.mjs` script reads the JSON and diffs against `git log --all` to print "what shipped since last time".

Then a polish pass: re-ordered the day's features chronologically by their earliest substantive commit, added a `time` field to every card (single time or HH:MM–HH:MM range), and captured all of this back into the skill's SKILL.md so the next session inherits the playbook.

SHIPPED
22:07–23:36·33 commits

Wall system — design, geometry, picker, end-to-end

The back walls used to be hand-placed sprite parallelograms hard-coded to a 10×10 room. We threw that out and built a real wall system: parallelogram geometry derived from pure math, a renderer that paints base / trim / baseboard / corner-cap from a `WallPreset`, and a `WallSystem` orchestrator that owns render + swapPreset + clear.

Backed by a `wall_presets` table with tier-gated unlocks (a starter cream preset ships with every founder, premium ones gate behind tier). Helpers cover get/list/set, GET /api/wall-presets returns the player's available presets, PATCH /api/rooms/[id]/wall-preset persists the choice.

On the front: a `WallPickerTab` slots into the edit-room drawer with a live preview before save, and a `wall:swap` bridge from the React HUD to Phaser swaps the rendered walls in place — no scene restart. Room.ts now delegates entirely to WallSystem; the legacy `wallBack*` PNG sprites and manifest entries were deleted.

  • WallGeometry has unit tests (adjacency, slope, slice invariants) under vitest
  • Migration 0018 ships wall_presets + rooms.wall_preset_id with FK + tier CHECK
  • Design spec lives at docs/superpowers/specs/2026-05-17-wall-system-design.md
SHIPPED
22:44–23:56·23 commits

Sprites → Vercel Blob with manifest + atlases

Sprite PNGs were piling up in git — fine at 30 files, miserable at 300. We moved them out: every sprite is now content-hashed, uploaded to Vercel Blob with a year-long Cache-Control, and recorded in `src/assets/sprite-manifest.json`. `public/assets/sprites/` becomes a gitignored local staging area; the manifest is the single source of truth.

The sync script (`npm run sprites:sync`) is idempotent — re-running with no changes uploads nothing. Sprites also get packed into TexturePacker atlases by `<scene>-<kind>` groups (7 atlases shipped at first cut: avatar / furniture / rug / decor / agent-sit / skyline / tower), so Phaser pulls one PNG + JSON per group instead of dozens of separate textures.

Wired through the codebase: `preloadForScene(scene, sceneKey)` in every BootScene-style preload, `addSprite(scene, x, y, key)` for new placements, `setSpriteTexture(sprite, key)` for swaps (Avatar), and `spriteUrl(key)` for React `<img>` tags. Scene tagging is rule-based in sync-sprites.mjs, so new sprites only ever need a rule entry, never per-file metadata.

  • Atlas-aware helpers fail loud if a key is unknown — no more silent missing-texture polygons
  • Vercel deploy re-triggered under the dante-perea identity so the Blob token resolves
  • CLAUDE.md updated with the new pipeline (generate → process → sync → use)
Monday
·

May 18, 2026

The polish

SHIPPED
00:10–00:58·4 commits

Walking polish — depth, direction, feet

Two visible bugs landed on the founder all at once. First: walking south or east, the destination floor tile rendered over the avatar's lower body for the full 280 ms of each step — the legs and torso would briefly disappear into the floor. Fixed by promoting the container depth to `max(srcDepth, dstDepth)` at segment onStart so the avatar always wins against either tile it's straddling.

Second: the gpt-image-2 walk-frame batch came back with the iso pair labels inverted across all four directions. The file labeled `-se-walk*` rendered a SW-facing character, `-ne-walk*` rendered NW, and so on. Added a small WALK_ASSET map that swaps each direction to its mirror file at texture-selection time. The standing-pose fallback for NE/NW rides the same map.

Topped off with an atlas-aware fix to the feet anchor: source dimensions now come from the manifest frame instead of the raw texture (which can be a single atlas page when packed), so the character always lands centered on the south point of the tile.

  • Long-term fix is to regenerate the four walk sheets with correct iso labels — the swap is a runtime workaround
SHIPPED
00:17–00:47·5 commits

Atlas robustness — placement, helpers, fail-loud

After the Blob + atlas move, a few corners weren't atlas-aware yet. Split the avatar atlas by archetype so a packer with multi-page output fails loud instead of silently dropping frames. Exported a `hasSprite()` helper from `iso/textures.ts` and switched every `scene.textures.exists()` call across the game to it — atlas frames don't register as top-level textures, so the old check was lying.

Furniture placement and the held-item ghost in edit-room mode were still using `scene.add.image()` directly; rewrote them to go through the atlas-aware `addSprite()` so rotated variants and ghosts both pull from the right page.

SHIPPED
00:40·1 commit

Inventory drawer respects admin grants

The edit-room drawer was reading from the items-catalog directly instead of the founder's actual inventory, so admin-granted items never showed up in the picker even though the database had them. Switched the drawer to /api/me/inventory; the grant now appears immediately on the next open.

SHIPPED
00:47–02:17·24 commits

Multiplayer presence — visitor rooms, end-to-end

The biggest feature of the day. Visitors can now drop into another founder's room over Supabase Realtime, see them walking around, and leave without writing anything to the host's db. Built on the Inbox realtime foundation from May 17 — same channel infrastructure, a new `room:<roomId>` presence channel per room.

Spec'd at noon, shipped by 2 AM. Started with a `handle` system: every founder now has a slug (validated, reserved-list filtered, debounced availability check in the onboarding wizard) and a stable `room_url_hash`, so their room URL is `foundr.world/game/room/<hash>` regardless of display-name churn. The wizard previews the URL live as you type.

Then the realtime layer: `useRoomPresence` syncs roster + broadcasts move / teleport / sprite-change. `RemoteAvatar` renders each presence record as a non-input avatar that mirrors the host's walks. `OfficeScene` wires `room:presence-sync` / `move` / `teleport` / `sprite` into the scene; `GameCanvas` joins the channel on bootstrap. Visitors are correctly suppressed from engaging NPCs or clicking on agent desks — they're tourists, not employees.

UI surface: a `share room` HUD button + modal for the owner with copy-link affordance, a "who is here" widget that lists the live roster, and a 404 page for invalid handles.

  • Migration 0019: founders.handle + room_url_hash columns + room_visits ledger (with 1h debounce)
  • Channel naming refactored: roomChannel(roomId) helper + RoomPresenceState type
  • Visitor route: /game/room/[handleHash] with leave button
  • Contract tests for presence + broadcast (gated behind GATE_REALTIME_TESTS)
  • Fallback to demo auth when ClerkProvider is missing (so unauthed visitors still join)
SHIPPED
01:14–02:23·3 commits

Game loading overlay — real progress, ETA, full-bleed

The game used to blank-flash for 3-5 seconds while Phaser pre-loaded the atlas and bootstrap data, with no signal anything was happening. Spec'd a proper loading overlay (real progress %, named current phase, rough ETA) and shipped it the same hour.

Late-night polish: the loader had a stuck state if bootstrap failed before Phaser started, and the framing was windowed instead of full-bleed. Both fixed — the overlay now covers the full viewport with a cinematic gradient and never gets stranded waiting for an event that won't fire.

SHIPPED
01:16·1 commit

Bootstrap unbreak — survive missing wall_presets migration

Production /api/game/bootstrap was returning 500. Root cause: the wall preset lookup added in 0d1cb8a joins against `wall_presets` and `rooms.wall_preset_id` — both from migration 0018, which hadn't been applied to prod yet. The `??` fallback only catches null returns, not thrown query errors, so bootstrap died entirely.

Wrapped the lookup in try/catch with `STARTER_CREAM_PRESET` as the fallback (the same value the migration would have backfilled). Production unblocked; the migration can land at any pace without further drama.

SHIPPED
02:42–03:16·3 commits

Avatar — feet grounding, shadow, frame-swap origin

The morning's walking-polish thread kept going past the first three fixes. We grounded the feet in the southern half of each tile so the character stands at the bottom corner the way Habbo characters do, not floating in the tile's center. We re-applied `setOrigin(0.5, 0.95)` after every `setFrame()` so atlas frame swaps stopped resetting the anchor to (0, 0) on every animation tick.

And we replaced the bouncy halo glow under the avatar — three-stop alpha gradient that animated with the bob — with a tight static contact shadow. Reads as "standing on the floor" instead of "floating with aura". Subtle but noticed immediately on the next snap.

SHIPPED
02:47–07:16·10 commits

Realtime hardening — visitor sprites, walks, presence

After multiplayer presence shipped at 2 AM, six follow-up bugs surfaced in production. Visitor avatars weren't always rendering (race between channel subscribe + scene ready), their sizes drifted from local (no shared display-size constant), their walks didn't animate during the move tween (texture key updated but no anim play), pathfinder routes were broadcasting only endpoints (remote saw a straight line; local took the A* path through obstacles), the "Who is here" widget flickered as presence sync re-fired every 200 ms, and the local player's position wasn't persisted across reconnects.

Each fix was small. Throttled `track()` calls to once per 300 ms with skip-snap so identical state doesn't re-broadcast, untrack on scene exit so closed tabs don't leave ghost rosters, broadcast the full route array so remote walks match the obstacle-aware path, sync display size from the same TILE_W constant on both sides, and replay the walk animation on the remote side whenever the texture key changes. Visitor mode now feels solid enough to invite real people.

  • WhoIsHere flicker fix — stabilize presence subscription, skip-snap on identical track payloads
  • Remote avatar render race + depth — atlas-ready check before adding sprite
  • Display-size parity — both sides read from TILE_W
  • Full pathfinder route broadcast — remote A* matches local A*
  • Walk anim plays during move tween (was: only on texture swap)
  • Local position persisted in presence track payload
SHIPPED
03:13–05:52·8 commits

Tower as landing — landmark centerpiece, no skyline

Two shifts. First: BootScene now lands players in TowerScene by default — you see your building from outside before stepping into the office, which sets up the "this is my company, in this city" framing better than dropping straight into a desk. The boot loader label and the office's "go to tower" CTA both got rewritten to match.

Second: the 10-tower skyline backdrop got stripped. The detailed landmark81 sprite already carries the scene; flanking towers were reading as visual noise. Replaced with a soft horizon-glow band (warm-light dust + dark curb) behind the building, and bumped the star count 55 → 90 so the night sky fills the cleared space. Habbo-style single-building vista, nothing competing.

Then iteration on the landmark itself: isolated, scaled up, floor badges removed, tighter sprite crop, responsive sizing so the tower hugs the viewport floor on any aspect ratio.

SHIPPED
03:46–04:07·2 commits

Cold-load slimdown — 92% smaller boot, 7× faster bootstrap

A 92% reduction on cold-load payload, two angles. The four founder-archetype atlases (~1.2 MB combined) now lazy-load on demand when the SkinDrawer opens, instead of preloading all five upfront — the player only ever wears one at a time. Palette PNGs that drive the wall-color swatches defer until edit-mode opens. Combined with smaller wins, the boot scene now loads ~90 KB instead of ~1.2 MB.

Bootstrap got the same treatment. Cached the postgres pool across requests (was instantiating per-call), parallelized the founder + room + walls + agents queries (was sequential await chain), and moved the items_catalog seed into the signup flow instead of the boot path. p50 dropped from ~600 ms to ~80 ms.

  • Cold load: ~1.2 MB → ~90 KB (lazy archetypes + deferred palette PNGs)
  • Bootstrap p50: ~600 ms → ~80 ms (cached pool + parallel queries + signup-time seed)
SHIPPED
05:28–06:29·19 commits

Multi-floor leasing — rooms you can rent in your own tower

The biggest new feature of the day. Founders can now lease additional rooms — they appear on new floors in their tower, each with its own slug + share URL. Schema: `rooms.floor_number` is authoritative, a partial unique constraint guarantees at most one primary per founder, and a second partial unique on (owner_id, floor_number) prevents collisions when the lowest free floor ≥ 3 gets picked at lease time. ROOM_SIZES schedule + MRR gating on the POST handler (`mrr_locked → 403`).

The "find a room" palette opens from any scene and lists publicly-shared rooms with isometric previews — per-tier props, wall tints, ambient glow on each card. A friends tab (rendered after the friends feature shipped two hours later) shows online-first contacts whose rooms you can drop into. The "my rooms" switcher lets you hop between your own leased rooms.

Two refactor passes in the same window: dropped `?room=<id>` query params in favor of route-based addressing (slug + 8-char url_hash, backfilled from founder.handle for legacy share links), and unified the visitor route so `/game/room/<handle>-<hash>` drops into the host's office instead of the tower stub.

  • Migration 0021: rooms.floor_number + partial unique constraints + backfill primaries to floor 2
  • Migration 0023: url_hash + slug + legacy primary-hash backfill (zero redirect debt)
  • ROOM_SIZES schedule + MRR gating: mrr_locked → 403, unknown_size → 400
  • POST /api/rooms (create), GET /api/rooms (own list), GET /api/rooms/public (catalog)
  • FindRoomPalette mounted in office, lobby, and guest room views
  • Isometric room previews with per-tier props on catalog cards
  • Visitor mode skips tower, lands directly in host's office
fresh lease
fresh lease
SHIPPED
06:41–14:48·20 commits

RoomContext — collapse the flag soup into one type

The third-room feature exposed a tangle. Ten different code paths each computed "am I visiting? is this primary? where do I land?" from a different combination of bootstrap flags, URL params, and ad-hoc booleans — and they didn't always agree. Collapsed into a single `RoomContext` type with a pure `computeRoomContext()` function: given the bootstrap response + an optional room hint, returns the full context as one value. Phaser registry gets a typed accessor; React side gets a `useRoomContext()` hook.

BootScene reads `initialScene` from it. OfficeScene reads `canEdit`. GameCanvas pushes one object instead of three. Four unit tests pin the four scenarios (primary, secondary, visitor, no-context-yet).

Afternoon: the visitor-mode unification spec'd in the morning landed in full. Bootstrap got renamed `activeRoom → viewerPrimary` (the alias on `primaryRoomId` is gone). BootScene + OfficeScene drop their `isVisitor` fallback entirely — `RoomContext.source` is the only signal. GameCanvas drops three legacy props (`remoteRoomId`, `isVisitor`, `primaryRoomId`) for `targetRoomId` + owner-identity. GameContent's HUD branches on `roomContext.source` for visitor vs. owner chrome. And the visitor route now uses the same `GameContent` as the owner route — `RoomGuestClient` is deleted. One component, two scenarios, zero parallel codepaths.

  • Single RoomContext type covers primary/secondary/visitor cases
  • Pure computeRoomContext() — testable with no Phaser or DB dependency
  • BootScene, GameCanvas, OfficeScene all read from one source
  • useRoomContext React hook over the Phaser registry
  • Visitor route unified — same GameContent as owner route, RoomGuestClient deleted
  • Bootstrap rename: activeRoom → viewerPrimary, primaryRoomId alias dropped
SHIPPED
06:47–14:38·19 commits

Missions ladder + credits + shop — 10x gameplay V1

A full game loop shipped end-to-end in one sitting. The game now has a 10-mission Garage-tier ladder (walk around, meet your team, open inbox, reply to a lead, visit lobby, visit tower, spend your first credits, decorate your office, send outreach, land $1 MRR), soft-currency credits earned on completion, and a HUD-button shop selling 18 furniture items at 10–200 credits — five of them gated behind specific missions.

Architecture: one engine module is the only path that mutates progression. Every gameplay event flows through `recordEvent()`, which is atomic, idempotent (deterministic key per logical action), and produces a typed result for the UI to render toasts + update the always-visible quest widget. Schema includes V2 columns (`credits_premium`, `counter_scope`, `repeatable`) populated with neutral defaults so daily quests, dual currency, and farmable missions can ship later with zero migration.

Industry research informed the design: counter-based engine pattern from GS2-Mission, event-ledger from Trophy.so ("single metric event"), idempotency keys from PlayFab Economy v2, always-visible quest widget from WoW Focus tracker + Genshin Impact. Spec, 21-task plan, and a subagent-driven execution log all in `/docs/superpowers/`.

One bug landed in production: every shop card rendered a broken-image placeholder. The ShopDrawer was hardcoding `/assets/sprites/<key>.png`, but furniture sprites moved to the Blob CDN with content-hashed filenames the week before. Fix was three lines — resolve through the manifest the same way Phaser scenes do, with a placeholder for keys missing from the manifest.

  • Migrations 0025 + 0026: 5 new tables (missions, mission_progress, mission_events, shop_items, shop_purchases) + founders.credits + realtime publication
  • Counter-based engine: event / count+dedupe / threshold variants under one recordEvent
  • 5 engine tests pass (idempotency, dedupe, prereq unlock, threshold, happy path)
  • 4 API routes: POST /api/missions/event, GET /api/missions/list, GET /api/shop/list, POST /api/shop/buy (atomic, idempotent)
  • 4 HUD components: QuestWidget (always-visible top-right), QuestLogDrawer, ShopDrawer, RewardToast
  • Realtime mission channel (postgres_changes on mission_progress) + useMissions hook
  • 10 event wirings: walk_tile, agent_clicked, inbox_opened, email_replied, lobby_visited, tower_visited, shop_purchase, item_placed, outreach_sent, mrr_changed
  • Shop button visibility gated on buy-something mission status (avoids the timing bug where buying before the mission existed orphaned the credits)
10 credits
10 credits
25 credits
25 credits
SHIPPED
07:02–07:21·11 commits

Friends — right-click on any remote avatar

Right-click on any remote avatar pops a mini menu over the canvas. "Add friend" fires a friend request that arrives in the target's inbox as an actionable card; they accept or decline; both sides see the edge update via the inbox handler. The find-a-room palette gained a friends tab (online-first sort) so accepted friends become first-class drop-in destinations.

Schema: a single `friend_edges` table with two directed rows per friendship — one row per direction, four-state lifecycle (pending / accepted / declined / blocked). The double-row model avoids ambiguity about which side initiated and lets either side block without ambiguity. `useFriendsMap` caches the caller's edges client-side so the right-click menu can color itself accurately (already friends? request pending you sent? request pending you received?).

  • Migration 0024: friend_edges (two-directed-row, four-state lifecycle)
  • POST /api/friends/request, POST /api/friends/respond, GET /api/friends
  • RemoteAvatarMenu component (right-click via context-menu event)
  • Inbox handlers: friend_accept / friend_decline actions
  • Friends tab in FindRoomPalette (online-first sort)
SHIPPED
14:55–15:07·2 commits

Every room is equal — the primary concept goes away

The "primary office" was a relic — a special room in a world where everyone has many rooms. We collapsed it. Routes drop the `?primary=1` branch entirely; HUD navigation goes through `/game/room/<slug>` regardless; the my-rooms dropdown gives every leased room the same chrome. QuestWidget moved to upper-left so the my-rooms dropdown has room to breathe.

Migration trail: the `primary` boolean stays in the schema as a no-op for now (so legacy data isn't a problem), but no read path consults it anymore. New rooms don't set it.

SHIPPED
15:19–15:30·2 commits

X-only sign-in

Cut the email/password and Google flows from /sign-in. Founders sign in with X — one button, one OAuth redirect, the identity that already matches how founders post in public. Migrated to Clerk v7's Signal API along the way (the older OAuth hook had drifted out of support).

SHIPPED
15:33–20:21·41 commits

Responsive trilogy — content pages, /game HUD, Phaser touch

A coordinated push to make Founder World survive a phone in landscape and a thumb on the canvas. Three sub-projects, each spec'd, planned, executed, and audited in the same five-hour window.

Sub-project A: content pages. SiteTopBar primitive across / and /whitepaper (mobile-clean nav, desktop-only secondary link). Hero overlay darkens for legibility on small screens. Whitepaper TOC becomes a sticky pill that opens a bottom-sheet on tap — Escape + backdrop close, useActiveTocSection extracted. Global `prefers-reduced-motion` guard at the CSS layer, plus a synchronous short-circuit in the reveal animation so motion-sensitive users see the page instantly.

Sub-project B: /game HUD. QuestLogDrawer and ShopDrawer both become floating mid-cards on <md, right-panel docks on md+. Mobile tab bar respects iOS safe-area-inset-bottom. QuestWidget gains a mobile full-width strip that auto-shows on quest change, with a Quests kebab item to re-open it manually. FOUNDER/WORLD logo stacks onto two lines at <sm so it clears the 375px viewport.

Sub-project C: Phaser touch. Built `iso-gestures` on Pointer Events (tap, pan, pinch — with proper cancel + teardown), backed by unit tests for state-machine transitions. `camera-fit` derives orthographic camera config from viewport + room bounds. Office / Tower / Lobby all wire both helpers; the scene-wide pointerdown listener is gone in favor of explicit `handleTap` per scene with shutdown cleanup.

  • SiteTopBar shared between / and /whitepaper — mobile-clean, desktop-extended
  • Bottom-sheet/overlay TOC component replaces the old floating chip
  • iso-gestures helper: Pointer Events, pan/pinch/tap, cancel + teardown, unit-tested
  • camera-fit helper: viewport + room bounds → camera position + zoom
  • QuestWidget mobile auto-show on quest change + Nav kebab manual-open
  • Sub-project A / B / C audit docs each record verification screenshots
SHIPPED
15:41–18:44·28 commits

Public + private rooms, plus drop-in guest viewers

Two coupled features that landed in the same window. Each room now has an `is_public` flag; the my-rooms dropdown shows a per-row globe/lock toggle plus copy-link, and the FindRoomPalette only lists the rooms whose owners flipped them public. /game routes get noindex headers, robots.txt disallow, and X-Robots-Tag from the proxy — search engines stop indexing private founder offices.

On top of that, the visitor route accepts anonymous viewers. Bootstrap branches: `viewerOrGuest` returns a `GuestBootstrap` shape on auth failure, the proxy allows anon traffic into `/game/room/*` and `/api/game/*`, items GET allows guests on public rooms, and `is_guest` rides on the presence record so the host's HUD impersonation filter knows who not to expose founder-only chrome to.

Guest mode is rate-limited (token-bucket on `/api/game/bootstrap`), origin-guarded, and capped client-side with a sign-in overlay once a session hits the cap. Guests pick a display name via URL param → sessionStorage → first-paint modal, walk around, and see a bottom-left guest pill. Owners see a "share room" affordance; guests see a sign-in CTA from the FindRoomPalette modal.

  • rooms.is_public column + PATCH /api/rooms/[id]/visibility
  • per-row globe/lock + copy-link in my-rooms dropdown
  • X-Robots-Tag (proxy.ts), robots.txt Disallow, noindex metadata on /game
  • Viewer type + loadGuestBootstrap + viewerOrGuest helper
  • Three-way route branch: founder / guest / redirect
  • Rate-limit + Origin guard on /api/game/bootstrap
  • Guest name entry: URL param → sessionStorage → first-paint modal
  • FindRoomPalette sign-in modal for guests
SHIPPED
15:39–20:21·23 commits

Game-load tier 0 — server prefetch + Phaser preinit

Cold-loading /game is the single most visible perf moment in the product. Tier 0 is the no-Suspense-changes set of wins. Code-split seven conditional HUD drawers + AgentChat via `next/dynamic` so they leave the boot bundle. Emit preconnect + preload `<link>` hints for sprite atlases so the CDN warms up before Phaser asks. Preinit the Phaser chunk on `/game` and `/game/room` route loads so the JS arrives before the canvas mounts. Server-prefetch the bootstrap response on the route, thread it as initial props through `GameContent` to skip the first client-side fetch.

Pushed `/game` auth into a Suspense child so the route can PPR — the shell paints immediately while the auth check resolves below. Brand-matched loading skeletons for `/game`, `/game/room`, and `/sign-in` so the first paint reads as Founder World, not a loading spinner. Speed Insights wired so the wins show up in production data.

  • code-split AgentChat + 7 conditional drawers via next/dynamic
  • preconnect + preload <link> tags for sprite atlases
  • Phaser chunk preinit on /game + /game/room
  • Server prefetch on both /game and /game/room routes
  • /game auth pushed into Suspense for PPR
  • Brand-matched skeletons for /game, /game/room, /sign-in
  • Speed Insights wired into the production build
SHIPPED
18:51–19:34·30 commits

Server Lobby + create-furniture skill

The Lobby is the first room the server owns rather than a founder. It seeds as `owner_id NULL`, a neutral 20×20 ground-floor space that any founder can drop into — the public square of Founder World. We wired `kind` and `pin` columns so `listPublicRooms` surfaces server rooms at the top of the find-a-room palette with a distinct SERVER badge, pinned above user rooms.

Built alongside it: the `create-furniture` skill — a CLI that takes a name and optional flags, generates a pixel-art sprite via pixellab.ai against the canonical style anchor, writes it to the sprites staging dir, runs `sprites:sync` to upload and pack, upserts the item into `items_catalog`, and writes a sidecar state file for reroll continuity. First-run verdict captured in the docs: pixellab anchor quality locked, `sprites:sync` env passthrough fixed.

Also landed in the same window: the cacheComponents flag enabled, cached bootstrap and room-layout loaders via `unstable_cache`, server pages calling the cached loaders, and a TowerCTA React overlay that replaces the old multi-floor selection UI.

  • Lobby row: owner_id NULL, kind = server, pin = 0 — always first in the palette
  • SERVER badge + distinct colour in FindRoomPalette rows
  • create-furniture: pixellab → sprites:sync → items_catalog upsert in one command
  • cacheComponents enabled + cached loaders for bootstrap + room-layout
  • TowerScene floor-selection UI stripped — TowerCTA overlay replaces it
first lobby piece
first lobby piece
SHIPPED
20:29–21:53·17 commits

foundr-world marketplace + create-furniture plugin restructure

The create-furniture script had grown a tangle of duplicated helpers — env loading, prompt building, pixellab calls, catalog upserts all inlined. We extracted them into a shared `plugins/foundr-world/lib/` layer: `env.mjs` for `.env.local` loading with worktree fallback, `prompt.mjs` for the canonical style + taxonomy maps, `pixellab.mjs` for anchor generation, and `catalog.mjs` for `items_catalog` + `collections` upserts.

Alongside the lib extraction we scaffolded the full `plugins/foundr-world/` marketplace shell with `create-furniture` relocated under it and a new `create-collection` sibling ready to generate entire themed sets in one command. `create-furniture`'s `generate.mjs` was refactored to import from the shared lib so the two skills stay in sync.

This is infrastructure, not feature — no new sprites landed yet. The payoff comes next.

  • plugins/foundr-world/ marketplace shell with manifest skeleton
  • lib/env.mjs, lib/prompt.mjs, lib/pixellab.mjs, lib/catalog.mjs — shared across both skills
  • create-furniture relocated + SKILL.md updated with new invocation paths
  • create-collection scaffolded: SKILL.md, arg parsing, prompt helpers, partitionPieces, main()
  • partitionPieces: skips sprites already in the manifest — safe to re-run mid-batch
SHIPPED
22:07·1 commit

Velvet collection — six pieces, one command

The first run of `create-collection velvet`. Six coordinated isometric pieces generated against the canonical style anchor in a single batch: `chair-velvet`, `desk-velvet`, `lamp-velvet`, `plant-velvet`, `rug-velvet`, `deco-velvet`. All six uploaded to Vercel Blob, packed into the appropriate atlases, and seeded into the sprite manifest in one commit.

Velvet is a premium aesthetic tier — deep jewel tones, rich surface texture, the kind of furniture that says "this founder has been at it a while." The collection ships into `items_catalog` under the `velvet` collection slug, ready for the admin panel to grant and the shop to sell.

  • 6 new sprites: chair, desk, lamp, plant, rug, deco — all in the velvet collection
  • Packed into office-furniture, office-plant, office-rug, office-decor atlases
  • chair-velvet and lamp-velvet tagged for both office + lobby scenes
  • items_catalog + collections rows seeded via create-collection's catalog.mjs upsert
velvet chair
velvet chair
velvet rug
velvet rug
velvet lamp
velvet lamp
SHIPPED
22:26–22:51·9 commits

Avatar walk — interruptible mid-step

Click while the avatar is already walking and it commits the rest of the current step, then picks up the new path. Phaser tween auto-interpolates the heading change so the turn reads smoothly instead of teleporting. `walkPath` returns a Promise<boolean> so callers can tell completion apart from interrupt — the office and lobby gate their door transitions on `completed`, which means a click on the lobby exit while mid-walk to a desk no longer triggers a scene change you didn't want.

Companion fix: the tap-detection window was 80ms, but real mouse clicks routinely exceed that. Widened to 250ms so legitimate clicks stop getting swallowed as pans.

SHIPPED
22:44–23:57·18 commits

Matrix collection + 8-direction rotations

Second run of `create-collection`, but the first with the v2 pipeline: pixellab rotations (`/v2/generate-8-rotations-v3`) bake 7 directional views per piece, the manifest learns a rotation-suffix rule that excludes `-<dir>` views from scenes and atlases (so they get used by the renderer, not preloaded as standalone art), and every collection now ships with a mandatory comparison report at `/reports/furniture/<slug>.html` — an 8-frame CSS-keyframe rotation animation per piece, ported from the matrix-report skill as a parameterized template.

Matrix is a six-piece cyberpunk-office set: `chair-matrix`, `desk-matrix`, `lamp-matrix`, `plant-matrix`, `rug-matrix`, `deco-matrix`. Velvet picked up the rotation treatment retroactively in the same window. Plugin/lib gained `pixellab.mjs#generateRotations()`, a defense-in-depth XSS escape on dynamic report fields, and a sprite-sync fix that skips atlas grouping for empty-scene rotation views.

  • pixellab /v2/generate-8-rotations-v3 + direction constants
  • sync-sprites rotation-suffix rule — -<dir> views excluded from scenes/atlases
  • plugin/lib/report.mjs — parameterized matrix-report template
  • 8-frame rot-stack CSS keyframe animation per piece
  • /reports/furniture/matrix.html + /reports/furniture/velvet.html shipped
matrix chair
matrix chair
matrix deco
matrix deco
SHIPPED
22:51–23:59·74 commits

Founder World becomes an MCP server

A second-largest-feature-of-the-day, shipped between 10:51 PM and midnight. Founder World now speaks Model Context Protocol — any LLM client (Claude Desktop, Cursor, VS Code Agent, Codex, the MCP SDK) can register an agent key, list public rooms, look around, talk to NPCs, enter rooms via the presence channel, and whisper to founders' inboxes. The agents become first-class players: when an NPC reads dialogue, that's an MCP-driven turn now, not just a server-side cron.

Bottom-up implementation: migration adds `mcp_agent_keys`, `mcp_presence_sessions`, `mcp_audit_log` (with realtime publication). The bearer-token helper `verifyAgentKey` resolves bearers to a founder. Three-axis token-bucket rate-limiting via `@upstash/ratelimit` (per-key + per-IP + per-tool). Audit-log helper with arg redaction so secrets don't end up in the ledger. Server-side Supabase Realtime broadcast helpers feed back into the same channels visitor avatars ride on.

Then the tools: `list_public_rooms`, `get_room_layout`, `look_around` (MCP-driven visitor list), `get_npc_roster`, `talk_to_npc` (bearer required, daily budget), `enter_room` (presence join, 5-min TTL), `leave_room` (idempotent), `heartbeat` (session refresh). Tool registry assembles them into one McpServer per request; the route handler chains auth → rate-limit → audit → per-request server. POST /api/agents/register mints keys; a one-minute cron sweeps expired sessions.

Eight audit fixes landed in the same window once an LLM-readability audit came back: `/llms.txt` + `/llms-full.txt` at the apex; `Accept: text/markdown` content negotiation on `/docs`; cross-tool instructions + `serverInfo` on initialize; RFC 9728 protected-resource metadata + WWW-Authenticate; `alternates.types` + Link + `X-Markdown-Tokens`; `/docs` becomes ◯ Static via `'use cache'` + Suspense; `generateStaticParams` + 1y CDN cache on `/docs/raw/[slug]`; per-client install snippets for Claude/Cursor/VS Code/Codex/SDK.

  • 9 MCP tools registered across `list_public_rooms`, `get_room_layout`, `look_around`, `get_npc_roster`, `talk_to_npc`, `enter_room`, `leave_room`, `heartbeat`
  • mcp-handler + @modelcontextprotocol/sdk + @upstash/ratelimit + zod deps
  • bearer-token verifyAgentKey + three-axis rate-limiter + redacted audit log
  • POST /api/agents/register mints keys; cron sweeps expired sessions
  • RFC 9728 protected-resource metadata + WWW-Authenticate
  • /llms.txt + /llms-full.txt at root; /docs becomes ◯ Static
  • Per-client install snippets — Claude / Cursor / VS Code / Codex / SDK
  • Integration test: list/talk/enter/heartbeat/leave roundtrip
Tuesday
·

May 19, 2026

Multiplayer everything

SHIPPED
00:05–02:28·29 commits

Tower HUD — always-on CommandBar replaces the palette

The FindRoomPalette modal felt like one too many clicks. Replaced with `CommandBar` — an always-visible WAI-ARIA combobox docked into a new `TowerHud` orchestrator. Up/down navigates, Enter picks, ⌘K focuses, and clicks on result rows always commit (no more stale `activeIdx` racing with the bridge effect). MRR pills, kebab popover (sideways-opening so it doesn't fall off the viewport), and an `EnterOfficeChip` (`next/link` styled in sage) round out the surface.

Bootstrap parallel-queries own + recent rooms so the CommandBar at-rest list is hot. `useRoomFinder` consolidates the global keydown registry — one module-level owner so the palette doesn't fight with mid-canvas Phaser hotkeys. QuestWidget gained a `tower` variant that reads as a coach mark rather than a HUD chip when the tower scene is active. Hard call: the old `FindRoomPalette` modal is gone everywhere — the always-visible CommandBar shows up in office, lobby, and tower, plus the "click any floor tile to walk" pill got removed (the room-finder is the discovery path now).

  • TowerHud orchestrator + error boundary
  • CommandBar with combobox a11y + useId + active-idx-bypass on pick
  • StatusPills (MRR, agent count) + sideways KebabPopover
  • useRoomFinder hook — module-level keydown registry, one global owner
  • FindRoomPalette modal removed; CommandBar is everywhere
  • QuestWidget variant="tower" coach-mark layout
SHIPPED
01:25–02:17·35 commits

Admin v2 — Founders / Catalog / Collections tabbed surface

The single `/admin` page split into a tabbed surface: Founders (the existing inventory editor, moved under `/admin/founders`), Catalog (every grantable item, with filter strip + grid + optimistic grantable toggle + hover-to-animate rotations via lazy `<img>` mount), and Collections (hero cards, piece counts, catalog/report cross-links). `/admin` redirects to `/admin/founders` as the default landing.

`is_admin` flag landed on `founders` so server-room edits gate cleanly. Search inputs everywhere — by name, sprite key, collection slug, founder handle. Founders list scaled up for readability. Audit log + DB invariant + a unified email allow-list closed the loop on who can grant what; the legacy environment-variable allow-list got merged into the new flag-based path.

  • AdminTabs shared layout: founders | catalog | collections
  • GET /api/admin/catalog/all + PATCH /:id/grantable (optimistic UI)
  • GET /api/admin/collections with piece + grantable counts
  • nuqs for type-safe URL search params
  • is_admin flag on founders + audit log + unified allow-list
  • await connection() before currentUser() — fixed an /admin "page couldn't load" race
SHIPPED
01:26–01:40·4 commits

MCP — canonical /mcp endpoint

The MCP server's original mount was `/api/mcp/mcp` — chosen yesterday because Next.js route nesting wanted it. Today it became `/mcp`. Dynamic `basePath` so both URLs respond during migration, then back-compat dropped. Cleaner install snippet for every MCP client.

SHIPPED
02:44–02:57·4 commits

Furniture taxonomy 7 → 25 kinds

The original `ItemKind` enum was seven values inherited from a one-day batch. A proper research pass on Habbo's catalog (decade-deep taxonomy: seating, sleeping, surfaces, storage, lighting, plants, deco, wall, window, rug, tech, food, pet, achievement, holiday, vehicle, custom…) produced a 25-kind extension. Migration widens the column; the TS union expands to match; a CI guard test prevents future drift between the migration enum and the TypeScript type.

Now `wall-art`, `window`, `wallpaper`, `currency`, and the rest have proper homes in the catalog without dropping everything into `custom`.

  • docs/reports/habbo-furniture-taxonomy.md — research memo
  • ItemKind union: 7 → 25 values
  • CI guard test — migration enum ↔ TS union drift
SHIPPED
03:22–03:40·4 commits

SYSTEM_FOUNDER_ID — server rooms get a real owner

`owner_id NULL` worked for the Lobby but spread null-handling cruft into every query. Replaced with `SYSTEM_FOUNDER_ID` — a dedicated sentinel founder row that owns the Lobby and any other server-managed room. Foreign keys stop being nullable, joins stop needing a `COALESCE`, and the admin-edit flow gets a clear rule: picking up a server-room item transfers ownership to the acting admin (instead of orphaning the row).

One Vercel-build casualty: the sentinel had to move to a zero-dep module so it could be imported from edge code without dragging postgres along. Otherwise the build broke at deploy time.

SHIPPED
03:57–15:55·14 commits

Edit-room — transactional save + drop-to-stash

The edit-room loop had bug debt. Save layout while holding committed the pickup half but lost the held item — now it's a real transactional commit. PATCH success merges the server's canonical item back into local state so optimistic placements that the server rotated didn't silently drift. A "to stash" action shows up while holding so you can explicitly drop the held item into inventory without placing it. Tap detection in iso-gestures now suppresses when the press started on an interactive (the chip buttons under the canvas were eating taps).

Anchoring polish: furniture sprite bottom-edges land exactly on each tile's bottom apex (per-sprite transparent-padding compensation), and the lobby door-marker plant became a real `room_items` row instead of a hardcoded sprite — so admins can move it like any other piece.

  • Save-layout while holding commits pickup to stash + save in one transaction
  • PATCH success merges server's canonical item back into local state
  • Explicit "to stash" action while holding
  • iso-gestures suppresses tap when press started on interactive
  • Furniture anchored to tile bottom apex + per-sprite padding compensation
  • Lobby door-marker plant is a real room_items row
SHIPPED
04:22–19:53·36 commits

Realtime hardening round 2 — per-client keys, ghost guard, anon JWT

A spread of presence + realtime fixes after multiplayer hit real concurrent users. Per-client presence key replaces a per-founder key — peers stop vanishing on F5 (the new tab's presence track no longer overwrites the old tab's). `sendBeacon`-driven leave broadcast on tab close so avatars actually disappear when someone hits ⌘W. Guest-mode duplicate-avatar bug fixed (guests saw themselves rendered twice on move). Lobby presence subscribe crash traced to channel-collision and fixed via a dedicated `room_channel` provider config.

`RoomRealtimeProvider` consolidates everything into one channel owner with three external stores (presence, chat, items). Hooks fragment cleanly: `useRoomPresence`, `useChatStream`, `setLocalPresence` are now selectors over the provider. GameCanvas wraps the game subtree in it; the `roomId` arg drops out of `useChatStream` because the provider owns the channel.

Then the Supabase auth saga. Realtime needs a JWT; guests have no Clerk session, so we shifted to `signInAnonymously()` for guests, split the auth-bearing client from the realtime-accessToken client (`isSingleton:false` so the split actually splits), wired periodic `setAuth` refresh instead of a racy one-shot setTimeout, and added a Phoenix ghost-guard so users who left don't shadow real peers on rejoin. Daily realtime-usage snapshot cron lands so we can watch the bill.

  • Per-client presence key — F5 no longer kills peers
  • sendBeacon leave broadcast on tab close
  • RoomRealtimeProvider — one channel, three stores
  • useRoomPresence, useChatStream, setLocalPresence become selectors
  • signInAnonymously() for guest realtime JWT
  • Two-client split: auth-bearing client + realtime-accessToken client
  • Periodic setAuth refresh, not racy setTimeout
  • Phoenix ghost-guard — shadow leavers until reaped or rejoined
  • Daily realtime-usage snapshot cron
SHIPPED
04:18–05:25·22 commits

Wall items — wall-art + windows, hit-tested + snap-placed

Floor items had been the only kind that existed; the back walls were just decoration. Wall items change that. Migration 0034 adds `surface` (floor/wall) + `wall_side` + `wall_panel` + `wall_x` + `wall_y` columns to `room_items`. Wall base polygons get tagged with `side+panel+vertices` for hit-testing, `WallItemGeometry` provides bilinear anchor + inverse so the renderer + edit-mode share the same math, and the edit-mode state machine for wall items (hold → hit-test → snap → place) mirrors the existing floor flow.

New routes: POST /api/rooms/[id]/wall-items + PATCH /api/rooms/[id]/wall-items/[itemId] (both TDD). Plugin/lib gets WALL_STYLE + per-side wall styles + refsForKind helper so generation prompts know whether to ask for left-wall or right-wall framing. `wall-art-matrix` ships first (then `wall-art-{banner,clock,monitor,neon,portrait,sign}`), promoted to the canonical wall-art reference.

  • Migration 0034: surface + wall_side + wall_panel + wall_x + wall_y
  • WallItemGeometry — bilinear anchor + inverse, pure math, testable
  • Edit-mode state machine: hold → hit-test → snap → place
  • POST + PATCH wall-items routes (TDD)
  • wall-art + window added to VALID_KINDS + PREFIX_TO_KIND in plugin
  • wall-art-matrix promoted to ref-wall-art for future generation
wall-art
wall-art
SHIPPED
18:13–22:53·26 commits

Wallpaper — matrix tile + iso-shear projection

Wallpaper is its own surface beneath wall items. Migration 0036 adds the `wallpaper` ItemKind and `rooms.wallpaper_sprite`; layout threads the chosen sprite through the cached room layout; `applyWallpaper` server action validates against four error codes. The first sprite is `wallpaper-matrix` — a seamless-tile matrix-themed wallpaper.

Rendering went through three iterations. First attempt: setMask the wallpaper texture onto the wall polygon. WebGL rejected it. Second attempt: DynamicTexture stamp+erase via the WallSystem renderer — Graphics-based erase, destroy AFTER `dt.render()` flush. Worked, but the wallpaper rendered un-sheared (flat, not iso). Third attempt (the one that ships): explicit `ISO_SHEAR_RATIO` constant + per-side iso-shear via manual offset stamping (the Habbo / scuti-renderer pattern). Matrix wallpaper got regenerated with seamless-edge tiling prompts, promoted to `ref-wallpaper`, and WALLPAPER_STYLE in plugin/lib demands seamless tiling explicitly.

UI: `WallpaperPicker` with confirmation modal (because previewing a wallpaper destructively trims wall items) + a minimal room-settings entry point that hosts it. Live in-canvas refresh on apply — no scene restart. Preserves across wall-preset swap (the wall color can change without losing your wallpaper).

  • Migration 0036: wallpaper ItemKind + rooms.wallpaper_sprite
  • WallSystem TileSprite branch with iso-shear stamping
  • Three rendering attempts: setMask → DynamicTexture mask → iso-shear stamp
  • WallpaperPicker + confirmation modal + RoomSettings entry point
  • Live in-canvas apply + preserves across preset swap
  • WALLPAPER_STYLE demands seamless-edge tiling
  • wallpaper-matrix regenerated + promoted to ref-wallpaper
matrix wallpaper
matrix wallpaper
SHIPPED
16:25–17:55·36 commits

In-room chat — spatial bubbles + MCP + A2A

Chat lands as a first-class room layer. Spec called for three identities: humans (Clerk-authed), agents over MCP, and agent-to-agent via A2A v0.3.0. Schema: `room_chat_audit` + `agent_meetings` + per-room `chat_disabled` toggle. Content moderation baseline (slur + prompt-injection regex), an Upstash rate-limit pair (bubbles + whispers), and a three-way sender-identity validator gate the write path. Audit + IP hashing for everything that hits the database.

Surfaces: POST /api/rooms/:id/chat for human bubbles; MCP `say_in_room` for agents emitting public bubbles; MCP `whisper_to_founder` for agents → inbox; A2A POST /api/agents/:id/whisper for human→agent relay; GET /api/rooms/:id/agents discovery + /api/agents/register accepting `agent_card_url` + version (so each room exposes its agents as proper A2A peers).

UI: `ChatBubbleLayer` — DOM bubbles overlayed and synced to Phaser sprite positions every frame. `ChatInput` — desktop docked / mobile overlay sheet. `ChatToggle` in room settings as the hard off-switch. Inbox renders `agent.whisper` events with an agent badge so founders can tell agent contact from human contact at a glance.

  • room_chat_audit + agent_meetings + chat_disabled toggle
  • Slur + prompt-injection regex moderation baseline
  • Upstash rate-limit axes for bubbles + whispers
  • Three identities: human (Clerk) / agent (MCP) / agent (A2A v0.3.0)
  • POST /api/rooms/:id/chat — human public bubble
  • MCP say_in_room — agent public bubble
  • MCP whisper_to_founder — agent→inbox
  • A2A POST /api/agents/:id/whisper — human→agent
  • ChatBubbleLayer — DOM bubbles synced to Phaser sprites
  • Inbox renders agent.whisper events with agent badge
SHIPPED
11:47–16:33·16 commits

RoomDock (variant F) — chat-pill + finder-pill replaces the old palette

A throwaway prototype round (chat-finder-overlap variants A/B/C, then D/E/F) verdict'd on F. The new `RoomDock` is one mobile-responsive shell: `ChatComposerPill` (expanded chat form / collapsed icon) and `FinderPill` (expanded combobox / collapsed icon) share a state machine — opening one collapses the other. `useChatSender` extracts cleanly from the old `ChatInput` so the same send logic powers the pill.

RoomDock wires into `GameContent` directly. The old `ChatInput` and `FindRoomPalette` mounts are deleted. A11y: aria-label on the chat form, role=alert on errors, dropped unused clearError state. Prototype directory cleaned up after the verdict.

  • Prototype variants A/B/C → D/E/F → F wins
  • ChatComposerPill (expand/collapse) + FinderPill (expand/collapse)
  • RoomDock state machine + mobile-responsive shell
  • useChatSender extracted from ChatInput
  • ChatInput + old FindRoomPalette mounts deleted
SHIPPED
05:25·1 commit

revalidateTag fix — updateTag E872 in route handlers

`updateTag` only works inside Server Actions (Next 16 throws E872 elsewhere). All four PATCH route handlers were using it, which meant /api/rooms/[id]/items had been silently 500'ing for cached founders since the migration to cached loaders. Replaced with `revalidateTag(tag, "max")` — the documented Next.js 16 replacement — across every route handler, plus a project-CLAUDE.md note explaining the gotcha.

WIP
23:10–23:58·7 commits

Stackable floor items — primitive spec + migration

Spec'd the Habbo free-pile model: any item can sit on top of any other floor item if both sides allow it, no fixed slots. Migration 0037 (idempotent: `IF NOT EXISTS` + DO-block) adds `z`, `stack_height`, `can_stack_on` columns. Kind-default backfill populates defaults so every existing piece gets sensible stacking metadata. Legacy `default_y_offset` audited and per-instance `z` backfilled.

Engine + scene + tests land tomorrow (May 20). Status here is in-flight — the bones are in, the renderer needs to catch up.

  • Habbo free-pile model — no fixed slots, mutual consent
  • Migration 0037: z + stack_height + can_stack_on (idempotent)
  • Kind-default backfill + per-instance z backfill
  • docs/specs/2026-05-19-stackable-floor-items-design.md (revised per audit)
  • docs/plans/2026-05-19-stackable-floor-items-plan.md — 20-task TDD
Wednesday
·

May 20, 2026

Agents as citizens

SHIPPED
00:01–01:18·25 commits

Stackable floor items — the engine lands

Engine, scene, tests, and visual verification — the whole stacking layer. `place()` runs under a Postgres advisory lock + a decorative branch + four 409 error codes (`StackingForbidden`, `OverStackHeight`, `IncompatibleCanStackOn`, `BelowExistingItem`). `pickUp()` zeroes z but leaves other rows floating (no cascade). `topOfStack` + `canStackHere` + a pinned FNV-1a `idHash` for deterministic depth tiebreak handle the placement logic; `getRoomItems` orders by tile then z asc so the renderer paints bottom-up.

Render: `TILE_UNIT_PX = TILE_H` (full lift, not half — the Habbo correction), `DEPTH_SCALE` + `Z_DEPTH_WEIGHT` + `FLOOR_BASE_OFFSET` constants. The Furniture depth formula incorporates z; sprite screen-y lifts by `z * TILE_UNIT_PX`. Pixel-perfect hit-test kicks in for stacks ≥2 so clicks on the visible item are routed correctly. Actors (avatars, NPCs) share the floor-item depth band so they don't pop through stacks. Newer placements always render above existing-at-same-tile to give immediate feedback.

Client-side: held-ghost preview lifts by the would-be z. Optimistic place includes z so the item lifts immediately. Hydrate `stack_height` + `can_stack_on` on `RoomItem`. The legacy `wouldCollide` helper is gone, replaced by `canStackHere/topOfStack`. Tests: boundary z=40, hash tiebreak, decorative chain, realtime — all pass. Documented as a primitive in `CONTEXT.md`.

  • place() — advisory lock + decorative branch + 4 409 error codes
  • TILE_UNIT_PX = TILE_H (full lift, the Habbo correction)
  • Furniture depth formula with z + FLOOR_BASE_OFFSET
  • Pixel-perfect hit-test for stacks ≥ 2
  • Actors share floor-item depth band
  • Held-ghost preview + optimistic place lift by z
  • CONTEXT.md primitive doc + manual visual verification screenshot
SHIPPED
00:02–08:26·6 commits

Chat polish — bubble math, ws sender, multi-stack

Three fixes plus one upgrade. Bubble fade timer was anchored to the sender's timestamp — peer clock skew made bubbles vanish before they should have. Now anchored to the receiver's clock. World→viewport mapping had been computing from screen-corner instead of camera-origin, so on a panned camera the bubble landed in the wrong spot. Both fixed. Optimistic bubble paints from the sender's side before the broadcast roundtrip.

Broadcast latency cut: sender's WebSocket carries the message instead of a server roundtrip, so sub-100ms peer latency. Multi-message bubbles stack vertically per sender. Enter-to-focus the composer from anywhere on the canvas, with a `chat_id` per sender so a fresh focus doesn't accidentally stomp the previous draft.

SHIPPED
12:16–14:06·27 commits

Door + spawn-point primitive

Every room has a door now. Migration 0039 (timestamp-renamed to 20260520120000_*) adds `door_tile_x`, `door_tile_y`, `door_dir` to `rooms`. The `doorWallPanel` pure helper maps (tile, dir) → (wall side, panel index) so the WallRenderer can paint a black-panel cutout where the door is. Avatars spawn at the door tile facing the door direction; `Rotation→Direction` map in `Avatar.setFacing` was off by one rotation step — fixed.

Floor items reject placement on the door tile (`DoorTileForbiddenError`, 409). Wall items reject placement on the door panel; PATCH wall reposition rejects moves onto the door panel. Default door moved from back-wall (2,0) to left-wall (0,2) so the avatar doesn't spawn on the lobby plant. Lobby door explicitly placed. SW-corner is a normal tile now (the legacy exit-to-lobby click went away with the proper door mechanic). `door-office` sprite generated via `create-furniture` (it accepts `door-` prefix → kind=custom), promoted, then the sprite got replaced by the WallRenderer cutout — the cutout reads cleaner than a layered sprite at all rotations.

  • Migration 20260520120000_door_spawn_point — door tile + dir on rooms
  • doorWallPanel pure helper: (tile, dir) → (side, panel)
  • DoorFixture GameObject → black-panel WallRenderer cutout
  • Avatar spawns at door tile facing door direction
  • place() rejects floor items on door tile → 409
  • placeWall() + repositionWall() reject moves onto door panel → 409
  • CONTEXT.md primitive doc
door cutout
door cutout
SHIPPED
16:55–19:55·37 commits

Inventory FAB (D2) — Habbo-style drag from tray

The edit-room drawer becomes a proper Habbo-style FAB. One floating action button bottom-right (the hand-agent sprite, which generated alongside this card). Click to expand into a tray with search + sort + cards. Drag a card out of the tray and the cursor becomes the held item via Pointer Events + a window-level CustomEvent bridge that lets Phaser pick it up cleanly. Optimistic placement on drop with 409 rollback + toast. Tray-as-drop-zone replaces the grid with a file-upload-style overlay when you're holding — you can return-to-tray with a single click on the FAB.

Room presets ship in the same surface. Save layout into a named preset (with a Phaser-snapshot thumbnail uploaded to Blob), apply a preset to load it, delete to clean up, capped per founder so the table doesn't bloat. Room gifts ride alongside: create / open / decline with rate limits. The grip-offsets file (per-kind + per-sprite) supplies the exact pixel offset between cursor and held sprite so dragging a chair vs. a lamp both look natural.

Edit-mode chrome got cleaner. The top edit banner is gone; the EDIT MODE chip lives top-center as a RoomFinderBar-style pill. Edit-mode is coupled to tray-open (the two states are one). FAB chrome gates `setInteractive` on furniture only when in edit mode so casual clicks don't accidentally trigger pickups. Keyboard a11y for tray cards + SR live region. Phaser DOM container `pointer-events: none` in postBoot so the canvas doesn't swallow drag intent.

  • Migration: room_presets + room_gifts + room_items recency
  • listInventory(sort) + place() updates last_placed_at
  • pure inventory-sort module (recent + kind)
  • room-presets: save / apply / delete + cap
  • room-gifts: create / open / decline + rate limits
  • grip-offsets.json + lookup helper
  • HeldCursor → legacy tile-ghost (HeldCursor removed after testing)
  • Per-founder Postgres Changes realtime hook
  • InventoryFab + .presets + .gift subcomponents
  • FAB-as-drop-target — click while holding to store
  • Preset thumbnail via Phaser snapshot → Blob
the hand
the hand
holding
holding
SHIPPED
12:05–15:03·34 commits

Hire agents — Vera, by briefing upload

A real hire flow lands. Migration 0039 adds `founder_agents` + agent-credits balance + a race-proof hire transaction. Stripe checkout completes → credit-pack → balance increment (with `event_id` dedupe). The interview turns through scripted prompts; an extractor produces a typed `draft_config` for the new agent; on hire the draft commits as the founder's `founder_agents` row.

UI: AgentDesk in the OfficeScene, HirePanel + InterviewThread + SignContract overlays in GameContent, EmptyOfficeNudge gently pointing at the desk. Plus a fourth path — "brief Vera by upload": paste-context or drop-target a doc, an ICP-anchored extractor (Spotlighting + raw briefing text path + `parsed_icp`) produces a deterministic 100pt `fit_score` rubric, and BriefingReview shows editable chips + dropdowns + flag-aware fields so the founder can correct the extraction before committing.

NPCs in the lobby become tiered by `fit_score` — A (high), B (mid), C (low) — with tiered opacity so the founder visually clusters who's worth talking to. Lobby filter toggle: A-tier only.

  • Migration 0039: founder_agents + agent-credits + race-proof hire txn
  • Stripe checkout.session.completed → credit pack (event_id dedupe)
  • Interview extractor with typed draft_config
  • Spotlighting + parsed_icp + deterministic 100pt fit_score rubric
  • BriefingUploader textarea + drop-target modal
  • BriefingReview — editable chips + dropdowns + flag-aware
  • Lobby NPC tiered opacity by fit_score (A/B/C)
SHIPPED
11:17–22:53·26 commits

Lab environment v2 — shared Supabase + GH alias action

Two parallel agents iterating on the same DB was causing constant migration churn. The lab environment got rebuilt around a shared lab Supabase project + a sync workflow that applies migrations to PROD first, then lab — fail loud if prod errors. `supabase/config.toml` for branching + GH integration; `seed.sql` for fresh-branch bootstrap (one founder + one NPC).

GitHub Actions: an alias action mints `<slug>.lab.foundr.world` aliases for every `experiment/*` preview deployment (the deploy ref is the SHA — resolve to branch name); a daily GC sweeps idle aliases + Supabase branches; a workflow injection vector got moved to env-vars. Skipped the $100/mo Preview Suffix add-on after research — Vercel's free wildcard domain covers it. `.claude/rules.md` lands as the canonical agent operational manual; AGENTS.md + CLAUDE.md become summaries pointing at it.

  • Shared lab Supabase project (NOT per-branch DB)
  • supabase-sync workflow: PROD first, then lab — fail loud if prod errors
  • GH Action — alias experiment/* to <slug>.lab.foundr.world
  • GH Action — daily GC of idle Supabase branches + aliases
  • Skipped $100/mo Preview Suffix add-on (free wildcard works)
  • .claude/rules.md as canonical agent operational manual
  • supabase/seed.sql + config.toml for branching
SHIPPED
07:52–14:41·6 commits

Garage + Matrix v3 — onboarding + cyberpunk continued

`create-collection garage` — scrappy startup furniture as the onboarding tier (chair, desk, lamp, plant, deco, rug — beat-up but charming). Matrix v3 extends the collection by 4 carpets + 2 custom decos (`deco-matrix-sentinel` + `deco-matrix-terminal`), bringing matrix to 15 pieces total. Sprites synced; collection rows seeded.

One stackable test piece (`custom-gold-lingot`) lands too, with the v6 rug anti-bleed and v7 wallpaper iteration logs documenting what the prompts learned.

garage chair
garage chair
garage plant
garage plant
SHIPPED
08:29–10:00·17 commits

Merge-safe sprites:sync + create-furniture preflight

Two parallel `sprites:sync` runs from different worktrees would clobber each other's manifest entries. Fixed via `mergeManifests` — a pure function that combines two manifests, aborts on collision (or accepts `--force` to override), and follows an additive rule for atlases (different from sprites: never drop an atlas page). Sync now uses merge semantics by default. `create-furniture` got a preflight uniqueness check + reroll passes `--force` so the explicit re-generation path keeps working.

  • mergeManifests pure function — collision-safe
  • sync uses merge semantics + collision abort
  • create-furniture preflight + --force on reroll
  • Atlas merge follows additive rule (don't drop pages)
SHIPPED
08:58–10:17·5 commits

Furniture hover inspector + sage-hover removed

The sage hover ring + per-item name label was readable enough but didn't feel Habbo. Removed. Replaced with a hover inspector card pinned to the cursor — title, kind, owner, rotation. Emits via `game.events` (not `scene.events`) so the React HUD can listen without scene knowledge. Clicking a placed sprite while holding now places the held item on that tile.

SHIPPED
16:31–20:42·48 commits

Agents as first-class citizens — OAuth 2.1 + handoff codes

The MCP server from two days ago issued bare bearer tokens. To do anything meaningful as an agent (place items, hire other agents, redeem credits) we needed proper authorization. This card is that — OAuth 2.1 with PKCE S256 + RFC 8707 resource indicators + 10-minute access TTL, backed by `oidc-provider` with a SHA-256-hashed-at-rest adapter against new `oauth_*` tables (migration 0041).

The novel piece: an IETF-draft browser-handshake flow. An MCP `request_browser_session_code` tool mints a short-lived handoff code; the agent surfaces a URL to the founder; the founder opens it; `/auth/agent-handshake` auto-submits per the IETF spec; `/auth/agent-handshake/redeem` mints an `fw-agent-session` cookie + 303 redirects into the app. Now the agent is "signed in" alongside the founder — the proxy short-circuits the agent cookie before Clerk, `requireCurrentFounder` is dual-path (Clerk OR fw-agent-session), and `actingAgentKeyId` rides on the founder context.

Every state-writing MCP tool now calls `writeAgentAction(agent_key_id, action, args, status)` so the audit log knows what each agent did and why. `whisper_to_founder` was hardened against the F5 abuse vector: requires co-presence (the agent must be in the room). Ten MCP tools that had been orphaned got registered. CVE-2025-29927 route-coverage check landed in the test suite — proxy.ts is no longer the sole gate for auth.

UI surface: `AgentActivityButton` + `AgentActivityPanel` drawer with paginated audit feed, mounted in GameContent. Per-founder Composio badge in the HUD + runner lookup. The /docs hub got a public agent-citizenship guide.

  • Migration 0041: OAuth 2.1 schema (clients, codes, tokens, ephemeral, handoff codes, agent_action_log)
  • oidc-provider w/ DB adapter (SHA-256-hashed tokens)
  • PKCE S256 + RFC 8707 audience check + 10-min access TTL
  • IETF-draft browser handshake — code → cookie → redirect
  • Dual-path requireCurrentFounder (Clerk OR fw-agent-session)
  • actingAgentKeyId on founder context + writeAgentAction audit
  • whisper_to_founder requires co-presence (F5 fix)
  • AgentActivityButton + Panel drawer in GameContent
  • CVE-2025-29927 route-coverage test guard
  • agent-jwt sign/verify (HS256 + RFC 8693 act claim)
SHIPPED
16:50–17:08·17 commits

Live MCP A2A briefing cutscene

Phase 2 of the agents-as-citizens push: the founder can have *their own existing agent* (a Claude session, a Cursor agent, whatever) brief Vera, and watch it happen as a live cutscene. Server-Sent Events emit per-turn updates; `BriefingCutsceneStage` renders the two avatars + bubbles in a cinematic overlay; `BriefingCutsceneDraft` shows a live-preview of the extracted draft_config alongside. `AgentConnectModal` walks the founder through the pairing if their agent isn't connected yet.

Backed by: a per-session SSE emitter with replay buffer (so reconnect doesn't lose history), A2A + sampling transport adapters for the conduct-meeting driver (scripted turns + transports + finally-persist), an MCP `brief_vera_for_role` tool + Streamable HTTP route, and a regression-guard test for the no-token-passthrough rule (so we don't accidentally surface OAuth tokens to other agents through A2A).

  • Per-session SSE emitter with replay buffer
  • A2A + sampling transports for conduct-meeting driver
  • MCP brief_vera_for_role tool + Streamable HTTP route
  • BriefingCutsceneStage + BriefingCutsceneDraft + AgentConnectModal
  • HirePanel 4th CTA: "Have my agent brief Vera"
  • No-token-passthrough regression guard test
SHIPPED
20:41–22:34·30 commits

Coin economy v1 — Stripe → coin grant → atomic redeem

Credits are abstract; coins are objects you place. Migration adds `currency` to ItemKind + room_gifts snapshot fields + auto-redeem status. The first sprite is `coin-1credit`. Centralized SKU table (credits-50 + coins-10), `/api/credits/checkout` accepts `sku` in POST body with Stripe Tax inclusive, the webhook dispatches a coin grant on `coins-N` SKUs. `coin-redemption` module atomically redeems with a stable idempotency key; bulk-insert `N` coins via `coin-grant` module. POST /api/rooms/[id]/items/[itemId]/redeem from the holding founder.

Gifts auto-redeem currency items — open a coin gift and you don't place it, you cash it. `createGift` populates snapshot fields. InventoryFab grows a redeem button on currency items; HirePanel adds a "buy coins (10 for $4.40)" sibling button to the existing credits flow. Inbox event fires when a coin gift auto-redeems so the recipient knows what landed.

Tooling: `scripts/grant-coin.mjs` admin CLI for testing, integration test covering the grant + redeem + gift auto-redeem lifecycle, and the legacy `/mcp` anonymous-bearer surface got formally deprecated (the new agent-citizenship path is the only way in).

  • Migration: currency ItemKind + room_gifts snapshot + auto-redeem status
  • coin-1credit sprite + SKU table (credits-50 + coins-10)
  • /api/credits/checkout accepts sku + Stripe Tax inclusive
  • Webhook dispatches coin grant on coins-N SKU
  • Atomic redeem with stable idempotency key
  • createGift auto-redeems currency items + populates snapshot
  • InventoryFab redeem button + HirePanel "buy coins"
  • Integration test: grant + redeem + gift auto-redeem
  • Deprecated legacy anonymous-bearer /mcp surface
1 credit coin
1 credit coin
SHIPPED
22:39–22:56·6 commits

/terms + /privacy

/terms covers credit + Credit Coin language (since we're now taking money for both). /privacy is GDPR-compliant with concrete data-region disclosures confirmed by the operator. Cross-linked from /terms and from HirePanel's purchase flow.

SHIPPED
15:09–22:20·25 commits

create-prototype + experiment plugins

`create-prototype` wraps the existing `/prototype` skill and bundles a comparison report — every variant gets served from a firewall-open port + linked from a single index page so reviewers can side-by-side them. `experiment` plugin packages six composable skills (research / plan / implement / test / promote / autonomous) that imitate the multi-agent flow used to ship most of the Phase 2 work in one day. Autonomous mode actually runs autonomous now.

Thursday
·

May 21, 2026

OAuth lands, perspective fixes

SHIPPED
06:39–15:29·31 commits

Iso-perspective fix for rugs + wall items

Rugs and wall-art kept rendering flat — like decals laid on top of an iso scene instead of part of it. The fix is a generation-pipeline overhaul. New `cleanPeripheryNoise` pass uses connected-component analysis to strip background noise without wiping the largest component (the actual sprite). `quantizePalette` via sharp (`palette:true, dither:0`) locks the limited-palette look. Per-side wall styles: left-wall vs. right-wall now generate with different lighting refs (`ref-wall-art-left.png` + `ref-wall-art-right.png`).

Plugin/lib grows `refsForKind(side)`, a `rewriteCircularToEllipse` safety net for rug prompts that drifted into circles, and a `WallItem` variant lookup with `setFlipX` fallback when only one side variant exists. `sync-sprites` learns `inheritScenesFromParent` for wall-side variants. `scripts/audit-iso-perspective.mjs` + `regenerate.mjs` sub-command let us work through the audit queue methodically. End result: all 21 catalog rugs got regenerated through the v9 pipeline.

  • cleanPeripheryNoise — CC noise removal, preserve largest component
  • quantizePalette via sharp (palette:true, dither:0)
  • Per-side wall styles + ref-wall-art-{left,right}.png
  • refsForKind(side) — left/right refs per kind
  • rewriteCircularToEllipse — rug-prompt safety net
  • WallItem variant lookup with setFlipX fallback
  • sync-sprites inheritScenesFromParent for wall-side variants
  • scripts/audit-iso-perspective.mjs + regenerate.mjs sub-command
  • 21 rugs regenerated through v9 pipeline
SHIPPED
06:43–17:43·35 commits

OAuth pairing — finally lands end-to-end

The agent-citizenship OAuth flow from yesterday had bugs only a long, granular debug session could surface. We did the session today. Discoveries, in roughly the order they landed: `oidc-provider`'s JSONB writes were double-stringifying (drop the inner `JSON.stringify`); `devInteractions` was overriding our custom `interactions.url` (disable it); Interaction/Session/Grant needed `oauth_ephemeral` persistence; the adapter payload needed `sql.json` wrapping; consent page needed to move out from under the `/api/ee/oauth` catch-all; the catch-all needed a mount-prefix proxy + direct Interaction API; `oidc-provider` had to be marked external in the Next.js build (otherwise minification mangled its model names and the adapter blew up at runtime); `AuthorizationCode` adapter wanted `codeChallenge` in camelCase; the consent action needed an explicit `Grant` creation (or we re-prompt forever); the AS metadata had to serve at the RFC 8414 root well-known path; `oauth_ephemeral` keyed on `id` alone is robust to minified model names; `findByUid` for session-bound token lookup was missing.

Then OAuth non-blocking hardening on top: CORS on `/api/mcp` + every well-known endpoint; Dynamic Client Registration rate-limited at 10/min/IP; refresh-token reuse detection + family revocation per RFC 9700; path-suffixed PRM per the 2025-11-25 MCP draft (root PRM stays for back-compat); `OAUTH_COOKIE_SECRET` required in production; cookies forced `Secure` through the catch-all; per-host `oidc-provider` singletons so lab + prod issuers don't collide.

  • Drop double-stringify on JSONB writes
  • Disable devInteractions; custom interactions.url
  • Persist Interaction/Session/Grant in oauth_ephemeral
  • sql.json wrapping for adapter payload
  • Move consent page out from under /api/ee/oauth catch-all
  • oidc-provider marked external so model names survive bundling
  • camelCase codeChallenge in AuthorizationCode adapter
  • Create a Grant on consent — no more re-prompt loop
  • AS metadata at RFC 8414 root well-known path
  • Implement findByUid for session-bound token lookup
  • CORS on /api/mcp + well-known endpoints
  • DCR rate-limit at 10/min/IP
  • Refresh-token reuse detection + family revocation (RFC 9700)
  • Path-suffixed PRM per MCP draft 2025-11-25
  • OAUTH_COOKIE_SECRET required in production + Secure cookies
  • Per-host oidc-provider singletons
SHIPPED
09:48–11:13·20 commits

Command-palette room actions — ⌘K hybrid

The yesterday-prototype `?variant=A|B|C|D` shootout for room-action UI verdict'd on a hybrid: `cmdk` + `vaul` — Dialog on desktop, Drawer on mobile. ⌘K opens it from anywhere (with an input-guard so it doesn't fire while typing). The visible `RoomActionBar` dock surfaces the primary actions inline with a ⌘K hint pill. A first-visit teaching bubble points at ⌘K so new founders know the shortcut exists. Imperative `open()` refs on My Rooms + Share Room buttons mean palette commands can invoke them without re-mounting.

Guests get the palette too — callbacks redirect to sign-in for write actions. Prototype variant directory deleted after the hybrid landed. shadcn slate tokens + tw-animate-css wired for cmdk/dialog styling. Screenshots verified on desktop + mobile.

  • cmdk + vaul hybrid (Dialog desktop, Drawer mobile)
  • use-command-shortcut: ⌘K/Ctrl+K global hotkey with input guard
  • use-palette-discovery: first-visit teaching bubble
  • RoomActionBar visible dock with ⌘K hint pill
  • Imperative open() refs on My Rooms + Share Room
  • Guests get palette (callbacks redirect to sign-in)
  • shadcn slate tokens + tw-animate-css
SHIPPED
07:47–08:58·1 commit

/admin/backlog — triage card grid

Backlog reports sit in `reports/backlog/*.md`. `/admin/backlog` reads them and renders a triage card grid — title, eyebrow, status, link to source. Replaces ad-hoc PDF skims with something you can actually filter and act on.

Friday
·

May 22, 2026

Agent-native backlog ships, prod survives

SHIPPED
20:01·1 commit

Cross-subdomain Clerk lab sessions

Lab subdomains finally inherit the founder's auth from www.foundr.world. The trick was setting the Clerk cookie domain to a shared parent (`.foundr.world`) and surfacing the right host on the lab side — fiddly because Vercel Preview hostnames and `*.lab.foundr.world` had to play nice without leaking session state into prod. Phases 1+2 of A6 landed.

SHIPPED
14:00–19:35·73 commits

Agent-native backlog — every agent can drop

The thing we'd been circling for weeks: a backlog you can drop into with one MCP call, from any authed agent. v1 shipped first — single-tenant per founder, 10 MCP tools, founder-scoped RLS, Linear-style UI at `/backlog/[handle]`. Then v2 layered on top: workspace install tokens + Dynamic Client Registration so a fresh agent can self-register against a founder by presenting a token and asking nicely; founder approves each registration from `/backlog/settings`; server auto-dedups every new item via hybrid BM25 + cosine-embedding similarity (Sentry pattern, ~$0.0001/check via Vercel AI Gateway); a `.well-known/foundr-agent.json` capability manifest so agents can discover the surface lazily.

Two separate MCP servers, audience-bound: `foundr-world` at `/api/mcp` for the game tools, `foundr-backlog` at `/api/mcp/backlog` for the backlog. Bearers minted against one audience can't call the other (RFC 8707). Delegation chain via `mcp_call_log.act_chain` (RFC 8693) records which install-token a bearer was minted from, so the audit trail names the human who issued it, not just the agent. 11 tools total: `backlog_register_agent`, `backlog_note` (thin drop verb), `backlog_create` (typed), `backlog_list`, `backlog_get`, `backlog_update`, `backlog_accept`, `backlog_reject`, `backlog_claim`, `backlog_release`, `backlog_complete`, `backlog_get_top_priority`.

Brand: paper-and-orange, not sterile dark. `bg-cream-100` page, white card surfaces, orange-500 accents, Bricolage Grotesque for the headings, JetBrains Mono for the FW-NNN identifiers. Status circles in warm tones, priority glyphs in golden + walnut + ink-300. Settings live at `/backlog/settings` because backlog is its own product, not buried under `/settings`.

  • 11 MCP tools at /api/mcp/backlog with audience-bound bearers (RFC 8707)
  • DCR onramp: install token → fresh agent registers → founder approves → bearer minted
  • Hybrid BM25 (Postgres tsvector + ts_rank_cd) + cosine embedding dedup at create-time
  • pgvector + text-embedding-3-small (1536d) via Vercel AI Gateway
  • .well-known/foundr-agent.json capability manifest + cross-links from llms.txt + PRM
  • act_chain on mcp_call_log records install-token provenance per RFC 8693
  • Server Action + cached-list patterns scope-bind to founder_id (auto-injected, never accepted from input)
  • Linear-style row UI: status-grouped sections, hover-reveal actions, inline title edit, claim badge
  • Avatar-menu Settings link with pending-count badge (polls every 8s)
SHIPPED
20:35–21:05·5 commits

Prod migration outage — and the hotfix

PR #29 merged. supabase-sync workflow fired. It crashed on the first backlog migration: `relation "agent_backlog_founder_status_priority" already exists` — residue from a subagent hand-application against prod during v1 work that never got recorded in `schema_migrations`. The workflow's default fail-fast cascaded: migrations 2 and 3 (which had no schema dependency on migration 1) never ran. Vercel had already deployed app code querying `backlog_install_tokens` and `mcp_call_log.act_chain` — neither existed on prod. Backlog routes 500'd.

Hotfix PR #30: wrap every `create index`/`create trigger`/`create policy` in idempotent guards (`if not exists` for indexes, `drop ... if exists; create ...` for triggers and policies). Lab was unaffected (already recorded as applied, sync skips by filename). Prod retried the migrations with the now-idempotent SQL. Backlog routes came back online within 15 minutes of the merge.

  • Root cause: subagent applied migration to prod-bound MCP session, not lab
  • Cascading failure: GH Actions default exits on first step error
  • Symptom: Vercel app code queries missing tables → 500s
  • Recovery time: 15 minutes from outage to PR #30 merged
SHIPPED
21:05·4 commits

Migration safety net — 4-layer defense

After the outage, we built defense-in-depth so the failure class can't happen again. L2: a PR-time lint (`scripts/lint-migrations.mjs` + `migration-lint.yml`) blocks any new migration with bare `create index`/`create trigger`/`create policy`/`create table`/`create extension`/`create function` — the exact 6 classes that crashed prod. 20 unit tests, scoped to changed files via `--changed-since origin/main`. L3: `supabase-sync.sh` now continues past per-migration failures instead of cascading — each migration is its own transaction, so applying N+1 while N failed is structurally safe. L4: `scripts/verify-target-schema.mjs` runs after each sync target, queries `information_schema`, asserts every expected table/column is present. 13 more unit tests. L5: `.claude/rules.md` Section 3 forbids subagents from calling `apply_migration` without first verifying the MCP is bound to lab (`pxmqbsxwvylgfyqbxstl`), and `assert-lab-db.mjs` checks `SUPABASE_MCP_PROJECT_REF` env as a programmatic backstop.

Researched against industry standards first — Atlas, Sentry CheckedMigration, GitLab migration style guide, Anthropic auto-mode patterns, Supabase's own GitHub Integration (rejected because the Vercel-Supabase Integration requires a fresh project we can't retrofit). Skipped the heavy stuff (Supabase Preview Branches per PR, Atlas migrate/lint with a Postgres container) because our shared-lab topology covers the same surface for less ceremony. Documented the `CREATE INDEX CONCURRENTLY IF NOT EXISTS` footgun for future scale.

  • L2 — scripts/lint-migrations.mjs + migration-lint.yml (PR-time, blocking)
  • L3 — supabase-sync.sh continues past per-migration failures, aggregates at end
  • L4 — scripts/verify-target-schema.mjs parses migrations dir, queries information_schema
  • L5 — rules.md + experiment skill prompts + assert-lab-db.mjs MCP-ref check
  • CONCURRENTLY footgun documented for future scale
  • 33 unit tests across the two lint scripts
Saturday
·

May 23, 2026

Private chat, sit-on-chairs, the backlog grows up

SHIPPED
00:14–00:20·3 commits

Stateful furniture hotfix + lint upgrade

The 2026-05-22 incident's twin — same root cause, different migration. The next supabase-sync attempt crashed on `20260521234020_stateful_furniture_schema` with `column "state_key" of relation "room_items" already exists` (residue from an experiment's hand-application). The safety net DID its job: L3 continued past the failure on prod, L4 surfaced the lab gap, the lint script was the only layer that failed — because PR #31's rule set covered CREATE-* but missed ALTER TABLE ADD COLUMN and ADD CONSTRAINT.

PR #33 closed the loop in one PR: added `BARE_ADD_COLUMN` and `BARE_ADD_CONSTRAINT` rules (now 26 tests, was 20), then made the stateful_furniture migration idempotent (3 `add column` → `add column if not exists`, 1 `add constraint` → paired `drop constraint if exists`). The next sync run went fully green — first time the workflow had 6 ✓ steps in a single execution. Lab caught up to prod within 30 seconds.

  • Lint rule additions: BARE_ADD_COLUMN, BARE_ADD_CONSTRAINT
  • Migration fix: 3 idempotent ADD COLUMN + 1 paired DROP/ADD CONSTRAINT
  • First fully-green supabase-sync run after the cascade
  • Lesson banked: enumerate ALL DDL grammar verbs when designing a lint rule set
SHIPPED
00:23·1 commit

Private chat + local agent bridge

Direct-message channels between founders + agents, plus a local-agent bridge for Claude-in-the-game. The whole experiment landed as PR #32 — claude_conversations table with founder-bound RLS, an idempotent provisionClaudeAgent helper, the AI-SDK tool wrappers that expose the 5 local-citizen MCP tools to Claude, the driver loop at `/api/claude/chat`, and the ClaudeChat panel + backtick hotkey + FAB on the game canvas. Founders can `\` open a Claude session right inside their room.

SHIPPED
00:33·1 commit

Habbo presence pass — grounding + tile cursor

Two small fixes that make characters feel grounded. The avatar gets a soft shadow at the feet so it sits ON the tile instead of floating above it, and the tile cursor (the highlighted square that follows your mouse before you click-to-walk) was tightened up to read clearly even when an item is on the destination tile. Reverted the over-aggressive shadow + drop-shadow outline pass partway through — what landed is the lighter version.

SHIPPED
01:53·1 commit

Ruflo, deleted

Pulled the last of the ruflo agent-loop scaffold out of the repo. It was reserved on day zero and never wired into anything that shipped — dead weight in the dependency tree and the settings file. Gone.

SHIPPED
04:37·1 commit

Private backlog access + lab DB hardening

The backlog view at `/backlog/[handle]` was readable by anyone with the URL. PR #36 gated it behind a Clerk-only founder check, pushed the auth gate above the streaming boundary so the page never renders before the founder is resolved, and added a proper not-found view for handles that don't exist. Locked the backlog MCP auth challenge under test so the bearer flow can't silently regress.

SHIPPED
05:20·1 commit

Sit on chairs — Habbo-true + create-character skill

Single-click a chair (the sprite or its tile) and your founder walks over and sits down on arrival, taking the chair's facing. Click anywhere else and you stand back up and walk. The chair tile is walkable only as the final step of a path — Habbo's "canSitNextTile" rule — so you can't cut through an occupied seat. Avatar inherits the chair's z, peers see the sit pose broadcast over the existing realtime channel.

Shipped alongside the `create-character` skill: a Pixellab pipeline that generates a Habbo-style character in 8 directions with identity locked across every view, so new agents and avatars come out consistent instead of drifting per-frame.

taking a seat
taking a seat
SHIPPED
05:34·1 commit

Presence throttle — stop the track spam

Presence track updates were firing on every micro-movement and flooding the realtime channel. PR #38 throttled them so walks broadcast at a sane cadence — same visible motion, a fraction of the messages.

SHIPPED
15:42·1 commit

Room agent command header

Prototyped a few top-bar variants for surfacing what the agents in a room are doing, then promoted variant B into the real game. The header shows live agent activity (polled and hardened against flaky responses) with the full agent avatar, so a room reads as a place where work is happening, not just furniture.

SHIPPED
23:19·1 commit

Backlog detail panel + To-do/Failed states + instant mutations

The backlog grew a Linear-style detail card. Single-click any row to open a native `<dialog>` panel (deep-linked via `?item=`, body scroll-locked, self-healing on stale params), with title editing moved out of the row and into the panel. Added To-do and Failed states to the lifecycle, and rebuilt every mutation as optimistic-commit so the list updates the instant you act instead of waiting on a server round-trip.

Also canonized the vocabulary: a Backlog section in `.claude/rules.md` (plus the CLAUDE.md / AGENTS.md summaries) establishing that in this repo, "backlog" means the `foundr-backlog` MCP — not Linear, not GitHub issues.

Sunday
·

May 24, 2026

Floors, rugs, agent avatars, and the backlog becomes an engine

SHIPPED
02:19–04:16·2 commits

Connectors — search, categories, live Google Meet

The connectors drawer grew from a flat list into a searchable, categorized catalog of 11 integrations. Picked Google Meet to make real first: a request-host OAuth callback + provisioned Composio auth config means you can jump straight into a voice room from the connector card, and a Meet shortcut now lives in the ⌘K palette with live detection of whether you've connected it.

  • Catalog expanded to 11 connectors with category sections + search box
  • launchVoiceRoom + POST /voice provisions a join URL (Meet + Discord)
  • Composio preview env wired so Meet works in lab
SHIPPED
04:48–06:32·6 commits

Selectable floor materials + multi-tile rug engine

Rooms got a floor. Eight materials — wood-dark, wood-light, concrete, grass, marble, parquet, tile-checker, matrix — selectable from the picker and persisted on `rooms.floor_sprite`, threaded through the cached room layout, bootstrap, and items API. On top of that, a Habbo-style multi-tile rug engine: rugs are floor coverings, walkable, and lay across several tiles instead of snapping to one.

Shook out a couple of sprite bugs in the same pass — a manifest pointing at stale rug + floor art, and an atlas repack that silently dropped 10 rugs from the office-rug group. Both restored.

matrix runner
matrix runner
parquet
parquet
SHIPPED
05:17–06:16·2 commits

Multi-tile furniture + the polar bear

Furniture footprints can now span more than one tile. The VIP sofa was the proving ground — center the sprite over a 2×1 footprint, highlight the full footprint while placing, and route a click to the actual seat you clicked rather than the anchor tile. Upright pieces got grounded on the tile's edge so they stop floating, and a polar-bear decor piece joined the catalog.

polar bear
polar bear
SHIPPED
05:44·1 commit

Work Missions V0

A first runtime for missions — the unit of work an agent actually runs in a room. Mission schema with founder-bound RLS, client-safe types, repo helpers that map backlog items into mission drafts, HTTP APIs to create + assign missions, and a database-level invariant that a room can have only one active mission at a time (inactive ones detach automatically). The skeleton the later room-assignment and spatial-board work hangs off of.

SHIPPED
13:35–14:31·13 commits

Live room agent avatars

Agents now show up in the room as walking avatars, driven off a sanitized presence projection — a server-side table that exposes only what's safe to broadcast (no founder column leakage, paired-listener verified before projection). Tightened the agent-avatar click hitbox and isolated the realtime channel topics so one room's agent presence can't bleed into another.

the agent, on the floor
the agent, on the floor
SHIPPED
13:39–14:43·14 commits

No more anonymous chat — guests get a sign-up wall

Drop-in guests could send room chat. They can't anymore. The send path is gated on an explicit local id, guest broadcasts are dropped on receive as well as blocked on send, and the composer is replaced — on desktop and mobile — with a sign-up prompt instead of a text box. The guest dock landed as variant C: a prompt card with paired Sign-up / Log-in buttons, the room finder dropped for visitors who haven't signed in.

SHIPPED
14:56–16:00·16 commits

The backlog becomes an engine

Pulled the backlog's core out of the app and into its own workspace package — a dependency-injected engine with seams for the SQL getter and the embedding provider, so the same logic can run inside Founder World or anywhere else. Moved the repo, the dedup, and the types into the package; Founder World now re-exports and wires them through a thin adapter that maps `founderId → ownerId`.

Hardened with a CI boundary: dependency-cruiser enforces the package can't reach back into the app, and a guard catches unresolved `server-only`/`next` bare imports that would break the boundary. The engine degrades to BM25-only when the embedder throws, so dedup never hard-fails on an embedding outage.

  • DI seam: injected sql getter + embedding provider
  • dependency-cruiser package-boundary enforcement in CI
  • Graceful degrade to BM25-only when embedder throws
  • management plugin + create-backlog-item skill
SHIPPED
16:03–16:41·15 commits

Admins can edit server rooms

Server rooms (owned by SYSTEM_FOUNDER_ID) had no owner to authorize edits, so admins couldn't arrange them. Introduced a uniform `adminOverride` flag threaded through every mutation path — wall placement/move/remove, floor + wallpaper materials, and item-state toggles — gated by `canMutateRoom` / `adminOverridesRoom` helpers that accept a nullable owner. Root-caused from a research doc that found the admin gate was simply missing on the wall/floor/state paths.

Same cluster shipped onboarding-item defaults: a set of starter furniture granted to every new founder so a fresh room isn't empty.

SHIPPED
16:39–19:09·18 commits

Public agent mentions — @your agent in room chat

You can now @-mention an agent in a public room and have it reply. A mention parser resolves owner public-agent handles, an audited send path enqueues the mention onto a delivery queue, and new MCP claim tools let the agent pick up and answer mentions addressed to it. Realtime carries the mention metadata so the bubble highlights for everyone in the room, and public replies are guarded against leaking anything secret.

SHIPPED
17:38–23:51·18 commits

Workspaces + missions land in the backlog

The backlog engine grew two organizing concepts. Workspaces: every task lives in one, defaulting to an auto-created Inbox, with expand/contract migrations and a workspace filter. Missions: a CRUD module with status, derived progress, embeddings, and a BM25 + cosine `mission_search` that falls back to keyword-only with no AI. Both surfaced as MCP tools (`workspace_create/list`, `mission_create/get/list/update/search`) plus optional `workspace` and `mission` params on `backlog_create` / `backlog_update`. These are engine-owned and deliberately separate from the room-runtime `work_missions` table.

  • backlog_workspaces + workspace_id on agent_backlog (default Inbox)
  • backlog_missions + mission_id, derived progress, embeddings
  • mission_search: BM25 + cosine with no-AI fallback
  • 7 new MCP tools mirroring the workspace/mission shape
SHIPPED
06:38–23:56·16 commits

Furniture rotation pipeline + the Matrix Chesterfield

Added the Matrix Chesterfield (`chair-matrix-sofa`, a 2×1 sittable) and used it to rebuild how furniture faces. The new model is a single data-driven resolver: `directionSpriteFor` maps a direction to either a real frame or a mirror of one, `sitFacingFor` is the one canonical entrypoint for seat facing, and per-sprite `anchorXRatio` corrects the offset when a sprite is mirrored. Wide two-seat sofas became 2-rotation (only SE/SW fronts exist as art; the others mirror). The invariant: sprite-facing equals sit-facing, resolved one way.

The seating itself was a grind that ran into the night and kept going for days — empirical sit-pose maps, flip overrides, mirrored offsets. This is where the model got laid down; the corrections kept landing on the 25th and 26th.

matrix chesterfield
matrix chesterfield
SHIPPED
06:43·1 commit

Preferences tab — display-size scaling

The backlog used hardcoded px font sizes, so a root font-size bump couldn't enlarge it. Added a Preferences settings tab with a Default / Big / Bigger chooser that applies a CSS `zoom` to the backlog root — text, glyphs, rows, and spacing scale together. Persisted per-device in localStorage and synced across tabs.

WIP
17:19–23:29·5 commits

Agent-first docs — prototyping the front page

Started exploring what the docs/landing surface should be when the primary reader is an agent, not a human. Several throwaway prototype layouts — agentic-first variants, a frame rebuilt from an Exa MCP wireframe, agent/human dual modes — all toggleable on the real route. Exploratory; nothing promoted yet.

Monday
·

May 25, 2026

Missions in the room, backlog search, agent pairing

SHIPPED
03:50–04:39·8 commits

Assign a backlog mission to a room

Wired the engine-owned backlog missions to the room-runtime `work_missions` via a `source_backlog_mission_id` FK (set null on delete). A room's empty state now offers a BacklogMissionPicker; pick a mission and `createFromBacklogMission` maps its tasks across, then the room renders the live engine tasks for the mission it's sourced from. The backlog mission becomes the live reference the room shows.

SHIPPED
04:52–13:47·6 commits

Founder sit poses — all four directions

The selectable founder avatar got a complete set of seated poses — south, west, north, east — generated through the create-character pipeline. Rerolled the inconsistent ones until identity held across all four, sped up the skin save path, and made seated skin swaps preserve the pose instead of snapping back to standing.

SHIPPED
00:50–06:02·11 commits

Seating, derived correctly

The sofa-seating saga, continued and mostly closed. Built a headless preview tool that composites the sprite plus a seated avatar per rotation, so seat facing could be checked without launching the game. Used it to derive the 2-rotation sofa's seat facing and offsets from rot0 by mirroring (keeping an override escape hatch), and to make chairs and rugs follow Habbo click rules — held chairs and rugs place by the pointer tile, not the anchor.

SHIPPED
15:04–15:58·3 commits

Room entry feels instant

Audited why walking into a room felt sluggish and cut the entry navigation down so it feels instant — and unblocked avatar skin loading that was stalling the first paint.

WIP
15:13–17:45·5 commits

Mission control — spatial board prototype

Prototyped a mission-control dashboard, then a spatial version: draggable windows you can move, focus, and navigate between with the arrow keys. Exploratory — feeling out whether mission oversight should be a flat dashboard or a spatial canvas.

SHIPPED
18:44–21:54·12 commits

Backlog search + signed-out redirect

Founder-scoped full-text search across the backlog: a `?q=`-driven, debounced search box (press `/` to focus) backed by a server-only search delegate, with URL-driven results. Signed-out visitors hitting `/backlog/*` now redirect to sign-in carrying a return URL so they land back where they were. Results group by status / workspace / mission, with per-row workspace and mission tags.

SHIPPED
20:08–23:31·11 commits

First-agent pairing + companion wake

The onboarding path for connecting your first agent. Founders choose their agent CLI, pair against a protected lab MCP command with a pinned OAuth scope for Codex, can tag offline paired agents and @-mention them, and trigger a companion wake flow with a handoff fallback when the agent isn't live. A first-agent setup chooser steers new founders through it.

SHIPPED
22:34–23:52·10 commits

Backlog hardening — dedup, pagination, dedupe tool

Slice 1 of a hardening pass. `backlog_list` now paginates with `total_count` + `has_more`, tool output strips the embedding and tsvector internals, and create blocks the "similar" dedup band unless you pass `force` (surfacing `similar_blocked`). Added a non-enforcing p0 priority hint for urgent items and a `backlog_dedupe` tool that reviews and merges near-duplicates via a centroid-star scan — deliberately no transitive closure, so merging A↔B doesn't silently drag in C.

SHIPPED
01:02–02:09·5 commits

Faster public mention replies

Tightened the public agent-mention reply path: sped up delivery, moved the reply endpoint under MCP auth, and inlined the agent context into bearer validation so a reply doesn't pay an extra lookup. Also stabilized mention-autocomplete presence so the @-list doesn't flicker.

Tuesday
·

May 26, 2026

OpenClaw becomes the front door

SHIPPED
01:19–01:30·6 commits

backlog_triage — batch review the inbox

Slice 2 of the hardening work. A `backlog_triage` tool that lists proposed items awaiting review and applies a batch of decisions — accept, reject, or merge — in one call, with the engine-side triage ops (`listProposedForTriage` + `applyTriage`) and an integration test against lab.

SHIPPED
00:18–21:46·27 commits

OpenClaw becomes the native front door for agents

A pivot in how founders connect their first agent. Instead of building a Foundr-owned local daemon (Companion V1), Founder World ships as a native channel in the OpenClaw Gateway — the way Discord, Slack, or Telegram plug in. The gateway already owns the local process, runtime selection, sessions, channel routing, and supervision; Founder World is just the channel, and OpenClaw decides which agent and runtime answers each message. The first-agent chooser now recommends OpenClaw.

The day was mostly making the channel reliable end-to-end: a private-chat runtime boundary, origin validation, nested reply-id handling, a publishable channel package that reliably emits its dist, then the long tail of delivery hardening — draining the private-chat queue, serializing token refresh, persisting refresh tokens, and acking delivered replies so nothing is sent twice or lost.

  • Founder World = OpenClaw channel plugin (not a model provider, not a daemon)
  • foundr channel core helpers + private chat runtime boundary + origin guard
  • publishable @openclaw foundr-world channel package
  • reply delivery: queue drain, serialized token refresh, persisted refresh tokens, ack-on-deliver
SHIPPED
02:07–02:44·3 commits

Auth gates survive streaming

Next.js was evaluating Clerk auth gates after the response started streaming, which could leak a dynamic gate past the Suspense boundary. Streamed the dynamic auth gates correctly under Suspense and preserved them before streaming begins, with proxy tests covering the Clerk gate behavior.

SHIPPED
03:21–04:17·3 commits

Sofa sit-facing aligns with the mirror

The seating saga's last knot for the sofa: align the sit facing with the mirrored sprite state so a seated avatar faces the right way on the flipped rotation. Also loosened furniture contract validation to allow geometry-only checks, and hardened the room + furniture generation skills.

Wednesday
·

May 27, 2026

The agent in black, public-room replies

SHIPPED
00:15–00:40·4 commits

Avatar texture + Habbo chat bubbles

A cluster of rendering fixes. Stopped the missing-texture placeholder from flashing on avatars, hid unloaded remote avatars until their sprites actually resolve (no more magenta squares mid-walk), silenced the remote-avatar missing-texture warning, and restyled room chat bubbles to read properly Habbo — speech that sits on the character, not a generic toast.

SHIPPED
00:57·1 commit

Visitor room furniture hydration

Visitors walking into someone else's room sometimes saw an empty floor — the furniture failed to hydrate on the visitor branch. Fixed so a visited room loads its items the same as the owner sees them.

SHIPPED
00:23–01:12·2 commits

OpenClaw public-room mention replies, streamed

Connected the OpenClaw channel to public-room @-mentions: mention an agent in a public room and its reply comes back through OpenClaw, streamed into the chat bubble token-by-token instead of appearing all at once.

SHIPPED
02:48–04:49·11 commits

The agent in black — a selectable avatar

A new selectable founder avatar: black suit, narrow tie, black sunglasses, slicked side-part — generated through the create-character pipeline as `char-founder-agent` with 8 standing directions, 4 walking directions, and the four cardinal sit poses, identity locked across every frame. It works everywhere the other archetypes do: onboarding, skin drawer, server validation, guest random selection, walking, seating.

Then made it agent-only by default — the black-suit look is reserved as the default skin for agents, hidden from the founder picker — and cleaned up the sprite catalog around it: grouped poses by base avatar, hid the legacy base character groups, hid remote agent labels by default, and replaced the agent's name label with a ground shadow that reads its status.

the agent
the agent
seated
seated