Installation¶
Loom is a single Python package that runs as a foreground host process. It is not published to PyPI yet; install it from the repo.
Requirements¶
| Component | Requirement | Notes |
|---|---|---|
| Python | 3.12 (>=3.12,<3.13) |
Pinned to one minor in pyproject.toml |
| uv | any recent | The project's only supported installer and runner |
| Lithos | reachable over HTTP | MCP over the /sse endpoint plus GET /events; must have the task-graph extension (lithos-loom doctor probes it) |
git |
with worktree support | Every plugin run and on-demand command works in a throwaway worktree |
docker |
host daemon | story-develop, develop review / converge / merge-gate run agents and checks in the sandbox image (default ralph-sandbox:latest) |
claude and/or codex CLI |
authenticated | Agent engines for plugin bodies only; the daemon itself needs neither |
gh CLI |
authenticated | PR delivery and the GitHub watcher (gh auth token is read at startup) |
| Obsidian Desktop + Templater + Tasks | vault host only | Templater runs the capture macros; Tasks renders the projected lines |
Why there is no Docker image
Lithos and Influx run as containers because they are services. Loom creates git worktrees you cd into, calls claude / codex / gh with your per-user credentials, spawns docker containers of its own, and must be on Obsidian Desktop's PATH. Containerising it would mean bind-mounting ~/.claude/, ~/.codex/, ~/.config/gh/, every project checkout, the vault and the docker socket — so it runs on the host.
Install¶
This is the install the Templater macros want — Obsidian Desktop inherits PATH from the desktop launcher session, not your shell rc, and ~/.local/bin is usually on it (Linux). See Obsidian bridge → CLI on Obsidian's PATH.
Configure¶
Loom reads one TOML file per process. Discovery order:
LITHOS_LOOM_CONFIG=/abs/path.toml— explicit, beats everything.- If
LITHOS_LOOM_ENVIRONMENT=<env>is set:config.<env>.tomlin./then$XDG_CONFIG_HOME/lithos-loom/(default~/.config/lithos-loom/). - Otherwise
config.tomlin the same two locations.
--config / -c <path> on any command overrides all three. python-dotenv loads a .env from the current directory at startup; LITHOS_URL set in the environment overrides [orchestrator].lithos_url.
mkdir -p ~/.config/lithos-loom
cp examples/lithos-loom.toml ~/.config/lithos-loom/config.toml
$EDITOR ~/.config/lithos-loom/config.toml
A minimal route-runner host — one project, the shipped story-develop route, no vault:
[orchestrator]
agent_id = "lithos-orchestrator-<host>" # claim attribution; unique per host
lithos_url = "http://localhost:8765"
work_dir = "/tmp/lithos-loom" # per-task staging tree + pidfile
[projects.my-project]
repo = "/abs/path/to/my-project" # the only required project field
[[routes]]
name = "story-develop"
command = "uv run python -m lithos_loom.plugins.story_develop --task-json {{task_json}} --work-dir {{work_dir}} --result-file {{result_file}} --repo {{repo}}"
max_runtime_seconds = 28800
completes_task = false # success = PR open, not task done
[routes.match]
tags = ["trigger:story-develop"]
[story_develop.default_models] # required for any tool an agent runs on
claude = "opus"
codex = "gpt-5.4"
The annotated example at examples/lithos-loom.toml covers every block ([obsidian_sync], [github_watcher], [[subscriptions]], [notifications]); the full reference is docs/SPECIFICATION.md §3.1. The config dataclasses in src/lithos_loom/config.py are the complete field list.
Per-environment configs
Keep a config.dev.toml pointing at a local Lithos and a config.prod.toml at the real one, then pick with LITHOS_LOOM_ENVIRONMENT=dev uv run lithos-loom run. Hot reload is not implemented — restart (or drain) after editing the TOML.
Validate¶
uv run lithos-loom validate-config # parse + typecheck; lists projects / routes / subscriptions
uv run lithos-loom validate-config --dry-run # also polls Lithos: which routes would fire per open task
uv run lithos-loom doctor
doctor prints PASS / FAIL per check and exits non-zero on any failure:
| Check | What it probes |
|---|---|
Vault (vault_path_exists, _lithos/ creatable, write+read round-trip) |
Skipped with a note when [obsidian_sync] is absent |
task_graph_extension |
Creates throwaway probe tasks and asserts blocks edges, cancelled-blocker semantics, task_type / lithos_task_spawn and pr gate readiness behave. The same probe gates run. |
| Projects | Every TOML [projects.<slug>] has a project-context doc at projects/<slug>/ in Lithos |
Run the daemon¶
run claims <work_dir>/supervisor.pid under an exclusive lock (one daemon per work dir), runs the task-graph boot gate against Lithos and refuses to start if the server is unreachable or incompatible, reaps containers a previous daemon left behind, then forks the children your config enables. It stays in the foreground; use tmux, nohup or a systemd --user unit for background operation.
| Signal | Effect |
|---|---|
SIGINT / SIGTERM |
Graceful stop: children are told to stop, in-flight plugin subprocesses are cancelled, and anything still alive after shutdown_grace_seconds (default 30) is killed. A killed remediation round is refunded. |
SIGUSR1 |
Drain: children stop admitting new work, finish what is in flight and exit 0. |
Drain and restart¶
To restart without losing a multi-hour agent run:
uv run lithos-loom drain [--timeout SECONDS] # returns when the daemon has exited
uv run lithos-loom run
drain finds the daemon through the pidfile and exits 0 when it has gone, 1 when there was nothing to signal, 2 when --timeout ran out with the daemon still draining.
Next steps¶
- Concepts → — what a route is, how a task gets claimed, what a plugin must write back
- Obsidian bridge → — add
[obsidian_sync]on the vault host and install the macros - GitHub watcher → — map repos and enable issue mirroring on one host