# Core concepts (/docs/concepts)



There are five ideas in the whole system. Once these land, the command reference is
mostly mechanical.

## The tree [#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 [#phases]

A phase is a **directory**, not a tag:

<Files>
  <Folder name=".tasks">
    <Folder name="auth-rework">
      <File name="token-schema.md" />

      <File name="wire-refresh.md" />
    </Folder>

    <Folder name="2026-q1">
      <File name="migrate-logging.md" />
    </Folder>
  </Folder>
</Files>

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](/docs/reference/configuration) to make `list`
and `ready` default to one phase without typing `-p` every time.

## Statuses and roles [#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 [#transitions]

The default rule is: &#x2A;*step forward or back by exactly one.**

```txt
                tasks claim              tasks status … DONE
        ┌──────────────────────┐   ┌──────────────────────────┐
        │                      ▼   │                          ▼
   ╭─────────╮           ╭─────────────╮                  ╭──────╮
   │ PENDING │           │ IN_PROGRESS │                  │ DONE │
   ╰─────────╯           ╰─────────────╯                  ╰──────╯
        ▲                      │   ▲                          │
        └──────────────────────┘   └──────────────────────────┘
              tasks release              tasks claim (reopen)
```

Two consequences worth internalising:

* `PENDING → DONE` is rejected with exit `4`. 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](/docs/guides/custom-statuses).

## Claims [#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*.

```sh
tasks claim token-schema --as agent-7
```

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

<Callout type="warn" title="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.
</Callout>

Leaving the claimed status clears `claimed_by` and `claimed_at` automatically, whether
you got there via `tasks status` or `tasks release`.

## Dependencies [#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/`:

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

```sh
tasks dep list wire-refresh
```

```txt
 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](/docs/reference/dependencies) for resolution rules and every
`reason` code.

## Ready [#ready]

`tasks ready` is the query the whole design exists to answer: &#x2A;*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.

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