loti (LOcal TIckets) — a local, markdown-backed ticket tracker driven
entirely through the loti CLI.
Warning
This project was developed by AI. The code was written by an AI agent
against a specific, structured specification authored by a human (see
docs/specs/). Its code has not been thoroughly reviewed by
hand. It is exercised by an automated test suite, but no line-by-line human
audit has been performed. If relying on unaudited AI-written code is a problem
for your use case, please refrain from using this tool.
loti is a ticket tracker that lives inside your repository as plain
markdown files and is driven entirely through a CLI — there is no server, no
database, and no web UI. It is built for two audiences sharing one store:
- AI agents that plan and execute work autonomously and need somewhere durable to record epics, tickets, status, and an attributed audit trail.
- One human monitoring and steering that work, who can read the store as ordinary markdown (or through the same CLI) at any time.
Work is organised as epics (top-level units of work), tickets (slices of
an epic), and subtickets (finer breakdowns, nestable to any depth). Every
node has a state (to-do, in-progress, blocked, done, closed), and can
carry labels, comments, and file attachments. Because the store is just
greppable markdown committed alongside your code, the plan and its history stay
with the project.
The behaviour is fully specified in docs/specs/:
core-spec.md (model, on-disk format, concurrency,
versioning) and cli-spec.md (command surface,
output, filtering, skill/help).
Get the binary (see Nix flake below, or cargo build), then:
1. Create a store in your project — a git-like upward search finds it from any subdirectory afterwards:
loti init
# → loti: initialised a store at /path/to/project/.lotiThe whole store lives inside the .loti/ container; nothing is scattered into
the project directory. To keep it elsewhere, loti init --root <path> uses that
path as the container directly and leaves a .loti.conf pointer behind.
2. Create an epic (the top-level unit of work). A longer body is read from
stdin or --file; here we give none:
loti epic create website-redesign \
--name "Website redesign" \
--summary "Refresh the marketing site" < /dev/null3. Add tickets to the epic (add --parent <epic>/<n> to make a subticket):
loti ticket create website-redesign --name "Audit current pages" --summary "Inventory existing content" < /dev/null
loti ticket create website-redesign --name "Design new layout" --summary "Wireframes and mockups" < /dev/null4. Drive status as work moves. Status is set-only; read it back with show:
loti ticket status website-redesign/1 --in-progress
# → loti: ticket website-redesign/1 (Audit current pages) is now in-progress
loti ticket status website-redesign/1 --blocked --reason "waiting on brand assets"
loti ticket status website-redesign/1 --done # allowed once all descendants are terminal5. Annotate and attribute. Attribution is carried by comments (-u for the
human, -a <name> for a named agent); labels and file assets organise and
evidence work:
loti ticket label add website-redesign/2 frontend design
echo "Kicked off the wireframes." | loti ticket comment add website-redesign/2 -u
loti ticket asset add website-redesign/2 --file ./mockup.png --description "First pass"6. Read and report:
loti ticket list website-redesign # indented tree + a per-status progress footer
loti ticket show website-redesign/1 # one node (add --json for machine output)
loti epic list # the roster of epics
loti ticket list website-redesign --status blocked # filter by state
loti ticket list website-redesign --label frontend # filter by label
loti ticket list website-redesign --match "wireframe" # regex over name/summary/bodyloti ticket list prints something like:
website-redesign/1 Audit current pages (in-progress)
website-redesign/2 Design new layout (to-do)
────────────────────────────────────────────────────
2 tickets · 1 to-do · 1 in-progress
Every read supports --json (the canonical form) so scripts and tools can
consume it. Run loti --help-full for the complete, annotated command surface.
7. Browse it full-screen:
loti tui # a two-pane browser: navigate left, read rightEpics are the top level; Enter opens an epic's tickets, then a ticket's
subtickets, with a breadcrumb showing where you are and a preview pane rendering
the same document loti ticket show prints (tables, code blocks and mermaid
diagrams included). Press ? for the keys. See docs/tui.md.
loti ships its own agent instructions. Running loti skill prints a static,
hand-authored SKILL.md — concepts, workflow, and gotchas — that teaches an
agent to drive the tracker correctly and points it at loti --help-full for the
exact command surface.
1. Install the skill. Point your coding agent at the built-in instructions:
Run
loti skilland install its output as a skill your tools can load, so you can drive thelotiticket tracker.
2. Delegate a feature. Describe what you want built and ask the agent to plan
it in loti:
I want to add passwordless email-magic-link login to our app. Using
loti, create an epic for this feature and break the work down into separate tickets (use subtickets where a ticket needs finer steps). Give each ticket a clear name and summary, and add labels where useful. When you're done, show me the plan withloti ticket list <epic>.
As work proceeds you can ask the agent to keep the plan live — moving tickets
through in-progress, blocked (with a reason), and done, and attributing
each meaningful change with a comment (-a <your-name>).
Once the skill is loaded, the agent can operate the tracker on its own: it plans by creating epics and tickets, records progress through status transitions, and leaves an attributed trail you can review at any time with the same commands — or by reading the markdown directly. The single rule the skill enforces is that every change goes through the CLI; the files are for reading, never for hand-editing (the CLI is what guarantees numbering, state transitions, attribution, and safe concurrent writes).
The repository ships a flake.nix that provides a dev shell plus a package
build. The Rust channel and components come from rust-toolchain.toml
and the package version from Cargo.toml, so both stay single-sourced.
Supported systems: x86_64/aarch64 Linux and aarch64 Darwin.
Enter the dev shell (Rust, clippy, rustfmt, rust-analyzer, and the musl
static-build targets):
nix developThen the usual cargo workflow inside the shell:
cargo build
cargo test
cargo clippy --all-targets -- -D warnings
cargo fmtBuild the binary through the flake:
nix build # or: nix build .#loti
./result/bin/loti --helpOr run it without building into the working tree:
nix run . -- --helpFor a single static binary (Linux), build the musl target from inside the
dev shell — the +crt-static rustflags are preset:
nix develop --command \
cargo build --release --target x86_64-unknown-linux-musl
# → target/x86_64-unknown-linux-musl/release/loti (statically linked)Swap x86_64 for aarch64 to target arm64.