The git-backed issue tracker for AI agents. tk is inspired by Joe Armstrong's Minimal Viable Program with additional quality of life features for managing and querying against complex issue dependency graphs.
Tickets are markdown files with YAML frontmatter, stored in .tickets/ and resolved automatically no matter which subdirectory you run tk from — see Where tickets live for the resolution rules. This allows AI agents to easily search them for relevant content without dumping ten thousand character JSONL lines into their context window.
Using ticket IDs as file names also allows IDEs to quickly navigate to the ticket for you. For example, you might run git log in your terminal and see something like:
nw-5c46: add SSE connection management
VS Code allows you to Ctrl+Click or Cmd+Click the ID and jump directly to the file to read the details.
From source:
git clone https://github.com/xymist/tkrs.git
cd tkrs && cargo install --path .From prebuilt binaries:
Download the binary for your system from the latest release; unpack it, make it executable, and put it somewhere on your PATH
Run tk update; if there is a new version it will download and unpack it over itself.
tk is a Rust binary with no system dependencies.
tk resolves the ticket store in this order:
TICKETS_DIR, if set (absolute, or resolved against the current directory).- The nearest
.tickets/directory found walking up from the current directory to the enclosing repo's root — except~/.ticketsitself, which is never picked up this way, since it's the shared container every home-fallback store below lives under, not a store in its own right. - The main repo's local
.tickets/, when the current checkout is a linked git worktree or ajj workspace addworkspace of a repo that already has one. ~/.tickets/<key>, so a repo with no local store gets one automatically without an agent having tomkdir .ticketsby hand.<key>is the spinal-cased content of a.tk-storefile committed at the main repo root (read only from the main checkout, so every worktree and workspace of a repo agrees), or else the main repo root's folder name, spinal-cased. Keying by folder name rather than full path means a~/.ticketssynced across machines resolves to the same store regardless of where each machine happens to check the repo out; two repos that share a folder name collide on one store by default, and a committed.tk-storeis the escape hatch for that collision.- A fresh
.tickets/at the main repo root, when no usable$HOMEis available (unset, empty, or a relative path) — landing at the main root rather than the current checkout so a linked worktree still converges on the same store as its main checkout.
Outside of any repo, tk falls back to .tickets in the current directory.
Add this line to your CLAUDE.md or AGENTS.md:
This project uses a CLI ticket system for task management. Run `tk help` when you need to use it.
Claude Opus picks it up naturally from there. Other models may need additional guidance.
tk create— create a ticket, with optional fieldstk start|close|reopen|status— set ticket status (all accept--note; close recordsclosed_atautomatically; reopen clearsclosed_atand can log a note)tk dep|undep|link|unlink— manage dependencies and links (usetk dep cycle --include-closedto scan closed tickets too;tk dep --check-cycle <bool>toggles the cycle guard, defaulttrue;unlinksupports--warn-missing;linksupports--dry-run;undepis idempotent and normalizes deps;dep treesupports--statusand--only-open)tk ls|list— list tickets with filters (supports--columns,--json, and--parent <id>to scope to direct children of a parent; the--jsonshape always includes aparentsarray derived from reverse dependencies)tk ready|blocked— show tickets with dependency readiness (readysupports--statusand--show-deps;blockedsupports--only-open)tk closed— list recently closed tickets (supports--limit,--since <RFC3339>,--assignee,--tags)tk show|edit|add-note— inspect and edit tickets (showsupports--jsonand lists derivedparents;editis non-interactive by default and accepts the same section flags ascreate(-d/--description,--implementation-plan,--acceptance,--external-ref,--body-from-file) to replace a section wholesale, or an empty string to clear it; pass-i/--interactiveto launch$EDITORinstead (combine with update flags to apply them first);--printprints the ticket path;add-noteis idempotent on headers and supports--tag <label>)tk query [FILTER]— output tickets as JSON; supports--format ndjson|prettyand built-in filters (field==valueexact match,field~substrcontains)tk tree— print the full ticket tree, fully expanded, as plain text (same ordering and nesting as thetk tuitree pane, no colour); supports-s/--status all|open|in-progress|closed(defaultopen); dependencies are omitted from the top level and nest under their dependants instead — a shared dependency repeats once under every root that depends on it, and once per branch within a single root too (a diamond dependency shows up under both branches that reach it, not just the first); every selection-matching ticket is guaranteed to appear somewhere, cycles included — a pure dependency cycle, or a ticket whose only dependant is filtered out (e.g. an open dependency of a closed ticket under the default open view), still renders as its own root instead of silently vanishing;--invertedwalks the reversed graph instead — roots are leaf work items with no resolvable dependency, and each node nests the tickets that depend on it, so an epic reachable from several leaves appears once per path (the same completeness guarantee applies here too, e.g. an open epic whose only resolvable dependency is a closed leaf still renders);-r/--root <id>restricts output to a single ticket's scope — that ticket plus its selection-filtered dependency closure, exactly the set the normal-orientation walk reaches (partial IDs accepted;--statusstill applies to the root itself, so a root that fails the filter yields empty output);--invertedonly changes how that fixed scope is presented — leaf-first, ascending back to the root — rather than changing which tickets are in scope, so a ticket outside the scope that happens to depend on one of its members never appears. Output repeats a ticket once per distinct dependency path reaching it, so on a densely diamond-shaped dependency graph the tree can grow rapidly; assembly is capped at 10000 rendered nodes per invocation (tk tuiuses the same cap to protect redraws), and a[!] output truncated at 10000 nodes ...marker is appended as the last top-level entry when the cap is hit — pass--unboundedto lift it, or prefertk graph, which dedupes each ticket to a single node and needs no captk graph— print the ticket graph as a Mermaidflowchart TDto stdout, for sharing project plans with nontechnical stakeholders; accepts the same-s/--status,--inverted, and-r/--rootflags astk treeand applies them identically (including--root's scope-then-orientation semantics above), but unliketk treeeach ticket is emitted as a single deduped node while every selection-passing dependency edge reaching it is still recorded (so a diamond-shaped dependency shows up as one node with two incoming arrows); without--root, every selection-matching ticket is guaranteed a node even when no root reaches it, so rootless dependency cycles still render; edges pointdependant --> dependencyby default anddependency --> dependantunder--inverted; the output opens with a%%{init: ...}%%directive forcing straight (non-curved) edgestk publish github <id> <owner/repo>— render a ticket as a GitHub issue-form submission and file it withgh issue create(nested likedep, so other publish targets can be added later); supports--assignee,--title-prefix(default"[Maintenance]: "),--fields-jsonto replace the default field set,--dry-runto print the title/body/gh command without creating anything,--body-fileto supply a pre-written body (title, pinning and priority still come from the ticket),--no-pin,--re-fileto re-file an already-pinned ticket,--no-priority,--priority-field(defaultPriority), and repeatable--label. Best-effort sets the repo's single-select Priority issue field from the ticket's priority after creation. tk ids are never written to GitHub: the outbound title/body is checked against the ticket's own id and every id in the local store before anything is created, and an OS-level advisory file lock (.tickets/.publish.lock) is held across the whole check-create-pin sequence, so a secondtk publish— even from a separate process — fails closed immediately instead of racing the first into double-creating an issue.tk update— self-updatetkto the latest release published on GitHub, installing it over the running binary; printsNo new version availablewhen already currenttk tui— EXPERIMENTAL - Start a TUI for browsing tickets with a little more context than just a bucket of IDs. Navigate with the keyboard (↑/↓ to move, →/← to expand/collapse,Tabto switch panes,Sto cycle the status filter,j/k/PageUp/PageDown to scroll the content) or with the mouse (click a ticket to select it, click a pane to focus it, scroll wheel to scroll whichever pane the cursor is over).
The repo ships tk-cli (CLI reference) and tk-to-github-issue (the
workflow guide for tk publish github — readability pass, field specs,
leak rules) for both Claude Code and Codex. Claude Code skills live under
skills/claude/; Codex-native equivalents, including Codex UI metadata and
workflow idioms, live under skills/codex/.
Run the installer to symlink each pair into any existing personal skills directory. It leaves correct links in place and refuses to overwrite conflicts, so the installed skills stay in lockstep with the checkout.
./skills/install.shPushing a tag matching vX.Y.Z triggers the GitHub Actions workflow .github/workflows/release.yml. It first ensures a GitHub release exists for the tag, then builds the Rust binary in release mode for each supported target and publishes a versioned, per-target tarball as a release asset. The workflow can also be run manually (workflow_dispatch) by providing the tag to build.
Built targets and asset names:
tk-vX.Y.Z-x86_64-unknown-linux-musl.tar.gz(Linux, x86_64; a fully static musl binary with no glibc dependency, so it runs on any x86_64 Linux distribution regardless of GLIBC version)tk-vX.Y.Z-aarch64-apple-darwin.tar.gz(macOS, Apple Silicon)
tk update self-installs by matching the running binary's target triple against these asset names, so each platform pulls its own build automatically.
MIT