A tiny coverage scheduler with memory, for AI coding agents running in loops.
trail hands an agent the next folder to work on for a named task,
leases it so parallel agents never collide, records the visit, and uses that
history to order future work. It replaces hand-written "go look for P0/P1/P2
bugs in feature X" JSON files that stop scaling as a codebase grows.
Name note:
trailis a working name (the tool leaves a trail of where agents have been). Rename later via the workspace package if you like.
When you run a Ralph-style loop that spawns a fresh agent each iteration, the
agents have no shared memory of where prior iterations already looked, so they
collide or re-tread the same folders. trail owns that coverage state as a
single small SQLite file and exposes it through a CLI (plus thin Python/Node
wrappers), so any harness that can run a shell command can coordinate cleanly.
- Folder is the unit of work.
- A sweep is one-pass coverage: each folder is handed out exactly once, then the sweep is complete. The outer loop owns starting the next sweep.
- Memory is per task-name and persists across sweeps. When a sweep opens, each folder's priority is frozen from staleness (time since it was last visited under this task) plus a static weight (file count / size / churn). If a sweep is cut short, the stalest and heaviest folders were surfaced first.
- Strategies only reorder the one-pass drain:
weighted(default),round-robin(pure least-recently-visited),random(seeded, reproducible). - Outcome feedback (opt-in via
strategy.outcome_weight): agents report findings withdone --found N/--clean, and folders that recently found more are surfaced earlier in future sweeps. Off by default. - Leases make parallel work safe: a claimed folder is held for a TTL; if the agent dies the lease expires and the folder returns to the pool, so a sweep completes only when every folder is genuinely covered.
cargo build --release # binary at target/release/trail
# install the `trail` binary onto your PATH:
cargo install --path crates/trail-cli
# (the crate is `trail-cli` but its [[bin]] is named `trail`, so the installed
# command is `trail`.)
# optional: enable the git-churn static signal (needs cmake for vendored libgit2)
cargo build --release --features churnThe language wrappers locate the binary via PATH, or via the TRAIL_BIN
environment variable (e.g. TRAIL_BIN=/path/to/trail).
Tagged releases (v*) build prebuilt binaries for Linux, macOS (Intel + Apple
Silicon), and Windows via GitHub Actions.
Shell completions:
trail completions zsh > ~/.zfunc/_trail # or bash / fish / powershell / elvishtrail init # scan the tree, register folders
trail next --task refine --agent a1 # claim a folder (JSON on stdout)
trail done --task refine --path src/api --agent a1
trail status --task refinetrail init also writes a sample .trail.toml.example when none exists (the
wrote_example_config field reports whether it did).
Exit codes carry the loop outcome: 0 ok, 3 sweep-complete, 4
none-available (leased elsewhere, retry), 1 error, 2 usage. See
skills/trail/SKILL.md for the full command reference
and the correct parallel-safe loop.
Commit .trail.toml; gitignore .trail/ (the state DB). .gitignore/.ignore
and hidden dirs are excluded for free; config only layers extra globs. See
.trail.toml.example.
Thin wrappers (shell out to the binary, parse JSON):
- Python:
wrappers/python - TypeScript / Node:
wrappers/typescript - Go:
wrappers/go - Ruby:
wrappers/ruby
Native, in-process bindings (no subprocess; same shapes as the CLI):
- Python via pyo3:
bindings/python - Node via napi-rs:
bindings/node
The shell-out + JSON + exit-code contract makes a thin wrapper in any language trivial; the native bindings run the scheduler directly for hot loops.
crates/trail-core library: walk, scoring, SQLite store + atomic claim/lease
crates/trail-cli the `trail` binary
wrappers/ thin Python + TypeScript wrappers (shell out)
bindings/ native in-process bindings (pyo3 Python, napi-rs Node)
skills/trail SKILL.md for agents
State lives in one SQLite file and the hot path is built to stay flat as a
codebase grows: claim, complete, and status are O(1) (per-sweep counters,
not COUNT(*) scans), and SQL is compiled once per process (prepare_cached).
Draining a 50k-folder sweep takes ~1.1s (~22us per claim+complete cycle, flat
from 1k to 50k folders). Run the micro-benchmark with:
cargo run --release --example bench -p trail-coreFor tight loops, the native bindings/ avoid per-call process
spawn entirely. If counters ever drift, trail gc --reconcile rebuilds them.
cargo test # unit, scoring, lifecycle, concurrency, CLI e2e