Markdown To

Specifications v0.1 draft Sheet 02 of 03

kanban@0.1

A board as a plain Markdown file — a level-2 heading is a column, a task item beneath it is a card, and the file is the whole board. It is a grouped view over exactly the same item model as todo@0.1, so that in an ordinary Markdown viewer a board degrades to a useful sectioned checklist rather than to noise.

Status
normative · draft, will change
Inherits
shared conventions · the item model
Artifacts
schema.json
fixtures/
Owns codes
MDTO200–MDTO299 9 allocated

§ 01

The file, and the board

On the left, a conformance fixture — specs/kanban/fixtures/valid/full-metadata.md, byte for byte. On the right, mdto render’s read-only board. Same bytes, no second copy, nothing derived that is not in the file.

Fig. 1 One file, two views — kanban@0.1 static render
launch.kanban.md
---
markdownto: kanban@0.1
title: Markdown To launch
id: mdto-launch
board-owner: akshay
---

# Markdown To launch

## Backlog

- [ ] Obsidian Kanban import adapter [priority:: low] #import
- [ ] MCP server over the same verbs [priority:: low] [effort:: 1w]

## Doing

- [ ] Build the patch engine [owner:: @akshay] [priority:: high] [due:: 2026-08-20] ^k3
- [ ] Fuzz the round-trip [owner:: @akshay] [priority:: medium] #infra ^k7

## Review

- [ ] Landing page copy [owner:: @akshay] [due:: 2026-08-18]

## Done

- [x] Settle the shared conventions [owner:: @akshay] ^k1
- [x] Buy the domains #ops
rendered · mdto render static · no script

Markdown To launch

2 doneof 7 cards across 4 columnsread-only

Backlog

0/2
  • Obsidian Kanban import adapter #import

    priority low

  • MCP server over the same verbs

    priority loweffort 1w

Doing

0/2
  • Build the patch engine

    priority highdue 2026-08-20owner @akshay^k3

  • Fuzz the round-trip #infra

    priority mediumowner @akshay^k7

Review

0/1
  • Landing page copy

    due 2026-08-18owner @akshay

Done

2/2
  • done

    Settle the shared conventions

    owner @akshay^k1

  • done

    Buy the domains #ops

The board scrolls horizontally inside its own container when it is wider than the pane; the page never does. Every value shown went through the escaper on its way out, and the pane carries no script — this is the static render adapter, not the live board. The markup was produced by @markdownto/web at build time; see site/tools/build-specs.mjs.

Why headings are columns

It is the only mapping that survives a viewer knowing nothing about Markdown To. Open a conforming board in GitHub, in Obsidian, or in less and you get a sectioned checklist that is still genuinely useful. That graceful degradation is the portability promise, not a side effect of it. Any encoding that made a board unreadable without the tool — a table of columns, a YAML block, HTML comments — would have broken the promise on line one.

§ 02

Grammar

Distilled for this page. A card is a todo item — the grammar is todo@0.1 verbatim, and the schema says so structurally: kanban’s card definition is a $ref to todo’s item definition, so the two cannot drift apart without someone deliberately breaking the reference.

2.1

The envelope, and the two keys kanban adds

the envelope columns optional
---
markdownto: kanban@0.1
title: Markdown To launch   # optional, reserved
id: mdto-launch             # optional, reserved
columns:                    # optional, spec-defined
  - Backlog
  - Doing
  - Done
---
columns absent
The normal case. The columns are whatever the document’s ## headings say — what a hand-author gets for free, because the document is the state.
columns present
The board opts in to strict checking. A declared column with no heading is MDTO206; a heading not declared is MDTO207; a different relative order is MDTO208, a warning. A wrong type on the key is MDTO007.
done-column
Optional. Names the column that means finished (§2.3), for a board whose last column is a parking lot. Matched case-insensitively after trimming; a name that is not on the board falls back to the last column, silently — the key carries no diagnostics in 0.1.
Why the declaration is optional

Requiring it would mean every hand-authored board starts with a declaration that duplicates the headings directly below it, and duplicated truth drifts. The declaration exists for boards where the column set is a policy — a shared team board where “someone added a Shipped column” should be caught — and declaring it opts into exactly that check, and nothing else.

2.2

Document structure

