name: aeries-daeya description: "Aeries Daeya: Privacy-first avatar social platform. Create a digital identity, style it with AI from photo references, share experiences without exposing your real face. Mutation explorer for iterating on looks and backgrounds." version: 1 vars: project_name: "" service_name: "daeya-api" worker_name: "media-worker" app_name: "studio-ui" steps: # --- Infrastructure --- create-project: action: api method: POST endpoint: /project body: name: "{{ .vars.project_name }}" description: "Aeries Daeya: Privacy-first avatar social platform" outputs: - project_id: .data.name - domain: .data.domain add-db: description: CockroachDB for characters, looks, albums, posts, mutation history depends_on: [create-project] action: api method: POST endpoint: "/projects/{{ .outputs.create-project.project_id }}/components" body: { type: postgres, name: "daeya-db" } add-redis: description: Redis for SSE pub/sub and AI generation job queue depends_on: [create-project] action: api method: POST endpoint: "/projects/{{ .outputs.create-project.project_id }}/components" body: { type: redis, name: "event-bus" } add-components: description: Add daeya-api + media-worker + studio-ui as a single atomic commit depends_on: [add-db, add-redis] action: api method: POST endpoint: "/projects/{{ .outputs.create-project.project_id }}/components/batch" body: components: - type: service name: "{{ .vars.service_name }}" - type: worker name: "{{ .vars.worker_name }}" - type: app-react name: "{{ .vars.app_name }}" wait-infra: description: Wait for CI to build and deploy all components depends_on: [add-components] action: wait_pipeline project_id: "{{ .outputs.create-project.project_id }}" max_attempts: 120 poll_interval: 10 # --- Feature 1: Avatar & Look Data Model --- implement-avatar-model: description: "Characters, looks, albums, posts, mutation history — full data layer + CRUD API" depends_on: [wait-infra] action: api method: POST endpoint: "/projects/{{ .outputs.create-project.project_id }}/builds" body: prompt: "/implement-feature avatar-model --requirements 'DB migrations in daeya-api: (1) CREATE TABLE characters (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, name TEXT NOT NULL, skin_tone INT NOT NULL DEFAULT 5, face_shape TEXT NOT NULL DEFAULT ''oval'', hair_color TEXT NOT NULL DEFAULT ''brown'', hair_style TEXT NOT NULL DEFAULT ''medium'', eye_color TEXT NOT NULL DEFAULT ''brown'', body_type TEXT NOT NULL DEFAULT ''average'', description TEXT NOT NULL DEFAULT '''', avatar_url TEXT, status TEXT NOT NULL DEFAULT ''pending'', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()); (2) CREATE TABLE looks (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), character_id UUID NOT NULL REFERENCES characters(id) ON DELETE CASCADE, name TEXT NOT NULL, outfit_description TEXT NOT NULL DEFAULT '''', outfit_url TEXT, source_photo_urls TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], status TEXT NOT NULL DEFAULT ''pending'', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()); (3) CREATE TABLE albums (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, name TEXT NOT NULL, cover_url TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()); (4) CREATE TABLE posts (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, character_id UUID REFERENCES characters(id), look_id UUID REFERENCES looks(id), album_id UUID REFERENCES albums(id), image_url TEXT, video_url TEXT, caption TEXT NOT NULL DEFAULT '''', location_name TEXT, location_lat DECIMAL, location_lng DECIMAL, prompt_text TEXT, show_prompt BOOLEAN NOT NULL DEFAULT false, frame_style TEXT NOT NULL DEFAULT ''none'', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()); (5) CREATE TABLE mutation_history (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), character_id UUID NOT NULL REFERENCES characters(id) ON DELETE CASCADE, parameters_json JSONB NOT NULL, result_url TEXT, committed BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()). Domain models: Character, Look, Album, Post, MutationRecord with same fields. Services with interfaces in ports: CharacterService (Create, GetByID, ListByUser, Update, UpdateAvatarURL), LookService (Create, GetByID, ListByCharacter, UpdateOutfitURL), AlbumService (Create, GetByID, ListByUser), PostService (Create, GetByID, ListByAlbum, ListByUser), MutationService (Create, GetByCharacter, Commit). Endpoints all under /api/daeya-api (require JWT auth via skeleton auth.Middleware): POST /characters (body: {name, description, skin_tone?, face_shape?, hair_color?, hair_style?, eye_color?, body_type?} → 202 + character), GET /characters (returns user characters), GET /characters/{id}, PATCH /characters/{id} (update fields), POST /characters/{id}/looks (body: {name, outfit_description?, source_photo_urls?[]}, → 202 + look), GET /characters/{id}/looks, POST /characters/{id}/mutations (body: {skin_tone?, background?, lighting?, style_filter?} → 202 + mutation_id), POST /characters/{id}/mutations/{mutation_id}/commit (commits mutation as new character baseline), POST /albums (body: {name}), GET /albums, GET /albums/{id}/posts, POST /posts (body: {character_id, look_id?, album_id?, caption?, location_name?, location_lat?, location_lng?, prompt_text?, show_prompt?, frame_style?} → post), GET /posts/{id}. After creating a character enqueue job {type:generate_character, character_id}. After creating a look enqueue job {type:generate_look, look_id}. After creating a mutation enqueue job {type:generate_mutation, mutation_id}. Publish SSE events via SSE hub: character_updated {character_id, status, avatar_url} to channel:daeya, look_updated {look_id, status, outfit_url} to channel:daeya, mutation_ready {mutation_id, character_id, result_url} to channel:daeya.'" auto_commit: true auto_push: true git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git" outputs: - build_id: .data.task_id wait-avatar-model: depends_on: [implement-avatar-model] action: wait_build build_id: "{{ .outputs.implement-avatar-model.build_id }}" max_attempts: 120 poll_interval: 5 # --- Feature 2: AI Generation Pipeline --- implement-generation: description: "Character portrait gen → look styling from photos → mutation explorer → action expressions" depends_on: [wait-avatar-model] action: api method: POST endpoint: "/projects/{{ .outputs.create-project.project_id }}/builds" body: prompt: "/implement-feature ai-generation --requirements 'Implement generation pipeline in media-worker using skeleton ai-client package (LAOZHANG_API_KEY for image generation via Gemini vision for photo analysis). Worker consumes jobs from queue by type: JOB generate_character {character_id}: Load character row. Build a portrait prompt from character fields: face_shape, hair_color, hair_style, eye_color, skin_tone (map 1-10 to descriptive terms: 1=very fair, 3=light, 5=medium, 7=tan, 9=deep, 10=very dark), body_type, description. Prompt template: \"Portrait photo of a [gender-neutral] person. [description]. [skin_tone_desc] skin, [face_shape] face, [hair_color] [hair_style] hair, [eye_color] eyes. [body_type] build. Front-facing, neutral expression, soft studio lighting, clean white background. High quality, photorealistic.\" Generate one image via LAOZHANG_API_KEY. Save URL to character.avatar_url, set status=ready. Publish SSE character_updated {character_id, status:ready, avatar_url}. JOB generate_look {look_id}: Load look + character rows. If source_photo_urls non-empty: use Gemini vision (GEMINI_API_KEY) to analyze first photo and extract style descriptors (garment type, color palette, fabric, silhouette, styling details). Build outfit prompt using descriptors + character appearance. If no source photos: use outfit_description directly. Prompt template: \"The same person from the reference portrait is now wearing [outfit_description]. Full body view. [character appearance details]. Natural lighting. High quality, photorealistic.\" Use character.avatar_url as style reference if available. Generate one image. Save to look.outfit_url, set status=ready. Publish SSE look_updated {look_id, status:ready, outfit_url}. JOB generate_mutation {mutation_id}: Load mutation_history + character rows. Parameters from parameters_json may include: skin_tone (1-10, update descriptive term), background (city_day|city_night|nature_park|studio_white|studio_dark|beach|mountain), lighting (natural|golden_hour|dramatic|neon|soft_box), style_filter (realistic|stylized|anime|painterly). Build variation prompt starting from character description but applying parameter overrides. Background mapping: city_day=busy city street daytime, city_night=city street at night with neon lights, nature_park=lush green park, studio_white=plain white studio, studio_dark=dark minimalist studio, beach=sandy beach golden hour, mountain=mountain vista clear sky. Style filter suffix: anime=anime art style illustration, painterly=oil painting impressionist style (others=photorealistic). Generate image. Save to mutation_history.result_url. Publish SSE mutation_ready {mutation_id, character_id, result_url}. JOB generate_action {character_id, look_id, action_text, post_id}: Load character + look. Build action prompt: the character performing the action described. Examples: spin while showing outfit=full body 3/4 turn pose showing clothing detail; say Happy Birthday=person holding birthday cake smiling waving; wave=person waving at camera smiling. Generate image (static). Save image_url to posts table for post_id. Publish SSE action_ready {post_id, image_url}.'" auto_commit: true auto_push: true git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git" outputs: - build_id: .data.task_id wait-generation: depends_on: [implement-generation] action: wait_build build_id: "{{ .outputs.implement-generation.build_id }}" max_attempts: 120 poll_interval: 5 wait-deploy-2: description: Deploy generation pipeline depends_on: [wait-generation] action: wait_pipeline project_id: "{{ .outputs.create-project.project_id }}" max_attempts: 120 poll_interval: 10 # --- Feature 3: Studio UI --- implement-studio-ui: description: "Character creation wizard, mutation explorer, look styling, album management" depends_on: [wait-deploy-2] action: api method: POST endpoint: "/projects/{{ .outputs.create-project.project_id }}/builds" body: prompt: "/implement-feature studio-ui --requirements 'Build the React studio UI using skeleton packages (@project/ui, @project/auth, @project/layout, @project/realtime, @project/api-client). DashboardShell layout from @project/layout. Routes: /login (public) and all others protected via ProtectedRoute from @project/auth. LOGIN PAGE: OTP flow — email input, Send Code, then code input + Verify. DASHBOARD (/): Header with \"Create Character\" button. Grid of CharacterCard components showing avatar_url (circular, 96px), character name, look count badge. Clicking card goes to /characters/:id. Empty state: centered illustration + \"Create your first character\" button. CHARACTER CREATION (/characters/new): 4-step wizard with progress indicator. STEP 1 SPARK: Large prompt: \"Describe your character\". Textarea for free-form description. Skin tone picker: row of 10 circular swatches from #FDDBB4 to #3D1A00, selected = ring highlight. Hair color pills: Blonde, Brown, Black, Red, Auburn, White, Silver. Face shape pills: Oval, Round, Square, Heart, Diamond. Step nav: Back (disabled on step 1) + Continue. STEP 2 SHAPE: Hair style pills: Short, Medium, Long, Curly, Wavy, Straight, Bob, Bun. Eye color pills: Brown, Blue, Green, Hazel, Gray, Amber. Body type pills: Slim, Athletic, Average, Curvy, Plus. Name field with placeholder \"Give your character a name\". Live summary card on right: lists all selected traits as pills. STEP 3 SOUL (optional): \"What does your character like to do?\". Interest pills (multi-select up to 3): Travel, Fashion, Music, Food, Art, Fitness, Books, Tech, Nature. tagline field: \"Add a tagline\" (optional). STEP 4 CREATE: Shows summary card. \"Generate Character\" button. On submit POST /api/daeya-api/characters. Shows generating spinner with messages cycling: \"Bringing your character to life...\", \"Crafting their appearance...\", \"Almost there...\". Redirects to /characters/:id on success. CHARACTER DETAIL (/characters/:id): Left column (1/3): circular avatar image (200px, pending state shows pulsing gray circle). Character name + tagline. Trait pills row (skin tone swatch + hair + eyes). \"Add Look\" button. Looks list: each row shows look name, outfit_url thumbnail (48px), status badge. Clicking look shows full outfit image modal. Right column (2/3): MUTATION EXPLORER panel. Header: \"Explore Variations\". Parameter controls: Skin Tone — horizontal slider (1-10) with live swatch preview at current value. Background — pill row: City Day, City Night, Nature, Studio, Beach, Mountain. Lighting — pill row: Natural, Golden Hour, Dramatic, Neon. Style — pill row: Realistic, Stylized, Anime, Painterly. \"Generate Preview\" button — calls POST /api/daeya-api/characters/:id/mutations with current params. Preview area: shows result_url when ready, else shows generating spinner. \"Commit as New Baseline\" button (enabled only when preview ready) — calls POST /api/daeya-api/characters/:id/mutations/:mutation_id/commit, then refreshes character avatar. \"Discard\" button resets params to character current values. ADD LOOK PANEL (slide-in from right): Look name field. Photo upload area: drag-drop or click to upload up to 3 reference photos. \"Or describe the outfit\" textarea (shown when no photos uploaded). \"Generate Look\" button. On submit POST /api/daeya-api/characters/:id/looks with uploaded photo URLs (or description). Shows generating state. ALBUMS (/albums): Grid of AlbumCard (cover_url thumbnail, name, post count). \"New Album\" button → modal with name input. REALTIME: useEventChannel from @project/realtime subscribed to channel:daeya. On character_updated: refresh CharacterCard avatar_url + status. On look_updated: refresh look thumbnail in character detail. On mutation_ready: show result in Mutation Explorer preview area. On action_ready: show generated post image.'" auto_commit: true auto_push: true git_clone_url: "https://git.threesix.ai/threesix/{{ .outputs.create-project.project_id }}.git" outputs: - build_id: .data.task_id wait-studio-ui: depends_on: [implement-studio-ui] action: wait_build build_id: "{{ .outputs.implement-studio-ui.build_id }}" max_attempts: 120 poll_interval: 5 wait-deploy-final: description: Deploy final build depends_on: [wait-studio-ui] action: wait_pipeline project_id: "{{ .outputs.create-project.project_id }}" max_attempts: 120 poll_interval: 10 # --- Verification --- verify-health: description: Verify daeya-api is healthy depends_on: [wait-deploy-final] action: shell command: | HEALTH=$(curl -sf "https://{{ .outputs.create-project.domain }}/api/daeya-api/health" | jq -r '.data.status // empty') if [ "$HEALTH" = "healthy" ]; then echo "daeya-api healthy" exit 0 else echo "daeya-api not healthy: $HEALTH" exit 1 fi verify-site: description: Verify studio-ui frontend loads depends_on: [wait-deploy-final] action: wait_site domain: "{{ .outputs.create-project.domain }}" project_id: "{{ .outputs.create-project.project_id }}" max_attempts: 30 poll_interval: 5 verify-auth-endpoint: description: Verify character endpoint requires auth (401 confirms it) depends_on: [verify-health] on_error: continue action: shell command: | DOMAIN="{{ .outputs.create-project.domain }}" STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ "https://$DOMAIN/api/daeya-api/characters" \ -H "Content-Type: application/json") echo "GET /characters without auth returned: $STATUS" if [ "$STATUS" = "401" ]; then echo "Auth guard confirmed"; exit 0; fi echo "Unexpected status — endpoint may not exist" exit 1 teardown: - description: Delete project and all provisioned resources action: api method: DELETE endpoint: "/project/{{ .outputs.create-project.project_id }}"