Task files & config
File layout
.cairn/
config.yaml # engine config
tasks/
PROJ-001.md # filename = task id; lookups are a direct file open
runs/ # gitignored check-run logsTask file format
A task is Markdown: YAML frontmatter (machine fields) plus a Markdown body (human intent). The engine never edits the body after creation.
Minimal valid task
---
id: PROJ-001
title: Fix the thing
status: backlog
---Three required fields: id, title, status. Everything else is optional.
Full-shape task
---
id: PROJ-002
title: Add idempotency keys to payment webhook
status: in_progress
assignee: agent:claude-1
deps: [PROJ-001]
context: # opaque to the engine; for humans/agents
files: [internal/payments/webhook.go]
checks:
- desc: duplicate webhook returns 200 no-op
cmd: go test ./internal/payments -run TestIdempotent
timeout: 300 # optional seconds; else check_timeout_default
cwd: . # optional; relative to repo root
result: pending # pending | pass | fail (engine-managed)
- desc: reviewed by a human
type: manual # no cmd ⇒ manual; result set by attestation
result: pending
provenance: # append-only audit log (engine-managed)
- {who: human:shah, at: 2026-06-21T10:00:00Z, did: created}
---
Prose intent and constraints go here.Field ownership
| Field | Owner | Notes |
|---|---|---|
id | engine | assigned at create: prefix + time-ordered base32 token; sorts by creation time; never reused |
title | caller | free text |
status | engine | one of config.states |
assignee | engine | set by claim (human:<name> / agent:<name>) |
deps | caller | task ids that must be closed first |
context | caller | opaque map; engine never interprets it |
checks | caller | see gates |
provenance | engine | append-only; one entry per write |
| any other key | (none) | preserved verbatim, ignored by the engine |
Unknown-key preservation is guaranteed. Add priority: high and it survives, with ordering and comments intact, across engine writes. (Writes edit the YAML node surgically, never a struct round-trip.)
config.yaml
prefix: PROJ # id prefix
counter: 2 # deprecated, unused; retained so existing configs still parse
states: [backlog, in_progress, in_review, done, canceled]
closed: [done, canceled] # subset of states considered "closed"
initial: backlog # state new tasks start in
check_timeout_default: 120 # seconds, when a check omits timeoutstatesare free strings you define; there is no hardcoded status enum.closeddrives deps-readiness and the checks gate.- Ids are minted at create as
prefixplus a time-ordered, collision-resistant base32 token, so concurrent creators in separate clones never collide: no shared counter, no merge conflict.counteris retained only for backward-compatible parsing and is no longer incremented.
Dependencies
- A task is ready when every id in
depsis in aclosedstate.readyis derived on read, never stored. - Gate point is START: a task can't leave
initialuntil its deps are closed. Deps do not gate closing. - A dep id not present in
tasks/(dangling) or a cycle is a hard error on load. Loud failure beats a silently-stuck task.
Gates
Transitions are free (any state to any state) except two gates:
- Deps gate: can't leave
initialunless all deps are closed. - Checks gate: can't enter a
closedstate unless allcheckspass.- Zero checks ⇒ passes vacuously.
- On closing, if checks aren't already all
pass, the engine auto-runs thecmdchecks, then closes on all-pass or refuses on any fail.
Reopening a closed task is allowed. Check results are not reset on reopen; they keep their last value, so a re-close reuses them.
TIP
For the gates and checks model (manual checks, exit codes, and run logs) see Checks & gates.
Checks
- A check with a
cmdis executed viash -c "<cmd>"; any shell line works (go test ./...,pytest -q && ruff check .,./scripts/verify.sh). - A check without a
cmdis manual: itsresultis set by attestation, not execution. A pending manual check blocks closing until resolved. - Exit code
0=pass, non-zero =fail. On timeout the process (and its group) is killed,result: fail. - Output (stdout+stderr, ~8KB tail) goes to
.cairn/runs/<id>-<timestamp>.log; the task file stores onlyresult:for clean diffs.
Provenance
Every write appends one entry {who, at, did[, text]}, stamped with the server's --actor. It is the task's append-only audit trail. note adds an entry with text and no state change.