Durability
The repository is the database, which puts the whole weight of the design on the write path.
Writes are atomic
Every file cairn writes — items, the roadmap, the configuration, exports — goes to a temporary file beside its target, is flushed to the device, and is renamed over it. Rename is atomic within a filesystem everywhere cairn runs, so an interruption leaves either the old contents or the new ones. Nothing is truncated in place.
Line endings survive
An item file is read with either line ending and written back with the one it
arrived with. Without that, a checkout configured with core.autocrlf would
have every write reformat the whole file, and a backlog shared across platforms
would churn continuously.
New items are written with line feeds, and cairn init installs a
.gitattributes pinning the item directory to eol=lf, so a repository has one
answer regardless of how each contributor’s client is configured.
One damaged file does not stop everything
Read-only commands — list, next, board, search, roadmap — report a
file they cannot parse and carry on with the rest. A backlog should not become
unreadable because one file is mid-edit.
Everything else refuses. check, render, export and every command that
writes stop with an error, because acting on a partial view is how data is lost:
rendering a roadmap that silently omits an item, or allocating an identifier
that a broken file already holds.
Branches merge without a fight
A new item’s number is one more than the highest any worktree or branch this clone has seen, so branches made on one machine never collide. The generated roadmap can still conflict: it is derived, so regenerate it after the item files have merged.
cairn init --git
That registers a merge driver for the roadmap and a post-merge hook that
repairs a collision from another machine and re-renders after the items
settle. It is safe on an existing project, and
doing it twice changes nothing.
The ordering constrains the design and is worth knowing. Git resolves paths in
index order and ROADMAP.md sorts before cairn/items/, so when the driver
runs the items it would render from are still unmerged. It therefore keeps your
side and the hook replaces it once the items settle.
Identifiers
Items are numbered, and each carries a hidden uid tag that never changes.
Two machines that had not fetched each other can still give the same number to
different work. cairn check reports it, and cairn renumber — run for you by
the merge hook — keeps the published item’s number, moves the arriving one,
and uses the tag to move exactly the references that came with it.
From format 4, migration restores every number items had and keeps each UUID
as the tag; bodies and history are untouched. Commit the project, migrate once,
and share that commit instead of migrating independent branches. If
interrupted, rerun cairn migrate; do not delete the recovery journal.
The format will not move under you
A project records the format version it uses. A minor release may add optional keys and nothing else; removing a key or changing its meaning needs a new version and a migration; a reader preserves keys it does not recognise, so an older cairn cannot silently strip what a newer one wrote.
A project written in a format this build does not know is refused with both numbers named, rather than read on a best-effort basis. Misreading data is worse than declining to read it.
What is tested
The frontmatter parser handles input typed by people, generated by models and carried in from other trackers, so it is property-tested rather than only exercised by examples: what cairn writes it reads back unchanged, rendering twice is byte-identical, line endings survive, and arbitrary bytes may be rejected but never cause a crash.
A corpus of item files, including ones cairn would never write, must keep parsing to the same values forever. And a soak test drives long random sequences of ordinary operations, checking after every step that the backlog still holds together — it found three real bugs on its first afternoon.