Session WebUI: - Add `web_ui` flag to session create — launches claude-code-ui in pod on port 3001 - Install @siteboon/claude-code-ui in claudebox Dockerfile, expose port 3001 - Migration 027: add web_ui column to sessions table - startWebUI/stopWebUI fire-and-forget helpers in SessionsHandler - Service selects preview port 3001 (web UI) vs 8080 (sidecar) based on flag Aeries Daeya cookbook: - Add cookbooks/trees/aeries-daeya.yaml: privacy-first avatar social platform (infra → avatar data model → AI generation pipeline → studio UI) - Add cookbooks/scripts/aeries-daeya-test.sh: run/status/diagnose/teardown harness - Fix race condition in common.sh wait_for_pipeline: detect already-running pipelines at startup and track directly instead of waiting for a newer one Docs/tooling: - Add SDK Update Workflow section to CLAUDE.md - Add `make sdk` and `make sdk-check` targets for OpenAPI spec management Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
189 lines
16 KiB
YAML
189 lines
16 KiB
YAML
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 }}"
|