rdev/cookbooks/trees/persona-community.yaml
jordan 62a9bbb237
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fix: resolve 7 root causes causing cookbook deployment failures
RC-1: Gitea org fallback already removed (no-op, confirmed)
RC-3: Push/pull now explicitly target origin main (HEAD:main) in both
  pod_git_operations.go and claudebox/git.go — fixes Woodpecker webhook
  trigger by ensuring pushes always land on the main branch
RC-4: wait_for_pipeline records baseline pipeline number before polling;
  only returns success when a NEWER pipeline completes — prevents false
  positive when a prior pipeline was already success
RC-5: Redis WRONGPASS fixed on live persona-community-5 instance; platform
  gap noted (no reprovision endpoint for Redis ACL drift)
RC-6: Removed on_error:continue from all infra provisioning steps (add-db,
  add-redis) across persona-community, slackpath-2/3/4/5 trees — infra
  failures now fail the tree instead of silently continuing to a crash
RC-7: Added .pnpm-store/ to skeleton .gitignore — prevents thousands of
  cache files being committed by agents after pnpm install
RC-2: Updated all 12 cookbook trees — git_clone_url jordan/ → threesix/
  (24 occurrences across all slackpath, aeries, full-stack, genkit trees)
Also: strings.Cut and strings.SplitSeq lint fixes in pod_git_operations.go
  and claudebox/git.go

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 18:49:09 -07:00

215 lines
11 KiB
YAML

