keel.pub yard

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.

KEEL The Salt Ledger › Vol I › s-0001 · map ◉ admin INTERACTIVE i s-0001 "The Crossing" · POV e-vess · Y812 d1 · ● canon keel keel frames codex ledger review ⇥ Tab ▸ 1 The Departure ● s-0001 Crossing ● s-0002 The Gate ◐ s-0003 The Toll ○ s-0004 Landfall ⚠ frontier: s-0003 detail s-0001 The Crossing ● canon · POV e-vess Y812 d1 · harbor 2400w Vess faces what the harbor demands next… IN desperate… OUT lighter by one… map j/k select in the rail (detail below the tree) · Enter opens the scene s-0001 — read-only preview · Enter to edit The ferryman did not look up. He had been paid already. 2402w · rev 1 · no lock · ⚑— · ✎0% ? help i edit ⇥ lens ^p F5 q quit ① head ② contract ③ lenses ④ the tree ⑤ detail ⑥ the view ⑦ foot
The C, as it actually renders: ① breadcrumb, version, signed-in editor, mode badge (INTERACTIVE / EDITING / PREVIEW, with the key that flips it) · ② the open scene's contract line — id, title, POV, story date, status — always visible · ③ the lens strip — Tab / ⇧Tab, or click · ④ what the active lens shows · ⑤ the selected node's card · ⑥ the active view · ⑦ status and key hints. Above it all sits the clickable toolbar (views · preview · palette · theme · help).

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.

keelthe spine tree — arc rows in bold ▸, scene rows with ○ draft · ◐ in-review · ● canon — with the frontier warning on the border
framesthe reader's chapters
codexentity names (the world bible)
ledgerpromises (✓ paid · … hanging) and facts
reviewamendments 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 select in the rail (detail below the tree) · Enter opens the scene to write s-0001 — read-only preview · Enter to edit The ferryman did not look up. He had been paid already.
j / k, ↓ / ↑move the selection through the tree
Enteropen 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.

AMENDMENTS a-1 the toll is memory open 2 pending land: keel land <id> · abandon: keel abandon <id> · reconcile: keel ci --changeset

ledger

Promises above, facts below — the story's memory. See The ledger.

PROMISES P-01 whose memory did he lose? opens s-0001 ✓ paid in s-0021 FACTS F-0001 e-harbor-town the harbor keeps a ledger of its own revealed s-0001 §1

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.

[temporal] ↑↓ select · h/l scroll · t/c mode · Enter open ────●───────│──●───●──│────●─────│─────────│───────── Mythic │ Old │ Present │ Future │ Far Future
↑ / ↓ (j / k)select a mark
h / lscroll the line
t / ctemporal / chronological
Enteropen 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 F1the help overlay — the full key and concept reference, always available
^kthe comment threads on the open scene — authored on the yard's scene edit page; d marks a thread done
^ssave — 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.
^gdavy — the context is pre-warmed in the background, so it starts instantly
changelogfrom the palette — what changed in this version
j / k · Escscroll · close, in any overlay
If you only remember one key: ?. 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.

PLAN the arc WRITE the scene REVIEW ◐→● RENDER F5 AMEND the contract blast radius ⛔ ⚠ · RECONCILE & LAND atomic · ⛔ resolved or waived the scenes: list proofread ✂ marks render warnings the prose fights the plan back to writing
The loop. Black is the daily path; red is what happens when the words disagree with the plan — which is a feature, not a failure.

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:

$ keel new arc-v1.2 --title "Salt and Ash" created arc-v1.2 — "Salt and Ash" $ keel new s-0007 --arc arc-v1.2 --after s-0005 --title "The River" created s-0007 — "The River" (○ draft · listed in arc-v1.2 after s-0005)
You do not plan everything up front. The spine is deep near the pen and shallow far away. When you open the last scene with prose — the book runs dry after it — Keel warns: in the foot, in the map, on the rail border. The frontier is a dashboard light, not a wall. There is no gate; a scene is writeable the moment it exists.

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:

$ keel ci s-0002 -m "the river crossing" checked in s-0002 r2

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:

$ keel proofread s-0001 s-0001 r4 — corrections ([-…-] {+…+}): He [-walkked-]{+walked+} to the gate. The toll [-was was-]{+was+} memory. apply with `keel proofread s-0001 --accept` — nothing was written $ keel proofread s-0001 --accept s-0001 r5 — corrections applied and marked proofread

--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:

$ keel review s-0007 s-0007 ◐ in-review — queued for approval on the yard's activity page

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:

$ keel render rendered .keel\render\book.pdf ✂ unproofread in the render: s-0002 r2 — `keel proofread s-0002` $ keel render --publish published — view it at http://localhost:8000/preview/the-salt-ledger.pdf

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:

$ keel amend s-0001 -m "the toll is memory" amendment a-1 opened — s-0001 (output) blast radius (2 impacts): ⛔ s-0002 s-0001 changed output — s-0002's input must be re-satisfied ⚠ P-01 "whose memory did he lose?" — opened in s-0001, contract fields changed: output reconcile: `keel co <object>` · edit · `keel ci <object> -m "…" --changeset a-1` — then `keel land a-1`

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:

