You need psql, bash 4+, and either a reachable Postgres 12+ or Docker. That's the whole requirements list. There is no daemon to keep running and nothing to deploy — the database is the only moving part, and once it exists, anything that can open a connection to it is a peer.
1. Install it
./quickstart.sh finds or starts a Postgres, applies the schema, installs the CLIs, and leaves you at a working lac doctor. It asks before it touches your machine, unless you pass --yes.
If nothing is reachable and Docker isn't running either, it says so and stops — it will not silently fall back to something you didn't ask for. Put the two lines it prints at the end into your shell profile:
LAC_NAME is never invented for you. It's how every claim, message and cost row gets attributed, and a missing value fails loudly rather than falling back to something generic.
2. Point it at a real project
Not a demo directory — whatever you're actually working on. A repository is just a corpus that happens to compile:
It walks the tree, skips .git, node_modules, .venv, __pycache__, dist, build and .next, and drops anything over 20 MB (raise it with LAC_INDEX_MAX_MB if you need to). Every file is content-hashed on the way in, so re-running this after nothing changed costs nothing — each one comes back "already held" instead of being stored twice.
Failure mode: if you index the same file from two different working directories, it's still recognized as one document — the leading ./ is stripped before the path is recorded. What it does not collapse is two different files with the same basename in different directories: the path is the identity, not the filename, specifically so a repository with seven README.md files doesn't get flattened into one fake version history.
3. Extract and map it
lac doc extract runs an extractor (extractors/auto by default — set LAC_EXTRACTOR for your own) over every document with no text yet, up to 500 at a time. For code that means symbols, imports and language; for anything else it's whatever the extractor can pull — EXIF, PDF metadata, dates, place names. A file with no extractable text but real metadata still counts: a photo nobody OCR'd is now catalogued by camera and date instead of being invisible.
4. See what's missing
That's real output from this project indexing itself. Four shapes of absence: a hole inside a covered span, a claim resting on one source, something named but connected to nothing, and documents held but never read. Point the same query shape at an archive and it reads as a missing year between two dated documents; point it at a codebase and it reads as undocumented files, symbols nothing calls, and orphaned files no other file names. No model runs any of this — it's set-based SQL over data you already hold, which is also why it's reproducible.
5. Turn the gaps into claimable work
Without --dispatch it just shows you the suggestions; with it, each one becomes a row in the task queue. Run it again later and it withdraws whatever's no longer suggested — if someone documents a file, the "undocumented" task for it disappears rather than sitting there stale. Use --limit to cap how many and --kind to filter by gap type.
6. Wire up an executor
There is no default executor, no default model and no default vendor — nothing spends your money until you wire one up. An executor is any program that takes a task on stdin and exits 0 (done), 2 (refused), or anything else (blocked, with the reason in its output). Three ship in executors/:
Failure mode: forget LAC_EXECUTOR and lac work refuses outright — "✗ LAC_EXECUTOR is not set — nothing to run work with." — rather than claiming tasks it can't do anything with. executors/shell is the simplest one worth understanding first: it runs a task's detail as a shell script, which turns the queue into a durable, leased, escalating cron. Read its header comment before pointing it at a shared database — it runs arbitrary commands with the drainer's privileges.
7. Run several, in the background
Each worker asks the board for the top task it's capable of, claims it with one atomic UPDATE, runs it through your executor, heartbeats while it works, and asks again. Two workers cannot claim the same row — the loser just takes the next one. Add --concurrency 3 to one worker instead of running three processes if you'd rather. Run one on your laptop and one on a server; they both drain the same queue.
Kill one mid-task and nothing is lost — its claim goes stale, whoever runs tasks/lac-keeper.sh releases it, and the next worker to poll picks it back up with the task's history intact. There's a 60-second scripted proof of exactly this in demo/crash-recovery.sh if you want to watch it happen with a real kill -9 rather than take it on faith.
8. Check in the morning
That's the real shape of a morning digest, straight from the README. Everything that's stuck is in one place, with the actual error rather than a vague "failed." lac inbox shows the same waiting-on-a-human items on their own if that's all you want.
What's worth checking next
Run lac doctor again after all of this — it's the honest end-to-end status check, and it'll tell you plainly if the keeper isn't scheduled or an escalation channel isn't set, both of which matter a lot more once you're not watching the terminal yourself. That's the whole subject of the next piece.