# todo@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). **Fixtures:** [`fixtures/`](fixtures/).
**Owns diagnostic codes:** `MDTO100`–`MDTO199`.

---

## 1. Purpose

`todo@0.1` describes a checklist or backlog as a plain Markdown file: task items, optionally grouped
into sections, where the file itself is the complete state of the list. It also defines the **shared
item model** that `kanban@0.1` builds on, so that a todo list and a one-column board are the same
thing seen from different angles.

---

## 2. Grammar

### 2.1 Envelope

```yaml
---
markdownto: todo@0.1
title: Household        # optional
id: household-list      # optional
---
```

`markdownto: todo@0.1` is required and must be the exact string form. `title` and `id` are the
reserved keys from conventions §2.2. `todo@0.1` defines **no additional frontmatter keys**; anything
else is an unknown key, preserved verbatim and never validated.

### 2.2 Document structure

| Construct | Meaning |
|---|---|
| One optional level-1 heading, before any other content | Document title (informative; does not create a section) |
| A level-2 heading | **Section** |
| A GFM task list item at the top level of a list | **Item** |
| A task list item nested inside another | **Subtask** of that item |
| Anything else — paragraphs, blockquotes, tables, code fences, images, thematic breaks | Prose. Preserved, rendered, never interpreted |

- Sections are **optional**. A file with no headings at all is valid; its items live in a single
  *implicit* section whose name is `null`.
- Items appearing before the first `##` in a document that does have sections also belong to that
  implicit leading section. This is the "inbox at the top" pattern and it is deliberately legal.
- A section runs from its heading to the next `##` (or end of file). Items are collected in document
  order **across list boundaries**: a paragraph between two lists does not split a section.
- Section names are compared **case-insensitively after trimming**, for uniqueness and for verb
  addressing. The source casing is preserved.
- Heading levels 3 and deeper are not part of `todo@0.1` (`MDTO101`). Nesting groups items; headings
  group sections; there is no third mechanism.
- A section heading carries no metadata: an inline field or block identifier on a heading is
  `MDTO104`.

**Recovery.** A heading that is invalid for its *level* (`MDTO101`) does not open a section; the items
below it stay in the enclosing section. A heading that is invalid for its *content* still opens a
section: for `MDTO104` the name is the heading text with the offending constructs removed, and for
`MDTO102` the name is the empty string. This keeps items from silently migrating into the previous
section while a file is being repaired.

### 2.3 Items

An item is a GFM task list item on a **bullet** list (`-`, `*`, or `+`):

```markdown
- [ ] Renew the car registration
- [x] Pay the electricity bill
```

- `[ ]` is unchecked, `[x]` and `[X]` are checked. **`todo@0.1` defines no other state** (`MDTO031`).
  That code is spec-relative: `backlog@0.1` legally uses `[/]` and `[-]`, and the same code reports
  them here (conventions §9.3, and §3 below for why).
- Item order within a section is **meaningful** — it is the author's priority order. Nothing sorts it.
- A nested task list item is a subtask. Nesting is unbounded. **A parent's checked state is never
  derived from its children** in 0.1: `- [ ]` with three checked subtasks stays unchecked until
  someone checks it.
- Every list item in the document body must be a task item (`MDTO030`). A bullet without a checkbox
  is not a note — notes are paragraphs.
- Ordered lists do not carry items (`MDTO032`).
- An item must have text after its metadata is removed (`MDTO033`).

### 2.4 Item metadata

All of this is inherited from conventions §3–§5; the summary is here so this document stands alone.

```markdown
- [ ] Publish the fixtures [priority:: high] [due:: 2026-08-20] #specs ^t7
```

| Construct | Meaning |
|---|---|
| `[priority:: low\|medium\|high]` | Priority. Case-insensitive, lowercased in the IR |
| `[due:: YYYY-MM-DD]` | Due date. A real calendar date, no time component in 0.1 |
| `[owner:: …]` | Owner. Free text; a leading `@` is conventional and preserved |
| `[anything:: …]` | Unknown field. Kept as a string, written back verbatim, never an error |
| `#tag`, `#tag/sub` | Label. Stays in the item's text as well as in `tags` |
| `[[Name]]`, `[[Name#^id]]` | Wikilink. Recognized and preserved, stays in the item's text, and **inert in `todo@0.1`** — it carries no meaning and no diagnostic here (conventions §5.1) |
| `^id` at end of line | Block identifier, pinned by tooling only |

Nothing inside a code span or a code fence is scanned.

### 2.5 Diagnostics owned by this spec

| Code | Severity | Meaning |
|---|---|---|
| `MDTO101` | error | Heading level is not valid in a todo document: only one leading `#` title and `##` sections. |
| `MDTO102` | error | Section heading has no name. |
| `MDTO103` | error | Duplicate section name. |
| `MDTO104` | error | Inline field or block identifier on a section heading; sections carry no metadata in 0.1. |

Everything else a todo file can get wrong is a shared code — see conventions §9.3.

---

## 3. Rationale

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

**Why the file is not "data with Markdown around it."** The whole promise is that the `.md` file
remains the canonical artifact — readable in any viewer, diffable in git, never captive to an app. So
the structure is made of things Markdown already means. A checklist in a Markdown To file is a
checklist in GitHub, in Obsidian, in a text editor, in `less`. Nothing needs the tool to be
meaningful.

**Why `##` for sections and nothing deeper.** Every construct has exactly one meaning, and every
meaning has exactly one construct. If `###` also grouped items, an item under `### Errands` under
`## This week` would belong to two sections, and a verb that moves it would have to guess which one
you meant. Two levels of grouping already exist — headings for sections, nesting for subtasks — and a
third would be ambiguity, not power. **If you are repairing a `MDTO101`: promote the heading to `##`
or nest the items; do not invent a section-path syntax.**

**Why sections are optional.** The smallest useful todo file is a frontmatter block and three lines.
Requiring a heading to write down three errands would be a tax on the most common case, and the
governing principle is that hand-authoring must be pleasant. The implicit section exists so that the
IR shape does not change between a bare list and a sectioned one — consumers write one code path.

**Why every list item must be a task item.** A bullet without a checkbox is genuinely ambiguous: is it
an item someone forgot to mark, or a note? Guessing either way loses data — a "note" that was really
an item vanishes from the list, an "item" that was really a note gets a checkbox it never had. So the
spec refuses to guess and says so with `MDTO030`. **The repair is to add `[ ]` if it is a task, or to
turn the line into a paragraph if it is not.**

**Why `- [/]` is an error here and legal elsewhere.** Extended checkbox states (`[/]` in progress,
`[-]` dropped) are a real convention in the wild, including in agentsFS backlog files. They were left
out of `todo@0.1` because a state machine is a bigger commitment than a checkbox — and they were left
out as a *declared extension point*, which [`backlog@0.1`](../backlog/SPEC.md) has since taken up: the
same two characters are legal there, in a spec that also defines what they mean for blockers and for
parent completion. So `- [/]` in a `todo@0.1` file is not a marker the format has never heard of; it
is a marker belonging to a different spec, and the envelope is what says which spec this file is.
`MDTO031` names the offending character precisely so a repairing agent maps it to `[ ]` or `[x]` on
purpose, rather than deleting the line. **The repair inside a `todo@0.1` file is always to map the
marker onto `[ ]` or `[x]`, never to start honouring it.** Reaching for the other spec instead is a
decision about the whole document, not a fix to one line — a backlog needs bands too (backlog §2.2) —
so make it deliberately or not at all.

**Why a parent's completion is not derived from its subtasks.** Two defensible semantics exist (a
parent is done when all children are; a parent is done when its own box is checked) and files
authored under one would silently mean something different under the other. 0.1 picks the one that
matches what the file literally says. **Do not "fix" a parent whose children are all checked.**

**Why there is no completion timestamp, no `created::`, no `status::`.** Every field a tool writes
that a human would not type is a small betrayal of §0 of the conventions. Git already records when
lines changed, and by whom. If a file needs those fields, an author can add them as unknown fields and
they will be preserved forever — but the spec will not generate them.

**Why identifiers are lazy.** A hand-written file has no identifiers because nothing has needed one
yet. Identifier noise on every line is the single most obvious way a format announces that it was
designed for a machine. When tooling needs a durable handle it pins one, once, and never takes it
back. **Never renumber, normalize, or garbage-collect identifiers, and never add one to an item you
were not asked to touch.**

**Why inline fields rather than a frontmatter table.** Metadata sitting next to the thing it describes
survives copy-paste, reordering, and moving an item between files, and it reads acceptably in a plain
viewer. A lookup table keyed by identifier would force identifiers onto everything, which is exactly
what conventions §0 rejects.

**Why invalid values are reported but never deleted.** `[due:: next Tuesday]` is wrong, but it is what
the author wrote and it is more informative than nothing. Validation is advice; the file belongs to
the author. This is also why the IR keeps every field in `fields` even when the typed projection is
absent.

**Deliberate non-features of 0.1**, each a plausible minor-version addition: recurring tasks,
completion timestamps, extended checkbox states, due times, dependencies between items, per-section
metadata, and sort directives.

---

## 4. Examples

### 4.1 Valid

The complete set lives in [`fixtures/valid/`](fixtures/valid/). Every one is a file a person would
plausibly have typed by hand — that is the bar a fixture has to clear.

**Minimal** ([`fixtures/valid/minimal.md`](fixtures/valid/minimal.md)) — no sections, no metadata, no
identifiers. This is the floor:

