cairn

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.