Skip to content

Repository files navigation

Tick

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.

License: MIT Go

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.

Why Tick?

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.

Key differences

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

Install

macOS

brew install leeovery/tools/tick

Linux

curl -fsSL https://raw.githubusercontent.com/leeovery/tick/main/scripts/install.sh | bash

Go

go install github.com/leeovery/tick/cmd/tick@latest

Quick Start

tick 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 → done

Commands

init

Initialize a new tick project in the current directory. Creates a .tick/ directory with an empty tasks.jsonl file.

tick init

create

Create 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 title

list

List 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 results

ready

Alias 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 3

blocked

Alias 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 backend

show

Display 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.

update

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-c3d4

start / done / cancel / reopen

Transition 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 → open

done 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

remove

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 prompt

Since tasks.jsonl is tracked in git, accidental removals can be recovered from history.

note

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"

dep

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 dependency

In 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 task

Full 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)

$ tick dep tree
tick-a1b2  Setup auth (done)
└── tick-c3d4  Login endpoint (open)
    └── tick-f3e4  Write tests (open)

1 chain, longest: 2, 2 blocked

TOON (flat edge list)

$ tick dep tree
dep_tree[2]{from,to}:
  tick-a1b2,tick-c3d4
  tick-c3d4,tick-f3e4

chains: 1
longest: 2
blocked: 2

stats

Show aggregate task counts grouped by status, workflow state (ready/blocked), and priority.

tick stats

In 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

doctor

Run diagnostic checks against your task data. Read-only, never modifies data.

tick doctor

Checks for: JSONL syntax errors, invalid IDs, duplicates, duplicate creation sequences, orphaned references, self-referential dependencies, dependency cycles, parent/child constraint violations, and cache staleness.

rebuild

Force a full SQLite cache rebuild from the JSONL source file, bypassing the freshness check.

tick rebuild

version

Print the tick version and exit. The --version global flag is equivalent.

tick version
tick --version

help

Show 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 help

migrate

Import 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-only

Imported 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.

Output Formats

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)

$ tick list
tasks[3]{id,title,status,priority,type}:
  tick-a1b2,Auth middleware,in_progress,1,feature
  tick-f3e4,Write tests,open,2,task
  tick-d5c6,Update docs,open,3,""

Terminal (Pretty)

$ tick list
ID          STATUS       PRI  TYPE     TITLE
tick-a1b2   in_progress  1    feature  Auth middleware
tick-f3e4   open         2    task     Write tests
tick-d5c6   open         3    -        Update docs

TOON (Token-Oriented Object Notation)

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>.

Pretty

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

Transition & Cascade Output

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)

$ tick start tick-a1b2
changed[1]{id,title,from,to,auto}:
  tick-a1b2,Setup auth,open,in_progress,false

Simple transition (Pretty)

$ tick start tick-a1b2
tick-a1b2: open → in_progress

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 (changed table)

$ tick done tick-a1b2
changed[2]{id,title,from,to,auto}:
  tick-a1b2,Setup auth,in_progress,done,false
  tick-c3d4,Subtask one,open,done,true

Pretty (tree with box-drawing)

$ tick done tick-a1b2
tick-a1b2: in_progress → done

Cascaded:
└─ tick-c3d4 "Subtask one": open → done

JSON

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"
  }
]

Partial ID Matching

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 resolved

Storage

Tick 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

Global Flags

--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.

License

MIT

About

Tick is a lightweight task management Go CLI designed for AI coding agents. It prioritises determinism, simplicity, and zero-friction git integration.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages