Codebase tour
Where the code for the API, web app, CLI, runner and API client lives, how each is layered, and the conventions to follow when you change it.
This page walks through each app in the order a request travels: the API that records everything, the generated client that types it, the web app and the CLI that call it, and the runner that executes runs. Read it before your first change in an area you have not touched.
API (apps/api)
FastAPI with synchronous SQLAlchemy 2 sessions on PostgreSQL. Endpoints are plain def functions, so FastAPI runs them in its threadpool.
Entry and wiring
| File | Role |
|---|---|
main.py | Entry point for uvicorn main:app. Imports the app from api/main_api.py. |
api/main_api.py | Builds the FastAPI app: lifespan (runs init_db()), structlog request logging, CORS, optional TrustedHostMiddleware, slowapi, error handlers, /health, and router mounting. |
company/routers/__init__.py | Lists the company routers in mount order and sets operation_id to the Python function name, so generated clients get readable names like create_task. |
api/api_key_endpoints.py | /api/me/api-keys. Uses the same operation-id function. |
api/auth_endpoints.py | Register, login, refresh, logout, password reset, /api/me, /api/me/preferences, and the get_current_user dependency every route uses. |
api/preferences.py | The strict schema for user preferences stored in users.settings, and the merge rules for PATCH /api/me/preferences. |
The other routers mounted by main_api.py (agent_endpoints, session_endpoints, project_endpoints, user_endpoints, dashboard_endpoints, websocket_endpoints) are the legacy pre-company API. They stay mounted but are unsupported and left out of the API reference; do not extend them.
Layers of the company domain
| Layer | File | Rules |
|---|---|---|
| Routers | company/routers/{companies,agents,tasks,runs,insights}.py | One module per area. Take ctx: CompanyContext = Depends(get_company_context) (or require_admin). Load rows with get_owned(ctx, Model, id, "Label") so cross-company ids are 404. Raise errors with not_found, bad_request or api_error. Write a docstring for every endpoint: it becomes the API reference. |
| Schemas | company/schemas.py | Request models (...Create, ...Update) and responses (...Out). Status values are Literal types. Updates apply only fields that were sent (model_dump(exclude_unset=True)). |
| Services | company/services.py | Rules shared by routers: queue_run, next_task_number, enforce_agent_budget, finish_run, expire_stale_runs, run_actor, spend queries. |
| Audit | company/audit.py | Every mutation calls audit.record(...) before commit() and audit.publish(...) after it. audit.diff(row, values) applies changes and returns {field: [old, new]} for the record. |
| Events | company/events.py | publish(company_id, type, **data) sends {"type": "<entity>.<verb>", "id": ..., ...}. Values are converted to strings. |
| Dependencies | company/deps.py | get_company_context, require_admin, get_owned, now() (aware UTC), aware() (treats SQLite's naive datetimes as UTC). |
| Models | database/models.py, database/company_models.py | models.py imports company_models at the bottom so every table is registered. The allowed status tuples live in company_models.py. |
The shape of a typical mutation:
@router.patch("/tasks/{task_ref}", response_model=TaskOut)
def update_task(task_ref: str, body: TaskUpdate, ctx: CompanyContext = Depends(get_company_context)):
"""Update a task; only fields present in the body are changed. ..."""
task = _find_task(ctx, task_ref) # 404 outside this company
actor = services.run_actor(ctx, body.run_id) # agent attribution, if any
values = body.model_dump(exclude_unset=True, exclude={"run_id"})
changes = audit.diff(task, values) # apply and collect {field: [old, new]}
if changes:
label = f"{services.task_key(ctx, task)} {task.title}"
verb = "moved" if set(changes) <= {"status", "sort_order"} else "updated"
audit.record(ctx, verb, "task", task.id, label, changes, **_actor_kwargs(actor))
ctx.db.commit()
audit.publish(ctx, "task", "updated", task.id) # after commit, never before
return _task_out(ctx, task)Conventions
- Errors have one shape:
{"error": {"code": "NOT_FOUND", "message": "...", "details": {}}}. Use an upper-casecodethat clients can branch on (RUN_LEASE_LOST,ALREADY_DECIDED,ORG_CYCLE). - Money is integer cents. A budget of
0means no limit. - Times are timezone-aware UTC. Compare with
aware(...)when a value may come from SQLite in tests. - Non-members get 404. Never return
403for a company the caller cannot see. - The API records; runners execute. Do not add code that runs agents or calls a model from the API. See Architecture.
- Migrations for every model change, additive by default. See Database.
- Regenerate the client after any API change:
bun run --cwd libs/api-client generate.
API client (libs/api-client)
@sat/api-client is the single typed boundary between the API and every TypeScript consumer.
| File | Role |
|---|---|
openapi.json | Exported from the FastAPI app by apps/api/scripts/export_openapi.py. Committed. |
src/schema.d.ts | Generated by openapi-typescript. Committed. Never edit by hand. |
src/client.ts | createApiClient({ baseUrl, tokens, onUnauthorized }): an openapi-fetch client plus call() (unwraps responses, throws ApiError) and authedFetch(). On a 401 it refreshes once, sharing one refresh across concurrent requests, then retries. |
src/events.ts | subscribeToCompanyEvents: reads the SSE stream with fetch (so it can send the Bearer header) and reconnects with backoff from 2 seconds, doubling up to 30 seconds. |
src/models.ts | Friendly type aliases (Task, Agent, Run, CompanyEvent, ...). |
src/vocabulary.ts | The one status vocabulary shared by the web app and the CLI: task statuses and labels, board columns, priorities, agent and run status labels, roles, adapters. |
Regenerate with bun run --cwd libs/api-client generate. CI type-checks the web app, CLI and docs against the committed types, so a stale client fails the build.
Web app (apps/web)
React 19 function components, React Router 8, SWR for server state, zustand for UI state, Tailwind 4 with tokens, components from @yarlisai/ui.
| Path | Role |
|---|---|
src/main.tsx, src/router.tsx | Entry and routes. Company pages live under /c/:companyId/... and are code-split per route. requireSignIn redirects to /login?next=.... |
src/pages/ | One file per screen: dashboard, inbox, tasks, task-detail, projects, goals, agents, agent-detail, org-chart, approvals, costs, activity, routines, run-detail, settings, sign-in and password pages, home (public homepage). |
src/components/ | Shared pieces: shell/ (layout, sidebar, top bar, command palette, company switcher, live indicator), kanban, task-list, run-table, approval-card, dialogs, status. |
src/lib/api.ts | The app's api client: base URL from VITE_API_URL, tokens in localStorage, redirect to /login when a session cannot be refreshed. |
src/lib/hooks.ts | One SWR hook per resource (useTasks, useAgents, useRuns, useDashboard, ...). Keys are [resource, companyId, ...args]. |
src/lib/mutations.ts | Writes that need shared handling (for example updateTask): call the API, refresh affected caches, toast on failure. |
src/lib/live.ts | useLiveEvents(companyId) subscribes to the company's SSE stream (origin VITE_EVENTS_ORIGIN) and maps each event's entity to the SWR keys it makes stale (AFFECTS). useLiveStatus drives the live indicator. |
src/lib/status.ts | Re-exports the vocabulary from @sat/api-client and maps every status to its colour token (statusColor). |
src/lib/preferences.ts, look.ts, format.ts | Account preferences, theme and density, locale-aware formatting. |
src/stores/ui.ts | zustand store (persisted) for UI-only state: theme, sidebar, panels, command palette, new-task dialog. |
src/index.css | The single token source: semantic colours, status colours, brand values, fonts. |
Conventions
- Server data only through the SWR hooks in
lib/hooks.ts. Do not fetch ad hoc in components. A new resource gets a hook with a[resource, companyId, ...]key, and an entry inAFFECTSinlib/live.tsso live events refresh it. - UI state in zustand, not in SWR or component trees that need it globally.
- Visual values only from tokens in
src/index.css(bg-background,text-muted-foreground,var(--status-...)). No raw colours in components. - One status vocabulary. Labels and order come from
@sat/api-client(TASK_STATUSES,PRIORITIES, ...); colours fromstatusColor. - Every screen answers three questions: what is happening, does it need me, what do I do about it.
CLI (apps/cli)
Bun and commander. sat covers the control plane, the runner and the scheduler.
| Path | Role |
|---|---|
src/main.ts | buildProgram(): global options and one register... call per command module. Also used by scripts/docs.ts to generate the CLI reference. |
src/api.ts | The session: HTTP helpers over @sat/api-client, current company resolution, and name, prefix or id resolution for companies, agents and tasks. |
src/config.ts | Profiles (local, staging, prod, plus your own) and environment overrides. |
src/credentials.ts | Credential storage: macOS Keychain, Linux Secret Service (secret-tool), or a 0600 file. SAT_API_KEY and SAT_TOKEN are never stored. |
src/commands/ | One module per noun: auth (login, whoami, apikey), company (companies, profiles), insights (dashboard, inbox, costs, activity), agents, tasks, planning (goals, projects, routines), runs (runs, approvals), runner (runner, scheduler), doctor, completion. |
src/output.ts, src/errors.ts | Table and JSON output; stable exit codes (0 ok, 2 usage, 3 auth, 4 not found, 5 conflict, 6 unavailable). |
src/events.ts | SSE subscription for the CLI. |
Conventions: every command supports --json; errors map to stable exit codes; anything an agent might call through sat adds run_id from SAT_RUN_ID so writes are attributed to the agent. Regenerate the CLI reference after changing commands: bun run --cwd apps/cli docs (CI runs docs:check).
Runner (apps/cli/src/runner)
| File | Role |
|---|---|
runner.ts | The engine: subscribes to events, claims runs up to --concurrency, prepares a workspace, runs the adapter, streams transcript (every 2 seconds or 4 KB), heartbeats (every 30 seconds), handles cancellation and lease loss, posts the final comment, moves the task and reports the result. |
adapters.ts | ClaudeCodeAdapter (claude -p --output-format stream-json), CodexAdapter (codex exec --json), EchoAdapter (simulation), UnsupportedAdapter. Each turns the CLI's event stream into transcript, usage and result events. |
prompt.ts | The system prompt (agent identity, manager, mission, instructions) and task prompt (task, goal chain, project, last 20 comments). |
workspace.ts | One git worktree per run for projects with a repository; a persistent folder per agent otherwise. |
process.ts | Spawns the agent CLI, reads its stdout line by line, keeps the last 4,000 characters of stderr. |
scheduler.ts | Client-side cron for routines and heartbeats, plus stale-run expiry. |
types.ts | The adapter interface. |
Tests in apps/cli/test run the runner and scheduler against in-memory fakes. See How runs work and Custom runner for the protocol.
Docs (apps/docs)
Next.js with Fumadocs. MDX pages in content/docs, the generated API reference from libs/api-client/openapi.json (lib/openapi.ts, lib/source.ts), and a link checker in scripts/check-links.ts. See Writing docs.