Follow "Running it locally" in the README. You'll need Docker for Redis/Neo4j/Qdrant even if you run the API and worker natively.
Backend
cd backend
pip install -r requirements.txt
pytestAll 23 tests should pass without a live Redis/Neo4j/Qdrant/Groq connection — LLM calls, the scraper, and the stores are mocked at the boundary. If you add a service or agent, add tests that mock its external dependency the same way.
Frontend
cd frontend
npm install
npm run lint
npm run buildnpm run build runs the TypeScript compiler as part of the Next.js build — a type error fails the build, not just the lint step.
- Backend: standard library
logging, no bareexcept:, Pydantic models for anything crossing a process boundary (API request/response, WebSocket events, Celery task payloads). No stub functions — either implement it or don't add it. - Frontend: components stay client (
"use client") only when they need state, effects, or browser APIs. Server Components by default. NolocalStorage— Redis is the only cross-request state. - No em dashes in copy shown to users (README prose is fine, in-app copy isn't). Keep in-app strings short and specific instead of generic ("Fetched Reuters: Nvidia earnings" not "Loading...").
Imperative mood, one line, no ticket-number cargo culting: Add confidence threshold to critic prompt, not Fixed stuff.
Use the issue templates under .github/ISSUE_TEMPLATE/. For bugs, include the job ID from the Agent Terminal header if the bug happened mid-run — it's how the Redis-buffered event log gets tied back to what you saw.
- All cross-process state lives in Redis, not in-process memory. The FastAPI web service and the Celery worker are separate processes (separate Render services in production) and never call each other directly.
- The WebSocket relay is dumb on purpose. It doesn't know anything about LangGraph or the swarm — it subscribes to a Redis pub/sub channel keyed by job ID and forwards whatever JSON shows up. If you add a new event type, add it to
app/models/schemas.py, publish it from wherever it happens, and it'll reach the frontend without touchingwebsocket_manager.py. - Agents don't call each other. They're pure
async def run(state, emit) -> dictfunctions wired together inapp/agents/graph.py. If you're tempted to have the Scraper call the Critic directly, put that logic in the graph's routing instead.