$ keel land a-1 keel: a-1 has unresolved blocking impacts — reconcile them into the changeset or land with --waive "reason" (recorded): ⛔ s-0002 — input must be re-satisfied $ keel ci s-0002 -m "re-satisfy entry" --changeset a-1 checked in s-0002 r3 — pending in a-1 $ keel land a-1 a-1 landed — 2 revisions canon tracked debt — these show ⚠ stale until reconciled: ⚠ P-01 — opened in s-0001 …

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 · arrowsmove (j/k follow display rows over soft wrap, with a sticky goal column)
w · b · eword forward · back · end of word
0 · $start · end of line
{ · }paragraph back · forward
i · a · Ainsert here · after cursor · at end of line
o · Oopen a line below · above
xdelete the character
vvisual selection (motions extend; d/y/c act on it; Esc leaves)
ppaste — linewise yanks paste as a line, charwise at the cursor
u · ^rundo · 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 · yywhole line (linewise)
dw · cw · ywto the next word
db · cb / de · ceback a word / through end of word
d$ · c$ / d0 · c0to 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

^ssave the working file (the foot shows ●unsaved until you do)
qquit — but a dirty scene refuses: "unsaved — ^s saves · ^q discards and quits"
^qdiscard 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

@name{...} INLINE @sc{USS ENTERPRISE} H@sub{2}O @name ... @end BLOCK @quote … @end quote @name arg key="val" ARGUMENTS one positional, then key="value" pairs \@ \* \{ ESCAPES literal @, asterisk, brace Braces mean inline. @end means block. That is the whole grammar.

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

Structural markup is emitted, never typed. @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 quoteblock quotation · takes credit="…"
@epigraph … @endchapter/book epigraph · takes credit="…"
@verse · @songpoetry and lyrics — line breaks preserved
@letter · @journal · @email · @chat · @transcriptinset documents, each styled by kind (@journal takes date="…")
@terminalcomputer output — monospace, preformatted
@document type="…"any other embedded document
@signaturea signature block
@center · @rightaligned text
@cut … @end cutauthor-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

@breaka scene break within the leaf — renders as the volume's scene-break style (* * *)
@dateline · @location · @timeplace and time headings, small caps
@theendthe 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:

before after `keel plank fmt s-0001` @letter @letter @verse @verse line one line one @end @end verse @end @end letter

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.

He thought @think{never} again. He thought never again. @quote kept words kept words ──► * * * @end quote tail @break @cut gone forever @end cut tail

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 ci s-0001 -m "broken" keel: plank errors in s-0001 — fix before check-in: @quote is never closed — add `@end quote` $ keel ci s-0001 -m "structural" keel: plank errors in s-0001 — fix before check-in: @chapter is structural — it is emitted from the spine at render time and never typed in prose

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.

CACHED PREFIX — stable, reused across calls S1 style contract S2 cast sheet S3 voice profile S4 world invariants changes rarely → cached → cheap VOLATILE — this scene, this moment V1 the arc premise + this scene's full contract V2 neighbouring scenes' contracts (write toward the next) V3 verbatim tail of the prior scene V5 the POV character's entity card V7 retrieved passages V8 DO NOT REVEAL — unpaid promises + truths the reader has not met TASK T1 your instruction T2 output contract: prose only · target = word_budget − written · one sentence per line · end on momentum
The recipe, top to bottom. Empty sections are omitted — except V8, which is always stated.

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:

┌ DAVY ─────────────────────────────┐ │ Kael realises the toll was a name. │ │ Four paragraphs, no dialogue.▏ │ │ │ │ Enter generate │ │ ^; inspect context │ │ Esc close │ └────────────────────────────────────┘

^; 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.

┌ CONTEXT ───────────────────────────┐ │ ◉ S1 style contract ~0.3k │ │ ◉ S2 cast sheet ~0.4k │ │ ◉ S3 voice profile ~0.6k │ │ ◉ V1 premise + contract ~0.5k │ │ ○ V3 verbatim tail ~1.2k │ ← unpinned: excluded │ ◉ V8 do not reveal ~0.1k │ │ ◉ T1 the ask · T2 contract ~0.1k │ │ │ │ Space pin/unpin · Enter generate · Esc back │ └────────────────────────────────────┘

Then the draft streams in. Esc cancels mid-stream. When it finishes, you review:

│ He counted the coins twice on the │ │ road, and both times arrived at │ │ six, which was how he knew the │ │ counting was not what went wrong. │ │ │ │ a accept · r reject │ │ $0.03 this call · $0.14 today │
Nothing is written until you accept. 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 / continueclaude-sonnet-5 — $3 in / $15 out per MTok
editor.line · ledger.extractclaude-sonnet-5
architect · continuity · readerclaude-opus-5 — $5 in / $25 out per MTok
editor.style_lintclaude-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:

$ keel ai usage today $0.14 30 days $2.87

Setup — and writing without a key

ANTHROPIC_API_KEYset 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 setdavy 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:

$ keel ai draft s-0068 -i "Continue the scene." context: 9 sections · ~4.2k tokens The ferryman did not look up... — claude-sonnet-5 · $0.0310 this call

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.

$ keel proofread s-0068 s-0068 r4 — corrections ([-…-] {+…+}): He [-walkked-]{+walked+} to the gate. The toll [-was was-]{+was+} memory. apply with `keel proofread s-0068 --accept` — nothing was written

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.

$ keel verify s-0068 s-0068 r4 — "The Toll" against its contract: verdict: partial input: opens mid-crossing — "The ferryman did not look up" output: the exit state never lands; the toll is still unpaid at the close overview: matches gaps: - the contract says Kael learns the price; the prose never names it nothing was written — the report is yours to act on

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

^gopen davy (Write view)
Entergenerate from the instruction
^;inspect the context — j/k move, Space pin/unpin
Escback / cancel the stream / discard
a · raccept · 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

# s-0007 — one stored document: the contract, then the prose --- id: s-0007 title: The Gate Opens at Dusk status: draft input: Kael desperate, believes coin will buy passage. output: Kael lighter by one memory he cannot name. overview: Kael bribes the gatekeeper and learns the toll is memory, not coin. pov: e-kael story_time: precision: day year: 812 day: 14 part: dusk word_budget: 2400 --- Kael crossed the causeway with the light failing behind the roofs. The gate stood open, which was the first wrong thing. @page The gatekeeper's booth smelled of salt and old paper.

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

# arc-v1.2 — checked out as checked-out/arc-v1.2.yaml id: arc-v1.2 title: Salt and Ash premise: The road south trades salt going down and ash coming back. intent: Strip Kael of the last thing he believes he owns. scenes: - s-0005 - s-0007 - s-0006

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:

$ keel new arc-v1.2 --title "Salt and Ash" created arc-v1.2 — "Salt and Ash" · co/ci to give it premise and intent $ keel new s-0007 --arc arc-v1.2 --title "The Gate Opens at Dusk" created s-0007 — "The Gate Opens at Dusk" (○ draft, empty body) · appended to arc-v1.2 $ keel new s-0008 --arc arc-v1.2 --after s-0005 created s-0008 (○ draft, empty body) · inserted after s-0005 in arc-v1.2

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 s-0007 s-0007 "The Gate Opens at Dusk" → ◐ in-review · queued for approval in the yard

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:

railthe keel lens carries ⚠ frontier: s-0007 on its border
footthe ⚠frontier chip appears while your open scene is the frontier
keel statusfrontier: 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 / kwalk the tree (Map view); the detail card follows below it
Enteropen 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):

