# Configuration (/docs/spec/configuration)



`.tasks/config.yml` is OPTIONAL. A project without one is configured entirely by the
defaults below, and MUST be treated as valid.

```yaml title=".tasks/config.yml"
version: 1
active_phase: auth-rework
statuses: [TODO, DOING, REVIEW, SHIPPED]
roles:
  ready: TODO
  claimed: DOING
  done: SHIPPED
transitions:
  - [TODO, DOING]
  - [DOING, REVIEW]
  - [REVIEW, SHIPPED]
  - [SHIPPED, DOING]
```

## Schema [#schema]

<TypeTable
  type="{
  version: {
    description: &#x22;Convention version this project is written against.&#x22;,
    type: &#x22;1&#x22;,
    default: &#x22;1&#x22;,
  },
  active_phase: {
    description:
      'Default phase scope for queries when the caller specifies neither a phase nor &#x22;all phases&#x22;.',
    type: &#x22;string&#x22;,
  },
  statuses: {
    description: &#x22;Ordered list of lifecycle states. Minimum two entries.&#x22;,
    type: &#x22;string[]&#x22;,
    default: &#x22;[PENDING, IN_PROGRESS, DONE]&#x22;,
  },
  roles: {
    description:
      &#x22;Maps behavioural roles onto status names. Each key independently optional.&#x22;,
    type: &#x22;{ ready?, claimed?, done? }&#x22;,
    default: &#x22;first / second / last of statuses&#x22;,
  },
  transitions: {
    description:
      &#x22;Explicit legal [from, to] pairs. Presence replaces the default rule entirely.&#x22;,
    type: &#x22;[string, string][]&#x22;,
  },
}"
/>

Unknown top-level keys MUST be **rejected**, failing the operation as a usage error.

<Callout type="info" title="Non-normative">
  Rejecting rather than ignoring is deliberate: a typo like `status:` for
  `statuses:` would otherwise leave the project silently running on defaults,
  and the resulting behavior would be baffling. This is the one place the
  convention chooses strictness over tolerance, because the file is small,
  closed, and hand-edited.
</Callout>

Invalid YAML, or a document that is not a mapping, MUST fail as a usage error naming the
file.

## Statuses [#statuses]

`statuses` is an **ordered** list of at least two distinct strings. Order is significant:
it determines both the role defaults and the default transition rule.

Status names are opaque. An implementation MUST NOT attach behavior to any particular
name — not `DONE`, not `BLOCKED`, not any other.

## Roles [#roles]

Every behavior binds to a **role**, never to a literal status name:

| Role      | Meaning                                                | Default                    |
| --------- | ------------------------------------------------------ | -------------------------- |
| `ready`   | The status a task must hold to be a candidate for work | first entry of `statuses`  |
| `claimed` | The status a task moves to when claimed                | second entry of `statuses` |
| `done`    | The status that satisfies another task's dependency    | last entry of `statuses`   |

Each key of `roles` is independently optional; an absent key takes its default.

Every role value MUST name a member of `statuses`. A role naming an unknown status MUST
fail as a usage error.

<Callout type="info" title="Non-normative">
  This indirection is the reason a project can rename its whole lifecycle to
  `[TODO, DOING, REVIEW, SHIPPED]` and have every implementation keep working.
  It also means a consumer reading a foreign tree must resolve roles from that
  tree's config before interpreting any status — including when judging a
  cross-repository dependency.
</Callout>

## Transitions [#transitions]

### Default rule [#default-rule]

When `transitions` is absent, a transition from status `a` to status `b` is legal if and
only if:

```txt
|index(b) − index(a)| == 1
```

Positions are taken in `statuses`. A transition from a status to itself is never legal.

Two properties follow, and both are intended:

* **No forward skipping.** A task cannot reach the last status without passing through
  every intermediate one.
* **The last status is not terminal.** Stepping back from it is legal, and is the defined
  way to reopen completed work.

### Explicit rule [#explicit-rule]

When `transitions` is present it **replaces** the default rule entirely. It is not
additive. Exactly the listed ordered pairs are legal; every other transition, including
every backward one, is illegal.

Each element MUST be a two-element sequence of status names, and every name MUST be a
member of `statuses`. A name outside `statuses` MUST fail as a usage error.

<Callout type="warn" title="Replacement, not augmentation">
  An implementation MUST NOT merge explicit transitions with the default step
  rule. A project that lists only forward pairs has, correctly, made release and
  reopen impossible.
</Callout>

## Coherence [#coherence]

Configuration MUST be validated when it is loaded, not when a command that depends on it
happens to run. In addition to the per-field rules above:

* The transition `roles.ready → roles.claimed` MUST be legal. A configuration where it is
  not makes claiming impossible by construction and MUST be rejected outright.

An implementation SHOULD name the offending key and suggest a repair in the error.

```txt
config.yml: the transition TODO -> REVIEW (roles.ready -> roles.claimed) is not legal,
so `tasks claim` could never succeed. Add it to `transitions` or adjust `roles`.
```

<Callout type="info" title="Non-normative">
  Only the `ready → claimed` edge is mandated because it is the one whose
  absence makes the project inert rather than merely awkward. A missing `claimed
    → ready` edge just means this project does not support releasing a claim,
  which is a legitimate choice.
</Callout>

## `active_phase` [#active_phase]

When set, `active_phase` is the default scope for queries whose caller did not specify
one. Scope resolution order is:

1. An explicit phase supplied by the caller.
2. An explicit "all phases" request by the caller, which yields no scope.
3. `active_phase`, if set.
4. Otherwise, no scope — the whole tree.

`active_phase` MUST NOT restrict anything other than query scope. Direct addressing of a
task by id, dependency resolution, and validation MUST always consider the entire tree.

<Callout type="warn" title="A common confusion">
  Because `active_phase` silently narrows queries, a task outside it appears not
  to exist. An implementation SHOULD make the active scope visible in query
  output.
</Callout>

## Migration [#migration]

Changing `statuses` does not rewrite existing task files. Tasks holding a status no
longer in the list become validation errors, and an implementation MUST NOT guess a
mapping to repair them.

An implementation SHOULD document that the safe migration is to widen `statuses` to
include both old and new names, move tasks across with normal status transitions, then
narrow the list again.