```markdown
---
markdownto: todo@0.1
---

- [ ] Renew the car registration
- [ ] Book a dentist appointment
- [x] Pay the electricity bill
```

**Sections** ([`fixtures/valid/sections.md`](fixtures/valid/sections.md)) — an optional `#` title and
two `##` sections.

**Full metadata** ([`fixtures/valid/full-metadata.md`](fixtures/valid/full-metadata.md)) — typed
fields, tags, two pinned identifiers on the items tooling has touched, an unknown frontmatter key
(`maintainer`) and an unknown inline field (`[effort:: 2d]`), both preserved:

```markdown
- [ ] Publish todo@0.1 fixtures [priority:: high] [due:: 2026-08-20] #specs
- [ ] Build the patch engine [owner:: @akshay] [priority:: high] ^t4
- [ ] Fuzz the round-trip … [priority:: medium] [effort:: 2d] #infra
```

**Nesting** ([`fixtures/valid/nesting.md`](fixtures/valid/nesting.md)) — subtasks three levels deep,
checked and unchecked items interleaved in author order, a parent left unchecked while its children
are checked.

**Edge cases** ([`fixtures/valid/edge-cases.md`](fixtures/valid/edge-cases.md)) — the constructs that
look like syntax and are not: `#1234` (all digits, so a literal issue reference), a field inside a
code span, and a fenced code block containing a complete item with an identifier. None of them
produce fields, tags, or identifiers.

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

Each of these is a fixture in [`fixtures/invalid/`](fixtures/invalid/) with a sibling
`.diagnostics.json` giving the expected `{code, severity, line, message}` list.

**`missing-envelope.md`** — a perfectly good checklist that never names a spec.

```markdown
# Household

- [ ] Renew the car registration
```

→ `MDTO001` error, line 1: *Missing frontmatter envelope: the file must begin with a `---` fenced YAML block naming the spec.*

**`bad-spec-identifier.md`** — `markdownto: todo 0.1`
→ `MDTO004` error, line 2: *Malformed spec identifier `todo 0.1`: expected the string form `name@major.minor`, for example `todo@0.1`.*

**`bad-ids.md`** — three identifier failures on four lines:

```markdown
- [ ] Renew the car registration ^t1
- [ ] Call the plumber about the kitchen tap ^t1
- [ ] Book a dentist appointment ^t_2
- [ ] Buy milk ^t4 (weekly)
```

→ `MDTO011` error, line 7: *Duplicate block identifier `^t1`: already pinned on line 6.*
→ `MDTO010` error, line 8: *Malformed block identifier `^t_2`: identifiers match `^[A-Za-z0-9][A-Za-z0-9-]{0,63}` — underscores are not permitted.*
→ `MDTO012` warning, line 9: *Block identifier `^t4` is not the final token of the line, so it is being treated as literal text.*

**`bad-fields.md`** — every inline-field failure mode, ending with the near-miss warning:

→ `MDTO022` error, line 8: *Invalid value for `due`: expected a YYYY-MM-DD calendar date, found `next Tuesday`.*
→ `MDTO022` error, line 9: *Invalid value for `priority`: expected one of `low`, `medium`, `high`, found `urgent`.*
→ `MDTO021` error, line 10: *Inline field `owner` has an empty value; omit the field instead.*
→ `MDTO022` error, line 11: *Invalid value for `due`: `2026-02-30` is not a real calendar date.*
→ `MDTO023` error, line 12: *Duplicate inline field key `priority` on one item; there is no defined precedence between the two values.*
→ `MDTO020` warning, line 13: *`[due: 2026-08-15]` looks like an inline field but uses one colon instead of two, so it is being treated as literal text.*

**`bad-items.md`** — four ways a list item fails to be an item:

```markdown
- [ ] Renew the car registration
- Call the plumber
- [/] Book a dentist appointment
- [ ] [priority:: high]

1. [ ] Pay the council tax
```

→ `MDTO030` error, line 9: *List item is not a task item: expected `- [ ]` or `- [x]`. Prose that is not a task belongs in a paragraph.*
→ `MDTO031` error, line 10: *Unrecognized checkbox state `/`: todo@0.1 defines only `[ ]`, `[x]`, and `[X]`.*
→ `MDTO033` error, line 11: *Item has no text: it consists only of metadata.*
→ `MDTO032` error, line 13: *Task checkbox on an ordered list item: todo items must be bullet list items (`-`, `*`, or `+`).*

**`bad-headings.md`** — every way the section structure goes wrong:

→ `MDTO104` error, line 6: *Inline field `priority` on a section heading: sections carry no metadata in todo@0.1. Move it to an item.*
→ `MDTO101` error, line 10: *Heading level 3 is not valid in a todo document: use `##` for a section, or nest items to group them.*
→ `MDTO102` error, line 14: *Section heading has no name.*
→ `MDTO103` error, line 18: *Duplicate section name `This week`: already declared on line 6.*

