The TUI tour
Keel opens on the editor login — a centered form, password only if the yard
already knows you (a stored session that still validates skips the screen; F2 enters
local dev mode with no yard at all). Everything after that is one screen: the C,
under a clickable toolbar. The C never rearranges itself. The toolbar, top strip and bottom strip
are permanent, the rail is permanent, and only the center pane changes as you move between views.
Two modes rule the keyboard, and the badge top right always names the one you are in:
INTERACTIVE, where every key drives the app (←→ moves the toolbar focus, Enter
activates, Tab steps the lens strip, j/k the content), and EDITING, where every
plain key belongs to your prose. i drops you in, typing immediately;
Esc taps back out. The whole surface answers the mouse: click a chip, a lens tab, a
rail row, a codex name, a worldline mark — click a selection again to open it; the wheel scrolls
lists, overlays, and preview pages.
Views and lenses are different axes
The toolbar changes the view — what fills the big center pane: click a chip, or
←→ then Enter in interactive mode. Tab steps the lens — what the
narrow rail shows (⇧Tab steps back). They are independent: you can write while the rail's ledger
lens keeps your unpaid promises in the corner of your eye. The rail is a glance; the views hold
the detail.
| keel | the spine tree — arc rows in bold ▸, scene rows with ○ draft · ◐ in-review · ● canon — with the frontier warning on the border |
| frames | the reader's chapters |
| codex | entity names (the world bible) |
| ledger | promises (✓ paid · … hanging) and facts |
| review | amendments and their state |
Map
Walk the spine. The tree is two levels — arc rows in bold, their scenes beneath in the arc card's order. The selection lives in the rail tree; the rail's detail box shows the selected scene's contract — id and status, POV, story time, budget, the overview, and the IN/OUT conditions — and the center pane shows a read-only preview of the prose you would be editing. Arcs hold no prose — select a scene to preview it.
| j / k, ↓ / ↑ | move the selection through the tree |
| Enter | open the selected scene directly in the editor |
Write
The editor — the pane grows a thick ✎ border so there is never any doubt you are
holding the pen. Entering the view checks the scene out (or resumes your working file); if someone
else holds the lock you get a read-only view naming the holder. You edit the prose body
only — the YAML contract is held aside and re-attached on ^s. When the body carries
@page markers the foot shows page 2/5, and ^PgUp/^PgDn
jump between the seams. The full treatment is in The editor.
Codex
The world bible: entity list on the left, the selected entity's prose on the right.
| j / k, ↓ / ↑ | select an entity |
Review
Amendments (changesets) and their state — open, landed, abandoned — with pending counts. The mechanics live in Amendments & blast radius.
ledger
Promises above, facts below — the story's memory. See The ledger.
yard
A placeholder today — the hosted admin surface reaches the TUI with hosted series. The web yard itself is live now: see The yard.
worldline
Every scene as a mark on a horizontal line. Chronological mode orders them as the reader meets them; temporal mode segments the axis into the calendar's eras and places each mark by its story-time year. Full story: worldline & calendar.
| ↑ / ↓ (j / k) | select a mark |
| h / l | scroll the line |
| t / c | temporal / chronological |
| Enter | open that scene in the editor |
The palette and the command console
^p opens the command palette: type to filter, Enter runs — every view, the mode
toggle, the changelog, quit. Type : as the first character and the palette becomes the
command console: any keel command runs right there, output shown in an overlay —
:snapshot draft-one, :export --epub, :promise list. Nothing
requires leaving the TUI.
Overlays
| ? or F1 | the help overlay — the full key and concept reference, always available |
| ^k | the comment threads on the open scene — authored on the yard's scene edit page; d marks a thread done |
| ^s | save — and if the prose holds a @ref naming an entity that doesn't exist, the quick-capture overlay opens: j/k select it, h/l pick its kind (character · location · faction · lore), Enter creates the stub without leaving the scene. Check-in warns about any you skip. |
| ^g | davy — the context is pre-warmed in the background, so it starts instantly |
| changelog | from the palette — what changed in this version |
| j / k · Esc | scroll · close, in any overlay |
?. It opens the
in-tool manual from anywhere. And q always leaves — guarded, never losing unsaved work.The writing workflow
Keel's thesis is that prose is written against a contract. The daily loop below is the whole method: structure first, words second, and when the words outgrow the structure — and they will — the structure is amended explicitly, with the damage computed, instead of letting the manuscript drift away from its outline.
1 · Plan the arc
An arc is a card: a title, a premise, an intent — and the ordered
scenes: list, which is the reading order. Scene ids are flat
(s-0007) and never change; position lives only in the list, so reordering a stretch of
the book is one arc-card edit. A scene not listed by any arc does not vanish — it surfaces,
visibly, as an orphan. Creating structure is two verbs:
2 · Write the scene
A scene is one document: a YAML contract as frontmatter — id, title, status,
input, output, overview, and optionally POV, story time, word budget — and the plank prose
body under it. Open it from the Map (Enter) and write against the contract that stays on screen in
the head. The scene is checked out to you — an exclusive lock, so conflicts cannot exist. Drop
@page markers to subdivide a long body into drafting pages (they render as nothing).
^s saves the working file; checking in makes a revision:
Everything is a revision. Nothing is ever overwritten; keel log shows the full
trail, keel diff shows word-level changes between any two.
3 · Proofread
Proofreading is part of the status chain. Every scene revision either carries a proofread mark or visibly lacks one — and because the mark is per revision, any later edit clears it automatically. The pass is a diff you accept, never a silent rewrite:
--mark-only records a human pass with no AI. Unproofread scenes surface with ✂ in
keel status and as warnings on every render and export — visible debt, never a block.
4 · Review — draft to canon
Every scene wears one of three statuses: ○ draft · ◐ in-review · ● canon. When a draft is ready, send it up:
The scene lands in the yard's approval queue; the book admin reads it and clicks
Approve, and the scene flips ● canon — an ordinary checkout/check-in under the
hood, so the trail stays complete. Feedback travels as comments on the yard's
scene edit page: a thread is one root and up to three flat replies, taggable to a member, and
either side marks it done. In the TUI, ^k lists the open scene's threads.
5 · Render — see the book
p in interactive mode — or F5 anywhere, or keel render — typesets the volume through Typst into a real
trade-format PDF at the volume’s trim — running heads, generated contents, the chosen
face justified on the exact line grid.
Your PDF viewer reloads on each re-render, so the loop is live:
6 · When the prose fights the plan — amend
Mid-scene you discover the exit condition is wrong. Do not quietly rewrite the frontmatter: check
the scene out, edit its contract, and run keel amend. Keel diffs the contract and
computes the blast radius — every neighbouring contract and ledger entry your
change touches, graded by severity:
The scene's prose and contract travel together in the one document, so the rewrite that provoked the amendment rides the same check-in — there is no separate "fix the scene" step.
7 · Reconcile and land
Each fix is an ordinary checkout/check-in tagged to the changeset, held pending. Landing flips everything canon in one transaction — and refuses while any ⛔ is unresolved, unless you waive it with the reason on record:
Warnings you chose not to fix become tracked debt — the entry shows ⚠ stale in
keel status until someone reconciles it. Changed your mind entirely?
keel abandon a-1 walks every head back; the revisions are retained, history stays
honest.
Then it repeats
Extend the arc's scenes: list before you need it (the frontier warning tells you
when), write, proofread, review, render, amend when reality demands it. keel today
opens each session with the frontier scene, hanging promises, unproofread scenes, yesterday's
words, and one suggested next action. The book accretes as canon revisions with a complete audit
trail — and the synopsis (keel synopsis) always reads as the current truth of the spine.
The editor
The editor — the heart of the helm — is not a borrowed widget. It was built for prose,
fenced to exactly six requirements, and nothing more. When it is on screen, the content pane wears a
thick ✎ border and the badge top right reads EDITING — you always
know when you are holding the pen. i takes you in, typing immediately;
Esc taps out to the app.
The session
Entering the Write view checks the scene out: an exclusive lock in the store, and a plain-text
working file in checked-out/ — so in-flight work survives any crash of anything. If a
working file already exists you resume it; if someone else holds the lock you get a
read-only view with the holder's name in the title. All editor keys are disabled
in read-only; everything else works.
The six requirements
1 · Soft wrap on the printed measure. Lines wrap at the same ~62-character measure the printed page uses, centred in the pane — what you see approximates the typeset width.
2 · Typewriter scrolling. The cursor stays pinned near 45% of the pane height; the document moves, your eyes don't.
3 · Focus dimming. ^f dims everything except the paragraph you are
in. Toggle it back off the same way.
4 · Decoration spans. Layers painted over the text without touching it: misspellings (red underline), AI suggestions (amber tint), continuity flags, plank directive tokens (dim purple), block interiors (faint tint). This is the requirement no off-the-shelf widget had — and the reason the helm's editor exists.
5 · Vim-lite modal keys. Three modes, one yank buffer, no counts, no macros, no ex. The exact bindings:
| h j k l · arrows | move (j/k follow display rows over soft wrap, with a sticky goal column) |
| w · b · e | word forward · back · end of word |
| 0 · $ | start · end of line |
| { · } | paragraph back · forward |
| i · a · A | insert here · after cursor · at end of line |
| o · O | open a line below · above |
| x | delete the character |
| v | visual selection (motions extend; d/y/c act on it; Esc leaves) |
| p | paste — linewise yanks paste as a line, charwise at the cursor |
| u · ^r | undo · redo |
| / | incremental search — type to jump live, Enter keeps it, Esc restores where you were, n finds next |
The operators d (delete), c (change), y (yank) take a
motion:
| dd · yy | whole line (linewise) |
| dw · cw · yw | to the next word |
| db · cb / de · ce | back a word / through end of word |
| d$ · c$ / d0 · c0 | to end of line / to start of line |
| d{ · c{ / d} · c} | to previous / next paragraph edge |
Insert mode takes printable characters, Enter, Backspace, Delete and arrows;
Esc returns to Normal.
6 · Undo/redo. A flat edit journal over the rope; consecutive single-character
insertions in the same word coalesce, so u steps back by intention, not by keystroke.
Saving and leaving
| ^s | save the working file (the foot shows ●unsaved until you do) |
| q | quit — but a dirty scene refuses: "unsaved — ^s saves · ^q discards and quits" |
| ^q | discard and quit, explicitly |
Saving writes the file; checking in (:ci -m "…" from the console, or
keel ci) turns it into a permanent revision, runs the plank gate, and releases the
lock.
^m — the markup palette
Contextual on your selection. With text selected it offers the inline directives and
wraps the selection — select a phrase, choose think, get
@think{…}. With no selection it offers the block directives and
inserts the frame — choose quote, get the open and @end quote with your
cursor on the empty interior line. Type-ahead filters; a live preview shows what Enter will produce.
The full vocabulary is in plank markup.
The helm is the editor
There is no hand-off to an external editor — the helm owns the typing as
well as the structure and review (the old ^e / $EDITOR
escape hatch retired with the windowed app). The working file under
checked-out\ is still plain text if you ever need to reach it
from outside; check in from the console when you do.
Spellcheck — and the echo detector
A bundled English dictionary plus your project's proper nouns — every entity name and
alias lands in .keel/dict.txt automatically whenever the loft changes, so
Kael and Hollow Gate are never flagged. Checks run in idle moments, only on
changed text; misspellings get the red underline decoration. If the dictionary ever fails to load
you get a visible notice, not silent nothing.
The same idle pass runs the echo detector: a distinctive word (five letters or longer, not a common word, not a name) repeated within about two pages gets a yellow underline on both occurrences — a nudge about "glittered … glittered", never an error, and nothing blocks. No AI involved; it is a plain text pass.
davy is one keystroke away
^g opens the AI writing partner against the full context of your scene —
pre-warmed on a background thread, so it opens instantly — see
davy. Nothing it writes enters your prose except through your
explicit accept.
Comments from your editors — ^k
^k opens the scene's comment threads — feedback left on the yard's scene
edit page (one root comment, up to three flat replies, taggable). Read them beside the
prose and mark a thread done from either side; the thread, not the margin, remembers.
plank markup
plank is the markup you write in. It stays out of the way: ordinary prose is ordinary text, emphasis is plain markdown, and directives are visible tokens — tinted in the editor, never hidden behind WYSIWYG. Your leaf remains a readable text file in any editor on earth.
The whole grammar, four rules
Markdown you already know passes through untouched: *emphasis*,
**strong**, ~~struck~~, - lists. One sentence per line is
the house style — it makes revision diffs readable.
Two tiers — and the rule that prevents a whole class of bugs
@chapter, @part, @titlepage and their kin come from the
spine and the frames at render time. If they could also be typed in prose, the book's structure
would exist in two places, and nothing could principledly decide which wins. So typing one is an
error, not a warning — the question cannot arise.Refused in prose: book · part · chapter · section · prologue · epilogue · contents ·
titlepage · copyright · dedication · preface · foreword · acknowledgments · appendix · glossary ·
scene. (A leaf is the scene — the boundary is the file, so even
@scene has nothing to say.)
Tier 2 · Inline — yours
| @sc{…} | small caps |
| @u{…} | underline |
| @sup{…} · @sub{…} | superscript · subscript (H@sub{2}O) |
| @think{…} | interior monologue — renders italic, but is semantically distinct from emphasis, which matters for POV checking |
| @note{…} | a printed footnote |
| @redact{…} | redacted on the page: ████████ |
| @lang fr{…} | foreign-language span — correct hyphenation and metadata |
| @ref{e-kael} | renders as its text only — makes the reference index exact instead of guessed |
| @fact{…} | renders as its text only — an explicit canon assertion the ledger extractor takes verbatim |
Tier 2 · Blocks — yours
| @quote … @end quote | block quotation · takes credit="…" |
| @epigraph … @end | chapter/book epigraph · takes credit="…" |
| @verse · @song | poetry and lyrics — line breaks preserved |
| @letter · @journal · @email · @chat · @transcript | inset documents, each styled by kind (@journal takes date="…") |
| @terminal | computer output — monospace, preformatted |
| @document type="…" | any other embedded document |
| @signature | a signature block |
| @center · @right | aligned text |
| @cut … @end cut | author-only: prose you removed but will not throw away — renders as nothing, stays in the file, greyed in the editor. Every tool without this grows a "graveyard" file. |
Tier 2 · One-liners
| @break | a scene break within the leaf — renders as the volume's scene-break style (* * *) |
| @dateline · @location · @time | place and time headings, small caps |
| @theend | the optional end-of-book marker |
plank fmt — bare @end is legal to type
You may close any block with a bare @end; formatting expands it to the named form so
nesting is unambiguous in the stored file and diffs stay clean. This runs automatically on every
check-in, or by hand:
plank strip — clean CommonMark on demand
The promise "your book is a folder of markdown" is kept honest by strip: inline
directives collapse to their text, block markers vanish (interiors stay), @cut
interiors disappear entirely, @break becomes * * *, escapes unescape.
This is also what the EPUB/DOCX exports feed to pandoc.
The gate at check-in
A leaf with plank errors cannot land — the check-in is refused with each problem named, your working file untouched, the lock still yours. Unknown directives are warnings (printed, allowed); broken structure and structural markup are errors:
keel plank check s-0001 runs the same analysis any time, against your working file
if the leaf is checked out, otherwise against the stored head.
davy
davy is Keel's writing partner: it drafts prose into the scene you are working on, one suggestion at a time, and nothing it writes ever enters your book until you accept it. The entire design hangs on one idea — you can always see exactly what the model is looking at. That is the trust story, and it is inspectable in the tool itself.
What davy sees — the context recipe
Every generation call is assembled from the same recipe. A stable prefix (cached between calls — the single largest cost lever), a volatile middle that changes with the scene you are writing, and the task itself.
V8 earns its keep. The dominant AI failure in fiction is resolving tension too early. The do-not-reveal list costs a couple of hundred tokens and prevents most of it. Even before the ledger has anything to embargo, the section is sent explicitly: “none recorded yet — still: never resolve tension the scene has not earned.” In a flashback it is the only thing keeping the scene from leaking the future.
The flow: instruct → inspect → stream → accept
In the Write view, ^g opens davy panel on the right —
instantly, because the recipe is pre-warmed on a background
thread while you write, never assembled while you wait. The target
length is the scene's word_budget (2,400 by default) minus
what is already written. Type what you want:
^; opens the inspector — the recipe above, as it will
actually be sent, with a token estimate per section. Every section
can be pinned or unpinned before you generate.
Then the draft streams in. Esc cancels mid-stream. When it
finishes, you review:
a inserts
the suggestion at your cursor; r or Esc discards it
without a trace. Every accepted span is recorded in
.keel/provenance.jsonl, and the foot shows your scene's
✎ AI-density — an honest answer if a publisher ever asks.
What it costs, and which model runs
Different jobs ride different models — prose rides the mid-tier, structural reasoning rides the top, mechanical lint rides the cheapest:
| DAVY.draft / continue | claude-sonnet-5 — $3 in / $15 out per MTok |
| editor.line · ledger.extract | claude-sonnet-5 |
| architect · continuity · reader | claude-opus-5 — $5 in / $25 out per MTok |
| editor.style_lint | claude-haiku-4-5 — $1 in / $5 out per MTok |
The cached prefix bills at a tenth of the input rate on reuse, which is why the stable sections exist. Every call is logged; the meter is one command away:
Setup — and writing without a key
| ANTHROPIC_API_KEY | set it in your environment and davy is live |
| KEEL_AI_MOCK=<file> | offline mode: streams the file's text as the “draft” — used by every test, useful for demos |
| neither set | davy tells you loudly, and everything else in Keel works untouched |
davy also works from the command line — same recipe, streamed to stdout, nothing written to the store:
Proofreading — the Editor's pass
The same machinery powers proofreading, in the status chain rather than
the margin. keel proofread s-0068 runs a strict copy-editing
pass: objective errors only — spelling, grammar, doubled words — never
style. You see a word-level diff first; nothing applies without
--accept.
Accepting applies the corrections as a new revision and marks it
proofread. The mark is per revision — edit the scene again and it
clears. Unproofread scenes show as ✂ in keel status
and as warnings on render and export. A human pass counts too:
--mark-only records it without the AI.
The contract verifier
keel verify s-0068 (backlog #7, 2026-08-30) asks one question: does the
prose deliver the contract? The scene’s input state, output state, and
overview go in with the prose; a compact report comes back — a verdict
(delivered · partial · missed), one line each on the input, output, and
overview with quoted evidence, and up to three gaps. Ephemeral by design:
nothing is stored, nothing gates — the report is yours to act on, and the palette console
runs it without leaving the helm. Rides sonnet.
The Reader — a simulated first read
keel reader 3 (backlog #8, 2026-08-30) files a first-read report on
chapter 3. The discipline is the whole point: the reader sees only what a real reader
would have — the stripped prose of chapters 1–3 in reading order. No contracts, no
ledger, no spine, nothing past the chapter. That makes it the one agent that can catch
author-knowledge leakage: the thing that lives in your head or the ledger
but never made it onto the page — the failure you are structurally unable to self-test,
because you cannot un-know your own book.
The report has three sections: confused (places it lacked information to follow, quoted), guessed it (predictions it formed, and when — your telegraphed twists), and landed (what actually hit). It is the priciest agent in the roster — opus over every page up to the target — so it is an on-demand ritual for a finished chapter, not a save hook. Ephemeral, like the verifier.
Keys
| ^g | open davy (Write view) |
| Enter | generate from the instruction |
| ^; | inspect the context — j/k move, Space pin/unpin |
| Esc | back / cancel the stream / discard |
| a · r | accept · reject the finished draft |
The spine handbook
Series, volumes, and arcs are containers — they carry a premise and an
intent, one line each, and nothing checks them. The arc card also carries
the ordered scenes: list, and that list is the spine.
The scene is where the story is actually designed.
Everything mechanical in Keel — contract checking, the promise ledger,
blast radius — reads scene contracts. This chapter is how to
work them.
The scene is one document — contract over prose
The contract lives between the --- fences —
id, title, status,
input, output, overview always;
pov, story_time, and word_budget
when you want them. Below the fence is the prose
body, in plank. The editor edits the body only: the contract is held aside
and re-attached on save.
input / output are the load-bearing
fields. A scene's output must satisfy the next scene's input —
which can be checked mechanically. It is a type system for story, and it is
what makes retroactive editing tractable: amend a canon scene's contract
(keel amend s-0007) and the
blast radius knows exactly what broke — a
changed output means the successor's input must be re-satisfied (⛔), a
changed input points back at the predecessor's output (⛔), and promises or
facts anchored in the scene get a ⚠.
"Pages" are drafting subdivisions, not book structure: an
@page marker is a void directive that renders as nothing in
the typeset book. While markers exist the editor's foot shows
page 2/5, and ^PgUp / ^PgDn jump between the
seams.
The arc card is the spine
The scenes: list is the whole ordering. Scenes keep flat
s-#### ids — position lives on the arc card, never in the id —
so reordering is one arc-card edit, never an identity change, never a
renumbering. A scene that exists in the store but sits on no arc's list is
an unlisted orphan: it shows up marked wherever the tree does —
visible, never hidden.
Building the spine — genesis, then scenes
A book starts from three cards: series, volume, arc. Once they exist, anyone with an editor or admin seat writes immediately — nothing stands in front of prose; review comes after the words, per scene:
A new scene arrives empty-bodied, its contract fields stubbed in the frontmatter, already on its arc's list. (Check-in validates the document: a broken YAML fence or an id that no longer matches the object is refused, loudly.)
From draft to canon — the review flow
Every scene carries one status:
○ draft · ◐ in-review · ● canon. The path from draft to canon
runs like a pull request:
keel review also lives in the TUI's palette console. The
scene joins the approval queue on the yard's activity page; the book admin
reads it and clicks Approve — an honest checkout/checkin
that lands status: canon.
Discussion lives with the scene, in the yard: on the scene's edit page,
a thread is one root comment plus at most three flat replies — no nesting —
and a root can be tagged to a member. Either side marks a thread done. In
the TUI, ^k lists the open scene's threads from the write view
and d marks one done.
The frontier — warned, never blocked
The frontier is the last scene in tree order that has prose — where the book runs dry. Writing at the frontier means nothing is contracted ahead of you, and generation quality drops when davy has no next contract to write toward. Keel warns, everywhere, and blocks nowhere:
| rail | the keel lens carries ⚠ frontier: s-0007 on its border |
| foot | the ⚠frontier chip appears while your open scene is the frontier |
| keel status | frontier: s-0007 "The Gate Opens at Dusk" — last scene with prose |
The working pattern: series premise and ending → arcs for volume one → scene contracts only where you are about to write. Arc III can stay a one-line premise for a year. Keep a scene or two contracted ahead of the pen, and the frontier warning stays quiet.
Reading the tree
| ▸ | an arc row — the map is a two-level tree: arcs, then their scenes |
| ● | canon — settled |
| ◐ | in-review — waiting in the yard's approval queue |
| ○ | draft |
| j / k | walk the tree (Map view); the detail card follows below it |
| Enter | open the selected scene directly in the editor |
Amendments & blast radius
Changing a canon scene's contract is not an edit — it is a migration with a computed impact set. Prose fights its contract; that is normal and expected. When it happens, you amend the scene, and Keel tells you exactly what the change breaks: which neighbouring contracts stop holding, which promises and facts anchored in the scene are stranded. The whole change then lands atomically — or not at all.
A worked amendment, end to end
Say the crossing scene taught you the toll is memory, not coin — the
scene's exit condition is wrong. Check the scene out and change the
output: line in its YAML frontmatter (the working file is the
whole document, contract and prose together):
The scene's own edit became the first member of changeset
a-1, landed as pending — visible to everyone, canon to
no one. And because contract and prose travel together in the one document,
the rewrite that provoked the amendment rides that same check-in: there is
no separate "direct scene" impact to chase. Try to land too early and Keel
refuses:
Reconcile the successor — an ordinary check-out and check-in, tagged to the changeset:
The warning you chose not to reconcile did not vanish — it became
tracked debt. Until that ledger entry is revisited,
keel status says so:
Why a changeset
A three-word canon change can touch six scenes. If each reconcile were its own review, nobody would use the tool for a week — so an amendment is reviewed once, as one changeset spanning many objects. While an object's head is pending in an open changeset, a plain check-in on it is refused: history cannot interleave.
Where the impacts come from
| CONTRACT | a changed output breaks the successor scene's input; a changed input questions the predecessor's output — ⛔ blocking |
| LEDGER | promises and facts anchored in the amended scene — a setup whose ground shifted, a fact whose provenance changed — ⚠ warning |
| ⛔ blocking | must be reconciled into the changeset — or waived with the reason on record |
| ⚠ warning | may land as tracked debt; shows stale until revisited |
| · advisory | listed, never gating |
Waiving, and walking away
Sometimes the blocking impact is wrong, or you know better. Landing with
--waive "reason" proceeds — and writes your reason into the
amendment's record permanently. The other exit is full retreat:
Nothing is ever deleted. Abandoning walks each object's
head back to its last revision outside the changeset — but the pending
revisions stay in the trail, visible in keel log. The next
revision simply lands above them. History only ever grows.
The ledger
The ledger is the reason Keel is worth more than a shared document. Prose does not just consume the world model — it feeds facts back into it. Write “Kael's left hand is scarred” and that becomes a first-class fact with provenance and a validity window; every later generation and every continuity check consults it.
What a fact is
Two timestamps, and they answer different questions. That distinction is the heart of the system.
The two clocks
true_at — story time: is this fact TRUE at
this moment in the world?
known_at — reading order: does the READER know
it yet? In a linear book these collapse into one sequence and every other
tool conflates them. The moment you allow a flashback, they come apart.
The payoff, concretely: a memory scene set in the Old Era can state a
fact from year −1500 that the reader has never met. The continuity check
does not scream — true_at(Y-1500) says the fact holds. And the
reveal system now knows that fact is spent for everything downstream in
reading order — known_at flips the moment the flashback's
position passes. Meanwhile must_not_reveal — the list of
unpaid promises plus truths the reader has not been shown — is computed
from exactly these two clocks, and is fed to davy as
section V8. In a flashback it is the only thing
keeping the scene from leaking the future.
Promises
Every setup, question, or threat gets an id and a due-by. The ledger surfaces what is still hanging — and payoffs that arrive with no setup:
Promises appear in the ledger view (5), in the rail's ledger
lens, and unpaid ones ride davy's do-not-reveal list until they are
discharged.
Getting facts in — always through your hands
Nothing enters the ledger without you. Three routes, from most to least explicit:
1. Record one directly:
2. Mark it in the prose. plank's @fact and
@ref make extraction deterministic — the extractor stops
having to infer:
3. Extract with triage. Deterministic markup facts always; AI proposals when a key is configured — and either way the list is proposals, not writes:
Without a provider it says so and does the markup half: (no AI provider — markup-only extraction; set ANTHROPIC_API_KEY for ledger.extract). A noisy ledger cries wolf and gets switched off — which is why triage is mandatory and confidence rides along on every AI proposal.
Refs — quote-anchored, always rebuildable
The third table is derived: which scene mentions which entity, anchored by
a short quote rather than a character offset — offsets rot the
moment text moves; a quote can be re-found. Refs come from explicit
@ref directives plus word-bounded name matches, and are
recomputed wholesale at every keel index --rebuild — exact for
canon, good enough for drafts. They are what makes the
blast radius deterministic instead of a
guess.
| keel fact add / list | record and inspect facts |
| keel promise add / pay / list | open, discharge, review promises |
| keel extract <scene> [--accept …] | propose facts from prose; you choose what lands |
| @fact{…} · @ref{…} | make it explicit in the prose itself |
| ledger view · ledger lens | the ledger in the helm |
worldline & calendar
View 7 is the worldline: every scene of the book laid out as a mark on one
horizontal, scrollable line. It answers the two questions a timeline can answer —
in what order does the reader meet these scenes? and when do they actually
happen in the world? — and it refuses to conflate them. Press t and
c to switch between the two answers.
Two arrangements, one line
Chronological (c) is reading order — the order the frames walk
the chapters, which is the order the reader turns pages. Marks are spaced evenly, one per
scene; scenes not yet allocated to a chapter append after the allocated ones, in spine
order, so nothing hides.
Temporal (t) is story time, read from each scene’s
optional story_time in its contract — a scene without one is simply not on
the line. The axis divides into the calendar’s eras, in order, each with its own
segment. A dated mark places itself within its era by year — earliest to latest across
the segment. A year-less mark (an era-precision scene, or the only dated mark in its era)
sits at its segment’s centre. A mark whose era the calendar doesn’t know centres
on the whole axis: visible rather than lost, and a nudge to fix the scene’s
contract.
A flashback is nothing special here — it is simply a scene whose temporal position sits left of its chronological one. Switching modes and watching a mark jump backwards is the fastest continuity check the tool has.
Driving it
| ← → | select the previous / next scene (clamped, no wrap) |
| h · l | scroll the line left / right by four columns |
| t · c | temporal · chronological |
| Enter | open the selected scene in the Write view |
The line is wider than the screen when the book demands it; the selection auto-scrolls
into view. Enter refuses politely if the scene currently open in the editor has unsaved
work — save with ^s first. An empty worldline says so: scenes appear on the
line as their contracts gain a story_time.
The five eras
The default calendar ships five eras — customizable per project, but the shape is deliberate: each era declares the finest dating it permits, and the validator holds every story time to it.
| Mythic | precision era — “roughly the Mythic era” is all a fact may claim; no faked days |
| Old | precision year — dated to a year before the Founding (negative years) |
| Present | precision day — full { year, day, part } dating; carries present: true |
| Future | precision year |
| Far Future | precision era |
Exactly one era carries present: true. It is the anchor: a story time that
names no era belongs to the present era, and the precision cap for undated material comes
from it. Without the marker, “current” would silently mean “last”
— wrong the moment futures exist.
Editing the calendar
The calendar is an ordinary object: keel co m-calendar, edit the YAML,
keel ci. Structure is load-bearing; names are yours to change freely.
Check-in validates before anything lands, loudly:
- month days must sum to
days_per_year; the month list may not be empty day_partsmust match the built-in ladder exactly — dawn · morning · noon · afternoon · dusk · night (it is the within-day sort key)- era names must be unique; the list may not be empty; exactly one
present: true epoch.beforemust benegative— the only defined convention- every story time on a scene must fit: fields must match its declared precision, the day must sit in 1..=360, and an era may never be dated more finely than its cap — a day-dated Mythic fact is refused, not rounded
The one permitted branch
The worldline may carry at most one branch — a single fork into a second strand, ever. One branch is a story choice; two is a spreadsheet. This is story-time branching — an alternate history, a prophesied future that doesn’t come to pass, a vision strand — never version control, which Keel deliberately does not have.
keel new m-worldline --name "The Unlived" creates the branch card (the card
IS the branch — a second create refuses on the duplicate id). The card declares the
branch’s name, its fork point in story time, and optionally a
rejoin point; keel co m-worldline / ci edits it
as TOML. A scene opts onto the branch by naming it in its contract:
In temporal mode the line forks: ┬ marks the fork on the main axis,
the branch runs as its own strand below with its scenes on it, and — when a rejoin is
declared — curves back into the main line at that story-time point. The rejoin is a
drawing rule, not a merge: nothing in the store merges; the strand simply ends
where history reconverges. Without a rejoin the branch renders open-ended
(⋯). In chronological mode the line stays single and branch scenes show
as ◆. A strand: tag that matches no branch card renders as
? — visible, never silently mainlined.
To promote the branch to real history: retag the scenes’ strand
fields and delete the card — ordinary checkouts, an amendment changeset when it spans
many scenes. Deleting the card while scenes still reference it leaves them flagged
? on the line until retagged.
keel.pub & the yard
keel.pub is the web surface; the yard is the editors' section inside it, where ships are built and serviced: accounts, projects, locks, previews, book design. Every judgement about the story (the scene contracts, blast radius, the two clocks) lives in the keel binary; the web stores, serves, and coordinates.
Starting it
Two containers: Postgres 16 (the database — source of truth for hosted
series) and the yard itself on port 8000. On startup the yard migrates
and seeds the admin account idempotently. The version in the header and footer of every
page is read from the same single source the TUI shows in its head — one number,
everywhere, in x.y.z form.
Signing in
username
admin · password mvpAdmins are de-facto editors: the same account signs into the web admin and the TUI.
Three doors, one account:
- The web admin — /admin/, the form above.
- The TUI’s editor login — the first thing keel shows: yard URL
and editor prefilled, masked password,
Entersigns in,Tabmoves between fields,F2drops to local dev mode (no yard),Escquits. A stored session that still validates skips the screen — being signed in is the login. - The CLI —
keel login --url http://localhost:8000 --username admin --password …for scripts,keel logoutto return to dev mode.
Sessions are bearer tokens stored under keel’s home directory; the TUI validates
the session at launch and says who is holding the pen in its head bar
(◉ admin).
What the admin manages
The admin pages (keel.pub branding, the site’s palette, a link back to the main site under the login box) expose the store itself:
| Series | the projects — slug, name, author |
| Objects | every scene, arc, volume, entity, matter — with head revision |
| Revisions | the immutable history: author, message, hash, status |
| Locks | who holds what, since when — exclusivity is structural |
| Changesets | amendments and their pending counts |
| Tokens | issued sessions |
Previews — the book, in the browser
Render locally, view from the yard:
The yard stores one preview per series; re-publishing overwrites it. Uploading requires
a signed-in session and a real PDF (anything without a %PDF- header is
refused with a loud error); viewing is open on your local network. Until hosted series
arrive, your machine renders and the yard displays — the honest local-first split.
The API
Interactive documentation lives at /api/docs. The surface, in families:
| POST /api/token | credentials → bearer token (the only unauthenticated write) |
| GET /api/version | the running version, no auth |
| /api/series | list and create projects |
| …/objects | create, read (any revision), checkout, checkin, unlock, log, locks — the same strict-RCS semantics as the local store, transaction for transaction |
| …/changesets | open, list, members, land, abandon — amendments |
| …/preview | upload the rendered PDF |
Every error body is machine-readable — {"error": …, "code": …}
with codes like locked (plus holder and since), stale_base (plus
head and base), pending_in_changeset — so the client maps failures without
parsing prose. The check-in path mirrors the Rust store exactly, down to the
“INTEGRITY VIOLATION” wording and the rule that revisions grow at
MAX(rev)+1 over retained history.
Clearly marked: coming next
- Store::yard — the TUI reading and writing hosted series over this API, with a local read-through cache keyed by (object, revision); revisions are immutable, so the cache never goes stale and navigating scenes never re-downloads what it has seen.
- Project setup in the browser reaching the TUI — create the series here, open it there.
- The yard — tree and card forms for volumes, arcs, and
scenes, for planning sessions, plus the approval queue on the activity page (where
the book admin lands canon) and comment threads on the scene edit page — one root,
up to three replies, read from the TUI with
^k; the TUI keeps full spine capability for write-time work (both surfaces, one store, same locks). - Build-the-sample-novel button, hosted rendering, and the nightly export job with a tested restore — the go-live checklist items.
The book pipeline
Keel’s preview is not a “reading mode” — it is the book, typeset for
real. Press p (or F5, or run keel render) and roughly a second later a
trade paperback exists as a PDF at the volume’s trim (novella 5×8″ by
default): front matter, running heads, folios, the chosen face justified and hyphenated on
an exact line grid. This chapter explains how a folder
of one-sentence-per-line scenes becomes that object.
Derived, not guessed
Words-per-page is not a configured estimate. Keel derives the page from
volume.toml — trim, margins, face, size, leading — so a chapter that reads
as fourteen pages is fourteen printed pages:
| trim | novella, 127 × 203.2 mm (5 × 8″ — the seed default since the publishing round, and printable; nine trims exist, three of them screen-only UK trade sizes) |
| margins | 18 inner · 14 outer · 16 top · 18 bottom (mm) — asymmetric so the gutter doesn’t swallow text |
| text block | 95 × 169.2 mm |
| measure | 95 mm → 269.29 pt ÷ (0.422 em × 10.5 pt) ≈ 60 characters per line |
| lines / page | 169.2 mm → 479.62 pt ÷ 13.6 pt leading = 35 lines |
| words / page | 60 × 35 ÷ 6 chars-per-word × 0.85 fill ≈ 298 effective — 150,000 words → ~504 pages |
One honest asterisk: the 0.422 em average advance is a calibrated constant, not
something read from font tables — and every face on the menu carries its own (measured
over the same reference sentence, pinned in the geometry tables on both sides). The
calibration is proven by rendering: keel geometry --check compiles a page at
exactly the derived line count and one more, and asserts the flip, so a font or Typst
update that moves the grid fails a test instead of silently reflowing your book.
The line grid itself is exact by construction. Typst’s default line box is cap-height to baseline, which makes leading font-dependent; the template pins the text box edges to exactly 1 em and 0, so every line advances by precisely 1 em + leading = 13.6 pt — the baseline grid, independent of any font’s metrics, verified at 35 lines on a filled novella page.
What your markup becomes
The mapping targets semantic functions the template defines, so rendering decisions live in the template, not the emitter. One-sentence-per-line maps naturally: Typst joins single newlines into a paragraph and breaks on blank lines — exactly the source’s convention.
| *em* · **strong** · ~~struck~~ | italic · bold · strikethrough |
| @sc @u @sup @sub | small caps · underline · super/subscript |
| @think | italic — semantically distinct from emphasis |
| @note | a real printed footnote |
| @redact | solid black bars |
| @lang fr | correct hyphenation and language metadata for the span |
| @quote · @epigraph | inset blocks; credit="…" becomes a right-aligned attribution |
| @verse · @song | line breaks preserved, inset (song in italic) |
| @letter @journal @email @chat @transcript @terminal … | inset documents; terminal in monospace |
| @break | the scene break — centred asterisks |
| @ref @fact | render as nothing but their text — they exist for the index and the ledger |
| @page | prints nothing — the drafting-page marker stays in the workshop |
| @cut | vanishes entirely — author-only material never reaches the page |
The book’s furniture
Structure is emitted from the spine, never typed, and the template does the book-setting a compositor would:
- Front matter — half-title (recto), blank, title page, copyright page with a dynamic year, generated table of contents from the frames, then the body opening on a recto.
- Running heads — author on the verso, title on the recto, suppressed on part and chapter openers and front matter.
- Folios — at the outer edge, old-style figures, suppressed on openers.
- Part pages land recto, with the blank inserted automatically; chapter openers are sunk — the drop from the head is a template constant (28 mm), not something set per chapter.
- Gaps are honest — a chapter whose scenes are still dry renders as its opener plus a stated page estimate from their word budgets, so the book’s true shape stays visible.
The live loop
Renders read the canon head revisions and land at
.keel\render\book.pdf, the same file every time — use a viewer that reloads
on change (SumatraPDF on Windows does; Acrobat will fight you) and the loop is genuinely
live: F5, glance at the page, keep writing. Unproofread scenes are named (✂) after
every render, so nothing ships unread by accident.
Exports & durability
The database is the book — so portability is bought deliberately, not inherited. Exports write the whole series as ordinary files anyone can read in fifty years; snapshots pin moments; and the durability story is designed so that no single failure, including Keel itself, can take your manuscript with it.
The export tree
Run it any time; it is deterministic and complete. One-sentence-per-line survives from the original design for a simple reason: it makes diffs readable — a changed sentence is a changed line.
E-books
The manuscript is built from clean CommonMark — every scene's body through
plank strip (markup collapsed to its text, @cut and @page dropped, scene
breaks as asterisks; frontmatter never prints), chapters as headings — and handed to pandoc with the title
and author. If pandoc is missing the error says exactly how to install it
(winget install JohnMacFarlane.Pandoc); nothing fails silently.
The EPUB is styled (backlog #17, 2026-08-30): the same cards that
shape the printed book shape the e-book. The stylesheet derives from the page config
(face, size, leading, justification, hyphenation) and m-design (the opener
style becomes a first-letter treatment); the volume’s chosen face —
roman and italic — is embedded from the vendored OFL files (nine faces since the font
round; pick one with its specimen page on the design page); and the cover the yard holds becomes the EPUB
cover, fetched exactly the way the PDF render fetches it (local series are coverless by
design). DOCX stays plain — it is a manuscript interchange format, not a rendition.
The print interior (POD)
The print-on-demand interior (backlog #17): the exact-trim book block — the same typst build the preview renders — built without the cover pages. The cover decision made the interior print-identical with or without one, so the coverless build IS the file KDP or IngramSpark ask for as the “interior”: exact trim, no bleed, no crop marks (body text never bleeds). The cover file itself is the separate artwork those services take alongside it. Unproofread scenes warn on the way out, exactly as the e-book exports do.
Publishing (the print package)
The whole road from finished book to printed copy (backlog #41). The helm renders the
two print-ready files — the exact-trim interior, and the one-piece cover
wrap (back + spine + front, the spine sized from the real page count and the
paper’s bulk, an eighth-inch of bleed all round) — and pushes both to the yard.
Everything else happens on the yard’s publish page (book admin
only, next to book design): the print configuration on the m-print card —
binding, paper, color, quality, cover finish, quantity, shipping — with the product code
echoed live from the pinned printer catalogue; the ship-to address; file validation,
quotes across every shipping level, submission, and the job’s status timeline
through to tracking links. A configuration the printer cannot manufacture, a page count
outside the binding’s range, or a wrap whose spine disagrees with the
printer’s own calculation is refused loudly before any money moves. Only trims
marked printable on the design page can publish; the older UK trade trims render
on screen but no print-on-demand service makes them. Printing stays dark until the
service credentials exist — and the sandbox never prints anything.
Snapshots
A snapshot pins every object’s head revision under a name — a reference point, not a copy (revisions are immutable, so pinning is just remembering numbers). Duplicate names are refused. Diffing against a snapshot (“what changed since draft one?”) is a backlogged, held-undecided item.
Stats and the burn-down
Everything is derived from revision history alone — per-day word deltas attributed to the day each revision landed. No tracking apparatus, no separate log to maintain; the store already remembers.
The durability story
- Immutable revisions. Nothing is ever deleted — not by check-in, not by abandoning an amendment (heads walk back; the rows remain). Every version of every scene is one query away, hashed and compressed, in a single SQLite file per series.
- Plain-text working files. Anything checked out is a real file in
checked-out\. In-flight writing survives a crash of Keel, SQLite, Docker, or the machine’s opinion of all three. - The export tree, on demand. The entire series as ordinary files — the format-independence escape hatch. Run it before anything that makes you nervous.
- The index is disposable. Search and refs are derived data,
rebuilt byte-identically from the store — delete
index.sqlitefreely.
keel export --tree
into something that syncs is a fine habit.
CLI reference
^p and start the line with : to get the
command console, which runs any of these in place and shows the output.
This reference exists for scripts, automation, and power use.
One binary, no daemon. Run keel with no arguments inside a
series folder and the TUI opens; run it with a subcommand and it does one
thing and exits. Every command fails loudly — error messages carry the
reason, the object, and usually the cure.
Workspace & revisions
The strict-RCS core: check out, edit plain text, check in. Reads never need a lock; edits always do.
| init NAME [--author A] | Create a series workspace under the current directory, fully seeded (spine, calendar, frames, ledger). Author defaults to your OS username. |
| co ID | Check out: take the exclusive lock, pull the head revision into checked-out/ as plain text. |
| co -l | List what you are holding, with lock ages. |
| ci ID -m MSG [--changeset a-N] | Check in: new immutable revision, release the lock. Structured content is validated first; scene bodies get plank-formatted (the frontmatter contract passes through untouched). With --changeset, the revision lands pending inside an open amendment. |
| log ID | The full revision trail — rev, date, author, status, message. |
| diff ID [A..B] | Word-level diff between two revisions; defaults to previous..head. |
| status | Every open checkout with holder and age, the spine frontier, stale amendment debt, and unproofread scenes. |
| today | The morning briefing: the frontier scene, hanging promises, unproofread scenes, yesterday's words, and one suggested next action (usually the next dry scene). |
Spine & structure
Arcs carry position in the id — arc-v1.2 — and hold the
ordered scenes: list, which is the reading order. Scene ids are
flat (s-0007) and never change; reordering is one arc-card
edit, and a scene no arc lists surfaces visibly as an orphan. There is no
gate: a scene is writeable the moment it exists.
| new arc-v#.# [--title T] | Create an arc — a card with title, premise, intent, and the ordered scenes: list. |
| new s-#### --arc arc-v#.# [--after s-####] [--title T] | Create a scene and list it in the arc — at the end, or after the scene named by --after. For entities: new e-slug [--name N] [--category character|location|faction|lore]. |
| new m-worldline [--name BRANCH] | Create the one permitted worldline branch — the card IS the branch, so a second create refuses. Edit its fork/rejoin via co/ci; scenes opt on with story_time.strand. |
| synopsis | The whole spine stitched into continuous prose — the book before it is written — plus the frontier. |
| frames | Chapters over scenes with word budgets and real printed-page estimates. |
Writing & proofreading
| review SCENE | Send a ○ draft scene into ◐ in-review — it joins the approval queue on the yard's activity page, and the book admin's Approve flips it ● canon (an ordinary checkout/check-in under the hood). |
| proofread SCENE | Run the strict AI copy-edit pass and show the corrections as a word diff. Nothing is written. |
| proofread SCENE --accept | Apply the corrections as a new revision and mark it proofread. |
| proofread SCENE --mark-only | Record a human proofread of the current head — no AI. Marks are per-revision; any edit clears them. |
| verify SCENE | Does the prose deliver the contract's input → output? A verdict with quoted evidence and up to three gaps. Ephemeral — nothing stored, nothing gated. |
| reader CHAPTER | Simulated first read of the chapter: confused / guessed it / landed. The reader sees only the prose up to and including it — nothing else. Opus over every page so far: an on-demand ritual, and ephemeral. |
| plank fmt ID | Expand bare @end to @end name in the working file's prose body — the frontmatter passes through (also happens on every check-in). |
| plank strip ID | Print the scene's body as clean CommonMark — markup collapsed, @cut interiors gone, @page markers rendered as nothing. |
| plank check ID | Report plank issues in the prose body; structural markup in prose and unclosed blocks are errors. |
The ledger & amendments
Facts and promises are canon memory; amendments are migrations with a computed blast radius, landed atomically.
| fact add --subject S --statement T --scene s-#### [--year Y] [--day D] [--quote Q] | Record a fact with provenance; validity defaults to the establishing scene's story time. |
| fact list | Every fact with subject, source, and reveal position. |
| promise add --statement T --opened-in s-#### [--due-arc N] | Open a promise (setup, question, threat) with a due-by. |
| promise pay ID --in s-#### | Mark it discharged by a scene. Paying twice is refused. |
| promise list | Paid and hanging promises, with due arcs. |
| extract SCENE [--accept all|1,3] | Propose facts from @fact/@ref markup (always) and the AI extractor (when keyed). Nothing lands without --accept — human triage is the design. |
| amend SCENE -m MSG | Diff the checked-out scene's contract — its YAML frontmatter — compute the blast radius, open changeset a-N with the scene as its first member, and print the reconcile tasks. |
| land CS [--waive REASON] | Flip every pending revision canon atomically. Refused while a blocking impact is unreconciled, unless waived with the reason on record. Unreconciled warnings become tracked stale debt. |
| abandon CS | Walk heads back over the retained pending revisions. History is never deleted. |
| changesets | Every amendment with status and pending count. |
The book & exports
| render [--open] [--publish] | Typeset the volume to a true trade-format PDF at its trim via Typst. --open launches your viewer (it reloads on re-render); --publish pushes the PDF to the signed-in yard, viewable at /preview/<slug>.pdf. |
| export --tree [--out DIR] | The whole series as ordinary files — the portability story, on demand. |
| export --epub | --docx [--out PATH] | E-book and manuscript formats via pandoc. The EPUB is styled from m-design and the page config, embeds the chosen face, and carries the yard's cover. |
| export --print [--out PATH] | The print-on-demand interior PDF: the exact-trim book block with the cover pages dropped — what KDP/IngramSpark want. No bleed, no crop marks. |
| print-package [--local] | Render the print-ready interior + cover wrap and push both to the yard's publish page. --local renders into .keel/render/print-package/ without pushing. |
| snapshot [NAME] | Pin every object's head under a name; with no name, list snapshots. |
| stats | Words against target, printed pages, weekly pace, and a fourteen-day burn-down. |
Search & the index
| search QUERY | Full-text search (FTS5) across every object; rebuilds a missing index automatically. |
| index --rebuild | Reconstruct the derived index (search, refs) and the project dictionary from the store alone — byte-identical every time. |
| geometry [--check] | The derived page geometry: trim, text block, characters × lines, ~300 words per printed page. --check proves the line count against a real typst render. |
davy & voice
| ai draft SCENE -i ASK | davy from the CLI: assembles the full context recipe, streams the draft to stdout, logs usage. Nothing is written to the store. |
| ai usage | Today's and the month's AI spend from the usage log. |
| voice seed FILE | Store ~1,500 words of your own prose as the voice profile davy writes toward. |
Session & meta
| login --url U --username N --password P | Sign in to the yard and lock the TUI to it. The password flag is the MVP test login only. |
| logout | Sign out; the TUI returns to local dev mode. |
| changelog | Print the changelog — the same text the TUI overlay and the yard show. |
| auto-novel TITLE [--pages N] [--setting S] [--world W] [--characters A,B] [--author A] [--offline] | Build a complete sample series from minimal input — every layer populated, deterministic offline, davy-drafted when keyed. Educational. |
Exit codes are meaningful: 0 success ·
1 error · 2 usage · 3 lock conflict ·
4 integrity violation (the loud, should-be-impossible one).
Scripts can branch on them.
Design rationale
Keel is opinionated. Every opinion below was argued out before the first line of code, and each one closes off a class of failure that plagues long manuscripts. This chapter preserves the why — so that when a rule gets in your way, you can weigh the reason before working around it.
Why strict RCS, and no git at all
Git is optimistic: everyone edits freely and conflicts are merged afterwards. RCS is pessimistic: take the lock and conflicts never happen. Layering one on the other fights itself — a lock means nothing when anyone can commit locally, and branches have no purpose when nothing ever diverges. So Keel keeps the half that matters for prose and drops the rest: every editable thing is an object with an exclusive lock, revisions are immutable and monotonic, and reads are always free.
Two subsystems got simpler the day git left: review stopped being a branch and became a status column, and retroactive edits stopped being branches and became changesets — named sets of pending revisions that land atomically or not at all. And offline got better, not worse: the lock is your offline token. Check out before you fly; nobody can touch the scene until you return.
Why review, not a write gate
An earlier design gated the prose behind the plan: no words until the
structure above them was specified. The simplification dropped it, because
the gate punished exactly the writers it meant to help — drafting is how
many people find out what the scene is. So now anyone drafts any scene,
and honesty moved to where it belongs: the scene's review status.
○ draft is yours, ◐ in-review is a pull request
(keel review s-0007), and ● canon is granted in
one place only — the book admin, in the yard's approval queue. Structure
still leads, but by contract rather than by refusal: when canon prose
fights its contract, you amend the contract, and the amendment is explicit
and blast-radius-checked instead of the manuscript drifting silently away
from its outline. That drift is the exact failure this tool exists to
prevent.
Why the scene is one document
The writing unit once split in two — a planning card in the spine and
prose "leaves" hanging beneath it — and the halves could drift apart,
which is the one thing a contract must never do. Now a scene is a single
document: the contract as YAML frontmatter (id, title, status,
input/output, overview) and the plank prose below it, checked out and
checked in together. @page markers subdivide the body into
drafting pages and render as nothing — pacing scaffolding, not structure.
Ids stay s-#### on the wire so nothing old breaks; ~2,400
words is the default budget.
Why two clocks
Allowing flashbacks forces apart two questions a linear book conflates:
is this fact true right now? (story time) and does the reader
know it yet? (narrative order). Keel keeps both orderings and never
substitutes one for the other — true_at answers the first,
known_at the second. The payoff: a memory scene set forty
years earlier can state a fact the reader has not met without the
continuity machinery screaming, while the do-not-reveal list still knows
that fact is spent for everything downstream in reading order. In a
flashback, that list is the only thing keeping the scene from leaking the
future.
Why the yard holds objects, not reasoning
Every deterministic rule — the spine graph, the ledger, blast radius, allocation, diffing — lives in the binary and runs locally. The yard stores objects, revisions, and locks, and does what needs a secret key. It reasons about nothing. This is why the same schema sits behind SQLite locally and Postgres hosted, why the TUI cannot tell the difference, and why both surfaces can edit the same spine with divergence impossible by construction. It is also why revisions being immutable makes the hosted cache trivially correct: content by (object, revision) can be cached forever; only "what is head?" ever needs the network.
Why one binary, local-first
keel.exe is a copy-paste install: no runtime, no interpreter, no package manager on the writing machine. The whole system — TUI, yard, Postgres — builds and runs locally under docker compose before anything touches a host. For a tool meant to hold a decade of work, the ability to run every part of it on one unplugged laptop is not a convenience; it is the durability story. The same instinct puts working copies on disk as plain text: anything checked out survives any outage of anything.
Why proofread marks are per-revision
A proofread that survives edits is a lie waiting to be printed. The mark records which revision was proofread; touch the scene and the mark is gone, visibly. Enforcement is deliberate too — tracked and warned at render, never blocking a drafting session. The gate hardens only where mistakes become expensive: at the book.
Troubleshooting & roadmap
Keel fails loudly on purpose — every error names the object, the reason, and usually the cure. This page maps the messages you will actually meet to what they mean, then lays out what is coming next.
"object … is checked out by … (since unix …)"
Someone — usually you, in another window or an open TUI session — holds
the exclusive lock. keel status shows every lock with its age;
keel co -l shows yours. If it is your own lock, finish the
thought: keel ci releases it (an unchanged check-in releases
the lock without creating a revision). Breaking someone else's lock is
deliberately absent from the MVP — the admin break arrives with M8 roles,
announced and never silent.
"INTEGRITY VIOLATION … check-in base rev X != head rev Y"
The loud one, and deliberately so: under exclusive locking this state
should be impossible, which is exactly why a silent success here would be
the bug worth catching. It means the object's head moved while you held
what you believed was the lock — in practice: an amendment was abandoned
under you, or something bypassed the store. Do not retry blindly. Inspect
keel log <id>, compare your working file against the head,
and re-checkout. Your text is never lost — the working file in
checked-out/ stays on disk through every refusal.
"the helm is login-locked and the yard did not accept your session"
You are signed in, but the yard is unreachable or rejected the token.
Check your connection to keel.pub (or, for
a local dev yard, docker compose up -d in the repo), or drop
to local dev mode with logout in the console. The login
screen offers F2 for dev mode directly. The local dev login is
admin / mvp, printed on the local admin login
page itself.
A key does nothing
First check the badge top right: in INTERACTIVE mode
plain letters drive the app, not your prose — press i to
type, Esc to tap back out. Still dead? You may be running a
stale binary — quit the app and relaunch with
start-keel.ps1 -Rebuild (a running keelhelm.exe locks itself
and silently blocks rebuilds). F1 always opens help.
The app will not start
keelhelm.exe writes its reason next to itself:
keelhelm-error.log for startup failures, keelhelm.log
for graphics warnings. A machine with no usable GPU adapter can fall back
to the in-terminal shell: set KEEL_DEBUG_TUI=1 and run
keel.exe from a series folder.
"could not run typst" / "could not run pandoc"
The book pipeline shells out: Typst typesets the PDF, pandoc builds EPUB/DOCX. Both errors say exactly this; both installs are one line.
plank refuses a check-in
@quote is never closed, @chapter is structural —
the scene's markup has errors, and broken markup cannot land. The message
lists each problem with its position; keel plank check s-####
re-runs the diagnosis. Structural directives (@chapter, @part…) are
emitted from the spine at render time and never typed — that refusal is
the two-tier rule doing its job.
Where things live
The index is derived data: deleting it costs nothing,
keel index --rebuild reconstructs it byte-identically. The
store is the book — that is what snapshots, exports, and (hosted) the
yard's Postgres protect.
The road ahead
Shipped through 1.3.0: Store::yard (hosted series in the helm, reading through a permanently-valid revision cache), the yard itself (tree, cards, approvals, book design that drives the printed page), roles, the native windowed app with the full-resolution in-app book preview. Still open: sovereign zones for delegated arcs and the nightly export job.
Chosen features, order TBD — the owner's kept picks:
- Three-variant DAVY — the spec's N=3 drafts as tabs
- Scene-contract verifier — does the prose deliver input→output?
- Reader simulation — a first-read report per chapter
- The Architect — proposed arcs and scene lists against the stated ending
- Entity quick-capture — @ref on an unknown name offers to create it
- worldline branch — the one permitted branch, rendered
- Styled exports — EPUB cover and CSS, print-ready PDF
- Preview navigator — chapter jump list with live page numbers
The file footprint
The complete footprint of the solution — every file, and what it is for. Seven Rust crates make the binary, one Django app makes the yard, and a small tools directory holds the vendored assets. Nothing else is hiding anywhere.
The repo root
crates/keel-core — the pure domain
Everything Keel knows lives here, with zero I/O: the spine, the ledger, plank, page geometry, the canonical file forms. Its test suite runs with no fixtures on disk — the design's testability anchor.
crates/keel-store — strict RCS over SQLite
crates/keel-helm — the helm's editor engine
The hand-built prose widget — the project's flagged biggest risk, fenced to six requirements and finished.
crates/keel-tui — the helm's shell (backend-generic)
crates/keel-wind — the native window
The mIRC shape: winit runs the window on the main thread; the whole shell above runs unchanged on a worker thread against a wgpu-rendered grid.