State machine
Default states
[states]
allowed = ["todo", "in_progress", "blocked", "done"]
terminal = ["done"]
A task starts in todo when created. The first claim transitions it to in_progress. blocked is for work paused awaiting external input. done is terminal.
Configurable extension
Teams that want a richer flow can extend the state list:
[states]
allowed = [
"todo", "ready", "claimed", "in_progress",
"needs_review", "changes_requested", "verified", "merge_ready",
"done", "blocked", "failed",
]
terminal = ["done", "failed"]
transitions = [
["todo", "ready"],
["ready", "claimed"],
["claimed", "in_progress"],
["in_progress", "needs_review"],
["needs_review", "changes_requested"],
["changes_requested", "in_progress"],
["needs_review", "verified"],
["verified", "merge_ready"],
["merge_ready", "done"],
["in_progress", "blocked"],
["blocked", "in_progress"],
["*", "failed"],
]
transitions is optional. When omitted, any-to-any transitions are allowed (subject to the invariants below). When present, only listed transitions are accepted; * on the from side is a wildcard.
Invariants (always enforced)
- The initial transition for a new task must have
from_status = NULL, andto_statusmust be inallowed. - A task in a terminal state cannot be transitioned out unless
fairway set-status --reopenis used (writes an explicit history row withreason = "reopen"). - Every transition writes one
task_state_historyrow in the same SQL transaction as thetask_stateupdate. - The
actorcolumn on history is populated from the active session ID when known, otherwise<os_user>@<host>— never NULL.
Hierarchy
The state machine applies uniformly to every node in the task tree (see hierarchy.md). An epic transitions to done the same way a leaf does. An epic's status is independent of its descendants — you can mark an epic done with open children; fairway warns but does not refuse. The dashboard shows both the explicit status and the X/Y descendants-done rollup.
Rationale for shipping 4 states by default
An earlier consumer orchestrator proposed an aspirational 11-state model that was never load-bearing in its store. Hardcoding it would bake in unvalidated distinctions (merge_ready vs verified vs done had no enforcement). Config-driven states let evidence from actual use determine whether a richer flow is worth the operator overhead.
If three months of usage shows everyone configuring the same 11 states, those will be promoted to a built-in [states] preset = "verified-merge" so users do not have to copy the same TOML block.