$ keel co s-0001 checked out s-0001 r3 → checked-out/s-0001.md # edit the frontmatter: output: Kael lighter by one memory he cannot name. $ keel amend s-0001 -m "the toll is memory" amendment a-1 opened — s-0001 (output) blast radius (2 impacts): ⛔ s-0002 s-0001 changed output — s-0002's input must be re-satisfied ⚠ P-01 "whose memory did he lose?" — opened in s-0001, contract fields changed: output reconcile: `keel co <object>` · edit · `keel ci <object> -m "…" --changeset a-1` — then `keel land a-1`

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:

$ keel land a-1 keel: a-1 has unresolved blocking impacts — reconcile them into the changeset or land with --waive "reason" (recorded): ⛔ s-0002 — s-0001 changed output — s-0002's input must be re-satisfied

Reconcile the successor — an ordinary check-out and check-in, tagged to the changeset:

$ keel co s-0002 # edit the frontmatter: input: he arrives lighter by one memory. $ keel ci s-0002 -m "re-satisfy entry" --changeset a-1 checked in s-0002 r2 — pending in a-1 $ keel land a-1 a-1 landed — 2 revisions canon tracked debt — these show ⚠ stale until reconciled: ⚠ P-01 — opened in s-0001 — contract fields changed: output

The warning you chose not to reconcile did not vanish — it became tracked debt. Until that ledger entry is revisited, keel status says so:

$ keel status no open checkouts frontier: s-0002 "The Gate" — last scene with prose ⚠ stale P-01 — opened in s-0001 — contract fields changed: output (landed a-1, unreconciled)

Why a changeset

canon a-1 LANDS ATOMICALLY a-1 s-0001 r4 · pending s-0002 r2 · pending one review · one transaction · no branches, no merge
Pending revisions accumulate on the changeset; landing flips them all canon in a single transaction. Abandoning discards them all.

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

CONTRACTa changed output breaks the successor scene's input; a changed input questions the predecessor's output — ⛔ blocking
LEDGERpromises and facts anchored in the amended scene — a setup whose ground shifted, a fact whose provenance changed — ⚠ warning
⛔ blockingmust be reconciled into the changeset — or waived with the reason on record
⚠ warningmay land as tracked debt; shows stale until revisited
· advisorylisted, 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:

$ keel abandon a-1 a-1 abandoned — 2 objects walked back (revisions retained)

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.

SCENES the prose you write LEDGER facts · promises · refs davy's context (V6, V8) blast radius · continuity …and back into the next scene you write extraction
The return loop: prose emits facts; facts steer everything that comes after.

What a fact is

# one line of ledger/facts.jsonl, unpacked id: F-0001 statement: the toll is memory, not coin subject: e-hollow-gate established_in: s-0068 # the scene with the establishing prose valid_from: year 812 d14 # WHEN it becomes true (absent = always) valid_until: — # absent = still true revealed_at: arc 1.1 · #2 # WHEN THE READER learns it (arc, position in arc) quote: "the toll is memory" # re-resolvable anchor, never an offset confidence: 1.0

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:

