Files
docs/runbook.md
-Puter e6685203fe feat: initial docs repo with project inventory and all documentation
- Added REPO_INVENTORY.md with all repos, branches, remotes, and staging info
- Added .gitignore
- Synced all existing docs from local workspace
- Centralized documentation hub for GrowQR team
2026-06-22 15:04:27 +05:30

18 KiB

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=<from backend .env> (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=<from backend .env>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:

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.

docker compose -f growqr-backend/docker-compose.yml up -d gitea

Wait until Gitea is healthy (~30 seconds on first startup):

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.

docker compose -f qscore-service/docker-compose.yml up -d --build

Wait until all QScore containers are healthy/running:

docker compose -f qscore-service/docker-compose.yml ps
curl http://127.0.0.1:8000/health
# Expected: {"status":"ok"}

2. Start Interview Service

docker compose -f interview-service/docker-compose.yml up -d --build

Verify:

curl http://127.0.0.1:8007/health
# Expected: {"status":"ok","service":"interview-service","a2a":true,"agent":true}

3. Start Roleplay Service

docker compose -f roleplay-service/docker-compose.yml up -d --build

Verify:

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.

# 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:

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:

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:

cd /Users/divyansh/Desktop/growQR/growqr-backend
npm run db:migrate

Start the backend:

cd /Users/divyansh/Desktop/growQR/growqr-backend
RIVET_RUN_ENGINE=1 npm run dev

Keep this terminal open. Verify in a separate terminal:

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.

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:

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.

cd /Users/divyansh/Desktop/growQR/growqr-app/frontend
npm install --legacy-peer-deps
npm run dev -- -p 3002

Keep both terminals open. Open:

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:

growqr-opencode-user_<userId>

If they are missing, sign into the dashboard and allow the user stack bootstrap to run. The local data is under:

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=<uuid>...
  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=<uuid>...
  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:

http://localhost:3002/service-sessions/interview?session_id=<uuid>
http://localhost:3002/service-sessions/roleplay?session_id=<uuid>

Health Checks

Use these after startup:

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:

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:

pkill -f "tsx watch" 2>/dev/null
pkill -f "tsx.*src/index" 2>/dev/null

Stop compose services:

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:

docker ps --filter "name=growqr-opencode-user"

Then stop/remove the shown containers by name:

docker stop <growqr-opencode-container>
docker rm <growqr-opencode-container>

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:

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:

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=<uuid>&role=<goal>&type=<type>
  • Roleplay: http://localhost:3002/service-sessions/roleplay?session_id=<uuid>&goal=<goal>&type=<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.