# backlog@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`.
**Extends:** [`todo@0.1`](../todo/SPEC.md) — the item model, referenced rather than restated.
**Schema:** [`schema.json`](schema.json), which `$ref`s [`../todo/schema.json`](../todo/schema.json).
**Fixtures:** [`fixtures/`](fixtures/). **Owns diagnostic codes:** `MDTO400`–`MDTO499`.

---

## 1. Purpose

`backlog@0.1` describes a prioritized backlog as a plain Markdown file: level-2 headings are
**bands** in priority order, task items beneath them carry a four-state marker, and a task may
declare what it is waiting on. It is `todo@0.1` plus exactly three deltas — an extended checkbox
enum, a reserved band vocabulary, and a blocker relation — so that the page an agent works from is
the same file a person reads.

---

## 2. Grammar

`backlog@0.1` is `todo@0.1` with three additions. Everything not described here is todo's grammar
verbatim: bullet task lists, unbounded nesting, inline fields, tags, lazy `^id`s, and nothing scanned
inside code spans or fences.

| Delta | What changes |
|---|---|
| 1. **State enum** (§2.4) | Four markers, not two: `[ ]`, `[/]`, `[x]`, `[-]`. |
| 2. **Bands** (§2.3) | `##` headings are a reserved, priority-ordered vocabulary, overridable in frontmatter. |
| 3. **Blockers** (§2.5) | A trailing clause naming what a task waits on, with wikilink targets that lift themselves. |

### 2.1 Envelope

```yaml
---
markdownto: backlog@0.1
title: Tidepool            # optional
id: tidepool-backlog       # optional
bands:                     # optional, spec-defined
  - Commissioned
  - Drafting
  - Done
---
```

`backlog@0.1` defines one additional frontmatter key:

| Key | Type | Meaning |
|---|---|---|
| `bands` | sequence of strings | Replaces the reserved band vocabulary, in priority order. |

A wrong type on the key is `MDTO007`.

A backlog page routinely carries other systems' frontmatter in the same block — an agentsFS page
declares `description:` and `agentsfs_role: backlog` beside the envelope. Those are unknown keys:
parsed, preserved, written back verbatim, never validated (conventions §2.2, §6.2). See
[`fixtures/valid/agentsfs-page.md`](fixtures/valid/agentsfs-page.md).

### 2.2 Document structure

| Construct | Meaning |
|---|---|
| One optional level-1 heading, before any other content | Document title (informative; does not open a band) |
| A level-2 heading | **Band** |
| A top-level task list item under a band | **Task** |
| A task list item nested inside a task | A **child** of that task |
| Anything else — paragraphs, blockquotes, tables, code fences, images | Prose. Preserved, rendered, never interpreted |

- **There is no implicit band.** A backlog is a partition by priority, so a task that is in no band
  has no priority. A task before the first `##` is `MDTO406`. Prose before the first band is normal
  and legal — most backlogs open with a legend explaining their own markers.