$ keel promise add --statement "whose memory did he lose?" --opened-in s-0003 --due-arc 1 P-01 opened — whose memory did he lose? $ keel promise pay P-01 --in s-0017 P-01 paid in s-0017 $ keel promise list P-01 whose memory did he lose? (opens s-0003 · ✓ paid in s-0017) P-02 the gatekeeper's warning (opens s-0003 · hanging · due arc 1)

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:

$ keel fact add --subject e-kael --statement "his left hand is scarred" --scene s-0068 F-0001 recorded — his left hand is scarred

2. Mark it in the prose. plank's @fact and @ref make extraction deterministic — the extractor stops having to infer:

He paid at last. @fact{the toll is memory} @ref{e-vess} watched him count.

3. Extract with triage. Deterministic markup facts always; AI proposals when a key is configured — and either way the list is proposals, not writes:

$ keel extract s-0068 proposed facts from s-0068: 1. [markup·1.0] e-kael — the toll is memory ("He paid at last") 2. [ai·0.7] e-vess — Vess counts what others owe ("watched him count") accept with --accept all or --accept 1,3 $ keel extract s-0068 --accept 1 recorded: F-0002

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 / listrecord and inspect facts
keel promise add / pay / listopen, 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 lensthe 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.

[temporal] ←→ select · h/l scroll · t/c mode · Enter open Mythic Old Present Future Far Future s-0007 arc 1.2 · The River · Present Y812 d31
Temporal mode: the axis is cut into the calendar’s five eras; the selected scene carries its id above the line and its arc, title, and date below.

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 · lscroll the line left / right by four columns
t · ctemporal · chronological
Enteropen 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.

Mythicprecision era — “roughly the Mythic era” is all a fact may claim; no faked days
Oldprecision year — dated to a year before the Founding (negative years)
Presentprecision day — full { year, day, part } dating; carries present: true
Futureprecision year
Far Futureprecision 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.

days_per_year: 360 months: # must sum to days_per_year - name: First Seed days: 30 ⋮ # twelve months of thirty day_parts: [dawn, morning, noon, afternoon, dusk, night] epoch: event: The Founding before: negative # years before Y0 count down eras: - name: Mythic precision: era - name: Old precision: year - name: Present precision: day present: true # exactly one era carries this - name: Future precision: year - name: Far Future precision: era story_present: year: 812

Check-in validates before anything lands, loudly:

  • month days must sum to days_per_year; the month list may not be empty
  • day_parts must 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.before must be negative — 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:

story_time: precision: year year: 813 strand: The Unlived # absent = the main strand

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

$ docker compose up -d Container keel-postgres-1 Healthy Container keel-yard-1 Started $ curl http://localhost:8000/api/version {"version": "0.9.0"}

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

The MVP test login — one seeded account, local only, printed on the login page itself because it is not a secret:
username admin  ·  password mvp
Admins 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, Enter signs in, Tab moves between fields, F2 drops to local dev mode (no yard), Esc quits. 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 logout to 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:

Seriesthe projects — slug, name, author
Objectsevery scene, arc, volume, entity, matter — with head revision
Revisionsthe immutable history: author, message, hash, status
Lockswho holds what, since when — exclusivity is structural
Changesetsamendments and their pending counts
Tokensissued sessions

Previews — the book, in the browser

Render locally, view from the yard:

$ keel render --publish rendered .keel\render\book.pdf published — view it at http://localhost:8000/preview/the-salt-ledger.pdf

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/tokencredentials → bearer token (the only unauthenticated write)
GET /api/versionthe running version, no auth
/api/serieslist and create projects
…/objectscreate, read (any revision), checkout, checkin, unlock, log, locks — the same strict-RCS semantics as the local store, transaction for transaction
…/changesetsopen, list, members, land, abandon — amendments
…/previewupload 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.

scenes bodies only, in frames order plank → typst markup mapped, @cut dropped book.typ meta + template + content Typst ~1 second book.pdf the trim F5 re-renders · the viewer reloads · you keep writing
The pipeline. Keel writes the metadata bindings, appends the template prelude and the mapped content, and shells out to Typst — the same way it shells out to your editor.

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:

trimnovella, 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)
margins18 inner · 14 outer · 16 top · 18 bottom (mm) — asymmetric so the gutter doesn’t swallow text
text block95 × 169.2 mm
measure95 mm → 269.29 pt ÷ (0.422 em × 10.5 pt) ≈ 60 characters per line
lines / page169.2 mm → 479.62 pt ÷ 13.6 pt leading = 35 lines
words / page60 × 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 @subsmall caps · underline · super/subscript
@thinkitalic — semantically distinct from emphasis
@notea real printed footnote
@redactsolid black bars
@lang frcorrect hyphenation and language metadata for the span
@quote · @epigraphinset blocks; credit="…" becomes a right-aligned attribution
@verse · @songline breaks preserved, inset (song in italic)
@letter @journal @email @chat @transcript @terminal …inset documents; terminal in monospace
@breakthe scene break — centred asterisks
@ref @factrender as nothing but their text — they exist for the index and the ledger
@pageprints nothing — the drafting-page marker stays in the workshop
@cutvanishes 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

