# GrowQR Demo Runbook Use this when restarting the full local demo after everything has been stopped. It should bring back the workflows dashboard, growqr-app frontend session launchers, backend, Rivet workflow actor runner, interview-service, roleplay-service, QScore service, resume-builder, matchmaking, and the per-user Gitea/OpenCode stack. ## What Must Be Running - Docker Desktop - `qscore-service`: Postgres, Redis, API, worker - `interview-service`: API, Postgres, MinIO, bucket setup - `roleplay-service`: API, Postgres, MinIO, bucket setup - `resume-builder`: Docker container with Postgres (port 5433), API (port 8002) - `matchmaking`: API on host port 8006 - `growqr-backend`: backend Postgres plus local Node backend - `workflows--dashboard`: Next.js frontend - `growqr-app/frontend`: Next.js frontend for the new interview/roleplay session UI - Per-user OpenCode containers, created by backend provisioning (Gitea is central/shared) The known demo ports are: - Workflows dashboard: `http://localhost:3000` - GrowQR app frontend session launchers: `http://localhost:3002` - Backend: `http://localhost:4000` - Backend Rivet proxy: `http://localhost:4000/api/rivet` - Interview service: `http://localhost:8007` - Roleplay service: `http://localhost:8008` - QScore service: `http://localhost:8000` - Resume builder: `http://localhost:8002` - Matchmaking service: `http://localhost:8006` - Resume builder DB: `localhost:5433` - QScore Redis: `localhost:6379` - Central Gitea: `http://localhost:3001` (shared org-wide, changes.md §2A) - Backend Postgres: `localhost:5432` - Per-user OpenCode containers: dynamically allocated from `20000-29999` ## Required Env Files Before starting, verify these files exist and contain the local demo values: - `growqr-backend/.env` - `growqr-backend/.env.local` - `workflows--dashboard/.env.local` - `growqr-app/frontend/.env.local` - `interview-service/.env` - `roleplay-service/.env` - `growqr-app/resume-builder/.env.local` (created by setup script) Do not print secrets into chat or commit them. The important non-secret shape is: - Backend service URLs should point to `127.0.0.1`: - `INTERVIEW_SERVICE_URL=http://127.0.0.1:8007` - `ROLEPLAY_SERVICE_URL=http://127.0.0.1:8008` - `QSCORE_SERVICE_URL=http://127.0.0.1:8000` - `RESUME_SERVICE_URL=http://127.0.0.1:8002` - `MATCHMAKING_SERVICE_URL=http://127.0.0.1:8006` - `GROWQR_APP_FRONTEND_URL=http://localhost:3002` - Backend per-user containers should use: - `OPENCODE_IMAGE=ghcr.io/anomalyco/opencode:latest` - `USER_CONTAINER_HOST=127.0.0.1` - `USER_DATA_ROOT=./.data/users` - Frontend should use: - `NEXT_PUBLIC_RIVET_ENDPOINT=http://127.0.0.1:4000/api/rivet` - `NEXT_PUBLIC_GROWQR_BACKEND_URL=http://127.0.0.1:4000` (NOT `GROWQR_BACKEND_URL` — must have NEXT_PUBLIC_ prefix for client-side) - `OPENCODE_API_KEY=` (must match backend) - `LLM_BASE_URL=https://opencode.ai/zen/v1` - `LLM_MODEL=kimi-k2.6` - Resume builder should use: - `DATABASE_URL=postgresql+asyncpg://postgres:postgres@host.docker.internal:5433/growqr_resume` (host.docker.internal when running in Docker, localhost when running locally) - `API_PORT=8002` - `OPENCODE_API_KEY=` — **CRITICAL: must be the EXACT same key as backend.** Both services share the same OpenCode account. - `OPENCODE_BASE_URL=https://opencode.ai/zen/v1` - `AI_MODEL=kimi-k2.6` - `CLERK_JWKS_URL=https://noted-elephant-23.accounts.dev/.well-known/jwks.json` - `CLERK_ISSUER=https://noted-elephant-23.accounts.dev` - `CORS_ORIGINS="http://localhost:3000,http://localhost:3001,http://127.0.0.1:3000"` - `A2A_ALLOWED_KEYS=dev-a2a-key` - `REDIS_URL=redis://host.docker.internal:6379/0` - `DEBUG=true` - `SERVICE_TOKEN` and `A2A_ALLOWED_KEY` must match across backend and services. - Clerk keys must match between frontend and backend. - Gemini and OpenCode keys must be present for the audio/session and LLM paths. ### OpenCode API Key Sync (IMPORTANT) The backend and resume-builder share the same OpenCode API key. When copying values, always use `growqr-backend/.env` as the **source of truth**. The resume-builder's `.env.local` was previously out of sync (25-char dummy key vs 67-char real key), causing silent LLM failures. To verify keys are synced: ```bash diff <(grep OPENCODE_API_KEY growqr-backend/.env | head -1) <(grep OPENCODE_API_KEY growqr-app/resume-builder/.env.local | head -1) # Should show no differences ``` Note: interview-service and roleplay-service use Gemini (not OpenCode) — they don't need an OpenCode key. ## Start Order Run all commands from `/Users/divyansh/Desktop/growQR` unless a command changes directory. ### 0. Start Central Gitea first (NEW per changes.md §2A) The central Gitea is now a shared compose service in the backend's docker-compose. **Note:** Gitea health check uses port 3000 internally (container), mapped to 3001 on host. Port mapping is `3001:3000`. ```bash docker compose -f growqr-backend/docker-compose.yml up -d gitea ``` Wait until Gitea is healthy (~30 seconds on first startup): ```bash curl http://127.0.0.1:3001/api/v1/version # Expected: {"version":"1.22.6"} ``` Per-user Gitea containers are NO LONGER spawned dynamically. All users share the central Gitea instance with one repo per user under the `growqr` org. ### 1. Start QScore first QScore owns the Redis instance exposed on `localhost:6379`. The interview service currently uses that host Redis for workflow task dispatch, so QScore should come up first. ```bash docker compose -f qscore-service/docker-compose.yml up -d --build ``` Wait until all QScore containers are healthy/running: ```bash docker compose -f qscore-service/docker-compose.yml ps curl http://127.0.0.1:8000/health # Expected: {"status":"ok"} ``` ### 2. Start Interview Service ```bash docker compose -f interview-service/docker-compose.yml up -d --build ``` Verify: ```bash curl http://127.0.0.1:8007/health # Expected: {"status":"ok","service":"interview-service","a2a":true,"agent":true} ``` ### 3. Start Roleplay Service ```bash docker compose -f roleplay-service/docker-compose.yml up -d --build ``` Verify: ```bash curl http://127.0.0.1:8008/health # Expected: {"status":"ok","service":"roleplay-service","a2a":true,"agent":true} ``` ### 4. Start Resume Builder (NEW) The resume builder runs as a Docker container built from `growqr-app/resume-builder/Dockerfile`. It uses its own Postgres database on port 5433. ```bash # Start resume-builder Postgres docker compose -f growqr-app/resume-builder/docker-compose.yml up -d postgres # Build resume-builder image (first time only) docker build -t growqr-resume-builder growqr-app/resume-builder # Run migrations cd growqr-app/resume-builder uv run --env-file .env.local alembic upgrade head # Start the service with synced OpenCode key from backend cd /Users/divyansh/Desktop/growQR OPENCODE_KEY=$(grep OPENCODE_API_KEY growqr-backend/.env | head -1 | cut -d= -f2) docker rm -f growqr-resume-builder 2>/dev/null docker run -d --name growqr-resume-builder \ -p 8002:8002 \ -e DATABASE_URL=postgresql+asyncpg://postgres:postgres@host.docker.internal:5433/growqr_resume \ -e OPENCODE_API_KEY="${OPENCODE_KEY}" \ -e OPENCODE_BASE_URL=https://opencode.ai/zen/v1 \ -e AI_MODEL=kimi-k2.6 \ -e CLERK_JWKS_URL=https://noted-elephant-23.accounts.dev/.well-known/jwks.json \ -e CLERK_ISSUER=https://noted-elephant-23.accounts.dev \ -e CORS_ORIGINS="http://localhost:3000,http://localhost:3001,http://127.0.0.1:3000" \ -e A2A_ALLOWED_KEYS=dev-a2a-key \ -e REDIS_URL=redis://host.docker.internal:6379/0 \ -e DEBUG=true \ growqr-resume-builder uvicorn app.main:app --host 0.0.0.0 --port 8002 # Verify curl http://127.0.0.1:8002/health # Expected: {"status":"healthy","service":"resume-builder","mcp":true,"agent":true,"a2a":true} ``` ### 5. Start Backend Postgres For the demo path that avoids Rivet `no_capacity`, run the backend locally with its embedded Rivet runner and start only the backend Postgres from compose: ```bash docker compose -f growqr-backend/docker-compose.yml up -d postgres ``` **Important:** If the backend was previously running with older schema, you may need to add missing columns: ```bash docker exec growqr-postgres psql -U growqr -d growqr -c " ALTER TABLE user_stacks ADD COLUMN IF NOT EXISTS gitea_repo_name TEXT; ALTER TABLE user_stacks ADD COLUMN IF NOT EXISTS gitea_repo_owner TEXT; ALTER TABLE user_stacks ADD COLUMN IF NOT EXISTS image_version TEXT; ALTER TABLE user_stacks ADD COLUMN IF NOT EXISTS migration_version TEXT; ALTER TABLE user_stacks ADD COLUMN IF NOT EXISTS prompt_version TEXT; " ``` Then migrate: ```bash cd /Users/divyansh/Desktop/growQR/growqr-backend npm run db:migrate ``` Start the backend: ```bash cd /Users/divyansh/Desktop/growQR/growqr-backend RIVET_RUN_ENGINE=1 npm run dev ``` Keep this terminal open. Verify in a separate terminal: ```bash curl http://127.0.0.1:4000/healthz # Expected: {"ok":true} ``` The backend will auto-provision OpenCode containers for existing users on boot (reconcileOnBoot). ### 6. Start Matchmaking The matchmaking service powers the Scout/job-search handoff. It runs on port 8000 internally and port 8006 on the host. ```bash docker compose -f matchmaking/docker-compose.yml up -d --build curl http://127.0.0.1:8006/api/v1/health # Expected: {"status":"healthy", ...} ``` ### 7. Start Frontends Start the dashboard on port 3000: ```bash cd /Users/divyansh/Desktop/growQR/workflows--dashboard npm run build npm run start ``` Start the growqr-app frontend on port 3002. This app now owns the product UI for interview and roleplay sessions launched from workflow chat. ```bash cd /Users/divyansh/Desktop/growQR/growqr-app/frontend npm install --legacy-peer-deps npm run dev -- -p 3002 ``` Keep both terminals open. Open: ```text http://localhost:3000/v2/workflows ``` ## Provision OpenCode Containers OpenCode containers are per-user and spawned dynamically by the backend Docker manager. Gitea is NO LONGER per-user — it's a central shared service (changes.md §2A). After provisioning, `docker ps` should show containers with names like: ```text growqr-opencode-user_ ``` If they are missing, sign into the dashboard and allow the user stack bootstrap to run. The local data is under: ```text growqr-backend/.data/users ``` That folder is intentionally gitignored and should stay local. ## Demo Path — Interview-to-Offer Accelerator Use this path for the Interview-to-Offer workflow demo: 1. Open `http://localhost:3000/v2/workflows`. 2. Type in the chat: `I have an interview at Google for SWE` 3. The AI responds with a conversational message and the **WorkflowDiscovery card** (red, 4 agents, 4 steps) appears below. 4. Click **"Start this workflow"** on the card. 5. The workflow progresses step-by-step: - **Step 1**: AI asks for job description → you respond with details - **Step 2**: AI asks for resume → you respond - **Step 3**: Resume Agent analyzes and tailors (shows "DONE") - **Step 4**: Sara interview session ready → shows **"Open" button** - **Step 5**: Emily roleplay session ready → shows **"Open" button** - **Step 6**: Quinn computes Q-Score → shows results 6. Click **Open** on Sara's card → new tab opens `http://localhost:3002/service-sessions/interview?session_id=...` 7. On the new GrowQR interview launcher, click **Start interview** and allow microphone/camera access. 8. Click **Open** on Emily's card → new tab opens `http://localhost:3002/service-sessions/roleplay?session_id=...` 9. On the new GrowQR roleplay launcher, click **Start roleplay** and allow microphone/camera access. 10. Completed steps are marked with **green checkmarks** in the chat. ### Alternative Workflow Trigger Queries | Workflow | Trigger Query | |---|---| | Interview-to-Offer | `I have an interview at Google for SWE` | | Career Switch | `I want to switch from marketing to product management` | | Resume Boost | `My resume isn't getting any responses` | | Job Search | `Find me backend engineering jobs` | | Job Preparation | `Prepare me for a role at Stripe` | Direct service demo pages are for low-level service debugging only. Product workflow links should use the new `growqr-app/frontend` launchers: ```text http://localhost:3002/service-sessions/interview?session_id= http://localhost:3002/service-sessions/roleplay?session_id= ``` ## Health Checks Use these after startup: ```bash curl http://127.0.0.1:4000/healthz # Backend curl http://127.0.0.1:8007/health # Interview curl http://127.0.0.1:8008/health # Roleplay curl http://127.0.0.1:8000/health # QScore curl http://127.0.0.1:8002/health # Resume builder curl http://127.0.0.1:8006/api/v1/health # Matchmaking curl -I 'http://localhost:3002/service-sessions/interview?session_id=demo-interview' curl -I 'http://localhost:3002/service-sessions/roleplay?session_id=demo-roleplay' curl http://127.0.0.1:3001/api/v1/version # Gitea docker ps # All containers ``` If the workflow actor fails with `no_capacity`, restart the backend with: ```bash cd /Users/divyansh/Desktop/growQR/growqr-backend RIVET_RUN_ENGINE=1 npm run dev ``` ## Shutdown Stop frontend and backend with `Ctrl+C` in their terminals. **Also kill any lingering backend Node processes:** ```bash pkill -f "tsx watch" 2>/dev/null pkill -f "tsx.*src/index" 2>/dev/null ``` Stop compose services: ```bash docker compose -f growqr-backend/docker-compose.yml down docker compose -f interview-service/docker-compose.yml down docker compose -f roleplay-service/docker-compose.yml down docker compose -f qscore-service/docker-compose.yml down docker compose -f matchmaking/docker-compose.yml down docker compose -f growqr-app/resume-builder/docker-compose.yml down docker rm -f growqr-resume-builder ``` Stop per-user OpenCode containers if still running: ```bash docker ps --filter "name=growqr-opencode-user" ``` Then stop/remove the shown containers by name: ```bash docker stop docker rm ``` They can be recreated later by the backend provisioning flow. The local persistent data remains in `growqr-backend/.data/users`. ## Build Rule Whenever code changes are made, run the relevant build before calling the demo ready: ```bash cd /Users/divyansh/Desktop/growQR/growqr-backend && npm run build cd /Users/divyansh/Desktop/growQR/workflows--dashboard && npm run build cd /Users/divyansh/Desktop/growQR/growqr-app/frontend && npm run build ``` For Dockerized Python services, rebuild with their compose commands: ```bash docker compose -f interview-service/docker-compose.yml up -d --build docker compose -f roleplay-service/docker-compose.yml up -d --build docker compose -f qscore-service/docker-compose.yml up -d --build ``` ## Agent Architecture & Flow The system has 5 real service-backed agents: | Agent | Name | Service | Port | Provider | Creates | |---|---|---|---|---|---| | Resume Agent | Mira | resume-builder | 8002 | OpenCode (kimi-k2.6) | Resume analysis + tailoring | | Interview Agent | Sara | interview-service + growqr-app frontend launcher | 8007 + 3002 | Gemini (audio) | Live interview sessions in new UI | | Roleplay Agent | Emily | roleplay-service + growqr-app frontend launcher | 8008 + 3002 | Gemini (audio) | Roleplay scenario sessions in new UI | | Q-Score Agent | Quinn | qscore-service | 8000 | Internal formula | Readiness scores | | Job Search Agent | Scout | matchmaking | 8006 | Internal ranking engine | Ranked opportunity feed | ### Chat Flow (Conversational Step-by-Step) 1. User types message → Frontend sends to `POST http://127.0.0.1:4000/api/chat` (with Clerk auth) 2. Backend tries Rivet actor first, falls back to direct LLM with tools 3. LLM responds conversationally OR calls a tool (`start_interview_session`, `analyze_resume`, etc.) 4. If tool is called, backend executes it against the real microservice 5. Response includes `{ reply, sessions[], workflow }` — sessions contain real URLs 6. Frontend renders agent cards with photos, status badges, and **Open** buttons ### Trigger Queries (what to type) | For this agent | Type this | |---|---| | Resume analysis | `analyze my resume for Google SWE` | | Interview session | `launch a behavioral interview for Software Engineer at Google` | | Roleplay session | `launch a roleplay for negotiating a Google offer` | | Q-Score | `compute my Q-Score` | | Full workflow | `I have an interview at Google for SWE` | ### Session URLs When a session is created, the backend preserves the `session_id` from the service configure response and returns one of these product URLs: - Interview: `http://localhost:3002/service-sessions/interview?session_id=&role=&type=` - Roleplay: `http://localhost:3002/service-sessions/roleplay?session_id=&goal=&type=` The old direct demo pages still exist for service debugging, but workflow chat should not link to them. ### Key Files - System prompt: `growqr-backend/prompts/system.txt` - Agent definitions: `growqr-backend/agents/*.md` - Service probes: `growqr-backend/src/services/service-agents.ts` - Chat route (with tool dispatch): `growqr-backend/src/routes/chat.ts` - Frontend chat component: `workflows--dashboard/src/components/home-v2/GrowChat.tsx` - New service launchers: `growqr-app/frontend/src/app/service-sessions/interview/page.tsx`, `growqr-app/frontend/src/app/service-sessions/roleplay/page.tsx` - Product flow requirements: `.growqr_memory.md` ### Known Quirks - **Roleplay configure is slow** (30-60s): The roleplay service calls an LLM to generate scenarios. Be patient. - **Rivet `/actors` returns 400**: Cosmetic console warning from the Rivet frontend SDK. Does not affect chat — chat uses HTTP fallback. - **Rivet proxy vs handler conflict**: When `RIVET_RUN_ENGINE=1`, the backend proxies `/api/rivet/*` to the engine at `localhost:6420` instead of using `registry.handler()`. This avoids the "Runtime already started as runner" error. - **Multiple backend processes**: `tsx watch` sometimes spawns orphans. Always `pkill -f "tsx watch"` before restarting.