Auto-Demo System — handoff (2026-07-12)
Start here to continue the auto-demo work. The full pipeline the user sketched — auto-pick a feature → auto-generate a narrated demo → render it to video → review → publish to a public /demos page + post to Bluesky, owned by a "Demo Manager" AI persona — is built and merged (Phases 0–3, PRs #1192–#1197). What remains is prod deploy/validation + refinements, not new capability.
Design + rationale: AUTO_DEMO_SYSTEM.md. This doc is the operational handoff.
1. ⚠️ Deploy checklist — nothing works on prod until this is done
The features degrade silently until these land. Do these first.
Migrations (apply to prod, in order):
472_studio_demos_authoring.sql— status/source/feature_name/generated_at. (user applied)473_studio_demos_video.sql— video_url/poster_url/video_status/video_rendered_at. (user applied)474_studio_demos_bluesky.sql— bluesky_uri/bluesky_posted_at. NOT yet applied — andstudio-demos.tsCOLSalready SELECTs these columns, so every studio_demos read/write on prod fails until 474 is applied. This is the #1 thing to verify. (A local Demo-Manager tick failed here 2026-07-12 — the code is correct; the column was just missing on prod.)475_ai_persona_demo_manager.sql— is_demo_manager flag. Needed before the Demo Manager cron can find a curator persona (it falls back to a super-admin otherwise).476_studio_demos_feature_hash.sql—feature_hashfor stale-demo regeneration.studio-demos.tsCOLSSELECTs it, so — same coupling as 474 — apply 476 before the code deploys or all studio_demos reads 500. Nullable, no backfill (existing demos aren't treated as stale; the cron adopts their baseline on next tick).
Env vars (all optional — each unset = that capability dormant):
DEMO_CAPTURE_BASE_URL— deployed frontend origin (e.g.https://www.graphene.fm). Unset = video frames are caption cards, not real-UI screenshots.DEMO_CAPTURE_USER_ID— the user whose signed-in view is captured for dashboard routes (magic-link auth). Unset = public routes only.BSKY_HANDLE+BSKY_APP_PASSWORD— enable the 🦋 Post to Bluesky button (same creds cafe-bluesky already uses; notBLUESKY_AUTOPOST_ENABLED— posting is manual).DEMO_MANAGER_USER_ID— fallback curator for auto-authored demos (or flag anis_demo_managerpersona instead).STUDIO_HOUSE_VOICE_ID— the house narration voice (already used by Demo Studio; defaults to Matilda).
Then, to turn on autonomous seeding: enable the demo_manager toggle at /dashboard/admin/crons (OFF by default).
2. What's live vs dormant after deploy
| Capability | Where | State |
|---|---|---|
| LLM demo authoring (✨ Auto-generate) | /dashboard/admin/demo-studio | live once 472 applied |
| Review lifecycle (Approve/Reject) | same | live |
Public /demos page (interactive narrated player) | /demos | live — already has a real Signal Union demo (authored + approved during dev) |
| Rendered MP4 (🎬 Render video) | admin + /demos <video> | live once 473 applied; caption cards unless DEMO_CAPTURE_BASE_URL set |
| Real-UI screenshot frames | — | dormant until DEMO_CAPTURE_BASE_URL set; needs prod validation (see §4) |
| Bluesky posting (🦋) | admin | dormant until BSKY creds set |
| Demo Manager auto-PICK cron | /dashboard/admin/crons | OFF by default; needs 474+475 + enable |
| Demo auto-post cron (earned autonomy) | /dashboard/admin/crons | OFF by default; needs BSKY creds + demo_autopost enable |
3. Ranked next steps (what a future session should pick up)
- Validate the video capture on prod (biggest unknown — see §4). Built blind (no local Chromium); uses prod-proven primitives but never run end-to-end.
Full-registry auto-PICK.DONE (2026-07-12). The cron no longer picks only from the curated 8 —fetchDemoableFeaturesmerges them with the drivable, user-facing slice of the frontendfeature-index.tsregistry, read at runtime from the publicGET /api/features/demoablefeed (the "expose FEATURES via a small public endpoint" option). Pure filter/merge (toDemoableFeatures/mergeDemoable) is unit-tested; the fetch is fault-tolerant (falls back to curated on any error). New routed features get demos automatically. Note: discovery needsDEMO_CAPTURE_BASE_URLset (same env as video capture) — unset = curated-only, which is safe.Stale-demo regeneration.DONE (2026-07-12, migration 476). Each demo stores afeature_hash(name+description+routes, route-order-insensitive) stamped at author time. Once all features are covered, the cron re-authors the first demo whose live feature drifted (pickStaleFeature) → back tostatus='generated'for review (supersede-via-review). Pre-476 demos aren't treated as stale — the cron adopts their current state as a baseline first (no LLM, no unpublish), so drift is measured from deploy forward.rejected/archivednever resurrected. New-coverage still beats stale-regen; one LLM author/tick.Earned autonomous posting (2c → auto).DONE (2026-07-12). Ademo_autopostcron (demo-autopost-cron.ts, 6h, heartbeatdemo_autopost_tick) posts one approved+published+video-ready demo not yet posted, via the samepostDemoToBluesky— so it can never publish on its own, only skip the manual click. Separate trust tier fromdemo_manager(two independent toggles). OFF by default (demo_autoposttoggle /DEMO_AUTOPOST_ENABLED); no-op without BSKY creds; won't double-post. To go live: set BSKY creds + flip thedemo_autoposttoggle at/dashboard/admin/crons. Guardrails unit-tested (demo-autopost.test.ts).- True screen-recording video (vs caption cards / screenshots). See the decision doc AUTO_DEMO_DISTRIBUTION_AND_CAPTURE.md §#5. Reframed finding: the full
chromiumbinary is already installed (nixpacks installs both it and headless-shell) — the limit is only that the pool launchesheadless: true(→ headless-shell, no screencast). So true recording is likely a code-only launch-mode change (full chromium in--headless=newfor the capture context), not a nixpacks/xvfb change. Recommendation: validate 2b screenshots first (step 1); build a flagged full-chromium capture only if motion fidelity is needed. - YouTube/TikTok/IG console distribution — feed approved demo MP4s into the existing human-in-loop
social-postingconsole. See AUTO_DEMO_DISTRIBUTION_AND_CAPTURE.md §#6. Honest blocker: demos are 16:9 landscape; the console's platforms (Shorts/Reels/TikTok) are 9:16 vertical — YouTube-landscape fits now (uploader half already written), the other three need a vertical demo re-render first. Kept human-in-loop (auto-post stays Bluesky-only).
4. How to validate the video capture on prod (the built-blind part)
The compose/mux (ffmpeg) is verified (produced a real drift-free MP4 locally). The frame capture (Chromium screenshot of the real app) is NOT locally runnable and needs a prod run:
- Apply migrations 473 + 474. Set
DEMO_CAPTURE_BASE_URL=https://www.graphene.fm+DEMO_CAPTURE_USER_ID=<a demo account>. - In
/dashboard/admin/demo-studio, on an approved demo, click 🎬 Render video. - Watch:
video_statusshould gorendering→ready; check the resulting MP4 (▶ MP4 link). - Confirm the frames show real UI (the captured page), not caption cards. If they're caption cards, capture failed — check backend logs for
demo-capturewarnings (magic-link auth, navigation timeouts, headless-shell screenshot support).
Fallback is safe: any per-step capture failure falls back to a caption card, so a render never fails outright — it just may be less rich than intended.
5. File map
Backend (apps/backend/src/):
services/demo-author.ts— LLMsteps[]authoring (purebuildAuthorPrompt/parseAuthoredDemo+authorDemoForFeature).services/demo-actions.ts— the generic action vocabulary (nav/click/scroll/type/highlight/wait) +mode:*legacy;normalizeAction.services/demo-video.ts—renderDemoVideoorchestration (narrate → frame → segment → concat → mux → upload).services/demo-video-args.ts— pure, dependency-free ffmpeg arg builders + frame HTML (golden-tested; import these in tests, not demo-video.ts).services/demo-capture.ts— real-UI screenshot capture (magic-link auth + per-step navigate/act/screenshot; config-gated, fallback-safe).services/demo-social.ts—postDemoToBluesky+buildDemoCaption(pure).services/demo-autopost-cron.ts—runDemoAutopostTick(earned autonomy: auto-posts an approved demo; separatedemo_autoposttoggle).services/demo-manager-cron.ts—runDemoManagerTick(adopt baselines → author new coverage → re-author drifted → queue).services/demoable-features.tsalso holdsfeatureHash/featuresNeedingBaseline/pickStaleFeature(stale-demo regeneration, pure + unit-tested).services/demoable-features.ts— auto-PICK source: curated list +fetchDemoableFeatures(merges registry-discovered features from the/api/features/demoablefeed) + puretoDemoableFeatures/mergeDemoable.services/studio-demos.ts— CRUD + lifecycle (upsertDemo/setDemoStatus/setDemoVideo/setDemoSocial/listPublicDemos/preparePublicDemo).COLSis the source of the migration-order coupling — keep it in sync with applied migrations.services/studio-narrate.ts— house-voice narration (content-hash cached).routes/studio-demos.ts— all/api/studio-demos/*incl. public/public,/author,/:slug/status,/:slug/render-video,/:slug/post-bluesky.- Cron wiring:
index.ts(runDemoManagerLoop, 6h),services/cron-toggles.ts(CRON_REGISTRY.demo_manager),services/system-health.ts(CRON_LABELS.demo_manager_tick). - Tests:
__tests__/demo-{actions,author,video,social}.test.ts,demoable-features.test.ts.
Frontend (apps/frontend/src/):
app/demos/page.tsx— public page (MP4 + "Try it live").components/demos/PublicDemoPlayer.tsx— narrated caption player.app/api/og/demo/[slug]/route.tsx— OG share card.app/api/features/demoable/route.ts— public registry feed the auto-PICK cron reads to discover new features.app/dashboard/admin/demo-studio/page.tsx— the admin board (Auto-generate / Approve / Render video / Post to Bluesky).
Migrations: 472–476.
6. Gotchas learned this build
studio_demos.COLScouples reads to applied migrations. Since the service SELECTs every lifecycle/video/social column, adding a column-bearing migration means that migration must be applied to prod before the code deploys, or all studio_demos queries 500. This bit the Phase-3 verify. When adding columns, apply the migration first.- ffmpeg golden tests must import a dependency-free module.
demo-video-args.tsexists precisely so tests don't boot supabase/playwright. Mocking../../supabase.jsin an ESMunstable_mockModuletest proved unreliable here — prefer a pure module + direct import. recordVideo≠ available. Headless-shell can screenshot but not record video. Don't reach forrecordVideowithout solving xvfb/full-Chromium first.created_byis a real FK. Authoring needs a realauth.usersid (super-admin or a persona's user_id) — not a placeholder UUID.- The house narration voice is shared + cached. Re-authoring/re-rendering is cheap; changing
STUDIO_HOUSE_VOICE_IDinvalidates the cache and re-renders lines.
Session summary: Phases 0–3 shipped as PRs #1192–#1197 (design doc #1191). Also this session: Landhouse added live to the Signal Union stream; the "Reframes" AR-glasses branding idea logged.