On-disk format
The files katra writes are its public API. They are markdown in your repository, readable and editable without the tool, and they outlive it. This page is the contract.
Layout
katra/
config.yml title, accent, hook behaviour, memory settings
entries/ one .md per post
tasks/ one .md per task
epics/ one .md per epic
decisions/ one .md per decision record
articles/ longer-form writing not tied to a commit
media/ images, gifs, video, html embeds
.state/ machine state — ledger, receipts, sessions (gitignored)
A directory is a katra if and only if it contains config.yml. The
conventional name is katra/ at the repo root; devlog/ is the pre-rename
name and is still discovered, so repositories created before the rename keep
working without migration.
Filenames are YYYY-MM-DD-slug.md. The date prefix orders the directory; the
slug is derived from the title. Neither carries identity — the file is the
node, and renaming it renames the node.
Frontmatter
YAML, then the markdown body. Every node type shares one schema; which keys are
meaningful depends on type.
---
title: Reworked the swing arc
date: "2026-08-03"
tags: [physics, gameplay]
hash: 5ddc0f5
summary: tuned the magnus model
---
The magnus model was fighting the animation, not the physics.
hash is the only key doing real work above: its presence is what makes this
entry logged rather than a draft. The full key set, by what it applies to:
Common to every node
| Key | Type | Notes |
|---|---|---|
title |
string | Required. |
date |
string | YYYY-MM-DD. Quoted, because YAML would otherwise parse it as a date. |
time |
string | HH:MM:SS. Orders nodes created on the same day. |
type |
string | entry (or absent), task, epic, decision, article. |
tags |
list | |
summary |
string | One line, shown in the index. |
An absent type means entry. That is the back-compatibility hinge: every
entry written before the node model existed still parses.
Entries
| Key | Type | Notes |
|---|---|---|
hash |
string | The commit this entry describes. Its presence is what makes the entry not a draft. |
hashes |
list | For a chapter spanning several commits. Use instead of hash. |
stat |
mapping | {f: files, a: added, d: deleted} — the computed diffstat. |
cover |
string | Path to a banner image. |
featured |
boolean | Lands in the “Deep Dives” zone. |
pinned |
boolean | Held at the top of the index. |
closes |
list | Task slugs this entry completes. Consumed at stamp time. |
advances |
list | Task slugs this entry moves forward (todo → doing). |
---
title: Reworked the swing arc
date: "2026-08-03"
time: "14:22:07"
tags: [physics, gameplay]
hash: 5ddc0f5
stat: {f: 12, a: 340, d: 50}
summary: tuned the magnus model
---
Tasks, epics and decisions
| Key | Applies to | Values |
|---|---|---|
status |
task | todo, specced, doing, done, cut |
status |
epic | planned, active, done, cut |
status |
decision | proposed, accepted, superseded, deprecated |
spec |
task | Reference to the committed spec artifact. Resolved as a node slug in the katra first, otherwise a path relative to the repository root (not the katra directory). Legal in any status; setting it never moves a status backwards. |
effort |
task | S, M, L |
horizon |
task | now, next, later |
epic |
task | Parent epic slug. |
entry |
task, decision | The entry slug that recorded or occasioned it. |
supersedes |
decision | Slugs this decision replaces. |
superseded-by |
decision | The mirror of the above. |
specced sits between todo and doing: a design exists, committed, and the
task points at it, but nothing has been built yet. It is optional — todo →
doing is still a legal transition, and katra epic rollup treats a
specced-only epic the same as planned, because a spec is thinking, not
work.
A draft is an entry with no hash
That is the entire state machine, and the most important thing to know about the format.
- No
hashand nohashes→ a draft. It renders in the In Progress panel. - Either present → logged. It renders in the log, with its diffstat.
There is no status: draft, no separate directory, and no scratch file. Adding
a hash is what publishes an entry, which is why stamping is the only publish
step and why nothing can get stranded.
A consumer deciding whether an entry is a draft should test for the absence of both keys, not for the presence of a status field.
Wikilinks
[[slug]] in any body links to another node. The renderer resolves the slug
against the node set and marks unresolvable links with a
dl-wikilink-missing class rather than failing — a link to a node you have not
written yet is a valid thing to write.
Links are the graph. There is no separate index file to keep in sync, and no link database: the graph is recomputed from the bodies on every render.
Rich components
A fenced code block whose language is registered renders as a component; see Components for the six built in and the keys each takes.
An unregistered language renders as an ordinary code block. This is a compatibility guarantee, not a fallback: an entry using a component your katra does not have still renders, showing the block’s YAML as source. Nothing errors, and nothing is lost. It is why adding a component is a safe, additive change.
Media
katra/media/ holds everything an entry references. Paths in frontmatter and
in component bodies are relative to the katra directory (media/shot.png), not
to the entry file.
katra capture imports a file, giving it a collision-free name. katra doctor
reports references that point at files which are not there — the one integrity
check the format needs, because everything else is just markdown.
The compatibility rules
For anyone writing a tool that reads or writes a katra, and for anyone changing katra itself:
- An unknown frontmatter key is ignored, never an error. Adding an optional
key is a safe change. A consumer older than
specignores it and shows the task with no reference at all — only the prose written from it is lost. - A new status value degrades silently, the same way. A consumer older
than
speccedfinds it outside every switch it wrote; it must fall into whatever default bucket it already has (aspeccedtask rendering astodo, say) rather than erroring or vanishing from a count it used to appear in. - An unknown fence language degrades to a code block.
- An unknown
typeis not an entry. Consumers filtering for entries should matchtypeabsent orentry, rather than excluding the types they know. - Renaming or repurposing a key is a breaking change, and so is making an optional key required. Somebody’s 2026 entry has to render next year.
The static build’s data.json is a rendering, not the format. It is
regenerated from the markdown on every build, its shape follows the viewer’s
needs, and it carries no compatibility promise. Read the markdown.
What is deliberately not in the format
- No entry ids. The file path is the identity. This makes
git mva rename and makes a hand-edited file still valid, at the cost of making a slug change break inbound wikilinks — whichkatra doctorwill tell you about. - No ordering field. Date plus time plus filename is the order. An explicit
order:would be a second source of truth that drifts. - No author field. Git already knows. A katra is per-repository, and the commit it is stamped with carries the author.