name: persona-community
description: "AI Persona Generation Community: describe a person, AI generates their full identity, photos, and videos. Browse the community grid."
version: 1
vars:
project_name: ""
service_name: "persona-api"
worker_name: "media-worker"
app_name: "creator-ui"
steps:
# --- Infrastructure ---
create-project:
action: api
method: POST
endpoint: /project
body:
name: "{{ .vars.project_name }}"
description: "AI Persona Generation Community"
outputs:
- project_id: .data.name
- domain: .data.domain
add-db:
description: CockroachDB for persona + media persistence
depends_on: [create-project]
action: api
method: POST
endpoint: "/projects/{{ .outputs.create-project.project_id }}/components"
body: { type: postgres, name: "persona-db" }
add-redis:
description: Redis for SSE pub/sub and 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 persona-api + media-worker + creator-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
verify-notify-domain:
description: Wait for the project email domain to be verified by Resend
depends_on: [wait-infra]
on_error: continue
action: shell
command: |
PROJECT_ID="{{ .outputs.create-project.project_id }}"
API_URL="${RDEV_API_URL:-https://rdev.masq-ops.orchard9.ai}"
for i in $(seq 1 12); do
STATUS=$(curl -sf "$API_URL/projects/$PROJECT_ID/notify/status" \
-H "X-API-Key: $RDEV_API_KEY" | jq -r '.data.status // empty' 2>/dev/null)
echo "notify domain status (attempt $i/12): $STATUS"
if [ "$STATUS" = "verified" ]; then
echo "Email domain verified — OTP and auth emails will work"
exit 0
fi
if [ "$STATUS" = "not_configured" ]; then
echo "Notify not configured — skipping"
exit 0
fi
sleep 30
done
echo "Email domain not verified after 6 minutes — continuing, but OTP emails may fail"
exit 0
# --- Feature 1: Persona Data Model & CRUD ---
implement-persona-model:
description: "Persona table, domain model, CRUD endpoints, SSE events"
depends_on: [verify-notify-domain]
action: api
method: POST
endpoint: "/projects/{{ .outputs.create-project.project_id }}/builds"
body:
prompt: "/implement-feature persona-model --requirements 'DB migration in persona-api: CREATE TABLE personas (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, handle TEXT UNIQUE NOT NULL, gender TEXT NOT NULL, description TEXT NOT NULL, tags TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], spec_json JSONB, anchor_url TEXT, avatar_url TEXT, banner_url TEXT, image_urls TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], video_urls TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[], status TEXT NOT NULL DEFAULT ''pending'', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()). Domain model Persona with same fields. PersonaService: Create(ctx, description, gender, customName string) (*Persona, error) — saves row then enqueues a generate_spec job. GetByID(ctx, id). List(ctx, limit, offset int) ([]*Persona, error). Queue job type PersonaGenerateJob {PersonaID string, Stage string} where Stage values are: spec, anchor, avatar, banner, gallery_batch, video. Endpoints all under /api/persona-api: POST /personas (body: {description, gender, custom_name?}, returns 202 with persona), GET /personas (query: limit=20, offset=0), GET /personas/{id}. All routes require JWT auth (use skeleton auth.Middleware). After any persona field update publish SSE event persona_updated to channel:personas via the SSE hub.'"
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-persona-model:
depends_on: [implement-persona-model]
action: wait_build
build_id: "{{ .outputs.implement-persona-model.build_id }}"
max_attempts: 120
poll_interval: 5
# --- Feature 2: AI Generation Pipeline ---
implement-generation:
description: "spec → anchor → avatar + banner → gallery batches → 4 videos"
depends_on: [wait-persona-model]
action: api
method: POST
endpoint: "/projects/{{ .outputs.create-project.project_id }}/builds"
body:
prompt: "/implement-feature persona-generation --requirements 'Implement the generation pipeline in media-worker using the skeleton ai-client package (LAOZHANG_API_KEY for images, GEMINI_API_KEY for video). Worker consumes PersonaGenerateJob from queue and executes the stage: STAGE spec — call LLM to generate a PersonaSpec JSON: {name, handle, gender, tags:[8 strings], personality_archetype, appearance:{hair, eyes, skin, body_type, distinctive_features}, style_signature, image_matrix:[{position:int, pose:string, clothing:string, background:string} x 30]}. Save full spec to spec_json, update name/handle/tags on the persona row. Then enqueue stage=anchor job. STAGE anchor — generate one reference face-front portrait image from appearance description. Save URL to anchor_url. Then enqueue stage=avatar AND stage=banner as parallel jobs. STAGE avatar — generate circular portrait derived from anchor appearance. Save to avatar_url. STAGE banner — generate 3:1 landscape lifestyle image derived from anchor. Save to banner_url. STAGE gallery_batch — generate 10 images using next 10 unfilled positions from image_matrix. Append URLs to image_urls array. If fewer than 30 images exist, enqueue another gallery_batch job. STAGE video — generate the next missing video from: [smile_reveal, personality_moment, lifestyle, invitation]. Append URL to video_urls. If fewer than 4 videos, enqueue next video job. After each stage: update persona row, publish job_update SSE event {persona_id, stage, status:complete|error, progress} to channel:personas.'"
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: Community UI ---
implement-ui:
description: "Community grid, persona card, creation form, real-time SSE, detail page"
depends_on: [wait-deploy-2]
action: api
method: POST
endpoint: "/projects/{{ .outputs.create-project.project_id }}/builds"
body:
prompt: "/implement-feature community-ui --requirements 'Build the React community UI using skeleton packages (@project/ui, @project/auth, @project/layout, @project/realtime, @project/api-client). App uses DashboardShell layout from @project/layout. Routes: /login (public) and / and /personas/:id (protected via ProtectedRoute from @project/auth). LOGIN PAGE: OTP flow — email input, Send Code button, then code input + Verify button. On success store token and redirect to /. COMMUNITY PAGE (/): Top bar has Create Persona button. Main area is a responsive grid of PersonaCard components. PersonaCard shows: banner_url as card background image, avatar_url as circular overlay (bottom-left), name + handle + up to 4 tags as pills, image/video count badges. While status=pending|generating show a subtle pulsing overlay with current stage label (Crafting identity... / Generating anchor... etc). Click card → navigate to /personas/:id. CREATE PANEL: slides in from right, contains: description textarea (placeholder: Describe your persona...), gender pill selector (Female / Male / Non-binary), optional Name field, Create button. On submit POST /api/persona-api/personas — new card appears immediately in grid with generating state. DETAIL PAGE (/personas/:id): full-width banner header, large circular avatar centered, name + handle + all tags. Image gallery grid (masonry or even grid, click any image to open fullscreen lightbox with arrow nav). Videos section shows 4 video players (smile reveal, personality moment, lifestyle, invitation) with labels. REALTIME: useEventChannel from @project/realtime subscribed to channel:personas. On persona_updated event refresh that card data. On job_update event show stage progress on the card overlay.'"
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-ui:
depends_on: [implement-ui]
action: wait_build
build_id: "{{ .outputs.implement-ui.build_id }}"
max_attempts: 120
poll_interval: 5
wait-deploy-final:
description: Deploy final build
depends_on: [wait-ui]
action: wait_pipeline
project_id: "{{ .outputs.create-project.project_id }}"
max_attempts: 120
poll_interval: 10
# --- Verification ---
verify-health:
description: Verify persona-api is healthy
depends_on: [wait-deploy-final]
action: shell
command: |
HEALTH=$(curl -sf "https://{{ .outputs.create-project.domain }}/api/persona-api/health" | jq -r '.data.status // empty')
if [ "$HEALTH" = "healthy" ]; then
echo "persona-api healthy"
exit 0
else
echo "persona-api not healthy: $HEALTH"
exit 1
fi
verify-site:
description: Verify creator-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 OTP auth endpoint exists (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}" -X POST \
"https://$DOMAIN/api/persona-api/personas" \
-H "Content-Type: application/json" \
-d '{"description":"test","gender":"female"}')
echo "POST /personas 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 }}"