Core concepts
Phases, statuses, roles, dependencies, claims — and what each one buys you.
There are five ideas in the whole system. Once these land, the command reference is mostly mechanical.
The tree
.tasks/ is discovered by walking up from the current directory, git-style: nearest
wins, no merging with a parent tree. Each package in a monorepo that has its own
.tasks/ is its own independent scope.
Everything the CLI knows is derived from files on disk on every invocation. There is no cache and no daemon, so editing a task body by hand, or pulling someone else's branch, takes effect immediately.
Phases
A phase is a directory, not a tag:
Making it a directory rather than a frontmatter field means a human can browse one body
of work in a file tree, and a diff of a phase is a diff of a directory. The name is a
free-form slug — a milestone (auth-rework), a quarter (2026-q1), a workstream, or
whatever fits.
The directory is authoritative. If frontmatter and directory disagree, the
directory wins and tasks validate --fix rewrites the field. This is also why you move
tasks with tasks move <id> <phase> rather than mv.
A project that does not want phases can put everything in one directory and never pass
--phase. Set active_phase in config to make list
and ready default to one phase without typing -p every time.
Statuses and roles
Statuses are an ordered, project-configurable list. The default is
[PENDING, IN_PROGRESS, DONE].
No command hardcodes a literal status. Each binds to one of three roles:
| Role | Meaning | Default |
|---|---|---|
ready | What tasks ready looks for | first status |
claimed | What tasks claim moves to | second status |
done | What satisfies another task's depends_on | last status |
That indirection is what lets a project use [TODO, DOING, REVIEW, SHIPPED] and keep
every command working. It also means an agent must never assume the default names — read
the real list from .tasks/config.yml, or from an error's details.
Transitions
The default rule is: step forward or back by exactly one.
tasks claim tasks status … DONE
┌──────────────────────┐ ┌──────────────────────────┐
│ ▼ │ ▼
╭─────────╮ ╭─────────────╮ ╭──────╮
│ PENDING │ │ IN_PROGRESS │ │ DONE │
╰─────────╯ ╰─────────────╯ ╰──────╯
▲ │ ▲ │
└──────────────────────┘ └──────────────────────────┘
tasks release tasks claim (reopen)Two consequences worth internalising:
PENDING → DONEis rejected with exit4. Nothing reaches done without having been worked, which is exactly the mistake an over-eager agent makes.- The last status is deliberately not terminal. Stepping back from it is the supported reopen path, because agents do mark things done in error.
Supplying an explicit transitions list in config.yml replaces this rule rather
than layering on it. See custom statuses.
Claims
tasks claim does three things at once: it checks the transition is legal, checks
dependencies are satisfied, and records who took the task and when.
tasks claim token-schema --as agent-7It is deliberately not the same as tasks status <id> IN_PROGRESS — that form is
refused with exit 2 and redirected here, so claim metadata can never be bypassed.
The claim is only useful if it happens before the work. It is what stops a second agent starting the same task; claiming afterwards records history but prevents nothing.
Internally the claim takes an exclusive O_EXCL lock in .tasks/.locks/ and re-reads
state inside it, so two concurrent processes cannot both win. A lock left behind by a
crashed process goes stale after 10 seconds and is reclaimed.
One filesystem, not one team
Locking protects concurrent agents on a single checkout. It does not coordinate across separate clones — two agents on two clones can both claim the same task and only find out at merge. For multi-clone work, treat claims as advisory.
Leaving the claimed status clears claimed_by and claimed_at automatically, whether
you got there via tasks status or tasks release.
Dependencies
depends_on is a list of references. A reference is either a bare id in this project,
or <relative-path>:<id> pointing into another repository's .tasks/:
depends_on: [token-schema, ../upstream-repo:publish-endpoint]A dependency is satisfied when the referenced task is in the done role — of its own
project, which matters for cross-repo references.
Everything else counts as blocking. Missing task, archived task, unresolvable path,
cycle: all blocking, never satisfied. The asymmetry is intentional. A typo that blocks
costs you a delay and a puzzled look at tasks dep list. A typo that reads as satisfied
sends an agent off to build on something that does not exist.
tasks dep list wire-refresh DEPENDENCY SATISFIED DETAIL
token-schema no token-schema (PENDING)Prefer several small dependent tasks over one large one. depends_on is what makes
tasks ready meaningful; a single task with a ten-item checklist in its body tells the
next agent nothing about what is unblocked.
See dependencies for resolution rules and every
reason code.
Ready
tasks ready is the query the whole design exists to answer: what can be worked on
right now?
A task is ready when its status is the ready role and every dependency is satisfied.
Nothing else qualifies — a claimed task is not ready, and a blocked task is not ready no
matter how long it has been waiting.
tasks ready --json | jq -r '.tasks[0].id'An empty result is a successful query, not an error: exit 0 with
{"count": 0, "tasks": []}. Agents must not treat "nothing to do" as a failure.