Local development
This page sets up a checkout for working on Loom's code. To evaluate Loom without changing it, the quickstart (loom up) is shorter.
Prerequisites
- Python 3.12 for the server (the SDK supports 3.11 and later).
- A C compiler and Python headers. The
devextra pulls inlemmagen3(the lexical leg's lemmatizer), which has no wheel and compiles againstPython.h:sudo apt-get install -y build-essential python3-devon Debian/Ubuntu,xcode-select --installon macOS. A plainpip install -e .needs neither. - Docker with Compose 2.34 or later (the dev compose file pulls ClickHouse daily), for ClickHouse and the stacks.
- Node 22 (the version CI uses) for the dashboard and the TypeScript SDK.
- Bash 5 or later on
PATHfor the shell scripts and their unit tests. On macOS,brew install bashandexport PATH="$(brew --prefix)/bin:$PATH"; checkbash --versionbefore running the tests. The system Bash 3.2 lacks associative arrays andmapfile.
Python environment
The make targets run .venv/bin/python by default (PYTHON ?= .venv/bin/python in the
Makefile); CI overrides it with PYTHON=python.
git clone https://github.com/ClickHouse/loom.git && cd loom
# with uv: the locked resolution CI installs
uv sync --locked --python 3.12 --extra dev --extra code
# or with pip
python3.12 -m venv .venv && . .venv/bin/activate
pip install -e ".[dev,code]"
make install runs pip install -e ".[dev]" into the active interpreter.
| Extra | Adds |
|---|---|
dev |
test, lint and type tools, the lemmatizer |
code |
tree-sitter grammars and numpy for the code index and its tests (CI installs .[dev,code]) |
local-embedder |
the CPU embedder |
ingest |
the S3 client dev/loom_ingest.py fetches an s3:// source with |
A local ClickHouse
Any ClickHouse 26.4 or later works (ClickHouse requirements); loom ch-setup prepares one Loom did not start. For a schema dump that matches CI, use the bundled config (the loom_reader user, the NLP flags, the Lemmagen3 dictionary and an embedded Keeper) through one of:
# 1. the CI container: same image and config CI uses, on 8123 / 9000
bash dev/ci_start_clickhouse.sh
# 2. only the ClickHouse service of the dev stack
make bootstrap # creates .env and named_collections.xml from their templates
docker compose --env-file .env -f compose.yaml -f dev/docker-compose.dev.yml --profile bundled up -d clickhouse
Both listen on localhost:8123 with user loom, password loomdev and database loom, matching LOOM_CH_* in .env.example. Both pin the same image, so a schema dump against either matches CI (migrations).
Then apply the schema:
make migrate # python -m server.core.schema.migrate
make migrate-status # applied and pending
make migrate-dry # what would apply
Env files
Settings are read once at startup from the environment and from the dotenv file named by LOOM_ENV_FILE (default .env in the working directory; empty reads none). make bootstrap creates .env from .env.example: single-user mode with fake providers, so no auth, no network, no key. The configuration reference lists every setting; deployment covers the essentials.
Compose reads the .env in the first -f file's directory. With compose.yaml first that is the root one; the cloud stack, whose first file is under dev/, needs --env-file .env. The make targets pass the flag everywhere.
Running the server from source
make server # uvicorn server.main:app on :7777 with --reload
make server sets LOOM_INSTANCE_LABEL=dev:make-server:7777 unless you set one. A process outside the compose deployment refuses to boot unlabelled; the label keeps its ledger rows and spans apart from a deployment's on a shared ClickHouse. The server applies pending migrations at boot (LOOM_BOOT_MIGRATE=true) before it answers.
loom serve [--host] [--port] [--reload] does the same through the CLI.
Running from a worktree or a second checkout. uvicorn imports server.main from its --app-dir, which defaults to the current directory and wins over PYTHONPATH. A server started in one checkout with another's path on PYTHONPATH serves the first checkout's code. Start it from the checkout you mean, or pass --app-dir, with its own port and label:
LOOM_INSTANCE_LABEL=dev:$USER:7790 .venv/bin/uvicorn server.main:app \
--app-dir "$PWD" --host 127.0.0.1 --port 7790 --loop asyncio --reload --reload-dir "$PWD/server"
Check build_commit and the boot log before trusting a measurement against it.
The dashboard
The server serves frontend/dist/ at / and answers 503 on dashboard pages until it is built. /v1/admin/* works either way.
make frontend-build # npm install && npm run build (tsc -b + vite build) into frontend/dist/
make frontend-dev # Vite on :5173, proxying /v1 and /mcp to the server on :7777
npm --prefix frontend run test # vitest (unit, component, axe)
npm --prefix frontend run e2e # Playwright specs in frontend/e2e/
Open http://localhost:7777/ and paste a token (loom mint-token … --save writes ~/.loom/token), or use it without one in single-user mode.
Fake providers
The stock .env pins LOOM_EMBEDDING_MODEL=fake-ngram-3072 and LOOM_LLM_PROVIDER=fake. The fake embedder is deterministic character n-grams at Loom's standard 3072-vector width; the fake LLM extracts nothing. The stack runs offline and tests are reproducible, but retrieval quality on it measures nothing. Benchmarks and quality gates need real providers (running benchmarks). With fake providers, the adaptive similarity thresholds fall back to their scalar settings.
For real providers, set the provider block in .env (deployment). The embedder's width is fixed when the vector tables are created, so recreate the local database when you change it (make down-all, or a fresh dev/ci_start_clickhouse.sh container).
Make targets
Every target below exists in the Makefile.
| Target | Does |
|---|---|
make bootstrap |
creates .env and deploy/clickhouse-config/named_collections.xml if missing; gives an existing .env without an extractor token one |
make dev / make dev-cloud |
the local stack with bundled ClickHouse / Loom against the ClickHouse in .env (both --build) |
make down / make down-all |
stop both stacks / also delete their volumes |
make server |
the server from source with --reload |
make migrate, migrate-status, migrate-dry |
apply / list / preview migrations |
make schema-dump, schema-check |
regenerate / verify schemas/schema.sql |
make frontend-build, frontend-dev |
build the dashboard / run Vite |
make format, format-check |
ruff, Biome, Prettier, Taplo, shfmt (pinned versions) |
make lint |
ruff, the function-length and secret gates, every generator's --check, Biome's dashboard accessibility rules |
make typecheck |
the mypy ratchet (mypy-gate) and mypy --strict on the Python SDK |
make test-unit |
pytest tests/unit and pytest bench |
make test / test-integration |
pytest tests/integration (needs a server and ClickHouse) |
make test-unit-cov |
the unit suite with branch coverage, the 40% floor and the per-module floors CI enforces |
make ingest FILE=… [TAGS=…] |
load a file into the knowledge plane with dev/loom_ingest.py |
make init / make onboard NAMESPACE=… AGENT=… |
provision a service account and .env for an agent / plus raised limits and a smoke test |
make bench OP=… C=… D=… |
concurrent load against one op (scripts/ops/benchmark.py) |
make bench-quick, bench-* |
quality and regression gates; need real providers (benchmarks) |
make clean |
down-all and remove Python caches |
Before a pull request, run make format, make lint, make typecheck and make test-unit (testing).