# kanban@0.1

**Status:** normative. **Inherits:** [`../conventions.md`](../conventions.md) — envelope, lazy
identifiers, inline fields, tags, wikilinks, preservation rules, and the shared diagnostic codes
`MDTO001`–`MDTO099`.
**Schema:** [`schema.json`](schema.json), which `$ref`s the item model from
[`../todo/schema.json`](../todo/schema.json) rather than restating it.
**Fixtures:** [`fixtures/`](fixtures/). **Owns diagnostic codes:** `MDTO200`–`MDTO299`.

**Companion skill:** [Authoring and editorial guidance](https://markdownto.ai/specs/kanban.skill.md) (non-normative).

---

## 1. Purpose

`kanban@0.1` describes a board as a plain Markdown file — a level-2 heading is a column, a task list
item beneath it is a card — where the file is the complete state of the board. It is a grouped view
over exactly the same item model as [`todo@0.1`](../todo/SPEC.md), so that in any ordinary Markdown
viewer a board degrades to a useful sectioned checklist rather than to noise.

---

## 2. Grammar

### 2.1 Envelope

```yaml
---
markdownto: kanban@0.1
title: Markdown To launch   # optional
id: mdto-launch             # optional
columns:                    # optional, spec-defined
  - Backlog
  - Doing
  - Done
---
```

`kanban@0.1` defines two additional frontmatter keys:

| Key | Type | Meaning |
|---|---|---|
| `columns` | sequence of strings | Optional declaration of the board's column set and order. |
| `done-column` | string | Optional name of the **done-role column** (§2.4). Default: the last column. |

When `columns` is **absent**, the columns are whatever the document's `##` headings say — the normal
case, and the one a hand-author gets for free. When it is **present**, the board opts in to strict
checking: a declared column with no heading is `MDTO206`, a heading not declared is `MDTO207`, and a
different relative order is `MDTO208` (warning). A wrong type on the key is `MDTO007`.

`done-column` names the column that means *finished*, for a board whose last column is not it — an
Icebox, an Archive, a parking lot. It has exactly one rule:

**`done-column` must name a column of this board.** It is matched against the column names the way
every column argument is — case-insensitively after trimming — so `done-column: shipped` names
`## Shipped`, and the IR keeps the spelling the author wrote rather than the heading's. A key that
names no column is `MDTO210`, a **warning**, and the done role falls back to the last column exactly
as it does on a board that declares nothing. A key that is not a string at all names no column
either, so it is the same warning and the same fallback; it is not `MDTO007`, and §3 says why.

The key travels in two places, both of them the declaration and neither of them the resolution: in
`frontmatter` verbatim, like every key, and — when it names a column — in the IR's own `doneColumn`,
beside `declaredColumns`. Which column actually carries the done role is §2.4's rule applied to it.

**If you are repairing a board, do not "correct" the key to the last column's name.** `MDTO210` says
the key names nothing; it does not say which column the author meant, and the two plausible repairs —
fix the spelling, or rename a column back — are the author's to choose. A tool that picked one would
be answering a question the diagnostic deliberately only asks. The one place a tool rewrites this key
is `mdto kanban rename-column`, which is not a guess: there the referent is being renamed in the same
edit, and leaving the key behind is how boards arrive in this state (§5).

### 2.2 Document structure

| Construct | Meaning |
|---|---|
| One optional level-1 heading, before any other content | Board title (informative) |
| A level-2 heading | **Column** |
| A top-level task list item under a column | **Card** |
| A task list item nested inside a card | An item on that card's **checklist** |
| Anything else — paragraphs, blockquotes, tables, code fences, images | Prose. Preserved, rendered, never interpreted |

- A board must have **at least one column** (`MDTO201`).
- **There is no implicit column.** Every card belongs to exactly one named column, so a card before
  the first `##` is `MDTO202`. Prose before the first column is fine — a board description is normal.
- Column order in the document is the board's left-to-right layout, and card order within a column is
  the priority order. Both are meaningful; nothing sorts them.
- A column runs from its heading to the next `##`. Cards are collected in document order across list
  boundaries, so a paragraph between two lists does not split a column.
- A column name is required (`MDTO204`) and unique (`MDTO205`), compared case-insensitively after
  trimming. Source casing is preserved.
- An empty column — a heading with no cards — is valid and common.
- Heading levels 3 and deeper are not part of `kanban@0.1` (`MDTO203`).
- A column heading carries no metadata: an inline field or block identifier on a heading is `MDTO209`.

**Recovery.** A heading invalid for its *level* (`MDTO203`) does not open a column; the cards below it
belong to the enclosing column, and produce no `MDTO202` as long as some column is already open. A
heading invalid for its *content* still opens a column: for `MDTO209` the name is the heading text
with the offending constructs removed, for `MDTO204` it is the empty string. An unusable `columns:`
declaration (`MDTO007`) is discarded, and the set and order checks `MDTO206`–`MDTO208` are skipped
rather than firing once per heading.

### 2.3 Cards

A card **is** a todo item. The grammar is [`todo@0.1` §2.3–2.4](../todo/SPEC.md) verbatim, and the
schema says so: `specs/kanban/schema.json` `$ref`s `specs/todo/schema.json#/$defs/item`. That object
is the shared item model ([conventions §12](../conventions.md)), which `kanban@0.1` adopts whole and
without extension — including todo's two-marker enum, because on a board the workflow state is the
column a card sits in (§2.4), not a wider checkbox.

```markdown
## Doing

- [ ] Build the patch engine [owner:: @akshay] [priority:: high] [due:: 2026-08-20] ^k3
  - [x] validate
  - [ ] render
```

- `[ ]` unchecked, `[x]` / `[X]` checked; no other state (`MDTO031`).
- Bullet lists only (`MDTO032`); every list item is a task item (`MDTO030`); a card needs text once
  metadata is removed (`MDTO033`).
- Nested task items are the card's **checklist**, not nested cards. Only top-level task items in a
  column are cards, at any board depth.
- A card's checked state is not derived from its checklist.
- Fields, tags, and lazy `^ids` work exactly as in todo. Nothing inside a code span or fence is scanned.

### 2.4 The done-role column

**In the grammar the two facts stay independent.** `- [x]` marks a card done **wherever it sits**;
an unchecked card in the final column is a legal file; a checked card in the first column is a legal
file. A parser reads exactly what is written, and neither fact is ever derived from the other. Every
diagnostic in §2.5 is unchanged by this section: there is no error, and no warning, for a card whose
box disagrees with its column.

**At the verb and UI layer they are coupled.** Exactly one column carries the **done role**, and a
card *arriving* in it is done:

- The done-role column is the **last column in document order** — the same rule whether the columns
  come from the headings or from a `columns:` declaration, and the same rule for any column
  vocabulary: Done, Shipped, Complete, Live, 完了, `Q3 — released`. Flow reads left to right and the
  rightmost column is where work stops; nothing in the rule reads a name.
- On a **one-column board** that column is both the first and the last, so it carries the done role
  and no move can cross the boundary: every move is a reorder, and the boxes stay whatever the author
  wrote. `done-column:` naming that column is a legal restatement of the default and changes nothing.
- `done-column: <name>` in the envelope (§2.1) overrides that, for a board whose last column is a
  parking lot rather than an ending. Here the done role is `Shipped`, and a card dragged into
  `Icebox` crosses *out* of it:

  ```markdown
  ---
  markdownto: kanban@0.1
  done-column: Shipped
  ---

  ## Doing
  ## Shipped
  ## Icebox
  ```
- A move that crosses **into** the done-role column also checks the box; a move that crosses **out**
  of it unchecks the box. A move *within* a column — including within the done-role column — is a
  reorder and touches no box, which is what leaves a hand-written `- [ ]` sitting in the last column
  showing its actual state instead of being quietly corrected.
- A board that is a mutation surface draws no per-card checkbox: the column is the control. Checked
  state is still rendered — struck through and dimmed — because the file may carry a state no
  gesture produced.

The two claims fit together because the coupling is a property of the *verbs*, not of the grammar.
`mdto kanban move` writes both facts (§5, and `--keep-state` opts out); `mdto kanban done` and
`mdto kanban edit --check` still write the box alone, and never move a card. A file therefore stays
hand-authorable in every combination, and a tool never has to guess which of two disagreeing facts
the author meant.

**What moves the done role.** The role is a position, so the operations that change positions can
hand it over — and each of them says so rather than letting it be discovered:

| What happened | What becomes of the done role |
|---|---|
| A column is added at the end of a board that declares no `done-column:` | It moves to the new column. Adding on the right extends the flow. |
| A column is added anywhere else | Unchanged. |
| The done-role column is **renamed** | It stays with the column. `rename-column` rewrites a `done-column:` that names it, in the same edit (§5). |
| A column is **moved** and the board declares no `done-column:` | It is wherever the last column now is. `move-column` reports the handoff. |
| A column is moved on a board that declares `done-column:` | Unchanged: the key names a column, not a position. |
| The done-role column is **removed** | It falls back to whatever is last afterwards, and `rm-column` reports the handoff. A `done-column:` naming the removed column would be `MDTO210`, so the op drops that key too (§5). |

A tool that changes the board's shape MUST tell the reader when the done role changed hands, because
the next drag will write a checkbox against the new answer.

**The open questions this section deferred are now answered**, each with fixtures:

- *Should a card whose box disagrees with its column be a diagnostic?* **No, and it never will be.**
  §3 has the argument in full: a checked card in Doing means something to the person who wrote it,
  and the grammar has always allowed it. `fixtures/valid/checklists.md` is the proof, and it is a
  *valid* fixture on purpose.
- *Should `done-column` naming no column be a diagnostic?* **Yes — `MDTO210`, a warning**, with the
  fallback unchanged (§2.1). `fixtures/invalid/bad-done-column.md` is the case, and
  `fixtures/valid/done-column.md` is the board it is a broken copy of.
- *Should `schema.json` constrain the key?* **Yes**: `doneColumn`, a string, present exactly when the
  key names a column — the same shape `declaredColumns` has, for the same reason.

### 2.5 Diagnostics owned by this spec

| Code | Severity | Meaning |
|---|---|---|
| `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 in a kanban document: 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 frontmatter `columns` declaration. |
| `MDTO209` | error | Inline field or block identifier on a column heading; columns carry no metadata in 0.1. |
| `MDTO210` | warning | Frontmatter `done-column` names no column on this board; the done role falls back to the last column. |

Everything a card can get wrong is a shared code from the item model — see conventions §9.3. That
overlap is the point: the same broken card produces the same code in every spec that shares the item
model — `todo`, `kanban`, and `backlog` alike — and only the message names the spec. `MDTO031` is
where that matters most, because the legal marker set differs between them (conventions §9.3).

---

## 3. Rationale

*Read this before repairing a file. Most bad repairs come from guessing at intent.*

**Why headings are columns.** It is the only mapping that survives a viewer that knows nothing about
Markdown To. Open a conforming board in GitHub, Obsidian, or `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.

**Why a board is a todo list with more columns.** One item model means one parser, one validator, one
patch engine, and one set of habits for anyone — human or agent — who has learned either spec. The
schemas make the claim structurally rather than by convention: kanban's card definition is a `$ref`
to todo's item definition, so the two cannot drift apart without someone deliberately breaking the
reference.

**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 that is in no column has no position on
the board, and rendering it would require inventing a column that the file does not contain. The
`MDTO202` repair is always to add a heading or move the card, never to invent an "Inbox".

**Why `columns:` 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. Deriving
from headings is the default because the document is the state. 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.

**Why order mismatch (`MDTO208`) is only a warning.** The document is authoritative about layout. If
the headings and the declaration disagree about order, the file still describes an unambiguous board;
only the author's intent is in question. Missing and undeclared columns, by contrast, are set
disagreements that a tool cannot render past, so they are errors.

**Why the column is the state.** A kanban board has exactly one idiom, and it is that you finish
work by dragging it to the right. A board that also asks you to tick a box has two controls for one
fact, which is one more than the fact has: sooner or later a card sits in the last column unticked,
or ticked three columns back, and now the file says two things and the tool has to guess which. So
`move` writes both facts and the board draws no checkbox. **This is the one place `kanban@0.1`
spends a guess deliberately, and §2.4 is where it is written down.**

**Why the file still degrades to a coherent checklist.** This coupling is not a retreat from the
portability promise; it is what protects it. Because a move writes the box as well as the position,
a board edited entirely through the verbs opens in GitHub or `less` as a sectioned checklist whose
ticks are *right* — the finished work is ticked, the rest is not. Under a rule where a move never
touched the box, the same board degraded to a checklist where nothing was ever ticked, which is a
worse lie than any coupling.

**Why the last column, rather than a column named "Done".** Boards call that column Done, Shipped,
Complete, Live, 完了, or `Q3 — released`, and a name-matching rule works for the first of those and
fails silently for the rest — worst of all in a language whose author never suspected the rule
existed. Position is the part of the idiom that is actually universal. `done-column:` exists for the
board whose last column is a parking lot, and it is an *override*, so the common case stays
zero-configuration. [`backlog@0.1`](../backlog/SPEC.md) makes the opposite call — it *reserves* the
names `Someday` and `Done` — for the opposite reason, and says so in its §3: a board is drawn by its
author, a backlog is queried. Neither spec's rule may be carried into the other.

**Why `done-column` warns instead of erroring, and why that was not a judgement call.** A key naming
a column the board does not have is plainly a mistake, and the first instinct is to make it an error
the way a `columns:` set disagreement is (`MDTO206`, `MDTO207`). It cannot be one. `kanban@0.1`
shipped saying this key "carries no diagnostics", and conventions §2.3 requires a file valid under a
minor version to stay valid under a later one — so an error here would retroactively un-conform
files that were written correctly against the spec as published. Conventions §9.2 now states that
rule for every spec. A warning says everything true that can be said: the key names nothing, the done
role is therefore somewhere the author did not choose, and the file is still a board.

It also lands on the right side of this spec's own polarity. `MDTO208` is a warning because the
document is authoritative about layout and only the author's *intent* is in question; `MDTO210` is
the same shape. The board renders, every card has a column, and §2.4's fallback gives the file one
unambiguous meaning — what was missing was anyone saying out loud that the meaning is probably not
the one that was typed. **The fallback did not change**, and that is the other half of the decision:
changing what an existing file means would have been the larger break of the two.

**Why the wrong type is `MDTO210` and not `MDTO007`.** `done-column: [Shipped]` is a type error, and
`MDTO007` is the code for exactly that everywhere else in the family. `MDTO007` is also an *error*,
which the paragraph above rules out here — and the choice between "a second warning code that means
wrong type" and "one code that means the key named no column" comes down to what a person needs told.
The rule they have to learn is *`done-column:` names one of your columns*. A sequence fails it as
completely as a misspelling does, and one code with two messages teaches one rule where two codes
would teach a distinction that only matters to the parser.

**Why a rename rewrites the key when a repair must not.** §2.1 forbids a tool from "correcting" a
`done-column:` that resolves to nothing, and §5 has `rename-column` rewriting the key without being
asked. These are not in tension. A repair is a guess: the file says `Shipped`, the board has no
`Shipped`, and nothing in the file says which column was meant. A rename is not a guess: the tool is
holding both names, because it is the one changing them, and the key is a reference to the thing it
is renaming. Leaving it behind is *how boards arrive in the broken state* — it is the exact edit
`fixtures/invalid/bad-done-column.md` is a record of. So the rename keeps the reference in step, for
the same reason `add --create-column` adds the name to `columns:`: the alternative is an operation
that hands back a file carrying a diagnostic the operation itself created.

**Why a hand-written disagreement is not a diagnostic.** A checked card in Doing means something to
the person who wrote it — "finished but not demoed", "done, waiting on review" — and the grammar has
always allowed it (§2.4). Coupling the *verbs* does not make that file wrong; it makes it a file no
verb produced. A tool renders it exactly as written, and does not tidy it up on the way past.
**Do not check, uncheck, or flag a card because of a disagreement you did not create.**

**Why nested items are a checklist, not sub-cards.** A board has one level of grouping — columns.
Cards nested inside cards would need a position, a column, and a rendering, and would make "move the
card at position 2" ambiguous. Nesting stays what it is in the item model: subtasks, drawn on the
card.

**Why WIP limits are not in 0.1.** `## Doing [limit:: 3]` is the obvious next feature and `MDTO209`
exists to say so out loud: column-level metadata is a *known extension point*, deliberately deferred,
not an oversight. Adding it means deciding what a validator does when the limit is exceeded (error?
warning? a `MDTO2xx` code?) and that decision belongs to a minor version with fixtures, not to an
implementation's private guess. **Do not silently start honouring `[limit:: …]`.**

**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.

**Deliberate non-features of 0.1**, each a plausible minor-version addition: WIP limits, swimlanes,
further column roles (`archive`, `blocked` — the done role is the only one 0.1 defines, §2.4),
card-level history, per-column sort directives, assignee validation, and extended checkbox states.

---

## 4. Examples

### 4.1 Valid

The complete set lives in [`fixtures/valid/`](fixtures/valid/).

**Minimal** ([`fixtures/valid/minimal.md`](fixtures/valid/minimal.md)) — three columns, four cards, no
metadata, no identifiers. Note that it is also a perfectly readable checklist:

```markdown
---
markdownto: kanban@0.1
---

## Backlog

- [ ] Write the conformance fixtures
- [ ] Sketch the landing page

## Doing

- [ ] Build the patch engine

## Done

- [x] Choose the portable envelope
```

**Declared columns** ([`fixtures/valid/declared-columns.md`](fixtures/valid/declared-columns.md)) —
frontmatter `columns:` matching the four headings in order.

**Full metadata** ([`fixtures/valid/full-metadata.md`](fixtures/valid/full-metadata.md)) — typed
fields, labels, an unknown frontmatter key (`board-owner`), an unknown inline field (`[effort:: 1w]`),
and identifiers on exactly the three cards tooling has touched.

**Checklists** ([`fixtures/valid/checklists.md`](fixtures/valid/checklists.md)) — cards with nested
checklists, a checked card sitting in Doing (the grammar keeps the two facts independent, §2.4), and
an empty `## Blocked` column.

**Done column** ([`fixtures/valid/done-column.md`](fixtures/valid/done-column.md)) — a board whose
last column is an Icebox, so `done-column:` names the one that means finished. Written `shipped`
against a `## Shipped` heading, because column names are matched case-insensitively after trimming
(§2.1) and this is the fixture that says so; the IR keeps the author's spelling in `doneColumn`.

**Edge cases** ([`fixtures/valid/edge-cases.md`](fixtures/valid/edge-cases.md)) — prose before the
first column (legal), `#1234` as a literal issue reference, a field inside a code span, and a fenced
code block containing a complete card with an identifier.

### 4.2 Invalid, with the exact diagnostics they produce

Each is a fixture in [`fixtures/invalid/`](fixtures/invalid/) with a sibling `.diagnostics.json`.

**`no-columns.md`** — a note that names itself a board:

```markdown
---
markdownto: kanban@0.1
title: Someday
---

Ideas I have not sorted into columns yet. …
```

→ `MDTO201` error, line 2: *Board has no columns: a kanban document needs at least one `##` column heading.*

**`card-before-column.md`** — cards above the first heading:

```markdown
- [ ] Build the patch engine
- [ ] Write the CLI verb layer

## Backlog
```

→ `MDTO202` error, line 6: *Card appears before the first column heading; kanban@0.1 has no implicit column.*
→ `MDTO202` error, line 7: *Card appears before the first column heading; kanban@0.1 has no implicit column.*

**`columns-mismatch.md`** — declared `Backlog, Doing, Done`; document has `Doing, Backlog, Shipped`:

→ `MDTO208` warning, line 4: *Column order differs from the `columns` declaration: declared `Backlog, Doing`, found `Doing, Backlog`.*
→ `MDTO206` error, line 7: *Column `Done` is declared in frontmatter `columns` but has no heading in the document.*
→ `MDTO207` error, line 18: *Column `Shipped` is not declared in frontmatter `columns`.*

**`duplicate-column.md`** — an unnamed column and a repeated one:

→ `MDTO204` error, line 14: *Column heading has no name.*
→ `MDTO205` error, line 18: *Duplicate column name `Doing`: already declared on line 10.*

**`bad-cards.md`** — column metadata, a level-3 column, and two shared item-model failures:

```markdown
## Backlog [limit:: 3]

- [ ] Obsidian Kanban import adapter

### Doing

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

→ `MDTO209` error, line 6: *Inline field `limit` on a column heading: columns carry no metadata in kanban@0.1.*
→ `MDTO203` error, line 10: *Heading level 3 is not valid in a kanban document: a column is a `##` heading.*
→ `MDTO031` error, line 12: *Unrecognized checkbox state `~`: kanban@0.1 defines only `[ ]`, `[x]`, and `[X]`.*
→ `MDTO033` error, line 13: *Card has no text: it consists only of metadata.*

**`bad-done-column.md`** — `done-column: Shipped` on a board whose Shipped column was renamed to
Released and whose last column is an Icebox:

→ `MDTO210` warning, line 4: *Frontmatter `done-column` names no column on this board: `Shipped` is
not one of `Doing`, `Released`, `Icebox`. The done role falls back to the last column `Icebox`
(kanban@0.1 §2.4).*

The file is still conforming — a warning does not make it otherwise (conventions §9.1) — and the
board still opens, edits and renders. What it has lost is the author's answer to "where does work
finish", and the next card dragged right would be filed as done in the parking lot.

**`bad-envelope.md`** — `title` as a sequence, `columns` as a comma-separated string:

→ `MDTO007` error, line 3: *Frontmatter key `title` has the wrong type: expected a string, found a sequence.*
→ `MDTO007` error, line 5: *Frontmatter key `columns` has the wrong type: expected a sequence of strings, found a string.*

---

## 5. Verb reference

`kanban@0.1` owns the vocabulary **`add`, `move`, `done`, `rm`, `edit`, `add-column`,
`rename-column`, `rm-column`, `move-column`**. The first five act on cards; the last four act on
the heading axis itself, and they are named for it because a board's columns are a thing a person
edits as often as its cards — a column is renamed, dropped, or dragged left, and until these existed
none of that was reachable from any tool, only from a text editor. It overlaps `todo@0.1` and
`backlog@0.1` because the three share an item model, not because any platform contract requires it —
and the overlap is in shape only. A verb with the same name does not have the same contract in
another spec: `kanban move` writes the checkbox when it crosses the done role (§2.4), while
`todo move` and `backlog promote`/`demote` never write one. **Read the verb reference of the spec the
file declares.** The universal commands `validate`, `render`, and `spec` come from the universal
adapters and are not listed here — though note that `render` still writes the validation report when
the file has error diagnostics, and exits 1 rather than 0 to say so.

**Why there is no `undone` verb, when todo has one.** Un-checking is a routine operation on a
checklist and a rare correction on a board, so kanban spends its vocabulary on `edit` — which changes
text, fields, labels, and checked state — instead of a dedicated inverse. `mdto kanban edit k3 --uncheck`
performs exactly the edit `mdto todo undone` would. This is what "verb vocabularies are spec-owned"
means in practice: each spec spends its verbs on what its domain does often.

### Addressing a card — `<ref>`

1. A pinned identifier, with or without the caret: `k3`, `^k3`.
2. Otherwise a case-insensitive substring of the card's normalized text: `"patch engine"`.

If a text `<ref>` matches zero or more than one card, the verb **fails with a non-zero exit code and
changes nothing**. Ambiguity is never resolved by picking the first match.

**A column argument is either a search narrower or a destination, never both — check which before you
pass one:**

| Verb | How the column is given | What it means |
|---|---|---|
| `done`, `rm`, `edit` | `--column <name>` | **Narrows the search**: only look for `<ref>` in that column. |
| `add` | `--column <col>`, **required** | The **destination**. There is no implicit column to fall back on, and it never narrows a search. |
| `move` | the **positional `<col>`**, not a flag | The **destination**. `move` takes no `--column`. |

Column arguments are matched case-insensitively against column names after trimming, the same way
`done-column:` is (§2.1).

### Common flags

`--file <path>` (default: the single `*.kanban.md` in the working directory) · `--json` (emit
`{edits, pinned, addresses, hash}`, where each address carries `id`/`text`/`group`/`line`) ·
`--dry-run` (print the unified diff, write nothing) · `--pin` (pin an identifier on the touched card
and return it) · `--expect <hash>` (apply only if the file still hashes to `<hash>`; conventions §6.5).

**No verb pins an identifier unless `--pin` is passed.** 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`.

**Every mutating verb requires a matching source hash before it writes.** The CLI computes it from the
file it just read unless `--expect <hash>` supplies one from an earlier read; a mismatch is a conflict
and nothing is applied (conventions §6.5, and see §3's rationale). `--force` overrides only the
validation refusal below, never this check.

### The verbs

| Verb | Signature | Source edit |
|---|---|---|
| `add` | `mdto kanban add "<text>" --column <col> [--create-column] [--priority p] [--due d] [--owner o] [--top\|--bottom\|--before <ref>\|--after <ref>]` | Inserts **one line**. |
| `move` | `mdto kanban move <ref> <col> [--keep-state] [--top\|--bottom\|--before <ref>\|--after <ref>]` | Deletes the card's lines, reinserts them under `<col>`; sets the box when the move crosses the done role. |
| `done` | `mdto kanban done <ref>…` | Replaces `[ ]` with `[x]` on one line. |
| `rm` | `mdto kanban rm <ref>…` | Deletes the card's line and its checklist lines. |
| `edit` | `mdto kanban edit <ref> [--text "…"] [--set k=v]… [--unset k]… [--tag +t\|-t]… [--check\|--uncheck]` | Rewrites **one line** in place. |
| `add-column` | `mdto kanban add-column "<name>" [--first\|--last\|--before <col>\|--after <col>]` | Inserts a bare `## <name>` and the blank line under it. |
| `rename-column` | `mdto kanban rename-column <col> "<name>"` | Rewrites the heading's **text span**; declarations follow. |
| `rm-column` | `mdto kanban rm-column <col> [--to <col>] [--keep-state]` | Deletes the heading and the lines it owns. |
| `move-column` | `mdto kanban move-column <col> --first\|--last\|--before <col>\|--after <col>` | Moves the column's whole block, verbatim. |

**`add-column`** — the one verb that writes an empty column, which §2.2 has always called valid and
common and which nothing else could produce: `add --create-column` writes a column *with a card in
it*, because a card is what it was asked for. The bytes are the same ones `--create-column` lays
down — heading, blank line — so `add-column X` followed by `add "c" --column X` is byte-identical to
`add "c" --column X --create-column`. `--last` is the default. On a board that declares `columns:`
the name is added there too. Adding at the right-hand end of a board with no `done-column:` hands
that column the done role (§2.4), and the verb says so.

**`rename-column`** — the heading's text and nothing else on the line: the `##`, the space after it,
an ATX closing run and any trailing whitespace survive verbatim, and no card moves. **A `columns:`
entry naming the column is rewritten with it, and so is a `done-column:` that names it.** That last
clause is the whole reason this verb did not exist in the first revision — a rename that left the key
behind moved the done role silently — and it is not the guess §2.1 forbids: the referent is being
renamed in the same edit. Changing only the casing (`## doing` → `## Doing`) is a legal rename;
renaming onto a name another column already has is a refusal, compared case-insensitively after
trimming. A name carrying an inline field or a block identifier is refused too (`MDTO209`).

**`rm-column`** — the heading, the lines beneath it, and the single blank line that separated it from
its neighbour. **A column that still holds cards is refused**; where they belong is not a thing a
verb can work out, and taking them with the heading is the one outcome nobody wants. `--to <col>`
answers it, and the relocation and the deletion are one atomic batch. The relocation is exactly what
`move` does, card by card, in the order they sit — including the checkbox when the destination
crosses the done role (§2.4), with `--keep-state` opting out of that half, spelled as it is on
`move`. A `columns:` entry for the column is dropped (leaving it would be `MDTO206`), and so is a
`done-column:` naming it (leaving it would be `MDTO210`). Removing the last column of a board is
refused: `MDTO201`.

**`move-column`** — the block moves verbatim: heading, cards, prose, and the blank lines inside it
come out in the same order with the same bytes. Only the blank line that *separates* the column from
its neighbour is arithmetic, because a separator belongs to a position rather than to a column — so
moving a column away and back is the file it started as. A destination is required; there is no
default position for a column that is already somewhere, and moving one to where it already is is a
successful no-op with an empty diff. Column order is the board's layout, so on a board with no
`done-column:` this can hand the done role to another column, and the verb says so. A `columns:`
declaration is reordered to match, because leaving it would be `MDTO208`.

**The three verbs that can move the done role report it.** The report is not advice and not a
warning: the file is correct and the operation did what it was asked. It exists because the *next*
drag will write a checkbox against a different answer, and a person who was not told has been
surprised by their own edit. On the command line it is a `note:` line; in `--json` it is `notes`; a
board that draws the done-role column has already said it another way (§2.4).

**`add`** — inserts a single line at the end of the target column (`--bottom`, the default), or at the
position given by `--top` / `--before` / `--after`. `--column` is required: there is no implicit
column to fall back on. The bullet marker and indentation are copied from the nearest sibling card.
Fields are appended after the text in the canonical order `[priority:: …] [due:: …] [owner:: …]`; tags
stay where the author put them in `<text>`. Targeting a column that already exists is never an error,
whether or not `--create-column` is passed — the card is simply appended there (a duplicate heading is
its own error, `MDTO205`, so create-or-fail is unreachable). A column that does not exist is an error
unless `--create-column` is passed, which inserts a blank line before the new `## <name>` heading
(only if the file does not already end in a blank line) and the card beneath it — heading, blank
line, card, the way every other column on the board is written — both at the end of the file. And
if the board declares `columns:`, it also adds the name to that declaration, because leaving them
inconsistent would immediately produce `MDTO207`.

**`move`** — the showpiece, and the operation the patch engine is built and fuzz-tested against. It
deletes the card's line together with its checklist lines and reinserts them at the destination with
the text, the fields, the tags, the identifier and the relative indentation **verbatim**. It
collapses at most one blank line left doubled by the removal. A drag on the live board is this verb,
and its diff is one removal plus one insertion. Moving a card to the position it already occupies is
a successful no-op producing an empty diff.

The checkbox is the one thing a move may rewrite, and only when the move **crosses the done role**
(§2.4): into the done-role column it is checked, out of it unchecked, and a card already in the
implied state is left alone. Reordering inside a column never touches it. The two changes are one
batch and one diff — the card's line comes out and goes back in with its box already set — so the
file is never briefly in a state where the column and the box disagree. `--keep-state` moves the
card and writes no checkbox at all, which is the escape hatch for a correction ("this was filed in
the wrong column") as opposed to a transition.

**§2.4 and this paragraph are the contract, and they supersede any earlier statement that a move
never touches the box.** That earlier rule appears in drafts of this spec and in tools written
against them; where the two disagree, this document wins. It is called out rather than quietly
replaced so that a reader who has met the older wording knows which one to trust. §3 has the
reasoning; the short version is that a board is finished by dragging right, and a file whose
checklist never ticks is not the graceful degradation this spec promised.

**`done`** — a three-character replacement inside the checkbox. The card does not move, no column
changes, no timestamp is stamped, the checklist is untouched. `done` on an already-checked card is a
successful no-op producing an empty diff. To uncheck, use `edit --uncheck`. The coupling runs one
way only: a move writes the box, but checking the box never moves the card — the position of a card
is information (`move` reorders as well as regroups) and no verb invents one.

**`rm`** — 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; it is never reissued.

**`edit`** — rewrites the card's own line and nothing else. `--text` replaces the text; the card's
identifier is always preserved. Fields are preserved too, but not in place: they are re-emitted after
the new text, in their existing source order — a field the author had placed *before* the prose moves
after it. **In-place positioning is promised only for `--set`.** Tags are preserved by default: any tag
that was in the old text and is missing from the new one is re-appended right after the new text,
before the fields (the patch engine's `keepTags: false` is the only way to opt out, and no CLI flag
exposes it). `--set k=v` adds or replaces one field in place, keeping its existing position on the
line if it is already there and appending in canonical order if it is not. `--unset k` removes one
field; unsetting a key that is not on the card is a no-op. `--tag +t` / `--tag -t` add or remove one
label; `-t` removes every occurrence of the tag on the line, and both are no-ops when the tag is
already in the requested state. `--check` / `--uncheck` set the checkbox. Unknown fields are only
touched when named explicitly.

### 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 on a card it was not asked to move, or on a move that stays inside one column
(§2.4) · moves a card because its box changed · pins an identifier without `--pin` · rewrites the
`markdownto` version · deletes cards along with the column they were in · "corrects" a `done-column:`
that names no column (§2.1) · leaves `columns:` or `done-column:` naming something the edit just
changed · edits a file that fails validation with an `error` (pass `--force` to
override, at your own risk) · writes a file whose source hash no longer matches the one it read (the
conflict surfaces instead).