p · F5 render the volume; opens your viewer the first time (p in interactive mode; F5 anywhere, including while editing) $ keel render render from the CLI $ keel render --open … and open the PDF $ keel render --publish … and push it to the yard at /preview/<slug>.pdf

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

$ keel export --tree exported tree → export\ export\ series.toml # imprint · author · canon policy loft\ calendar.yaml characters\kael.md # frontmatter + prose, one file each locations\ factions\ lore\ ledger\ facts.jsonl # one fact per line, sorted, stable bytes promises.yaml volumes\01\ volume.toml # title · target · page geometry frames\chapters.yaml keel\arc-v1.1\ arc.yaml # the arc card: premise + the ordered scenes: list s-0001.md # one scene, one file — contract frontmatter + prose planking\ ch01\01-s-0001.md # ONE SENTENCE PER LINE, reading order unassigned\ # scenes not yet in a chapter — never hidden

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

$ keel export --epub # or --docx exported epub → export\book.epub

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)

$ keel export --print exported print interior → export\book-print.pdf

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)

$ keel print-package interior: 312 pages → .keel\render\print-package\interior.pdf wrap: → .keel\render\print-package\wrap.pdf print package #4 pushed — 312 pages · 0500X0800.BW.STD.PB.060UC444.MXX

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

$ keel snapshot "Draft One" snapshot draft-one — 14 objects pinned $ keel snapshot draft-one 2026-08-25 21:14 UTC 14 objects

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

$ keel stats words 41208 / 150000 (27%) · ~137 of ~502 pages pace 5810 words this week (~830/day) burn ~131 days to target at this pace last 14 days: 2026-08-24 1240 ▮▮▮▮▮▮ 2026-08-25 2402 ▮▮▮▮▮▮▮▮▮▮▮▮

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

What protects your book, layer by layer:
  • 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.sqlite freely.
The honest gaps, until hosting: there is no scheduled nightly export yet, and no off-machine copy unless you make one — both arrive with hosted series (managed Postgres with point-in-time recovery, the nightly tree export to object storage, and a restore that has actually been tested). Until then: keel export --tree into something that syncs is a fine habit.

CLI reference

The zero-CLI principle. Nothing on this page is required. Every command here is also reachable from inside the TUI — open the palette with ^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 IDCheck out: take the exclusive lock, pull the head revision into checked-out/ as plain text.
co -lList 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 IDThe full revision trail — rev, date, author, status, message.
diff ID [A..B]Word-level diff between two revisions; defaults to previous..head.
statusEvery open checkout with holder and age, the spine frontier, stale amendment debt, and unproofread scenes.
todayThe morning briefing: the frontier scene, hanging promises, unproofread scenes, yesterday's words, and one suggested next action (usually the next dry scene).
$ keel init "The Long Year" --author reza $ cd the-long-year $ keel co s-0001 checked out s-0001 r1 → checked-out\s-0001.md $ keel ci s-0001 -m "the ferryman scene" checked in s-0001 r2 $ keel diff s-0001 # word-level: [-removed-] and +added+ $ keel log s-0001 $ keel status $ keel today

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.
synopsisThe whole spine stitched into continuous prose — the book before it is written — plus the frontier.
framesChapters over scenes with word budgets and real printed-page estimates.
$ keel new arc-v1.2 --title "Salt and Ash" $ keel new s-0007 --arc arc-v1.2 --title "The River" created s-0007 — "The River" (○ draft · listed in arc-v1.2) $ keel new s-0008 --arc arc-v1.2 --after s-0005 --title "The Toll" created s-0008 — "The Toll" (○ draft · listed in arc-v1.2 after s-0005) $ keel new e-kael --name Kael --category character $ keel synopsis $ keel frames

Writing & proofreading

review SCENESend 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 SCENERun the strict AI copy-edit pass and show the corrections as a word diff. Nothing is written.
proofread SCENE --acceptApply the corrections as a new revision and mark it proofread.
proofread SCENE --mark-onlyRecord a human proofread of the current head — no AI. Marks are per-revision; any edit clears them.
verify SCENEDoes the prose deliver the contract's input → output? A verdict with quoted evidence and up to three gaps. Ephemeral — nothing stored, nothing gated.
reader CHAPTERSimulated 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 IDExpand bare @end to @end name in the working file's prose body — the frontmatter passes through (also happens on every check-in).
plank strip IDPrint the scene's body as clean CommonMark — markup collapsed, @cut interiors gone, @page markers rendered as nothing.
plank check IDReport plank issues in the prose body; structural markup in prose and unclosed blocks are errors.
$ keel proofread s-0001 s-0001 r2 — corrections ([-…-] +…+): He [-walkked-]+walked+ to the gate. $ keel proofread s-0001 --accept $ keel plank check s-0001 s-0001: clean (4 plank tokens) $ keel review s-0001 s-0001 ◐ in-review — queued for approval on the yard

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 listEvery 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 listPaid 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 MSGDiff 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 CSWalk heads back over the retained pending revisions. History is never deleted.
changesetsEvery amendment with status and pending count.
$ keel co s-0001 # edit output: in the frontmatter… $ keel amend s-0001 -m "the toll is memory" amendment a-1 opened — s-0001 (output) blast radius (2 impacts): ⛔ s-0002 input must be re-satisfied ⚠ P-01 opened in s-0001 $ keel co s-0002 # reconcile the successor… $ keel ci s-0002 -m "re-satisfy entry" --changeset a-1 $ keel land a-1 a-1 landed — 2 revisions canon $ keel promise add --statement "whose memory did he lose?" --opened-in s-0001 --due-arc 1 $ keel extract s-0001 --accept all

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.
statsWords against target, printed pages, weekly pace, and a fourteen-day burn-down.
$ keel render --open --publish rendered .keel\render\book.pdf published — view it at http://localhost:8000/preview/the-long-year.pdf $ keel export --tree $ keel export --epub $ keel snapshot "draft one" $ keel stats