Also fixtured: `broken-yaml.md` (`MDTO002`), `no-spec-key.md` (`MDTO003`), `unsupported-version.md`
(`MDTO006`).

---

## 5. Verb reference

`todo@0.1` owns the vocabulary **`add`, `done`, `undone`, `rm`, `move`**. It overlaps `kanban@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: each spec's verbs mean what that spec says they mean, so read the
verb reference of the spec the file declares, never this one by analogy. `audio@0.1` shares nothing
with any of them. 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.

Verbs exist so that an agent never has to rewrite a file freehand. Every verb: validates first, edits
the smallest possible span, leaves every other byte alone, and refuses rather than guesses.

### Addressing an item — `<ref>`

1. A pinned identifier, with or without the caret: `t4`, `^t4`.
2. Otherwise a case-insensitive substring of the item's normalized text: `"plumber"`.

If a text `<ref>` matches zero or more than one item, the verb **fails with a non-zero exit code and
changes nothing**. Ambiguity is never resolved by picking the first match. `--section <name>` narrows
the search on `done`, `undone`, and `rm`; on `add` and `move` it names the *destination* instead, and
never narrows a search — see each verb below.

### Common flags

`--file <path>` (default: the single `*.todo.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 item
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 exception is a live editing surface such
as the web board, which must track cards across re-renders and therefore pins on first interaction —
which is why the 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). `--force` overrides only the validation refusal below,
never this check.

### The verbs

| Verb | Signature | Source edit |
|---|---|---|
| `add` | `mdto todo add "<text>" [--section <name>] [--create-section] [--priority p] [--due d] [--owner o] [--top\|--bottom\|--before <ref>\|--after <ref>]` | Inserts **one line**. |
| `done` | `mdto todo done <ref>…` | Replaces `[ ]` with `[x]` on one line. |
| `undone` | `mdto todo undone <ref>…` | Replaces `[x]`/`[X]` with `[ ]` on one line. |
| `rm` | `mdto todo rm <ref>…` | Deletes the item's line and its subtasks' lines. |
| `move` | `mdto todo move <ref> [--section <name>] [--top\|--bottom\|--before <ref>\|--after <ref>]` | Deletes those lines and reinserts them verbatim elsewhere. |

**`add`** — inserts a single line after the last top-level item of the target section (`--bottom`, the
default), or at the position given by `--top` / `--before` / `--after`. The bullet marker and
indentation are copied from the nearest sibling item so the insertion matches the file's existing
style. Fields are appended after the text in the canonical order `[priority:: …] [due:: …]
[owner:: …]`; tags stay wherever the author wrote them in `<text>`. With no `--section`, the item goes
to the implicit leading section, creating the list if the document has none. Targeting a section that
already exists is never an error, whether or not `--create-section` is passed — the item is simply
appended there (a duplicate heading is its own error, `MDTO103`, so create-or-fail is unreachable). A
named section that does not exist is an error unless `--create-section` 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 item beneath it — heading, blank line, item, the way every other section in the file is
written — both at the end of the file. Nothing above or below the inserted lines changes.

**`done` / `undone`** — a three-character replacement inside the checkbox, and nothing else. The item
does not move, no section changes, no `completed::` field is stamped (0.1 has no such field), subtasks
are untouched. `done` on an already-checked item is a successful no-op producing an empty diff.

**`rm`** — removes the item's line and every line of its nested subtasks, then collapses at most one
blank line left doubled by the removal. It never renumbers, reflows, or re-indents siblings. Removing
a pinned item retires its identifier permanently; it is never reissued.

**`move`** — deletes the item's lines and reinserts them **verbatim** at the destination, adjusting
only leading indentation if the nesting depth changed. Text, fields, tags, identifier, and checked
state all survive unchanged: **in `todo@0.1`, moving and completing are independent facts, and `move`
never checks a box for you.** That is a statement about this spec's verbs and not about the family —
a `todo@0.1` section carries no role, so there is no position for a move to cross. `kanban@0.1` gives
one column the *done role* and its `move` does write the box (kanban §2.4); `backlog@0.1`, like this
spec, never does (backlog §2.4). Do not carry either rule across.

`--section` names the destination only; it never narrows which item `<ref>` addresses. Moving to a
section that does not exist is an error; `move` does not create sections. Moving an item to the
position it already occupies is a successful no-op producing an empty diff.

### What no verb ever does

Reformats a line it was not asked to change · normalizes bullet markers, indentation, or blank lines ·
reorders anything it was not asked to reorder · drops an unknown field or an unrecognized value ·
sorts a list · pins an identifier without `--pin` · rewrites the `markdownto` version · 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).
