Directory layout
Where .tasks/ lives, how an implementation finds it, and what it may contain.
Discovery
An implementation MUST locate the project by walking from a starting directory toward
the filesystem root, testing each ancestor for a directory named .tasks. The first
match is the project; the search MUST stop there.
- The directory containing
.tasks/is the project root. - An implementation MUST NOT merge a discovered tree with any tree found further up.
- If no ancestor contains
.tasks/, the operation MUST fail as not found.
Non-normative
Nearest-wins with no merging is what makes a monorepo package with its own
.tasks/ an independent scope. It matches how git finds .git/, so the
behavior is already familiar and needs no explanation to users.
Contents
.tasks/
config.yml OPTIONAL project configuration
README.md OPTIONAL human-facing description
.gitignore OPTIONAL
_index.md OPTIONAL generated overview
<phase>/ zero or more phase directories
<id>.md one task
.archive/ OPTIONAL archived tasks, flat
.locks/ OPTIONAL transient lock filesA .tasks/ directory containing nothing but phase directories is valid. Every file
above is OPTIONAL.
Phase directories
A phase directory name MUST match:
^[a-z0-9][a-z0-9._-]*$Every entry in .tasks/ that is a directory, is not .archive or .locks, and whose
name matches that pattern is a phase. Its name is the phase of every task inside it.
An implementation MUST create a phase directory on demand when a task is placed in it, and SHOULD remove a phase directory that it empties.
Nested phase directories are not defined by v1. An implementation MUST NOT descend into subdirectories of a phase when enumerating tasks.
Tasks
Each task is one file, directly inside a phase directory, named <id>.md where <id>
is that task's id. See task file.
A file inside a phase directory that does not end in .md MUST be ignored. A .md file
that cannot be parsed as a task MUST be reported as malformed rather than silently
skipped.
.archive/
Archived tasks live directly in .tasks/.archive/, named <id>.md. The archive is
flat — it has no phase subdirectories — so an archived task's originating phase is
carried only by its frontmatter phase field.
An implementation:
- MUST exclude archived tasks from every default query.
- MUST treat an archived task as not satisfying a dependency.
- MUST treat an archived id as reserved: creating a new task with that id MUST fail.
Non-normative
Reserving archived ids is what stops a new task from silently inheriting a
deleted task's inbound dependencies. Without it, depends_on: [old-id]
elsewhere in the tree would quietly re-point at unrelated work.
.locks/
.tasks/.locks/ holds transient lock files used to serialize concurrent mutation. See
concurrency.
It MUST NOT be committed to version control. An implementation that scaffolds a project
SHOULD write a .tasks/.gitignore containing .locks/.
_index.md
_index.md is a derived view of the tree, regenerated on demand.
- An implementation MUST NOT treat it as a source of truth, and MUST NOT read it to answer any query.
- An implementation MAY generate it. Its content and formatting are not specified.
- An implementation that detects it is out of date MUST report that as a warning, never an error.
Non-normative
It exists because .tasks/ is committed and browsed on the web, where a
directory of markdown files is hard to skim. Since it is committed, a stale
one misleads reviewers in a diff — hence the warning. Since it holds no truth,
failing a build over it would be absurd — hence never an error.
Writes
Every write of a task file, config.yml, or _index.md MUST be atomic: the
implementation writes to a temporary file in the same directory and renames it into
place. A reader MUST never be able to observe a partially written file.
Serialization MUST be canonical — see task file — so that repeated writes of unchanged state produce identical bytes and a file's diff shows only what actually changed. An implementation MAY additionally skip a write whose result would be byte-identical, but is not required to.
Relocation
Moving a task between phases MUST be performed as write destination, then remove source — never the reverse, and never a bare rename that could leave neither.
Interrupted between the two steps, this leaves the same task present in two phases. That is a detectable, repairable state; losing the task is not.
Concurrency
An implementation that mutates a task in a way that depends on that task's current state MUST do so under an exclusive lock, and MUST re-read the task's state from disk inside the lock before deciding.
- The lock MUST be acquired by an operation that atomically fails if the lock already
exists (
O_EXCLor equivalent). - A lock MUST be scoped to a single task id, not to the tree.
- A lock older than a defined staleness threshold MUST be reclaimable. The reference implementation uses 10 seconds; an implementation MAY choose another value and SHOULD document it.
Scope of the guarantee
This serializes processes sharing one filesystem. It does not coordinate across separate clones of a repository. Two agents working in two checkouts can both claim the same task and will discover the conflict only at merge. An implementation MUST NOT present claims as a distributed lock.
Version control
.tasks/ is intended to be committed. An implementation MUST NOT require any state
outside the project directory — no database, no cache directory, no user-level
configuration — to interpret a tree correctly.
A fresh clone of a repository MUST yield identical query results to the checkout it was cloned from, given the same implementation.