A plain-text, markdown-based ticket system for solo developers. Tickets are .md files with YAML front matter — readable by humans and AI alike.
Plain text as the data layer. No database, no lock-in. Git for versioning. A TUI for human ergonomics; a CLI for scripting and AI access.
Running tickets with no subcommand launches an interactive kanban board (via ratatui). Columns default to todo / in-progress / done, configurable via [tui] kanban_columns in .tickets.toml.
cargo build --release
# Binary: target/release/tickets# Initialise a tickets directory
tickets init
# Create a ticket
tickets new --title "Fix login bug" --type bug --tag auth
# List all tickets
tickets list
# Show a single ticket
tickets show <id>
# Edit a ticket
tickets edit <id> --status in-progress
# Archive done tickets
tickets archive <id>
# View the dependency graph
tickets graphEach ticket is a .md file:
tickets/
├── all/ # active tickets
└── archived/ # archived tickets
Filename: <6-char-id>_<slugified-title>.md — e.g. a3f9c1_fix-login-bug.md
---
id: a3f9c1
title: Fix login bug
type: task
status: draft
tags: []
parent: null
blocked_by: []
created_at: 2026-04-30T19:00:00Z
updated_at: 2026-04-30T19:00:00Z
---
Ticket body in markdown.Only title is required. All other fields have defaults.
epic / story / task / bug
draft / todo / in-progress / done / rejected
Titles must be 120 characters or fewer and may only contain letters, numbers, spaces, _, -, and ..
tickets init [--force]
Scaffolds tickets/all/, tickets/archived/, and .tickets.toml.
--forceoverwrites an existing.tickets.toml, backfilling any missing keys (existing values are preserved).
tickets new --title "..." [--type <type>] [--status <status>] [--tag <tag>]...
[--parent <id>] [--blocked-by <id>]... [--body -]
--body -reads the ticket body from STDIN.--tagis repeatable.
tickets edit <id> [--title "..."] [--type <type>] [--status <status>]
[--tag <tag>]... [--parent <id>] [--clear-parent]
[--blocked-by <id>]... [--clear-blocked-by] [--body -]
Only fields explicitly passed are updated. updated_at is bumped automatically.
tickets list [--status <status>]... [--type <type>]... [--tag <tag>]...
Prints all tickets in tickets/all/, sorted by status then creation date.
--status/--typeare repeatable with OR semantics (match any given value).--tagis repeatable with AND semantics (ticket must have all given tags).
Pretty-prints a single ticket with emojis, human-readable timestamps, and optional body rendering.
- Empty/null fields (
tags,parent,blocked_by) are omitted. - Timestamps shown as
YYYY-MM-DD · N days ago. - Body is rendered via
bat --language=mdif available, otherwise printed raw.
tickets archive <id>...
tickets archive --all-rejected
Moves tickets to tickets/archived/. Pass one or more IDs, or --all-rejected to bulk-archive all rejected tickets.
Prints the dependency graph derived from blocked_by relationships.
- No ID: renders the full forest (all root tickets and their trees).
- With ID: renders the tree rooted at that ticket.
- Warns on stderr if a dependency cycle is detected.
| Flag | Description |
|---|---|
--dir <path> |
Override the tickets directory (takes precedence over TICKETS_DIR) |
--version, -V |
Print the CLI version and build SHA |
| Variable | Description |
|---|---|
TICKETS_DIR |
Path to the tickets directory (default: ./tickets/) |
- Rust
- clap — CLI argument parsing
- serde + serde_yaml — YAML front matter
- nanoid — ticket ID generation
- chrono — timestamps
- ratatui + crossterm — TUI
Create tickets/.tickets.toml (via tickets init) to configure per-repo behaviour:
[git]
auto_commit = true # stage and commit the ticket file after new/edit/archive
[new]
default_type = "task" # default --type for `tickets new`
default_status = "draft" # default --status for `tickets new`
[tui]
kanban_columns = ["todo", "in-progress", "done"] # columns shown in the TUI boardtickets search <query>— fuzzy search across title, tags, and type