Search & the index

search QUERYFull-text search (FTS5) across every object; rebuilds a missing index automatically.
index --rebuildReconstruct 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.
$ keel search "poured pewter" s-0001 scene … the river went past them the colour of [poured pewter] … $ keel index --rebuild $ keel geometry

davy & voice

ai draft SCENE -i ASKdavy from the CLI: assembles the full context recipe, streams the draft to stdout, logs usage. Nothing is written to the store.
ai usageToday's and the month's AI spend from the usage log.
voice seed FILEStore ~1,500 words of your own prose as the voice profile davy writes toward.
$ keel voice seed my-old-story.txt $ keel ai draft s-0002 -i "He realises the toll was a name. No dialogue." $ keel ai usage today $0.14

Session & meta

login --url U --username N --password PSign in to the yard and lock the TUI to it. The password flag is the MVP test login only.
logoutSign out; the TUI returns to local dev mode.
changelogPrint 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.
$ keel login --url http://localhost:8000 --username admin --password mvp signed in to the yard at http://localhost:8000 (v0.9.0) as admin $ keel auto-novel "The Salt Ledger" --pages 24 --characters "Kael,Vess,Immy" $ keel changelog $ keel logout

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.

git (optimistic) merge — and prose merges badly Keel (pessimistic) lock · edit · check in one line of history per object; a conflict cannot be constructed
Reflowed paragraphs are the worst possible merge input. Locks make the merge problem unrepresentable instead of merely rare.

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.

The shape of every rule here: make the failure unrepresentable if possible; if not, make it loud; never make it silent. Locks over merges, contracts over guidelines, marks that expire over marks that rot, and integrity errors that say "this should be impossible — investigate" instead of retrying quietly.

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 …)"

$ keel co s-0001 keel: object s-0001 is checked out by reza (since unix 1787654321) exit 3

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"

$ winget install Typst.Typst $ winget install JohnMacFarlane.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

C:\ai\workspace\keel\ the main keel dir — everything lives here target\release\keelhelm.exe the app (keel.exe beside it is the console CLI) novels\<series>\ a series workspace .keel\store.sqlite the store: objects · revisions · locks .keel\index.sqlite derived: search + refs (rebuildable, deletable) .keel\dict.txt the project dictionary (spellcheck) .keel\render\book.pdf the last render .keel\ai-usage.jsonl the cost meter's log .keel\provenance.jsonl accepted DAVY spans (the ✎% record) checked-out\ your working files — plain text, always yours .keel-home\credentials.toml the yard session (starter-launched)

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
Something misbehaving that is not on this page? Screenshots are the fastest bug reports — most of the UI fixes in this build started as one.

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

C:\ai\workspace\keel\ Cargo.toml the workspace: members, shared deps, THE version (x.y.z single source) Cargo.lock pinned dependency graph CHANGELOG.md the recorded history — shown by keel changelog, the TUI overlay, and /changelog BACKLOG.md decisions, milestones, the owner's kept feature picks docker-compose.yml the yard + Postgres 16, one `docker compose up` from zero start-keel.ps1 build-and-launch convenience for keelhelm.exe (-Rebuild) deploy.ps1 fly.toml the one-command whole-solution deploy to Fly.io (keel.pub) SPEC.md the implementation state + re-entry guide .gitignore target/, novels/, .keel-home/, python artifacts .dockerignore keeps target/ and git out of the image novels\ your series live here (git-ignored) .keel-home\ credentials/session when launched via the starter (git-ignored) temp\ untracked scratch (screenshots) — not part of the solution

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-core\ Cargo.toml deps: serde, yaml/toml, similar, thiserror — no I/O crates, by rule src\lib.rs module map + re-exports src\id.rs ObjectId + kinds — filename-safe ids, prefix carries the kind src\model.rs the scene contract, review statuses, entities, frames src\spine.rs SpinePath (position from id), Spine, frontier, synopsis, sparkline src\time.rs StoryTime with era precision; unix formatting; day parts src\calendar.rs the five-era calendar, story-time validation, the total order src\ledger.rs facts, promises, refs, the two clocks, blast radius src\plank.rs the markup: parser events, fmt (bare @end), strip (CommonMark) src\typst.rs plank → Typst emission for the book pipeline src\context.rs davy's recipe: pinnable sections S1–T2, token estimates src\geometry.rs trim/margins/face → chars × lines × ~300 words/page, with a calibrated advance per vendored face src\files.rs canonical parse/serialize for every file form + the seeds src\diff.rs word-level diff (line diffs on prose are unreadable) src\echo.rs the echo detector — distinctive-word repeats, pure, no AI