a leading #
Board title. Informative; it opens nothing.
##
A column. Column order in the document is the board’s left-to-right layout. Names are required (MDTO204) and unique (MDTO205), compared case-insensitively after trimming. An empty column — a heading with no cards — is valid and common.
- [ ] at top level
A card. Position within a column is the priority order. Both orders are meaningful; nothing sorts them.
nesting
An item on that card’s checklist — not a sub-card. Only top-level task items in a column are cards, at any board depth.
a card before any ##
MDTO202. There is no implicit column. Prose before the first column is fine — a board description is normal.
no columns at all
MDTO201. A board needs at least one.
### and deeper
MDTO203. A board has one level of grouping.
metadata on a heading
MDTO209. ## Doing [limit:: 3] is the obvious next feature, and this code exists to say out loud that column-level metadata is a deferred extension point, not an oversight.
Why there is no implicit column, when todo has an implicit section

The two specs differ here on purpose. A todo list is fundamentally a list, and grouping is an optional convenience — so a bare list is valid. A board is fundamentally a partition: a card in no column has no position on the board, and rendering it would require inventing a column the file does not contain. The MDTO202 repair is always to add a heading or move the card, never to invent an “Inbox”.

2.3

The done-role column

In the grammar the two facts stay independent: - [x] marks a card done wherever it sits, a parser reads exactly what is written, and a checked card in Doing is a legal file with no diagnostic attached. At the verb and UI layer they are coupled. The last column in document order carries the done role — the same rule for Done, Shipped, Live or 完了, because position is the part of the idiom that is universal — and a move that crosses into it checks the box, while a move out of it unchecks. Reordering inside a column touches no box. done-column: in the envelope overrides the default for a board whose last column is a parking lot.

a state the grammar still allows
## Doing

- [ ] Build the patch engine [owner:: @akshay] [due:: 2026-08-20] ^k3
  - [x] validate
  - [ ] render
- [x] Freeze the item model
Why the column is the state

A board has one idiom, and it is that you finish work by dragging it right. A board that also asks you to tick a box has two controls for one fact, and sooner or later they disagree and the tool has to guess. So move writes both facts and the live board draws no checkbox — which is also what keeps the promise: a board edited through the verbs opens in any viewer as a checklist whose ticks are right. A disagreement you did not create is still a legal file: render it as written, never tidy it up on the way past.

Read kanban@0.1 in full — the normative text Shared conventions

§ 03

Verb reference

kanban@0.1 owns add, move, done, rm, edit. There is no undone: un-checking is routine on a checklist and a rare correction on a board, so kanban spends the slot on edit instead. That is what “verb vocabularies are spec-owned” means in practice — each spec spends its verbs on what its domain does often.

verbsignaturesource edit

add

mdto kanban add "<text>" --column <col> [--priority p] [--due d] [--owner o] [--top | --before <ref> | --after <ref>]

Inserts one line at the end of the target column. --column is required: there is no implicit column to fall back on. A column that does not exist is an error unless --create-column is passed — which also adds the name to columns: if the board declares one, because leaving them inconsistent would immediately produce MDTO207.

movethe showpiece

mdto kanban move <ref> <col> [--keep-state] [--top | --bottom | --before <ref> | --after <ref>]

Deletes the card’s line together with its checklist lines and reinserts them with the text, fields, tags, identifier and relative indentation verbatim. It collapses at most one blank line left doubled by the removal. The checkbox is the one thing it may rewrite, and only when the move crosses the done role: into the last column it is checked, out of it unchecked, and a reorder inside a column leaves it alone. --keep-state opts out. A drag on the live board is this verb, and its diff is one removal plus one insertion.

done

mdto kanban done <ref>…

A three-character replacement inside the checkbox. The card does not move, no column changes, no timestamp is stamped, the checklist is untouched. To uncheck, use edit --uncheck.

rm

mdto kanban rm <ref>…

Removes the card’s line and every line of its checklist, then collapses at most one doubled blank line. It never renumbers, reflows or re-indents neighbouring cards. Removing a pinned card retires its identifier permanently.

edit

mdto kanban edit <ref> [--text "…"] [--set k=v]… [--unset k]… [--tag +t | --tag -t]… [--check | --uncheck]

Rewrites one line in place. --set adds or replaces a field, keeping its existing position on the line if it is already there and appending in canonical order if it is not. Unknown fields are only touched when named explicitly.

3.1

Addressing a card, and the common flags

<ref>
A pinned identifier with or without the caret (k3, ^k3); otherwise a case-insensitive substring of the card’s normalized text ("patch engine"). If it matches zero or more than one card, the verb fails with a non-zero exit code and changes nothing.
--column
Narrows the search. Column arguments are matched case-insensitively after trimming.
--file
Defaults to the single *.kanban.md in the working directory.
--dry-run
Prints the unified diff and writes nothing.
--pin
Pins an identifier on the touched card and returns it. No verb pins one otherwise. The live web board is the standing exception: it must track cards across re-renders, so it pins on first interaction — which is why exactly one card in the conventions example carries ^t3.
Why the minimal-diff rule matters most here

