Task management for agentic engineering
A Go CLI that gives AI agents deterministic, token-efficient task tracking,
without the complexity of full project management tools.
Install · Quick Start · Commands · Output Formats · Why Tick?
Tick is a lightweight CLI for tracking tasks, dependencies, and status transitions inside your project. It stores everything in a plain JSONL file (human-readable, git-friendly) with a SQLite cache for fast queries. Run tick init, and you're set.
It's built to be used by AI agents as much as by humans. Output auto-switches between a token-efficient format for agents and clean tables for terminals, so the same commands work in both contexts.
Claude, Cursor, and other AI coding agents need a way to track tasks across sessions. The built-in approaches have problems:
- TodoWrite / in-context lists — lost between sessions, no persistence, no dependency tracking
- Markdown files — no structure, agents parse them inconsistently, output is verbose
- Beads / full PM tools — too much complexity for a coding session, heavy overhead
Tick sits in between: structured enough for agents to reason about reliably, simple enough that it doesn't get in the way.
| Tick | TodoWrite | Markdown | Beads | |
|---|---|---|---|---|
| Persists across sessions | Yes | No | Yes | Yes |
| Dependencies & blockers | Yes | No | No | Yes |
| Token-efficient output | TOON (30-60% savings) | N/A | No | No |
| Deterministic format | Yes | Varies | No | Yes |
| Complexity | Low | Minimal | Minimal | High |
| Setup | tick init |
None | None | Config |
macOS
brew install leeovery/tools/tickLinux
curl -fsSL https://raw.githubusercontent.com/leeovery/tick/main/scripts/install.sh | bashGo
go install github.com/leeovery/tick/cmd/tick@latesttick init # create .tick/ in your project
tick create "Build auth module" # create a task
tick create "Write tests" --priority 1 --blocked-by tick-a1b2
tick list # see all tasks
tick ready # tasks with no blockers
tick start tick-a1b2 # open → in_progress
tick done tick-a1b2 # in_progress → doneInitialize a new tick project in the current directory. Creates a .tick/ directory with an empty tasks.jsonl file.
tick initCreate a new task. Returns the full task detail on success, with a changed section listing any task whose status moved as a result — creating under a done parent reopens it (see Transition & Cascade Output).
tick create <title> [flags]| Flag | Type | Default | Description |
|---|---|---|---|
--priority |
0-4 |
2 |
0 critical, 1 high, 2 medium, 3 low, 4 backlog |
--description |
string | Task description (supports multi-line) | |
--type |
string | Task type: bug, feature, task, chore |
|
--tags |
strings | Comma-separated tags (kebab-case, max 10) | |
--refs |
strings | Comma-separated external references (URLs, issue keys) | |
--parent |
ID | Make this a subtask of another task | |
--blocked-by |
IDs | Comma-separated list of tasks this depends on | |
--blocks |
IDs | Comma-separated list of tasks this blocks |
tick create "Build auth module"
tick create "Critical fix" --priority 0 --type bug
tick create "Write tests" --blocked-by tick-a1b2,tick-c3d4 --tags backend,testing
tick create "Login endpoint" --parent tick-a1b2 --refs https://github.com/org/repo/issues/42
tick create -- "--dry-run support" # -- passes a dash-leading titleList tasks with optional filters. Results are sorted by priority (ascending), then creation date; within a priority band, tasks tied on creation date come back in creation order.
tick list [flags]| Flag | Type | Default | Description |
|---|---|---|---|
--status |
string | Filter by status: open, in_progress, done, cancelled |
|
--priority |
0-4 |
Filter by priority level | |
--type |
string | Filter by type: bug, feature, task, chore |
|
--tag |
string | Filter by tag (repeatable, see below) | |
--parent |
ID | Show descendants of a task | |
--ready |
bool | false |
Show only ready tasks (open or in_progress, no unresolved blockers, no open children, no dependency-blocked ancestor) |
--blocked |
bool | false |
Show only blocked tasks (open or in_progress with unresolved blockers, open children, or dependency-blocked ancestor) |
--count |
int | Limit results to N tasks |
--ready and --blocked are mutually exclusive.
Tag filtering supports AND/OR composition:
--tag ui,backend— AND: tasks must have both tags--tag ui --tag api— OR: tasks with either tag--tag ui,backend --tag api— mixed: (ui AND backend) OR api
tick list # all tasks
tick list --status open # filter by status
tick list --priority 0 # only critical tasks
tick list --type bug # only bugs
tick list --tag backend # tasks tagged "backend"
tick list --parent tick-a1b2 # descendants of a task
tick list --count 5 # first 5 resultsAlias for tick list --ready. Shows tasks that are open or in progress, have no unresolved blockers, no open children, and no dependency-blocked ancestor. Accepts the same filter flags as list (--status, --priority, --type, --tag, --parent, --count).
tick ready
tick ready --count 1 # next task to work on
tick ready --type bug --count 3Alias for tick list --blocked. Shows tasks that are open or in progress but waiting on dependencies, have open children, or have an ancestor with unresolved blockers. Accepts the same filter flags as list.
tick blocked
tick blocked --tag backendDisplay full detail for a single task, including type, tags, refs, notes, blockers, children, and description.
tick show <task-id>
tick show <task-id> --field <name,...>| Flag | Type | Description |
|---|---|---|
--field |
strings | Select fields by name (comma-separated) |
--fields |
strings | Alias of --field |
The accepted names are the ones the output document uses: id, title, status, priority, type, parent, created, updated, closed, description, notes, tags, refs, children, blocked_by. Every list section — notes, tags, refs, children, blocked_by — also accepts a 1-based position, written notes.2. The flag may be repeated, so --field title --field status selects both; whitespace around a name is ignored, and a name given twice counts once.
One name returns the bare value: no key, no quoting, nothing around it. The two lines below are the description's own bytes, not a formatted block.
$ tick show tick-a1b2 --field description
Full task description here.
Can be multiple lines.
Several names return the normal document with only those sections in it, in normal output order rather than the order they were typed.
$ tick show tick-a1b2 --field description,notes
notes[2]{index,text,created}:
1,Discussed approach with team,"2026-01-19T14:00:00Z"
2,Blocked on the migration landing,"2026-01-19T15:00:00Z"
description: "Full task description here.\nCan be multiple lines."
A lone name of a list section returns that whole section. A lone position on notes, tags or refs names one value and returns it bare.
$ tick show tick-a1b2 --field notes.2
Blocked on the migration landing
A lone position on children or blocked_by returns its one-row section instead, because a row is not a value. Inside a multi-name selection a position always returns a section, narrowed to the items it names while the count follows the selection. A narrowed notes section keeps each row's real 1-based index, so --field title,notes.2 renders notes[1]{index,text,created}: with a row indexed 2 and the position you read is still the one to act on. The other sections carry no index column, so a narrowed one hands back the item without its position.
A request that returns a bare value ignores --toon, --pretty and --json; a request that returns a document honours them. --quiet together with a field selection is refused. An unrecognised name and a position outside its section both exit non-zero.
Modify one or more fields on an existing task. At least one flag is required. Returns the full task detail, with a changed section for any task whose status moved as a result: moving a task under a done parent reopens it, and moving the last unfinished child away from a parent auto-completes it.
tick update <task-id> [flags]| Flag | Type | Description |
|---|---|---|
--title |
string | Set a new title |
--description |
string | Set or replace the description |
--clear-description |
bool | Remove the description (mutually exclusive with --description) |
--priority |
0-4 |
Change priority level |
--type |
string | Set task type (bug, feature, task, chore) |
--clear-type |
bool | Remove the type |
--tags |
strings | Replace tags (comma-separated) |
--clear-tags |
bool | Remove all tags |
--refs |
strings | Replace refs (comma-separated) |
--clear-refs |
bool | Remove all refs |
--parent |
ID | Set or change the parent task (pass empty string to clear) |
--blocks |
IDs | Comma-separated list of tasks this blocks |
tick update tick-a1b2 --title "Revised title" --priority 1
tick update tick-a1b2 --type bug --tags critical,backend
tick update tick-a1b2 --parent tick-c3d4Transition a task between statuses.
tick start <task-id> # open → in_progress
tick done <task-id> # in_progress → done
tick cancel <task-id> # any → cancelled
tick reopen <task-id> # done/cancelled → opendone and cancel set a closed timestamp. reopen clears it.
Cascading: Status changes automatically propagate through parent/child hierarchies:
- Start cascades up — starting a child auto-starts open ancestors
- Done/Cancel cascades down — completing or cancelling a parent cascades to non-terminal descendants
- Done/Cancel cascades up — when all children are terminal, the parent auto-completes (done if any child is done, cancelled if all cancelled)
- Reopen cascades up — reopening a child reopens done ancestors
- Adding a child to a done parent auto-reopens it; adding to a cancelled parent is blocked
- Reparenting — moving a child away from a parent triggers completion re-evaluation: if all remaining children are terminal, the old parent auto-completes
Permanently delete one or more tasks. Removing a parent cascades to all descendants. Dependency references on surviving tasks are automatically cleaned up.
tick remove <id> [<id>...] [flags]| Flag | Type | Description |
|---|---|---|
--force, -f |
bool | Skip confirmation prompt |
tick remove tick-a1b2 # remove with confirmation
tick remove tick-a1b2 tick-c3d4 -f # remove multiple, skip promptSince tasks.jsonl is tracked in git, accidental removals can be recovered from history.
Add or remove timestamped notes on a task.
tick note add <task-id> <text>
tick note remove <task-id> <index>tick note add tick-a1b2 "Discussed approach with team"
tick note remove tick-a1b2 1 # remove note at index 1 (1-based)
tick note add tick-a1b2 -- "--json output was wrong here"Manage and visualize task dependencies. Tick validates all dependency changes and prevents cycles, self-references, children blocked by their own parent, and dependencies on cancelled tasks.
tick dep add <task-id> <blocked-by-id>
tick dep remove <task-id> <blocked-by-id>
tick dep tree [task-id]tick dep add tick-a1b2 tick-c3d4 # tick-a1b2 is now blocked by tick-c3d4
tick dep remove tick-a1b2 tick-c3d4 # remove that dependencyIn TOON and pretty, dep add and dep remove print a one-line confirmation. Under --json they return an object with action, task_id and blocker keys.
dep tree — Visualize dependency chains. Two modes:
tick dep tree # full graph: all dependency chains
tick dep tree tick-a1b2 # focused: upstream + downstream from a taskFull graph covers every task that participates in a dependency, plus a summary line. All three formats cover every participant: the pretty tree draws root tasks (tasks that block others but aren't blocked themselves) with their downstream chains, then seeds a tree from each participant nothing already drawn reaches — a cycle's member, or a blocker ID that no longer matches a task — with its downstream chain nested beneath it. A dangling blocker ID is thus the top-level entry itself, drawn with status missing and no title, above the task it blocks. Focused view walks both directions from the target — what blocks it and what it unblocks. The pretty and JSON trees draw a diamond's shared branch under each of its blockers; the toon edge lists carry one row per stored dependency — dep_tree over the whole graph, and the focused blocked_by/blocks sections over the dependencies inside the target's neighbourhood.
When no task is blocked, pretty prints No dependencies found.; TOON and JSON return the populated document emptied — a count-zero dep_tree section beside chains: 0, longest: 0 and blocked: 0 — so an agent parses one shape either way and reads the counts. The focused TOON and JSON documents open with the target's id, title and status, then its blocked_by and blocks edge sections, which carry a count-zero header when the task has no dependencies in that direction.
|
Pretty (box-drawing tree) |
TOON (flat edge list) |
Show aggregate task counts grouped by status, workflow state (ready/blocked), and priority.
tick statsIn TOON the counts are top-level fields beside a per-priority table:
$ tick stats
total: 3
open: 2
in_progress: 1
done: 0
cancelled: 0
ready: 3
blocked: 0
by_priority[5]{priority,count}:
0,0
1,1
2,1
3,1
4,0
Run diagnostic checks against your task data. Read-only, never modifies data.
tick doctorChecks for: JSONL syntax errors, invalid IDs, duplicates, duplicate creation sequences, orphaned references, self-referential dependencies, dependency cycles, parent/child constraint violations, and cache staleness.
Force a full SQLite cache rebuild from the JSONL source file, bypassing the freshness check.
tick rebuildPrint the tick version and exit. The --version global flag is equivalent.
tick version
tick --versionShow usage information. With no argument, lists all commands and global flags. With a command name, shows detailed help including flags.
tick help # list all commands
tick help create # detailed help for create
tick help --all # full reference of all commands and flags
tick create --help # same as tick help create
tick -h # same as tick helpImport tasks from external tools.
tick migrate --from <provider> [flags]| Flag | Type | Default | Description |
|---|---|---|---|
--from |
string | required | Provider to import from (currently: beads) |
--dry-run |
bool | false |
Preview what would be imported without persisting |
--pending-only |
bool | false |
Only import tasks not yet migrated |
tick migrate --from beads
tick migrate --from beads --dry-run --pending-onlyImported titles and descriptions are trimmed of leading and trailing whitespace, as create does, so a value read back out of tick show and written back lands unchanged.
Tick auto-detects the context and picks the right format:
| Context | Default format | Override |
|---|---|---|
| Terminal (TTY) | --pretty |
--toon, --json |
| Pipe / agent | --toon |
--pretty, --json |
|
Agent / pipe (TOON) |
Terminal (Pretty) |
Designed for AI consumption. A list of same-shaped rows becomes a tabular section — a name[N]{cols}: header declaring the schema once, followed by compact CSV-like rows; a single object becomes named key: value fields; a collection of scalars becomes an inline name[N]: a,b list. Uses 30-60% fewer tokens than equivalent JSON.
tasks[2]{id,title,status,priority,type}:
tick-a1b2,Setup auth,done,1,feature
tick-c3d4,Login endpoint,open,1,task
id: tick-a1b2
title: Setup auth
status: in_progress
priority: 1
type: feature
created: "2026-01-19T10:00:00Z"
updated: "2026-01-19T14:30:00Z"
blocked_by[1]{id,title,status}:
tick-c3d4,Database migrations,done
children[0]{id,title,status}:
tags[2]: auth,backend
refs[1]: "https://github.com/org/repo/issues/42"
notes[1]{index,text,created}:
1,Discussed approach with team,"2026-01-19T14:00:00Z"
description: "Full task description here.\nCan be multiple lines."
TOON cannot carry a C0 control character other than tab, newline and carriage return — an ANSI escape pasted in from terminal output is the usual way one arrives. A command whose TOON document would carry such a value fails, naming the field or section it could not encode and the task carrying the refused value — the offending row, where the document is a task list — rather than printing a document without it; the section name stands alone only where no task can be attributed. The value is stored and read back intact either way: --json returns it, and so does tick show <id> --field <name>.
Clean aligned columns for terminals. No borders, no colors, no icons.
ID STATUS PRI TYPE TITLE
tick-a1b2 done 1 feature Setup auth
tick-c3d4 open 1 task Login endpoint
When you run start, done, cancel, or reopen, the output lists every task whose status changed: the task you named first, then any task the change cascaded to. The auto column marks the cascaded ones.
create and update can move other tasks' statuses too, through the cascade rules above. Their output is the task's detail document with the same changed table as a section after notes, present even when nothing moved (changed[0]{id,title,from,to,auto}:); pretty prints the transition lines after the detail instead. show, note add and note remove carry no changed section.
|
Simple transition (TOON) |
Simple transition (Pretty) |
Simple transition (JSON) {
"changed": [
{
"id": "tick-a1b2",
"title": "Setup auth",
"from": "open",
"to": "in_progress",
"auto": false
}
]
} |
Cascade — completing a parent cascades to children:
|
TOON ( |
Pretty (tree with box-drawing) |
Standard 2-space indented JSON with snake_case keys. A task detail carries each note with its 1-based index, and create/update add a changed list mirroring the TOON table.
[
{
"id": "tick-a1b2",
"title": "Setup auth",
"status": "in_progress",
"priority": 1,
"type": "feature"
}
]Task IDs can be abbreviated to any unique prefix. If only one task matches, it resolves automatically. This works everywhere a task ID is accepted.
tick show tick-a1 # resolves to tick-a1b2c3 if unique
tick start a1b2 # tick- prefix is optional
tick dep add a1 c3 # both IDs resolvedTick stores data in a .tick/ directory at your project root:
tasks.jsonl— append-only source of truth (one JSON object per line, human-editable, git-friendly)cache.db— SQLite cache (auto-rebuilt when JSONL changes, do not commit)lock— file lock for safe concurrent access
Add to .gitignore:
.tick/cache.db
.tick/lock
--help, -h Show help (tick --help or tick <command> --help)
--version Print version and exit (equivalent to `tick version`)
--quiet, -q Minimal output (IDs only where applicable)
--verbose, -v Debug logging to stderr
--toon Force TOON format
--pretty Force pretty format
--json Force JSON format
-- End of flags; every argument after it is text
Global flags are accepted on every command. Unknown or misspelled flags are rejected with a helpful error:
$ tick list --stauts open
unknown flag "--stauts" for "list". Run 'tick help list' for usage.
Flags come before --; everything after it is text, including an argument that spells a global flag exactly, so tick note add tick-a1b2 -- --json stores the literal note --json. On note add the marker is recommended rather than required — dash-leading note text that does not spell a global flag is read as text without it. create checks its title against its own flags, so a dash-leading title needs the marker. A flag that takes a value also accepts it attached as --flag=value, which is how a value that is exactly --, or one that spells a global flag, is written: tick update tick-a1b2 --title=-- --description=--json.
MIT