crates/keel-store — strict RCS over SQLite

crates\keel-store\ Cargo.toml rusqlite (bundled), zstd, blake3 src\lib.rs module map src\store.rs the Store trait — one schema, Local now, yard next src\schema.sql objects · revisions · locks · changesets, monotonicity trigger src\local.rs LocalStore: exclusive locks, MAX(rev)+1, loud stale-base, changesets src\error.rs every failure named: Locked, StaleBase, PendingInChangeset…

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-helm\ Cargo.toml ropey, ratatui, unicode-width src\lib.rs the pinned public surface src\view.rs the helm's editor state: rope, cursor, selection, decorations, focus src\wrap.rs soft wrap at display columns, per-line cache src\keys.rs vim-lite modal keys (normal/insert/visual, the fenced set) src\journal.rs flat undo/redo with same-word coalescing src\render.rs typewriter offset, decoration merge, cursor cell tests\helm.rs the public-API suite: motions, undo, search, wrap, dimming tests\markup_api.rs selection wrap + block insertion (the ^m palette's hooks)

crates/keel-tui — the helm's shell (backend-generic)

crates\keel-tui\ Cargo.toml ratatui, crossterm (types), spellbook, keel-helm, keel-client src\lib.rs AppData assembly; run_at / run_standalone; open_store_at src\app.rs the pure reducer: views, lenses, modes, mouse, overlays — all testable src\event.rs the blocking loop: key routing, DAVY drain, F5 render + pages, refresh src\source.rs the EventSource seam: terminal or window, the loops cannot tell src\spawn.rs where child keel verbs run (the sibling console CLI) src\session.rs WriteSession: checkout/resume/read-only, save, spell tick src\login.rs the centered editor login (F2 dev mode, Esc quit) src\picker.rs the series picker outside any workspace — open-only src\console.rs the : command console — any keel command, run in place src\davy.rs recipe pre-warming (background thread) + the live stream + provenance src\spell.rs spellbook + the project dictionary → misspelling ranges src\theme.rs the keel/paper themes, ground+ink, persistence src\term.rs TerminalGuard for the debug terminal fallback src\ui\mod.rs layout: toolbar, bordered head/rail/content/foot, overlays src\ui\toolbar.rs the clickable chip row (views · preview · palette · theme · help) src\ui\head.rs breadcrumb, mode badge, the scene contract line, ◉ editor src\ui\rail.rs lens strip + per-lens content + the split detail box src\ui\foot.rs words · rev · lock · ⚑ proofread · ✎% · key hints src\ui\views.rs per-view centers: map preview, codex, ledger, review, write src\ui\worldline.rs the era-segmented timeline, both modes, scroll math src\ui\davy.rs the ^g panel: instruct/inspect/stream/review src\ui\comments.rs the ^k overlay: the scene's yard comment threads, mark-done src\ui\markup.rs the ^m contextual markup palette with preview src\ui\palette.rs the ^p command palette src\ui\console.rs the console output overlay src\ui\help.rs the ? help — the manual inside the tool src\ui\theme_modal.rs the ^t theme picker src\ui\changelog.rs the changelog overlay (embedded at build time)

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.

crates\keel-wind\ Cargo.toml winit, ratatui-wgpu, wgpu, image (png), pollster build.rs embeds the k. icon into keelhelm.exe (winresource) assets\keel.ico the typst-drawn app icon src\lib.rs the winit shell: window, worker, DPI-scaled fonts, F11, close src\translate.rs winit keys/mouse → crossterm events (⌘→^ on mac) src\backend.rs the ResizeBackend newtype: resize, rescale, preview image src\processor.rs the wgpu post-processor: text blit + the full-res page quad

crates/keel-client — AI and the yard, over the wire

crates\keel-client\ Cargo.toml tokio, reqwest, eventsource-stream — async lives HERE only src\lib.rs module map src\provider.rs the AiProvider contract: stream events over a sync channel src\direct.rs the raw Messages API: JSON + SSE, cache_control, no SDK src\mock.rs the offline provider (KEEL_AI_MOCK) — every test runs without a key src\usage.rs model routing per agent + the cost meter's math and log src\creds.rs stored yard credentials (shared by login and the TUI screen) src\yard.rs yard HTTP: token login, session validation src\yard_store.rs Store::yard — the Store trait over the yard's HTTP API src\yard_cache.rs the (object, revision) read-through cache — immutable, never stale src\yard_config.rs workspace.toml — the marker that routes a workspace to the yard src\context_cache.rs small pure helpers (day boundaries for the meter) tests\mock_stream.rs byte-exact mock streaming + provider selection

crates/keel-index — derived data, rebuildable

crates\keel-index\ Cargo.toml rusqlite src\lib.rs deterministic rebuild: docs + FTS5 + refs; search; dict.txt tests\determinism.rs byte-identical rebuilds — the discipline test tests\search.rs bm25 hits, snippets, query wrapping tests\dict.rs sorted dictionary generation from the loft

crates/keel — the binaries

