python3 scripts/check_local_setup.py --mode <mode> (or uv run python scripts/check_local_setup.py --mode <mode> for the historical modes) and follow its first failing check before working through this page — the doctor is local, non-mutating, and never calls a remote service or reveals a secret value.
A stale process is already bound to :8000
A stale process is already bound to :8000
services/api/main.py (the basic/mvp1 research app) defaults to 127.0.0.1:8000. If a previous uv run uvicorn or scripts/run_local.py invocation didn’t shut down cleanly, the new one will either fail to bind or you’ll be talking to the old process without realizing it. Probe the port directly:GET /connectors is implemented in services/api/clients_api.py and needs no authentication, so it’s a cheap way to confirm something Causeloop-shaped is actually listening. If it doesn’t respond, find and stop the stale process before starting a new one; if it responds but the rest of the app behaves oddly, restart it anyway — an old --reload process can get into a broken state after enough file changes.This is a different port from the product stack’s API — that one is 18000 (see Local product stack), and its own liveness probe is GET /health or GET /health/live (both implemented in services/api/product_app.py), not the legacy GET /health on services/api/main.py this accordion is about.404 from the MVP1 latest report on a fresh clone
404 from the MVP1 latest report on a fresh clone
GET /mvp1/issue-intelligence/latest (services/api/main.py, backed by Mvp1IssueIntelligenceService.latest_strict_issue_intelligence) only returns a report once a complete strict run has actually been persisted — it looks for a succeeded run whose artifact matches the exact strict analysis contract and whose provider status is openai_llm_complete. A fresh clone has never run that analysis, so 404 here is expected, not a setup failure. Run the strict analysis in mvp1 mode (Research modes) if you need a real report.Postgres is not ready immediately after `docker compose up`
Postgres is not ready immediately after `docker compose up`
infra/docker-compose.yml postgres) and the product stack’s postgres service (infra/docker-compose.product.yml) start the container before Postgres has finished initializing. The product service has a pg_isready healthcheck (3-second interval, 30 retries) that gates the migrate service, so under product Compose this mostly self-heals — migrate simply waits. Under the relational mode there is no such gate in front of your own alembic upgrade head; if it fails immediately after up -d postgres, wait a few seconds and rerun:The relational workbook check fails: '@oai/artifact-tool' not found
The relational workbook check fails: '@oai/artifact-tool' not found
scripts/check_local_setup.py’s workbook-runtime check resolves CAUSELOOP_NODE_PATH (or whatever node is on PATH if unset) and looks for @oai/artifact-tool next to workers/artifacts/node_modules or next to that Node binary’s own node_modules. If neither location has the package, it fails with “Node runtime does not expose @oai/artifact-tool.” Set CAUSELOOP_NODE_PATH to the bundled Node executable discovered through the workspace-dependency tool — do not point it at an arbitrary Node install that doesn’t carry the package, and don’t install an unreviewed replacement package to work around it.The frontend build stalls or OOMs on a low-memory Docker Desktop
The frontend build stalls or OOMs on a low-memory Docker Desktop
Invitation email never arrives in Mailpit
Invitation email never arrives in Mailpit
product-worker decrypts and sends it over SMTP to Mailpit — the job type is send_email, one of the three job types (materialize_insights, send_email, train_tenant_model) that product-worker polls for. If a message doesn’t show up at http://127.0.0.1:18025:job complete id=... type=send_email line (or a job ... status=... failure) from the causeloop.job_worker logger. If you don’t see the job attempted at all, confirm product-worker is actually running and has passed its own readiness heartbeat — see the next entry. Mailpit itself is an ephemeral capture sink with no application state; it is not where you debug why a job was never queued in the first place, only whether it was sent.`/health/ready` stays `not_ready` on worker heartbeats
`/health/ready` stays `not_ready` on worker heartbeats
GET /health/ready (services/api/product_app.py) checks, among other things, that both product-worker and provisioner-worker have written a RuntimeHeartbeat row within the last CAUSELOOP_WORKER_READY_SECONDS seconds (default 45) — CAUSELOOP_REQUIRED_WORKERS (default product-worker,provisioner-worker) controls which component names are required to heartbeat at all. Each heartbeating worker must also advertise the job types the hardcoded REQUIRED_WORKER_JOB_TYPES mapping expects of it (product-worker: materialize_insights, send_email, train_tenant_model; provisioner-worker: provision_tenant). If the worker check stays false:Up and not crash-looping — a worker that exits right after migrate/minio-init complete (its depends_on conditions) never gets the chance to heartbeat. Both workers must heartbeat before readiness succeeds; a database that’s reachable but has zero recent heartbeats is a worker problem, not a database problem, even though the check is reported under checks.worker in the same JSON body as checks.database.`provisioner-worker` never sends email or writes an upload — by design
`provisioner-worker` never sends email or writes an upload — by design
infra/docker-compose.product.yml, CAUSELOOP_WORKER_JOB_TYPES): product-worker handles materialize_insights, send_email, and train_tenant_model; provisioner-worker handles only provision_tenant. In local Compose the provisioner’s environment simply omits Redis, email, and SOURCE_CREDS_KEY (credential-encryption) configuration — it isn’t wired to touch any of those. Its object-store scope is a design/cloud-IAM property, not something the local compose file enforces: locally it’s given the same MinIO root credentials as product-worker, so “read-only object access” describes the intended cloud permission boundary, not a restriction you can observe in this file. If you’re debugging a job that seems stuck and it’s an email or upload job, check product-worker’s logs, not the provisioner’s; a job routed to the wrong worker will simply never be picked up.Port collision with something else on your machine
Port collision with something else on your machine
13000, 18000, 55433, 16379, 19000/19001, or 11025/18025 is already taken locally, override it before bringing the stack up:`npm run e2e` fails immediately with connection errors
`npm run e2e` fails immediately with connection errors
playwright.config.ts starts (or reuses) the Next.js dev server on http://localhost:3000 for you, but it does not start the backend. If the FastAPI product app isn’t already listening on http://127.0.0.1:18000, every Playwright spec that hits a real page will fail on its first backend call. Start the product Compose stack first (docker compose -f infra/docker-compose.product.yml up -d --build), confirm python3 scripts/smoke_product.py passes, and only then run npm run e2e. If the authenticated specs (auth.spec.ts, console.spec.ts) are being skipped instead of failing, that’s expected — they require CAUSELOOP_E2E_WORKSPACE, CAUSELOOP_E2E_EMAIL, and CAUSELOOP_E2E_PASSWORD for a tenant seeded ahead of time; see Testing and verification.`docker compose down` looked like it deleted local product data
`docker compose down` looked like it deleted local product data
down preserves the named product_postgres, product_redis, and product_minio volumes (infra/docker-compose.product.yml) — your tenants, uploads, and users survive a restart. Data is only actually gone if -v was passed (as CI’s teardown step deliberately does) or the volumes were pruned some other way. Don’t add -v to your own local down unless you mean to reset the stack from scratch.General approach
Identify the mode
basic/mvp1/relational? Commands, ports, and failure modes don’t transfer between them — see Research modes.Run that mode's doctor
Probe the specific service
/health, /health/live, /health/ready, /connectors) or docker compose ... logs <service> to find which container is actually unhealthy, rather than guessing from symptoms in the UI.Never work around a secret or auth check by exposing it
.env.local or infra/causeloop.local.env to make a check pass.