product

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 — and studio-demos.ts COLS already 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.sqlfeature_hash for stale-demo regeneration. studio-demos.ts COLS SELECTs 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; not BLUESKY_AUTOPOST_ENABLED — posting is manual).
  • DEMO_MANAGER_USER_ID — fallback curator for auto-authored demos (or flag an is_demo_manager persona 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

CapabilityWhereState
LLM demo authoring (✨ Auto-generate)/dashboard/admin/demo-studiolive once 472 applied
Review lifecycle (Approve/Reject)samelive
Public /demos page (interactive narrated player)/demoslive — 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 framesdormant until DEMO_CAPTURE_BASE_URL set; needs prod validation (see §4)
Bluesky posting (🦋)admindormant until BSKY creds set
Demo Manager auto-PICK cron/dashboard/admin/cronsOFF by default; needs 474+475 + enable
Demo auto-post cron (earned autonomy)/dashboard/admin/cronsOFF by default; needs BSKY creds + demo_autopost enable

3. Ranked next steps (what a future session should pick up)

  1. 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.
  2. Full-registry auto-PICK. DONE (2026-07-12). The cron no longer picks only from the curated 8 — fetchDemoableFeatures merges them with the drivable, user-facing slice of the frontend feature-index.ts registry, read at runtime from the public GET /api/features/demoable feed (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 needs DEMO_CAPTURE_BASE_URL set (same env as video capture) — unset = curated-only, which is safe.
  3. Stale-demo regeneration. DONE (2026-07-12, migration 476). Each demo stores a feature_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 to status='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/archived never resurrected. New-coverage still beats stale-regen; one LLM author/tick.
  4. Earned autonomous posting (2c → auto). DONE (2026-07-12). A demo_autopost cron (demo-autopost-cron.ts, 6h, heartbeat demo_autopost_tick) posts one approved+published+video-ready demo not yet posted, via the same postDemoToBluesky — so it can never publish on its own, only skip the manual click. Separate trust tier from demo_manager (two independent toggles). OFF by default (demo_autopost toggle / DEMO_AUTOPOST_ENABLED); no-op without BSKY creds; won't double-post. To go live: set BSKY creds + flip the demo_autopost toggle at /dashboard/admin/crons. Guardrails unit-tested (demo-autopost.test.ts).
  5. True screen-recording video (vs caption cards / screenshots). See the decision doc AUTO_DEMO_DISTRIBUTION_AND_CAPTURE.md §#5. Reframed finding: the full chromium binary is already installed (nixpacks installs both it and headless-shell) — the limit is only that the pool launches headless: true (→ headless-shell, no screencast). So true recording is likely a code-only launch-mode change (full chromium in --headless=new for 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.
  6. YouTube/TikTok/IG console distribution — feed approved demo MP4s into the existing human-in-loop social-posting console. 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:

  1. Apply migrations 473 + 474. Set DEMO_CAPTURE_BASE_URL=https://www.graphene.fm + DEMO_CAPTURE_USER_ID=<a demo account>.
  2. In /dashboard/admin/demo-studio, on an approved demo, click 🎬 Render video.
  3. Watch: video_status should go renderingready; check the resulting MP4 (▶ MP4 link).
  4. Confirm the frames show real UI (the captured page), not caption cards. If they're caption cards, capture failed — check backend logs for demo-capture warnings (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 — LLM steps[] authoring (pure buildAuthorPrompt/parseAuthoredDemo + authorDemoForFeature).
  • services/demo-actions.ts — the generic action vocabulary (nav/click/scroll/type/highlight/wait) + mode:* legacy; normalizeAction.
  • services/demo-video.tsrenderDemoVideo orchestration (narrate → frame → segment → concat → mux → upload).
  • services/demo-video-args.tspure, 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.tspostDemoToBluesky + buildDemoCaption (pure).
  • services/demo-autopost-cron.tsrunDemoAutopostTick (earned autonomy: auto-posts an approved demo; separate demo_autopost toggle).
  • services/demo-manager-cron.tsrunDemoManagerTick (adopt baselines → author new coverage → re-author drifted → queue).
  • services/demoable-features.ts also holds featureHash/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/demoable feed) + pure toDemoableFeatures/mergeDemoable.
  • services/studio-demos.ts — CRUD + lifecycle (upsertDemo/setDemoStatus/setDemoVideo/setDemoSocial/listPublicDemos/preparePublicDemo). COLS is 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: 472476.


6. Gotchas learned this build

  • studio_demos.COLS couples 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.ts exists precisely so tests don't boot supabase/playwright. Mocking ../../supabase.js in an ESM unstable_mockModule test proved unreliable here — prefer a pure module + direct import.
  • recordVideo ≠ available. Headless-shell can screenshot but not record video. Don't reach for recordVideo without solving xvfb/full-Chromium first.
  • created_by is a real FK. Authoring needs a real auth.users id (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_ID invalidates 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.

AUTO DEMO HANDOFF — Docs | HiveJournal