crates\keel\ Cargo.toml clap + every crate above; two [[bin]] targets src\main.rs keel.exe — the console CLI: dispatch, exit codes src\main_app.rs keelhelm.exe — the windowed app (also runs verbs when spawned with args; bootstraps KEEL_HOME and novels\) src\cli.rs the whole command surface (see the CLI reference) src\exit.rs error → exit-code mapping (3 locks, 4 integrity) src\workspace.rs workspace discovery, working-file paths, identity src\spine_load.rs store → Spine, loudly src\ledger_io.rs ledger load/save under locks, next-id, narrative positions src\commands\ one file per verb: init.rs co.rs ci.rs log.rs diff.rs status.rs the RCS lifecycle new.rs synopsis.rs frames.rs the spine proofread.rs plank.rs the writing chain fact.rs promise.rs extract.rs the ledger amend.rs blast radius · land · abandon · list render.rs export.rs snapshot.rs stats.rs the book and its numbers search.rs index.rs geometry.rs derived data review.rs `keel review` — the PR verb: draft → in-review ai.rs voice.rs davy from the CLI today.rs the daily ritual: frontier, promises, pace login.rs changelog.rs auto_novel.rs session · history · the sample tests\ end-to-end against the real binary: cli_roundtrip.rs index_determinism.rs M0 acceptance m1_loft.rs m2_spine.rs m3_plank.rs per-milestone suites m5_ai.rs m6_ledger.rs m7_launch.rs m7_proofread.rs m8_login.rs m8_auto_novel.rs typst_geometry.rs 62×34 vs real renders (ignored by default)

yard/ — the web admin

yard\ Dockerfile python-slim image: migrate, seed admin, serve manage.py Django entry requirements.txt django, django-ninja, psycopg, blake3, zstandard, pytest-django pytest.ini test config (sqlite for speed) yard\settings.py DATABASE_URL or sqlite; version parsed from Cargo.toml yard\urls.py admin · api · docs · changelog · preview routes yard\asgi.py wsgi.py servers' entries core\apps.py the app config core\models.py Series, KeelObject, Revision, Lock, Changeset, Token, PreviewPdf, CoverImage, Membership, Profile, Comment core\store.py the semantics engine — mirrors local.rs checkin exactly core\codec.py canonical card bytes — parity-pinned against files.rs core\api.py django-ninja endpoints with machine-readable error codes core\planner.py the yard pages: tree, cards, approvals, comments, design, activity core\spine.py tree building, next ids, geometry echo, glyphs core\seeds.py the init seed set (m-design included), shared with sample core\sample.py the Salt Ledger built server-side, blake3-identical core\admin.py KEEL-branded Django admin registrations core\views.py home, docs sections, changelog, preview + cover + print-file serving core\lulu.py the Lulu Print API client (OAuth, validation, quotes, jobs, webhook HMAC) — sandbox by default, dark without credentials core\printing.py the PrintProvider abstraction + SKU composition against the pinned catalogue; rfq lines for tier-2 bulk runs core\lulu_products.json the pinned printer catalogue (byte-parity twin of tools\lulu\pod-packages.json) core\context_processors.py the version into every template core\management\commands\seed_admin.py the idempotent local dev login core\migrations\ the schema · preview PDFs · covers, roles, profiles · print packages, jobs, ship-to core\tests\ api parity · codec byte-parity · planner · sample · showcase · lulu (publishing) · docs static\yard.css the shared shell styles (wifuture tokens) static\fontsamples\ one rendered book page per face (gen-font-samples.ps1) templates\base.html the site shell: palette, favicon, header, version templates\home.html pricing.html login.html the public face templates\changelog.html the rendered changelog templates\planner\ the yard: index, tree, edit, design, publish, activity… templates\docs\ THIS documentation: the one-page manual (all.html) + the Begin chapters templates\admin\base_site.html admin branding + palette templates\admin\login.html the login page: home link + the dev credentials

tools/ and docs/ — assets and the founding papers

tools\ typst\book.typ the book template: running heads, the baseline grid, m-design styles (TOC, openers, drop caps, cover) typst\wrap.typ the one-piece print cover: back + spine + front, bleed typst\font-sample.typ the one-page specimen behind the design page's face picker typst\geometry-check.typ the acceptance harness that pins the grid, parameterized fonts\ nine vendored book faces (OFL alongside each) + the app faces: JetBrainsMono, DejaVuSansMono lulu\pod-packages.json the pinned printer catalogue — every orderable product, page ranges, paper bulk; gen_capability.py regenerates it e2e-lulu.ps1 the sandbox end-to-end (token, spine formula, quote) gen-font-samples.ps1 regenerates the design page's specimen images icon\keel-icon.typ the k. mark, drawn by typst; build-icon.ps1 emits the .ico dictionaries\en\ hunspell en dictionary powering spellcheck (index.aff/.dic) docs\ keel-spec.html the design spec, rev 0.7 — where all of this was decided keel-surfaces.html the three surface mocks the UI was built to calendar-draft.yaml the settled world-calendar structure verification-m0.md the M0 acceptance walkthrough
Reading order for the curious: keel-core/src/model.rs for the shape of the thing, keel-store/src/local.rs for how nothing is ever lost, then keel-tui/src/app.rs for how it all drives. The spec in docs/ is the long answer to every "why".