- A backlog **may have no bands at all**. An empty backlog is a real state and a file should not have
  to carry a ceremonial heading to be one. (This is where backlog parts from kanban's `MDTO201`.)
- A band runs from its heading to the next `##`. Tasks are collected in document order across list
  boundaries, so a paragraph between two lists does not split a band.
- Task order within a band is **priority order** — top is highest. Nothing sorts it. Ordering is
  never sequencing: reordering lines is the safe way to reprioritize.
- Band names are compared **case-insensitively after trimming**, for the vocabulary check, for
  uniqueness, and for verb addressing. Source casing is preserved.
- A band name is required (`MDTO404`) and unique (`MDTO402`).
- Heading levels 3 and deeper are not part of `backlog@0.1` (`MDTO405`). Nesting breaks a task down;
  headings group bands; there is no third mechanism.
- A band heading carries no metadata: an inline field or block identifier on a heading is `MDTO407`.

### 2.3 Bands

**The default vocabulary is reserved.** With no `bands:` declaration, a backlog's bands are exactly
these five names, in this priority order:

| Band | Rank | Meaning |
|---|---|---|
| `Now` | 0 | Being worked, or next to be picked up. |
| `Next` | 1 | Committed, not started. |
| `Later` | 2 | Real work, not yet scheduled. |
| `Someday` | 3 | **Parked.** Never offered as ready work; ideas kept rather than lost. |
| `Done` | 4 | **Closed.** Holds terminal tasks and is safe to prune. |

Any other `##` heading is `MDTO401`. Empty bands **MAY be omitted**, and MAY be written out empty;
both are correct and neither is a diagnostic. The smallest useful backlog is an envelope, one
heading, and one line.

**`bands:` replaces the vocabulary, it does not merely check it.** When the key is present, the
declared names are the complete vocabulary and the declaration order is the priority order; a heading
outside the declaration is `MDTO401`, and a declared band with no heading is *not* a diagnostic
(empty bands are omittable — this is where backlog parts from kanban's `MDTO206`).

**Reserved semantics attach to names, not to positions.** A declaration that keeps `Someday` or
`Done` keeps their meanings; a declaration that drops them means the document simply has no parked
band and no closed band. `bands: [Commissioned, Drafting, Editing, Someday, Done]` is a magazine's
production stages with parking and closing intact.

**Document order is presentation; the vocabulary order is priority.** When the two disagree, the
vocabulary wins and `MDTO403` (warning) says so.

### 2.4 Tasks and the state enum

A task is a GFM task list item on a bullet list: the shared item model
([conventions §12](../conventions.md)), which `backlog@0.1` adopts and extends rather than restates.
The additions are the four-state marker below, the blocker clause (§2.5), and the promoted `reason`
field; every property a task shares with a todo item is a `$ref` into
[`../todo/schema.json`](../todo/schema.json) rather than a copy of it, so the two cannot drift apart.
**`backlog@0.1` declares its full marker enum, per conventions §9.3:**

```markdown
- [ ] Ship a proper icon
- [/] Cache the tide tables so the app works offline
- [x] Pick a tide-data source with a licence we can live with
- [-] Rewrite the parser in Rust
```

| Marker | State | Terminal? |
|---|---|---|
| `[ ]` | `open` | no |
| `[/]` | `in-progress` | no |
| `[x]`, `[X]` | `done` | **yes** |
| `[-]` | `dropped` | **yes** |

**These four and no others.** Any other marker character is the shared `MDTO031`, whose message names
backlog's enum rather than todo's. Every tool — including tools implementing specs *without* this
extension — MUST preserve an unrecognized marker character verbatim rather than coercing it to open
or checked (conventions §9.3).

`[x]` and `[-]` are the **terminal** states: the pair is what the blocker relation (§2.5) and the
parent-completion convention (§2.6) are defined against. `done` and `dropped` are different
*outcomes* of the same fact — nobody is waiting on this any more.

**In `backlog@0.1`, done-ness and band are independent — in the grammar and at the verb layer
alike.** `- [x]` marks a task finished wherever it sits; moving a task into `Done` does not check its
box and checking its box does not move it. **There is no done-role band**, and no verb here behaves
the way `kanban move` does across kanban's done-role column (kanban §2.4, and §3 below). What `Done`
reserves is what a *tool* may do with the band (prune it), not a coupling between the marker and the
heading. A terminal task outside `Done` is ordinary and produces no diagnostic — the state is true
the moment the author writes it, and filing follows later. The converse is `MDTO421` (warning): a non-terminal
task in `Done` would be destroyed by a prune, so it is called out.

**Recognized field.** `backlog@0.1` promotes one inline field to a recognized key:

| Field | Type | Meaning |
|---|---|---|
| `[reason:: <text>]` | string | Why a task was dropped or parked. Free text; any value is valid. |

It is what `drop --reason` writes. It carries no diagnostic of its own and is meaningful on any task,
so a parked idea can record why it is parked. Every other field behaves exactly as in todo:
`priority`, `due`, `owner` typed as in conventions §4.3, everything else an unknown field preserved
verbatim.

### 2.5 The blocker relation

A task may end with a **blocker clause** saying what it is waiting on:

```markdown
- [ ] Home-screen widget showing the next high tide — blocked by [[#^offline-cache]] ^widget
- [ ] Submit to the App Store — blocked by [[#^dst-bug]], [[#^icon]] ^submission
- [ ] Ship a proper icon — blocked by Marina's illustration ^icon
```

**Grammar.** At the end of the task's text: a space, an em dash `—` (U+2014), a space, the words
`blocked by` (matched case-insensitively), a space, then a non-empty **comma-separated list** of
blockers.

- The clause is the last thing on the line **except for the block identifier**, which stays the final
  token exactly as conventions §3.1 requires. The canonical line order is
  `text  [fields]  #tags  — blocked by …  ^id`.
- The list is split on commas outside code spans. A trimmed element that is **exactly one wikilink**
  is a **wikilink blocker**; anything else is a **prose blocker**. An empty element — a trailing
  comma, or `a,,b` — is `MDTO410`.
- Nothing is scanned inside code spans or fences (conventions §4.5), so a file can document this
  clause without writing one. See [`fixtures/valid/edge-cases.md`](fixtures/valid/edge-cases.md).
- The clause **remains part of the task's `text`**, like tags and unlike fields: it reads as part of
  the sentence and a plain Markdown viewer should show it (conventions §5.1). For the "task has no
  text" check (`MDTO033`) it does not count — `- [ ] — blocked by [[#^a]]` is metadata only.

**When a blocker lifts.**

| Blocker | Resolution | Lifts when |
|---|---|---|
| `[[#^slug]]`, target found in this file | `satisfied` / `pending` | The target task reaches `[x]` or `[-]`. Automatic. |
| `[[#^slug]]`, no such identifier in this file | `unresolved` (`MDTO411`) | Never. Fix the reference. |
| `[[Name#^slug]]` | `external` | Not determined in 0.1 (see below). |
| `[[Name]]`, no `#^` fragment | `external` | Never on its own — a document has no state. |
| Prose | `pending` | Never on its own. It holds until an author edits it away. |

A task is **blocked** when it has at least one blocker that is not `satisfied`. Prose blockers are
first-class and expected: "blocked by Marina's illustration" is more honest than inventing a task to
point at.

**Cross-file targets are out of validation scope in 0.1, deliberately.** Validation is a **single-file
parse**: a conforming file is the complete state of one document, wikilinks resolve by filename and
are path-independent (conventions §5.1), and validation performs no I/O and no network access
(conventions §8). Resolving `[[handover#^dataset]]` needs a workspace index that this spec does not
define and a validator has no right to assume. So a cross-file blocker is recorded, reported to
consumers as `external`, and never diagnosed. A workspace-aware tool MAY resolve them; a future minor
version may define a code for an unresolvable one.

**`[[Name#^slug]]` is always external, even when `Name` is this file's own name.** The envelope, never
the filename, is authoritative (conventions §2.4) and a parser may have been handed a string rather
than a path, so it cannot know that `[[backlog#^x]]` means itself. Inside a backlog, write `[[#^x]]`;
from other files, write `[[backlog#^x]]`.

**A cycle is a permanent block** and is reported once per member as `MDTO415` (warning); a task that
blocks itself is the one-member case.

Three findings exist because the failure they describe is *silent*: the author believes a task is
blocked and a tool believes it is ready. `MDTO411` (target does not resolve), `MDTO413` (near-miss
opener), and `MDTO414` (a wikilink buried in a prose element) all point at that one gap.

### 2.6 Parent completion is a soft convention in 0.1

> A parent finishes only when its children do.

That is how backlogs are authored, and `backlog@0.1` **states it without enforcing it**: a terminal
task with a non-terminal child is `MDTO420`, a **warning**. The file is still unambiguous — it says
exactly what it says — and conventions §6.4 is emphatic that validation is advice, not ownership.

This is a deliberate, documented departure from todo. `todo@0.1` refuses to *derive* a parent's state
from its children and tells repairing agents not to "fix" a parent whose children are all checked
(todo §3). backlog@0.1 keeps that rule — nothing is ever derived, the marker on the line is the
state — and adds only an observation in the other direction: a *terminal* parent with *open* children
is probably a mistake, so say so, once, quietly.

Verbs are stricter than validation, and that is the intended split: **validation describes a file,
verbs act on it.** `finish` and `drop` refuse rather than guess (§5).

### 2.7 Ready work

The band vocabulary only means something through this derivation, so it is normative for any tool
that offers "what should I pick up".

A task is **ready** when all of:

1. its state is `open`;
2. it is not blocked (§2.5);
3. its band is not `Someday` and not `Done`.

A task whose state is `in-progress` is **active**, not ready; active work is offered first. A child of
an open parent is itself ready — decomposition means the children *are* the work. Bands rank ready
work: `Now` before `Next` before `Later`, and within a band, document order.

The IR carries the inputs (`state`, `blocked`, the band's `reserved` name and `rank`) and not the
conclusion, so that a tool with more context — a filter, a person's own rules — is not arguing with a
precomputed boolean.

### 2.8 Identifiers

Conventions §3 applies unchanged: identifiers are optional, `^` plus `[A-Za-z0-9][A-Za-z0-9-]{0,63}`,
final token of the line, unique in the file, never renumbered or garbage-collected, and never pinned
by tooling on an item it was not asked to touch.

**In a backlog, hand-pinning is normal.** Blockers are written by hand, and `[[#^tts-adapter]]` is a
sentence a person can read while `[[#^t7]]` is a lookup. So `backlog@0.1` **RECOMMENDS** kebab-slug
identifiers naming the task — `^offline-cache`, `^spec-backlog` — matching
`^[a-z0-9]+(-[a-z0-9]+)*$`. This is a style, not a rule: there is **no diagnostic** for an identifier
that does not match, and `^t7` is perfectly valid. What is not optional is the grammar itself
(`MDTO010`) and uniqueness (`MDTO011`).

### 2.9 Diagnostics owned by this spec

**Bands (401–409)**

| Code | Severity | Meaning |
|---|---|---|
| `MDTO401` | error | Unknown band name: it is neither a reserved band nor declared in frontmatter `bands`. |
| `MDTO402` | error | Duplicate band. |
| `MDTO403` | warning | Band order in the document differs from the vocabulary's priority order. |
| `MDTO404` | error | Band heading has no name. |
| `MDTO405` | error | Heading level is not valid in a backlog document: only one leading `#` title and `##` bands. |
| `MDTO406` | error | Task appears before the first band heading; there is no implicit band. |
| `MDTO407` | error | Inline field or block identifier on a band heading; bands carry no metadata in 0.1. |

**Blockers (410–419)**

| Code | Severity | Meaning |
|---|---|---|
| `MDTO410` | error | Malformed blocker clause: the list is empty, or an element is empty. |
| `MDTO411` | error | A `[[#^slug]]` blocker names no block identifier in this file. |
| `MDTO412` | warning | Blocker clause on a task that is already terminal; it has no effect. |
| `MDTO413` | warning | Text looks like a blocker clause but is not introduced by ` — `, so it is prose and the task is not blocked. |
| `MDTO414` | warning | A blocker element contains a wikilink but is not exactly one, so it is a prose blocker and will never lift on its own. |
| `MDTO415` | warning | Blocker cycle: the tasks in it can never become unblocked. |

**State and hierarchy (420–429)**

| Code | Severity | Meaning |
|---|---|---|
| `MDTO420` | warning | Terminal task has a non-terminal child. |
| `MDTO421` | warning | Task in the `Done` band is not in a terminal state. |

`MDTO400`, `408`–`409`, `416`–`419`, and `422`–`499` are unallocated. Codes are never reused,
renumbered, or repurposed (conventions §9.2).

Everything else a backlog file can get wrong is a shared code — see conventions §9.3. In particular
`MDTO031` (unrecognized checkbox state) is shared and **not** owned here: the same broken marker
produces the same code in every spec, and only the message names the spec's enum.

**Diagnostic anchors.** `MDTO403` reports the `bands:` key's own line when the file declares one, and
otherwise the line of the first heading that breaks the reserved order. `MDTO415` reports one
diagnostic per task in the cycle, on that task's line. `MDTO420` reports the parent's line and names
the child's. Everything else reports the line of the construct it is about (conventions §9.1).

### 2.10 Recovery

Validation is a single pass that reports everything it can find (conventions §9.4). What each invalid
construct recovers to:

- A heading invalid for its **level** (`MDTO405`) does not open a band: the tasks below it stay in the
  enclosing band and produce no `MDTO406` as long as some band is already open.
- A heading invalid for its **content** still opens a band, so tasks never migrate into the previous
  one while a file is being repaired: for `MDTO407` the name is the heading text with the offending
  constructs removed, for `MDTO404` it is the empty string. A heading that reports `MDTO404` does not
  additionally report `MDTO401`, and neither gets a `rank`.
- An unusable `bands:` declaration (`MDTO007`) is discarded, and the vocabulary check (`MDTO401`) and
  order check (`MDTO403`) are **skipped with it** rather than falling back to the reserved names — a
  cascade of consequences would bury the one diagnostic that matters.
- **A task whose blocker relation is broken stays blocked, never ready.** A malformed clause
  (`MDTO410`) recovers to a single prose blocker holding the clause body verbatim; an unresolvable
  target (`MDTO411`) is kept with resolution `unresolved`; a cycle (`MDTO415`) leaves every member
  blocked. Recovery always errs toward *not* offering work that an author said was waiting.
- An unrecognized marker (`MDTO031`) recovers to `state: "open"`, `checked: false`, with the marker
  character preserved verbatim in `raw` for serialization. No verb may act on it: verbs refuse to
  edit a file that fails validation with an error (§5).

---

## 3. Rationale

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

**Why this spec exists at all, given todo.** An agentsFS backlog page already shared roughly eighty
per cent of todo's item model before either was written down — the same bullets, the same nesting,
the same lazy identifiers. What it needed on top was small and specific: a marker for work in
progress, a vocabulary for priority, and a way to say "not until that". Three deltas is the whole
spec. If a fourth had been needed, this would have been a different format rather than an extension.

**Why four states and not a status field.** `[/]` and `[-]` are what people already type in backlogs
in the wild, including every agentsFS instance. They keep the file scannable — the left edge of the
line tells you everything — and they degrade to something readable in a viewer that has never heard
of Markdown To. The alternative, `[ ] … [status:: in-progress]`, adds a field a person would not type
and puts the most important fact about a line at its far right. Conventions §9.3 anticipated exactly
this extension and required the full enum to be declared; §2.4 declares it. **When repairing, map an
unknown marker onto one of the four on purpose; never delete the line.**

**Why `[x]` and `[-]` are both terminal.** A dropped task is not a done task — the outcomes differ and
the file records the difference — but for everyone downstream they are the same fact: nobody is
waiting on this any more. Making the blocker relation and the parent convention key on "terminal"
rather than on "done" is what lets a blocker lift when its target is abandoned. Otherwise dropping a
task would silently deadlock everything behind it, which is the single most annoying failure mode a
dependency feature can have.

**Why band names are reserved, when kanban refuses to reserve "Done".** kanban is right for boards:
they are drawn by their author, "Done" might mean "done this sprint", and hard-coding a name would be
a guess about somebody's workflow. A backlog is not drawn, it is *queried* — "what should I pick up"
— and that question cannot be answered without knowing which band is parked and which is closed.
kanban's `columns:` is opt-in strictness over a free vocabulary; backlog's `bands:` is opt-out from a
fixed one. The polarity is inverted on purpose, and it is why `MDTO401` fires on an undeclared
`## Icebox` where kanban would happily render it.

**Why the reserved semantics still do not touch the checkbox.** Reserving `Done` says what a tool may
do with the *band* — prune it — not that entering it finishes a task.

**This is the one place backlog and kanban genuinely differ, so do not reason across.** Both specs
keep the two facts independent *in the grammar*: neither a band nor a column is ever derived from a
marker, or a marker from a band or column. But kanban then couples them **at the verb layer** — its
last column carries a *done role* and `kanban move` writes the box when it crosses that role
(kanban §2.4) — and `backlog@0.1` takes no such step. There is no done-role band. `promote` and
`demote` never write a checkbox, whatever the destination band is called, and `Done` is a filing
decision that follows the state rather than causing it (§2.4, §5). **Do not check a box because you
moved a task into `Done`, and do not move a task because you checked its box.**

**Why an empty band may be omitted, when kanban makes a missing declared column an error.** A board
with no "Review" column cannot be laid out; a backlog with nothing in `Next` is just a backlog with
nothing in `Next`. Requiring five ceremonial headings on a file with three tasks is precisely the tax
§0 of the conventions rejects.

**Why the blocker clause is prose and not a field.** `[blocked-by:: t7]` would have been easier to
parse and would have failed the governing principle: it is not a sentence, it does not read in a
plain viewer, and nobody types it. `— blocked by [[#^offline-cache]]` is what an author writes anyway,
in a vocabulary (wikilinks) they already know. Conventions §5.1 kept wikilinks inert in the core and
said a spec would assign them meaning; this is that spec, and this is that meaning.

**Why the clause goes before the identifier.** Conventions §3.1 requires the identifier to be the
final token of its line, and that rule is load-bearing: it is how every implementation's shared item
parser finds an id at all. A backlog that put the clause last would either break that parser or force
a spec-local exception to a shared MUST. So the order is `… — blocked by [[#^a]] ^slug`. Files written
under the older agentsFS convention have it the other way round and need a mechanical reorder before
they will validate — without it the identifier becomes literal text (`MDTO012`) and every reference
to it fails to resolve (`MDTO411`).

**Why an unresolvable same-file blocker is an error but a cross-file one is not diagnosed at all.**
`[[#^slug]]` is a claim about *this* document, and this document is entirely in front of the
validator; a claim it can check and that fails is an error. `[[other#^slug]]` is a claim about a file
the validator has not been given and has no right to open (conventions §8 forbids I/O during
validation, and §2.4 says the filename is not authoritative, so it cannot even reliably identify
itself). Reporting an error on a reference it cannot evaluate would be a guess. **Do not "fix" a
cross-file blocker by deleting it.**

**Why recovery always leaves a broken blocker blocking.** The two possible mistakes are not
symmetrical. Treating a blocked task as ready hands an agent work that cannot be done and hides the
author's own note that it was waiting; treating a ready task as blocked costs one glance. Every
recovery rule in §2.10 takes the second failure over the first.

**Why parent completion is a warning and `finish` is a refusal.** Both settings are defensible and
they answer different questions. `validate` answers "is this file conforming?" — and a file where
someone checked a parent early is unambiguous, hand-written, and theirs; conventions §6.4 says
diagnostics are advice. `finish` answers "should I write this?" — and writing a state that validation
will then complain about is exactly the guessing that todo §5 forbids verbs to do. So validation
describes and verbs refuse, with `--cascade` as the explicit way to say what you meant.

**Why identifiers are recommended by hand here, when todo calls them noise.** Todo's point stands:
identifiers exist for tooling and a hand-written file should have none. But a blocker *is* a
hand-written reference, and a reference needs a referent. So a backlog author pins ids for the tasks
they point at, and only those — the fixtures show files where most lines carry none. The recommended
kebab slug is for the reader of the reference, not for the machine, which is why a non-matching id is
valid and produces nothing.

**Why `graduate` is specified but not shipped.** The verb is real and rule 13 of agentsFS describes
it: a task that accumulates state becomes its own note. But it writes two files, and the patch
engine's whole safety story is one file, one source hash, all-or-nothing. Specifying the edit without
shipping it records the design while refusing to invent a multi-file transaction contract inside a
verb. **Do not implement it by writing the note first and hoping.**

**Deliberate non-features of 0.1**, each a plausible minor-version addition: cross-file blocker
resolution, a `prune` verb, priority-inversion warnings (a `Now` task blocked by a `Someday` one),
band-level metadata such as WIP limits, per-task history or completion timestamps, `blocks` as the
inverse relation, recurring tasks, and any derivation of a parent's state from its children.

---

## 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)) — two bands, three tasks, no
metadata, no identifiers, the other three bands simply not written down. This is the floor:

```markdown
---
markdownto: backlog@0.1
---

## Now

- [/] Rewrite the studio's about page
- [ ] Photograph the new work for the portfolio

## Next

- [ ] Move the mailing list off the old provider
```

**Full bands** ([`fixtures/valid/full-bands.md`](fixtures/valid/full-bands.md)) — the showpiece: all
five reserved bands, all four states, nesting, typed fields, tags, kebab identifiers on exactly the
tasks something points at, and every kind of blocker:

```markdown
## Now

- [/] Cache the tide tables so the app works offline [owner:: @rosa] ^offline-cache
  - [x] Decide the on-disk format — one row per station-day
  - [/] Write the importer
  - [ ] Evict stations nobody has opened in ninety days

## Next

- [ ] Home-screen widget showing the next high tide — blocked by [[#^offline-cache]] ^widget
- [ ] Ship a proper icon — blocked by Marina's illustration ^icon

## Later

- [ ] Submit to the App Store — blocked by [[#^dst-bug]], [[#^icon]] ^submission
- [ ] Move the harbour dataset notes into the handover doc — blocked by [[handover#^dataset]]

## Done

- [-] Rewrite the parser in Rust [reason:: the Swift one is already fast enough] ^rust-parser
```

Note the em dash on the nested "Decide the on-disk format" line: an em dash is only a clause opener
when `blocked by` follows it.

**Bands override** ([`fixtures/valid/bands-override.md`](fixtures/valid/bands-override.md)) — a
magazine's production stages declared in frontmatter, keeping `Someday` and `Done` and therefore
their reserved meanings, with the declared `Editing` band omitted because it is empty this week.

**agentsFS page** ([`fixtures/valid/agentsfs-page.md`](fixtures/valid/agentsfs-page.md)) — the
envelope coexisting with `description:` and `agentsfs_role: backlog` in one frontmatter block, a
blockquote legend before the first band, wikilinks in task text (inert), and a `[reason::]` on a
dropped task.

**Edge cases** ([`fixtures/valid/edge-cases.md`](fixtures/valid/edge-cases.md)) — the constructs that
look like syntax and are not: a clause inside a code span, a complete blocked task inside a fenced
block, `#1234` as a literal issue reference, an em dash followed by ordinary prose, the words "blocked
by" in a sentence, an empty band written out, and a legal prose blocker.

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

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

**`bad-bands.md`** — every way the band structure goes wrong:

```markdown
## Now [priority:: high]

### Plumbing

##

## Later

## Later
```

→ `MDTO407` error, line 6: *Inline field `priority` on a band heading: bands carry no metadata in backlog@0.1. Move it to a task.*
→ `MDTO405` error, line 10: *Heading level 3 is not valid in a backlog document: use `##` for a band, or nest tasks to break one down.*
→ `MDTO404` error, line 14: *Band heading has no name.*
→ `MDTO402` error, line 22: *Duplicate band `Later`: already opened on line 18.*

**`bands-mismatch.md`** — declared `Commissioned, Drafting, Editing, Done`; document has
`Drafting, Commissioned, Spiked, Done`:

→ `MDTO403` warning, line 4: *Band order differs from the `bands` declaration: declared `Commissioned, Drafting`, found `Drafting, Commissioned`.*
→ `MDTO401` error, line 19: *Unknown band `Spiked`: this document declares `bands`, so the band names are `Commissioned`, `Drafting`, `Editing`, `Done`.*

The declared-but-absent `Editing` produces **nothing** — empty bands are omittable.

**`task-before-band.md`** — tasks above the first heading:

→ `MDTO406` error, line 6: *Task appears before the first band heading; backlog@0.1 has no implicit band.*
→ `MDTO406` error, line 7: *Task appears before the first band heading; backlog@0.1 has no implicit band.*

**`bad-blockers.md`** — every way the relation goes wrong:

```markdown
- [ ] Ship the home-screen widget — blocked by
- [ ] Wire up the settings screen — blocked by [[#^onboarding]]
- [ ] Write the App Store copy — blocked by [[#^screenshots]] and the new icon
- [ ] Record the demo video, blocked by [[#^copy]] ^screenshots
- [ ] Draft the release notes — blocked by [[#^launch]] ^copy
- [ ] Book the launch tweet — blocked by [[#^copy]] ^launch
```

→ `MDTO410` error, line 8: *Malformed blocker clause: `— blocked by` is followed by nothing.*
→ `MDTO411` error, line 9: *Blocker `[[#^onboarding]]` names no task in this file.*
→ `MDTO414` warning, line 10: *Blocker `[[#^screenshots]] and the new icon` mixes a wikilink with prose, so it is one prose blocker and will never lift on its own.*
→ `MDTO413` warning, line 11: *`blocked by [[#^copy]]` looks like a blocker clause but is not introduced by ` — `, so it is prose and this task is not blocked.*
→ `MDTO415` warning, lines 12 and 13: *Blocker cycle: `^copy` is blocked by `^launch`, which is blocked by `^copy`.*
→ `MDTO412` warning, line 17: *Blocker clause on a task that is already done; it has no effect.*

**`bad-states.md`** — an out-of-enum marker and the two soft-invariant warnings:

```markdown
- [x] Replace the worktop
  - [ ] Template the cut-outs for the sink
- [~] Sand the floorboards
- [-] Retile the splashback
  - [/] Choose the tiles

## Done

- [ ] Fit the new extractor fan
```

→ `MDTO420` warning, line 8: *Task is done but its child on line 9 is still open.*
→ `MDTO031` error, line 11: *Unrecognized checkbox state `~`: backlog@0.1 defines only `[ ]`, `[/]`, `[x]`, `[X]`, and `[-]`.*
→ `MDTO420` warning, line 12: *Task is dropped but its child on line 13 is still in progress.*
→ `MDTO421` warning, line 17: *Task in the `Done` band is not in a terminal state.*

**`bad-envelope.md`** — `title` as a sequence, `bands` 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 6: *Frontmatter key `bands` has the wrong type: expected a sequence of strings, found a string.*

The document's `## Icebox` produces no `MDTO401`: the unusable declaration is discarded and the
vocabulary check goes with it (§2.10).

---

## 5. Verb reference

`backlog@0.1` owns the vocabulary **`add`, `start`, `finish`, `drop`, `block`, `unblock`, `promote`,
`demote`, `reorder`, `graduate`**. It overlaps `todo@0.1` and `kanban@0.1` in shape because the three
share an item model, not because any platform contract requires it — and it spends its vocabulary
differently, on the state machine and the relation those two do not have. Where a name is shared the
contract still is not: `backlog promote`/`demote` never write a checkbox, while `kanban move` writes
one when it crosses that spec's done role (kanban §2.4). **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.

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 a task — `<ref>`

1. A pinned identifier, with or without the caret: `offline-cache`, `^offline-cache`.
2. Otherwise a case-insensitive substring of the task's normalized text: `"tide tables"`.

If a text `<ref>` matches zero or more than one task, the verb **fails with a non-zero exit code and
changes nothing**. Ambiguity is never resolved by picking the first match. `--band <name>` narrows the
search on `start`, `finish`, `drop`, `block`, `unblock`, and `reorder`; on `add`, `promote`, and
`demote` it names the *destination* instead and never narrows a search. Band arguments are matched
case-insensitively after trimming.

### Common flags

`--file <path>` (default: the single `*.backlog.md` in the working directory; failing that, the
single page whose frontmatter declares `markdownto: backlog@0.1`, which is how an agentsFS backlog
page is found without knowing its name) · `--json` (emit `{edits, pinned, addresses, hash}`) ·
`--dry-run` (print the unified diff, write nothing) · `--pin` (pin a minted identifier on the touched
task and return it) · `--id <slug>` (pin *this* identifier instead of a minted one; refuses on
collision, `MDTO011`) · `--expect <hash>` (apply only if the file still hashes to `<hash>`;
conventions §6.5).

**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, never this
check.

**No verb pins an identifier unless asked** — with one deliberate exception, `block`, documented
below.

### The verbs

| Verb | Signature | Source edit | Kind |
|---|---|---|---|
| `add` | `mdto backlog add "<text>" --band <name> [--create-band] [--id <slug>] [--priority p] [--due d] [--owner o] [--blocked-by <ref>…] [--top\|--bottom\|--before <ref>\|--after <ref>]` | Inserts **one line**. | patch op |
| `start` | `mdto backlog start <ref>…` | Writes `/` into the checkbox on one line. | patch op |
| `finish` | `mdto backlog finish <ref>… [--cascade]` | Writes `x` into the checkbox on one line, or on the task and every non-terminal descendant. | patch op |
| `drop` | `mdto backlog drop <ref>… [--reason "<text>"] [--cascade]` | Writes `-` into the checkbox, optionally appending `[reason:: …]`. | patch op |
| `block` | `mdto backlog block <ref> --on <ref>… [--prose "<text>"]…` | Rewrites **one line's** blocker clause. | patch op |
| `unblock` | `mdto backlog unblock <ref> [--on <ref>…]` | Rewrites or removes **one line's** blocker clause. | patch op |
| `promote` | `mdto backlog promote <ref>… [--to <band>] [--create-band] [--top\|--bottom\|--before <ref>\|--after <ref>]` | Deletes the task's lines, reinserts them verbatim under the destination band. | patch op |
| `demote` | `mdto backlog demote <ref>… [--to <band>] [--create-band] [--top\|--bottom\|--before <ref>\|--after <ref>]` | Same, toward lower priority. | patch op |
| `reorder` | `mdto backlog reorder <ref> [--top\|--bottom\|--before <ref>\|--after <ref>]` | Deletes and reinserts the task's lines **within its own band**. | patch op |
| `graduate` | `mdto backlog graduate <ref> --to <path>` | Writes a new note and rewrites the task's text to link it. | **multi-file — specified, deferred (see below)** |

Every "patch op" verb is a single-file edit expressible in the existing patch engine's operations:
insert a line, replace a span inside a line, or delete lines and reinsert them verbatim. A verb that
touches several lines (`finish --cascade`) is a **batch** of those operations, applied all-or-nothing
against one source hash.

**`add`** — inserts a single line at the end of the target band (`--bottom`, the default) or at the
position given by `--top` / `--before` / `--after`. `--band` is **required**: there is no implicit band
to fall back on. The bullet marker and indentation are copied from the nearest sibling task. Fields
are appended after the text in the canonical order `[priority:: …] [due:: …] [owner:: …]`; tags stay
where the author put them in `<text>`; `--blocked-by` appends the clause after the fields, and `--id`
pins the identifier last, giving `text [fields] — blocked by … ^id` (§2.5). Targeting a band that
already exists is never an error, whether or not `--create-band` is passed (a duplicate heading is its
own error, `MDTO402`, so create-or-fail is unreachable). A band that does not exist is an error unless
`--create-band` 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 task beneath it, both at the end of the file — and
if the document declares `bands:`, also adds the name to that declaration, because leaving them
inconsistent would immediately produce `MDTO401`.

**`start` / `finish` / `drop`** — the state machine. Each writes one character inside the checkbox and,
apart from `drop --reason`, touches nothing else on the line. The task does not move, no band changes,
no timestamp is stamped (0.1 has no such field), and children are untouched unless `--cascade` is
passed. Each is a successful no-op producing an empty diff when the task is already in the requested
state.

- `start` is defined **from any state**, so it doubles as the reopen path: `start` on a `[x]` task
  writes `[/]`. This is the same trade kanban makes when it spends its vocabulary on `edit` instead of
  an `undone` — each spec spends its verbs on what its domain does often, and a backlog reopens work
  far more often than it un-starts it. **0.1 has no verb that writes `[ ]` onto an existing task**;
  edit the line. That is deliberate, and narrow enough to be honest about.
- `finish` and `drop` **refuse** — non-zero exit, nothing written — when the task has any non-terminal
  descendant, and say which lines. `--cascade` performs the batch: the task and every non-terminal
  descendant, to `[x]` for `finish` and `[-]` for `drop`. Descendants already terminal are left
  exactly as they are, including their markers, so cascading a `finish` never overwrites a `[-]`.
  Refusing where validation only warns is the intended split (§2.6).
- `drop --reason "<text>"` appends `[reason:: <text>]` in the canonical field position — after the
  text and any existing fields, **before** the blocker clause and the identifier. Without `--reason`
  the line's text is untouched.

**`block` / `unblock`** — the only verbs that write a blocker clause, and the only place `block` is
allowed to pin.

- `block <ref> --on <target>` resolves `<target>` as an ordinary `<ref>` and writes `[[#^slug]]`. If
  the target task has no identifier, **`block` pins one** — a slug derived from the target's text,
  uniqueness-checked — because a reference needs a referent and the alternative is refusing to do the
  thing that was asked. It reports every identifier it pinned in `--json`. This is the same standing
  exception the live board holds (conventions §3.2). A `--on` that matches no task is an error, not a
  prose blocker; write prose deliberately with `--prose "<text>"`.
- `block` on a task that already has a clause **appends** to the existing list rather than replacing
  it, preserving the existing elements' text and order. Adding a blocker that is already listed is a
  no-op producing an empty diff.
- `block` refuses on a terminal task rather than writing something validation would immediately flag
  as `MDTO412`.
- `unblock <ref>` removes the entire clause, together with the ` — ` that opened it, leaving the
  identifier where it was. `unblock <ref> --on <target>` removes one element and its separating comma,
  and removes the whole clause if it was the last one. `--on` matches a wikilink element by its target
  and a prose element by case-insensitive substring; an `--on` matching zero or several elements
  refuses.

**`promote` / `demote`** — the priority moves. With no `--to`, each moves the task **one band** along
the vocabulary's priority order: `promote` toward `Now`, `demote` toward `Done`, skipping bands the
document does not contain. With `--to <band>` it names the destination outright, and the direction is
checked: `promote --to` a lower-priority band is an error (use `demote`), because a verb that silently
did the opposite of its name would be a trap. Either way the task's lines — its own and its children's
— are deleted and reinserted **verbatim**: same text, same fields, same tags, same identifier, same
blocker clause, same markers, same relative indentation. **Neither verb ever changes a checkbox**,
whatever the destination band is called (§2.4). Promoting a task already in the top band, or demoting
one already in the bottom band, is a successful no-op producing an empty diff.

**`reorder`** — the same delete-and-reinsert, constrained to the task's current band. Reprioritizing
within a band is the most common edit a backlog gets, and keeping it in a separate verb from
`promote`/`demote` means "move this up" can never accidentally cross a band boundary. Moving a task to
the position it already occupies is a successful no-op producing an empty diff.

**`graduate` — specified, deferred.** `mdto backlog graduate <ref> --to <path>` writes a new note at
`<path>` — frontmatter carrying `description:` derived from the task's text, then the task's children
as the note's own list — and rewrites the task's line so its text becomes a wikilink to the new note,
preserving the marker, fields, blocker clause, and identifier. That is the edit agentsFS rule 13
describes when a task accumulates enough state to deserve its own page.

**Implementations SHOULD NOT ship it in 0.1.** It is the only verb here that is not a patch-engine
operation: it writes two files, and the engine's entire safety story is one file, one source hash,
all-or-nothing. A correct `graduate` needs a multi-file transaction contract — two hashes, a defined
apply order, a defined rollback when the second write fails — that this spec does not define and that
does not belong in a verb's private judgement. `mdto backlog graduate` MAY exit with "not implemented
in this build". The edit is specified so that the design is recorded and so that the contract, when it
is written, has something to satisfy.

### What no verb ever does

Reformats a line it was not asked to change · normalizes bullet markers, indentation, or blank lines ·
reorders tasks or bands it was not asked to reorder · drops an unknown field, an unrecognized value,
or an unrecognized marker character · derives a parent's state from its children, or a child's from
its parent · checks a box because of which band a task landed in, or moves a task because its box was
checked · lifts a blocker by editing the clause when the target moved · prunes the `Done` band (there
is no `prune` verb in 0.1, and any tool that prunes MUST NOT remove a non-terminal task — see
`MDTO421`) · pins an identifier without being asked, except `block` on a target that has none ·
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).
