Working with agents
The loop an agent should follow, the skill that teaches it, and the rules that bite.
The CLI is usable by a person, but it exists because agents need somewhere to put state that outlives a context window. This page is what you would tell an agent — and, via the bundled skill, what it gets told automatically.
The skill
tasks init installs an agent skill into
.claude/skills/tasks/. It is what makes an agent reach for this CLI on its own rather
than inventing a TODO.md: it documents when persistent tasks beat a session checklist,
the claim-before-working loop, the invariants, and the JSON contract.
tasks skill install [--force]Add it to a project that predates it, or refresh it after upgrading the CLI, with the
command above. An existing install is left alone unless --force, so local edits
survive an accidental reinstall.
Commit .claude/skills/tasks/ so every agent working in the repo gets it.
Skip it if you want
tasks init --no-skill scaffolds .tasks/ without touching .claude/.
The loop
tasks ready --json # unblocked and available right now
tasks claim <id> # take it — records who and when
# ...do the actual work...
tasks status <id> DONE # complete itTwo things about this that are easy to get wrong:
Claim before working, not after. The claim is what stops a second agent starting the same task, and it is only useful if it happens first. Claiming on the way out records history but prevents nothing.
Update status as you go, not in a batch at the end. A session that dies mid-task
leaves a claimed task, which is recoverable — you can see who had it and when. A session
that dies leaving everything PENDING loses the work record entirely.
Before anything else
tasks list --jsonExit 3 with No .tasks/ directory found means the project is not set up. Ask before
running tasks init — creating it is a project-level decision, not a side effect of a
question about work.
If tasks is not on PATH, use npx @speekl/tasks in place of tasks everywhere.
Rules that will bite
Every field has a command: status, claim, release, dep add / dep rm, move.
Editing the YAML directly bypasses the transition and dependency checks that make the
tree trustworthy.
The body below the frontmatter is yours — edit it freely with normal file tools. That is
in fact the expected way to write a task: tasks new scaffolds the headings and returns
a path; you open that path and fill it in.
The directory determines the phase. Use tasks move <id> <phase>, which relocates the
file, rewrites the frontmatter, and cleans up an emptied phase directory.
By default a task steps forward or back exactly one status, so PENDING → DONE is
rejected with exit 4. Claim it first. The error's details.allowed tells you what is
legal from where you are.
Exit 2, redirecting to tasks claim. This exists so claim metadata can never be
bypassed — anything that reaches the claimed status has a recorded owner and timestamp.
Pick a different task from tasks ready. Do not reach for --force.
--force on claim overrides dependencies, not other people's claims. Only use it when
the user has explicitly said the dependency does not apply.
To reopen a completed task, tasks claim <id> steps it back from DONE. Agents mark
things done in error; this is the supported recovery, not a hack.
PENDING / IN_PROGRESS / DONE are defaults, not guarantees. A project may use
[TODO, DOING, REVIEW, SHIPPED]. Read the real list from .tasks/config.yml, or from an
error's details.statuses, rather than assuming.
Creating tasks well
tasks new <phase> <name...> [--id <id>]
tasks dep add <id> <ref>...The phase directory is created on first use. The id defaults to a kebab-case slug of the
name and must be unique across the whole tree, including .archive/.
Write the body after creating. tasks new scaffolds ## Description,
## Acceptance criteria, and ## Notes; fill them in with a normal file edit at the
path returned in the JSON. A task whose body is empty is not a task, it is a reminder.
Put enough in it that a different agent arriving cold in a later session can do the work
without asking.
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.
Parsing output
Use --json whenever you parse. The plain tables are for humans and their layout is not
stable. See the JSON contract for shapes and codes.
id=$(tasks ready --json | jq -r '.tasks[0].id // empty')
[ -n "$id" ] && tasks claim "$id"Without -p / -a, list and ready scope to active_phase from .tasks/config.yml
when it is set. If a task you expect is missing, retry with --all before concluding
it does not exist.
When something looks wrong
Run tasks validate --json before hand-repairing anything. It reports id/filename
mismatches, phase drift, unknown statuses, broken or cyclic dependencies, and a stale
_index.md. --fix repairs the mechanically safe subset and leaves the rest for a
human.
Do not fix drift by editing YAML. See troubleshooting.