A small, agent-first task tracker that lives in your repo as an append-only log and projects to a
generated TODO.md. Tasks are nodes wired by two edge kinds — a needs prerequisite (a DAG) and an
in arc membership — and the headline query, next, returns the prereqs-met ready frontier. It is
self-contained: one Zig host tool, no daemon, no database.
Why it exists: a flat prose backlog forgets prerequisites and goes stale. A real DAG makes "what's ready
to work on now" a query instead of a memory exercise, and the whole surface is explorable from the tool
itself (trk <verb> --help) — designed to be driven by agents as much as by humans.
trk needs only a Zig toolchain and builds native for Linux, macOS, and Windows
(trk.exe, chosen automatically from the build target).
git clone <this-repo> trk && cd trk
zig build -Doptimize=ReleaseSafe # -> zig-out/bin/trk
Install onto your PATH with Zig's built-in --prefix (installs to <prefix>/bin):
zig build --prefix ~/.local -Doptimize=ReleaseSafe # Linux/macOS -> ~/.local/bin/trk
zig build --prefix C:\Tools -Doptimize=ReleaseSafe # Windows -> C:\Tools\bin\trk.exe
If your PATH directory is flat (no trailing bin), drop the binary straight in with --prefix-exe-dir .:
zig build --prefix <dir> --prefix-exe-dir . -Doptimize=ReleaseSafe # -> <dir>/trk
Re-run the same command to redeploy after pulling changes. Run zig build test to run the host test suite.
cd your-project
trk init # scaffold .tracker/ + config.json + a starter TODO.md
trk add "Add dark mode" --tag ui # create a task (prints its id)
trk next # the ready frontier (prereqs met)
trk render # regenerate TODO.md (destination from .tracker/config.json)
trk finds .tracker/ by walking up from the current directory (git-style), so it runs from any
subdirectory of a project. The walk stops at a linked git worktree's root (its .git is a plain file,
never a directory) — it never escapes a worktree into an enclosing repo's tracker, even when the
worktree has no .tracker of its own yet. Set TRK_READONLY=1 in the environment to refuse every
mutating verb outright (a belt-and-suspenders guard for a dispatched agent's env, independent of
discovery).
- Task = a node with
id(a ULID, minted once),title,body,state,priority,tags, doc-refs. A needs B— a prerequisite edge, forming a DAG. Cycles are rejected.trk dep <needer> --needs <prereq>.T in X— taskTbelongs to arcX. An arc is a task that's either declared (trk arc <id>, ortrk add --arc— works even with zero members) or has ≥1 directinmember.trk in <task> <arc>.trk list --no-arclists every task in no arc.- State —
open→done→archived(viatrk archive, which graduates done tasks to changelog bullets and tombstones them), plusblocked(held) anddropped(won't-do). next— the ready frontier: everyopentask whose prerequisites are all satisfied. An arc root is a container ("do the arc" = do its non-parked members):nextholds it back until the members are finished, then surfaces it once as the close-out prompt — closing the root is what marks the goal complete and unblocks anything thatneedsthe arc..tracker/— the append-onlylog.jsonl(+ an optional compactedsnapshot.jsonl). It union-merges on concurrent appends, so parallel workers on disjoint tasks can each close their own without conflict.- Short ids are frozen at mint time and never change.
trk addprints the full ULID, and every human projection displays a short id — for a task minted going forward, that short is set ONCE and stays identical across every future add/archive/compact. A task that predates this (every task minted before it shipped) still gets a dynamically-computed short that can move as the live id set changes; runtrk migrate-shortsonce to freeze those in place too (current values only — see its--help).trk migrate-shorts --min <n>is a one-time REPAIR that also LENGTHENS an already-frozen short belownchars (e.g. bring a repo's ids up to match what's already been written down elsewhere) — run it once, deliberately, since it changes existing ids.
add · dep · undep · in · unin · arc · migrate-arcs · migrate-shorts · state · edit · show · next · list · render · tree · log · doc · compact · archive · init
Every verb self-documents: trk <verb> --help (or trk help <verb>) prints its synopsis, flags, and an
example; bare trk prints the overview.
.tracker/config.json (written by trk init) persists where render/archive write, so you don't pass
--out every time:
{ "render": { "out": "docs/TODO.md" }, "archive": { "out": null } }render truncates its target (a regenerated projection); archive appends under a ## YYYY-MM-DD
heading (a changelog accumulates), so its target can safely be the changelog itself.
Precedence for the output path: explicit --out > config value > stdout. A repo with no config behaves
exactly as if the fields were unset.
The full rationale — the two-edge data model, the next query, the state lifecycle, the union-merge model
for parallel writers, and the rejected alternatives — is in docs/design.md.
GPL-3.0-or-later — see LICENSE. Copyright (C) 2026 Scott Lowe.