This is the spec that proves the round-trip promise. Dragging a card on the live board must produce exactly the source edit a person would have made by hand — one line removed, one line inserted, unrelated prose untouched, unknown fields intact — and if the file changed underneath the board, the conflict surfaces via the source hash rather than being overwritten. A board that reformats the file on save has failed at the only thing that makes this format worth having.

What no verb ever does

Reformats a line it was not asked to change·normalizes bullet markers, indentation or blank lines·reorders cards or columns it was not asked to reorder·drops an unknown field or an unrecognized value·checks a box because of which column a card landed in·pins an identifier without --pin·rewrites the markdownto version·writes a file whose source hash no longer matches the one it read.

§ 04

Diagnostics carry the repair

Everything a card can get wrong produces a shared code from the item model — the same broken card reports the same diagnostic in both specs, and that overlap is the point. The board’s own structure owns MDTO200MDTO299.

launch.kanban.md 4 errors
---
markdownto: kanban@0.1
title: Markdown To launch
---

## Backlog [limit:: 3]

- [ ] Obsidian Kanban import adapter

### Doing

- [~] Build the patch engine
- [ ] [priority:: high]

mdto validate · verbatim 4 diagnostics

  • MDTO209 error line 6

    ## Backlog [limit:: 3]

    Inline field limit on a column heading: columns carry no metadata in kanban@0.1.

  • MDTO203 error line 10

    ### Doing

    Heading level 3 is not valid in a kanban document: a column is a ## heading.

  • MDTO031 error line 12

    - [~] Build the patch engine

    Unrecognized checkbox state ~: kanban@0.1 defines only [ ], [x], and [X].

  • MDTO033 error line 13

    - [ ] [priority:: high]

    Card has no text: it consists only of metadata.

Recovery is local, and deliberate

A heading invalid for its level does not open a column, and the cards below it belong to the enclosing one — so they do not additionally report MDTO202 while a file is being repaired. A heading invalid for its content still opens a column, so items cannot silently migrate into the previous one. An unusable columns: declaration is discarded whole rather than firing a set-mismatch error once per heading.

4.1

The codes this spec owns

MDTO201 error Board has no columns.
MDTO202 error Card appears before the first column heading; there is no implicit column.
MDTO203 error Heading level is not valid: only one leading # title and ## columns.
MDTO204 error Column heading has no name.
MDTO205 error Duplicate column name.
MDTO206 error Column declared in frontmatter columns has no heading in the document.
MDTO207 error Column heading is not declared in frontmatter columns.
MDTO208 warning Column order differs from the declaration. Only a warning: the document is authoritative about layout, so the board is still unambiguous — it is the author’s intent that is in question.
MDTO209 error Inline field or block identifier on a column heading; columns carry no metadata in 0.1.

§ 05

For agents

Agents are the primary users of the CLI, and the value over freehand editing is determinism: guaranteed-valid output, minimal diffs, no whole-file context required. This sheet is the human edition; the agent edition is the same document, fetched.

mdto — working name in progress
$ mdto spec kanban
kanban@0.1 — agent-facing specification
  1. Purpose   what the format is for
  2. Grammar   heading = column, item = card
  3. Rationale why it is shaped that way
  4. Examples  valid + invalid + diagnostics
  5. Verbs     add · move · done · rm · edit

$ mdto validate launch.kanban.md
ok conforms to kanban@0.1 · 7 cards, 4 columns

$ mdto kanban move k3 Review --dry-run
--- launch.kanban.md
+++ launch.kanban.md
-- [ ] Build the patch engine [priority:: high] ^k3

 ## Review

+- [ ] Build the patch engine [priority:: high] ^k3
ok 1 removal, 1 insertion · checkbox untouched
  • One item model, one implementation

    One parser, one validator, one patch engine, and one set of habits for anyone — human or agent — who has learned either spec. If you know how to repair a todo item you already know how to repair a card, because they are the same object by construction.

  • Fetchable, not scrapeable

    The agent-facing document lives at a stable URL beside an llms.txt so an agent never has to scrape a human documentation site to author a conforming board.

    fetch markdownto.ai/specs/kanban.md

    fetch markdownto.ai/llms.txt

  • Fixtures are the contract

    An independent implementation conforms if and only if it agrees with specs/kanban/fixtures/. Valid fixtures parse with no error and round-trip byte for byte; every invalid one carries a sibling .diagnostics.json naming the exact codes, severities and lines.

    universal validaterenderspec