An AI-native task management platform where a Large Language Model operates the application through a secure, modular Agent Harness. Instead of suggesting actions, the AI selects tools, validates parameters, performs CRUD operations, updates application state, and confirms completed work — autonomously.
Status: Vertical-slice MVP. The core architecture, the LangGraph agent service, the Supabase edge-function streaming proxy, task CRUD, chat UI, analytics, Kanban, Calendar, observability, and bulk operations are fully implemented. The TS harness in
packages/shared/aiis retained as the reference implementation and is kept at byte-level SSE parity with the Python agent via golden-file parity tests.
- Monorepo: pnpm workspaces + Turborepo (TS/JS) +
uvworkspace (Python) - Frontend: React 19, Vite, TypeScript, TailwindCSS, shadcn/ui, Radix UI, Zustand (5 stores), TanStack Query, React Hook Form, React Router, Framer Motion, Lucide, react-markdown
- Agent service: Python 3.11, FastAPI, LangGraph, LangChain, sse-starlette, Pydantic v2, pydantic-settings, PyJWT, httpx, supabase-py
- Edge function: Deno +
@supabase/supabase-js(thin JWT-verifying SSE proxy) - Database / Auth: Supabase (Postgres + RLS + JWT auth)
- AI: OpenRouter (GPT-4o-mini default; GPT-5 mini, Claude 3.7 Sonnet allowlisted)
via a provider-agnostic
LLMClientadapter with a deterministicMockLLMfor tests (no API key required) - Testing: Vitest (TS unit) + pytest (Python unit + parity) + Playwright (e2e)
Browser (React)
│
│ CRUD (direct, RLS-scoped via anon key + JWT)
├──────────────────────────────► Supabase (Postgres + RLS)
│
│ SSE (text/event-stream)
▼
Supabase Edge Function (agent-run) ── verifies JWT, forwards
│
│ SSE (text/event-stream) + x-tusk-jwt / x-tusk-user-id headers
▼
Python Agent Service (FastAPI + LangGraph) ── re-verifies JWT via JWKS (defense in depth)
│
├── LangGraph StateGraph (init → think → call_model → execute_tools → finalize → suggestions)
├── 24 tools (CRUD, lifecycle, bulk, analysis) bound via LangChain `bind_tools`
├── Memory / ContextBuilder / Validator / Executor / ErrorRecovery / Formatter
├── LLMClient ──► OpenRouter [or MockLLM in tests]
└── TaskService ──► Supabase (service-role key, bypasses RLS, scopes by userId)
The browser never sees the OpenRouter API key or the service-role key. CRUD goes directly to Supabase via the anon key + user JWT (RLS enforces per-user isolation). AI agent requests go to the edge function, which verifies the JWT and proxies the SSE stream to the Python agent; the agent re-verifies the JWT via Supabase's JWKS (defense in depth) and runs the LangGraph harness with the service-role key, streaming text deltas, thinking steps, tool-call cards, a final structured result, and follow-up suggestion chips back via SSE.
- Node.js >= 20 and pnpm 11.x (
npm i -g pnpmorcorepack enable) - Python >= 3.11 and uv (
pip install uvorcurl -LsSf https://astral.sh/uv/install.sh | sh) - A Supabase account (free tier is fine) — supabase.com
- An OpenRouter API key (optional for tests, required for real AI) — openrouter.ai/keys
You need a Supabase project for the database (Postgres + RLS), auth (JWT), and the edge function that proxies agent requests.
- Go to supabase.com → New Project.
- Pick a name (e.g.
Tusk), choose a region close to you, set a database password. Wait ~2 min for it to provision. - Once active, note the Project URL and the anon + service-role keys from Project Settings → API.
Run these three SQL files in order against your project. You can do this
via the Supabase Dashboard (SQL Editor → paste → Run), the Supabase CLI
(supabase db push after mirroring them into supabase/migrations/), or the
Supabase MCP server:
packages/db/migrations/0001_initial_schema.sql— tables, enums, indexespackages/db/migrations/0002_rls.sql— Row Level Security policiespackages/db/migrations/0003_user_id_defaults.sql—user_iddefaults fromauth.uid()
The frontend uses Supabase email/password auth. In the Supabase Dashboard:
- Authentication → Providers → Email: make sure Email is enabled.
- Authentication → URL Configuration: set the Site URL to
http://localhost:5173for local dev (add your production URL later). - (Optional) Disable "Confirm email" under Authentication → Providers → Email for faster local dev sign-up, or use the user that Supabase creates automatically.
The agent-run edge function is only needed if you want to test the
production request path (Browser → Edge Function → Python agent). For local
dev you can skip this and use the direct-to-agent bypass (see below).
To deploy it:
supabase link --project-ref <your-project-ref>
supabase secrets set AGENT_SERVICE_URL=http://localhost:8000 # or your deployed agent URL
supabase functions deploy agent-run --no-verify-jwtThe repo uses three env files (all gitignored — never commit secrets):
cp .env.example .envFill in:
| Var | Value |
|---|---|
SUPABASE_URL |
https://<ref>.supabase.co |
SUPABASE_SERVICE_ROLE_KEY |
from Supabase → Settings → API |
SUPABASE_ANON_KEY |
from Supabase → Settings → API |
OPENROUTER_API_KEY |
your OpenRouter key (optional for tests) |
OPENROUTER_MODEL |
openai/gpt-4o-mini (default) |
cp apps/agent/.env.example apps/agent/.envFill in:
| Var | Value |
|---|---|
TUSK_SUPABASE_URL |
https://<ref>.supabase.co |
TUSK_SUPABASE_SERVICE_ROLE_KEY |
service-role key (server-side only) |
TUSK_SUPABASE_JWT_ISSUER |
https://<ref>.supabase.co/auth/v1 |
TUSK_OPENROUTER_API_KEY |
your OpenRouter key (optional for tests) |
TUSK_OPENROUTER_MODEL |
openai/gpt-4o-mini |
TUSK_AGENT_TRUST_EDGE |
0 for local dev, 1 for production |
TUSK_MAX_TURNS |
10 |
Create this file (Vite only reads VITE_-prefixed vars):
VITE_SUPABASE_URL=https://<ref>.supabase.co
VITE_SUPABASE_ANON_KEY=<your-anon-key>
VITE_AGENT_URL=http://localhost:8000
VITE_AGENT_URLis the local-dev bypass: when set, the web app calls the Python agent directly athttp://localhost:8000, skipping the edge function. This is the easiest way to develop locally. Unset it in production so requests route through the Supabase edge function.
You need two terminals — one for the web app, one for the Python agent.
# Terminal 1 — install deps (one-time)
pnpm install
cd apps/agent && uv sync --extra dev && cd ../..
# Terminal 1 — web app (Vite dev server on :5173)
pnpm dev
# Terminal 2 — Python agent (uvicorn on :8000)
make -C apps/agent agent-dev
# or: cd apps/agent && uv run uvicorn agent.main:app --reload --port 8000Open http://localhost:5173. You'll see the login page — sign up with email/password (Supabase auth), then you're in.
With VITE_AGENT_URL=http://localhost:8000 set:
Browser → http://localhost:8000/agent/run (direct to Python agent)
Without VITE_AGENT_URL (or in production):
Browser → Supabase Edge Function (agent-run) → Python agent
Both the TS Vitest suite and the Python pytest suite use mock LLM clients
(MockLLMClient / MockLLM) and in-memory task services, so they run with
zero external dependencies:
pnpm test # TS unit tests
pnpm agent:test # Python unit + parity testsAt runtime the agent needs a real TUSK_OPENROUTER_API_KEY — without it,
_build_llm falls back to MockLLM, which only matches a few hardcoded
intents and is not useful for real work.
| Script | Description |
|---|---|
pnpm dev |
Start the web (Vite) dev server |
pnpm build |
Build all TS workspaces (Turborepo, cached) |
pnpm build:edge |
Bundle @tusk/shared into the edge function bundle |
pnpm typecheck |
TypeScript typecheck (zero implicit any) |
pnpm lint |
Lint all TS workspaces (ESLint) |
pnpm test |
Run Vitest unit tests |
pnpm test:e2e |
Run Playwright e2e tests (requires dev server) |
pnpm format |
Format the TS codebase with Prettier |
pnpm agent:test |
Run the Python agent pytest suite |
pnpm agent:lint |
Ruff check + format check on the Python agent |
pnpm agent:typecheck |
mypy --strict on the Python agent |
make -C apps/agent agent-dev |
Start the Python agent (uvicorn :8000) |
tusk/
├── apps/
│ ├── agent/ # Python LangGraph agent service (FastAPI + SSE)
│ │ ├── agent/ # graph, tools, schemas, auth, sse_emitter, ...
│ │ ├── tests/ # pytest unit + parity/golden tests
│ │ ├── Dockerfile # multi-stage production image (port 8000)
│ │ └── pyproject.toml # uv-managed deps
│ └── web/ # React 19 frontend (ChatGPT-style shell + task views)
│ └── src/
│ ├── features/ # tasks, chat, analytics, activity, observability, settings, auth
│ ├── store/ # 5 Zustand stores
│ ├── hooks/ # use-tasks, use-agent, use-observability, use-keyboard
│ └── lib/ # agent-client (SSE), supabase-client, env, utils
├── packages/
│ ├── shared/ # Framework-agnostic core (TS)
│ │ └── src/
│ │ ├── schemas/ # Zod domain schemas
│ │ ├── ai/ # The reference TS Agent Harness (kept at parity)
│ │ ├── services/ # SupabaseTaskService (production)
│ │ ├── testing/ # InMemoryTaskService (tests only)
│ │ └── constants.ts # priorities, models, token budgets, retry policy
│ └── db/ # Supabase SQL migrations (0001 schema, 0002 RLS, 0003 defaults)
├── supabase/
│ └── functions/agent-run/ # Deno edge function (JWT verify + SSE proxy)
└── docs/ # design specs + implementation plans
The codebase contains two implementations of the same agent loop:
packages/shared/ai(TypeScript) — the original reference harness (Dispatcher,ToolRegistry,StreamingManager, …). It is framework-agnostic (no React/DOM/Node imports) and is exercised by the Vitest suite. It is not wired into the production request path; it is kept as the source of truth for the SSE wire contract.apps/agent(Python / LangGraph) — the production harness. AStateGraphmirrors the TSDispatcher.dispatchloop 1:1 (init → think → call_model → execute_tools → finalize → suggestions).
The two are kept in lockstep by parity tests
(apps/agent/tests/parity/test_parity.py): golden SSE event sequences captured
from the TS harness are replayed through the Python run_graph and the event
type sequence, terminal turns, and (normalised) assistantText are
asserted to match. This is the core regression gate for any change to either
harness.
See ARCHITECTURE.md for the full graph topology, module responsibilities,
the SSE event contract, the tool catalogue, and extension points. See
AGENTS.md for build/test/lint commands.