Specifications v0.1 draft Sheet 04 of 04
backlog@0.1
A prioritized backlog as a plain Markdown file — level-2 headings
are bands in priority order, each task carries one of four markers, and a task may
say what it is waiting on. It is [email protected] plus exactly three deltas, so that
the page an agent works from is the same file a person reads.
- Status
- normative · draft, will change
- Inherits
- shared conventions · the item model
- Artifacts
- schema.json
fixtures/ - Owns codes
- MDTO400–MDTO499 15 allocated
§ 01
The file, and the ladder
On the left, a conformance fixture —
specs/backlog/fixtures/valid/full-bands.md, byte for byte. On the right,
what mdto render makes of it. A checklist has one axis and a board has one;
a backlog has three at once — which band a task sits in, which of
four states it is in, and what is holding it — so the page is built
to show all three without deriving any of them.
[email protected]
static render
--- markdownto: [email protected] title: Tidepool id: tidepool-backlog --- # Tidepool > Bands run in priority order: Now, Next, Later, Someday, Done. Top of a band is > highest priority — reordering lines is how we reprioritize. Nest children to > break a task down; a parent finishes only when its children do. ## 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 - [ ] Fix the DST jump that shifts every time by an hour twice a year [priority:: high] [due:: 2026-09-15] ^dst-bug ## Next - [ ] Home-screen widget showing the next high tide — blocked by [[#^offline-cache]] ^widget - [ ] Station search that tolerates misspelled harbours [priority:: medium] #ux - [ ] 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]] - [ ] Swell height as well as tide height [priority:: low] ## Someday - [ ] Apple Watch complication - [ ] Barometric pressure overlay, if anyone ever asks for it ## Done - [x] Pick a tide-data source with a licence we can live with ^data-source - [x] Skeleton app with one hard-coded station - [-] Rewrite the parser in Rust [reason:: the Swift one is already fast enough] ^rust-parser
Tidepool
12 openof 16 tasks across 5 bands · 4 blockedread-only
Markersopenin progressdonedropped (cancelled)
Now
2 open · 2 in progress · 1 doneCache the tide tables so the app works offline
Decide the on-disk format — one row per station-day
Write the importer
Evict stations nobody has opened in ninety days
Fix the DST jump that shifts every time by an hour twice a year
Next
3 openHome-screen widget showing the next high tide
Station search that tolerates misspelled harbours #ux
Ship a proper icon
Later
3 openSubmit to the App Store
Move the harbour dataset notes into the handover doc
Swell height as well as tide height
Someday
parked2 openApple Watch complication
Barometric pressure overlay, if anyone ever asks for it
05Done
closed2 done · 1 dropped
Pick a tide-data source with a licence we can live with
Skeleton app with one hard-coded station
Rewrite the parser in Rust
The bands are drawn in the file’s own order and never sorted, so the rank
numerals beside them are the priority the vocabulary gives each band rather than a
position the renderer chose. Someday and Done step back onto
a recessed ground because their names reserve a meaning; a held task loses its lift
rather than its contrast, because the text of a blocked task is the text most worth
reading. Every value went through the escaper on its way out — band names, task
text, [reason:: …] values and a blocker’s own words are all
untrusted input. The markup was produced by @markdownto/web at build
time; see site/tools/build-specs.mjs.
“Ready” is the question a backlog exists to answer, and this page does not
answer it. The spec defines ready work precisely — open, unblocked, and outside
Someday and Done — and then has the intermediate
representation carry the inputs rather than the conclusion, so that a tool
with more context is never arguing with a precomputed boolean.
The same restraint runs the other way. A blocker that has lifted is drawn as nothing at all, because leaving it as a chip would put the word “blocked” on a task that is not — and following blocked by ^offline-cache lands on the row it names, because that is a question the page can answer in one click without answering a different one on the reader’s behalf.
§ 02
Grammar
[email protected] is [email protected] with three additions and nothing else:
an extended marker enum, a reserved band vocabulary, and a blocker relation. Everything
not below is todo’s grammar verbatim. Distilled for this page; the normative text
is specs/backlog/SPEC.md, and it is the one that governs.
The envelope, and the one key backlog adds
A conforming file begins with YAML frontmatter naming exactly one spec. The envelope,
never the filename, is authoritative — .backlog.md aids discovery and
nothing more.
--- markdownto: [email protected] title: Tidepool # optional, reserved id: tidepool-backlog # optional, reserved bands: # optional, spec-defined - Commissioned - Drafting - Done ---
markdownto- Required. The exact string form
[email protected]. A sequence or mapping here isMDTO004. title,id- The reserved keys from the shared conventions. Optional, both strings.
bands- A sequence of strings, and the only key this spec adds. It replaces the
reserved vocabulary rather than merely checking it: the declared names become the
complete vocabulary and the declaration order becomes the priority order. A wrong type
is
MDTO007. - anything else
- An unknown key: parsed, preserved, written back verbatim, never validated.
This is what lets an agentsFS page carry
description:andagentsfs_role: backlogin the same frontmatter block as the envelope and lose nothing on round-trip.
Bands, and the vocabulary they come from
A level-2 heading is a band. With no bands: declaration, a
backlog’s bands are exactly these five names, in this priority order — and
any other ## heading is MDTO401.
## Now- Rank 0. Being worked, or next to be picked up.
## Next- Rank 1. Committed, not started.
## Later- Rank 2. Real work, not yet scheduled.
## Someday- Rank 3. Parked. Never offered as ready work — ideas kept rather than lost.
## Done- Rank 4. Closed. Holds terminal tasks and is safe to prune, which is the one thing reserving the name buys.
There is no implicit band. A backlog is a partition by priority, so a task in no
band has no priority: a task above the first ## is MDTO406, and
the repair is a heading, never an invented inbox. Prose before the first band is normal
and legal — most backlogs open with a legend explaining their own markers. A
backlog may also 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.
Empty bands may be omitted and may be written out empty; both are correct and
neither is a diagnostic, even when the document declares bands:. Heading
levels 3 and deeper are not part of the spec (MDTO405): nesting breaks a
task down, headings group bands, and there is no third mechanism. A band heading carries
no metadata — an inline field or block identifier on one is MDTO407.
Task order within a band is priority order, top highest, and nothing sorts it.
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. So kanban’s
columns: is opt-in strictness over a free vocabulary, and
backlog’s bands: is opt-out from a fixed one. The polarity
is inverted on purpose, which is why ## Icebox is
MDTO401 here and renders happily there.
Reserved semantics attach to names, not to positions. A declaration that keeps
Someday or Done keeps their meanings; one that drops them
means the document simply has no parked band and no closed band. And document order is
presentation while the vocabulary order is priority — when the two disagree the
vocabulary wins and MDTO403 says so, as a warning.
Four markers, two of them terminal
A task is a GFM task list item on a bullet list, and [email protected] declares
its full marker enum. [/] and [-] are what people already type
in backlogs in the wild: they keep the file scannable, because the left edge of the line
tells you everything.
- [ ] 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 [reason:: the Swift one is already fast enough]
[ ]open. Not terminal.[/]in-progress. Not terminal — and active rather than ready, which is why a tool offers it first.[x],[X]done. Terminal.[-]dropped. Terminal. A different outcome from done, and the same fact for everyone downstream: nobody is waiting on this any more.- anything else
- The shared
MDTO031, whose message names backlog’s enum rather than todo’s. Every tool must preserve an unrecognized marker character verbatim rather than coerce it to open or checked. [reason:: …]- The one inline field this spec promotes to a recognized key. Free text, no
diagnostic of its own, meaningful on any task — so a parked idea can record why
it is parked. It is what
drop --reasonwrites.
Done is a band; [x] is a state
They 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 — promote and demote never write a
checkbox, whatever the destination is called.
This is the one place backlog and kanban genuinely differ, so do not reason
across. Both keep the two facts independent in the grammar, but kanban couples
them at the verb layer: its last column carries a done role and
kanban move writes the box when it crosses it. backlog takes no such
step. Do not check a box because you moved a task into Done, and
do not move a task because you checked its box. What the reserved name buys
is what a tool may do with the band — prune it — which is why a
non-terminal task sitting in Done is called out as
MDTO421: a prune would destroy it.
The blocker relation
A task may end with a blocker clause saying what it is waiting on: a space, an em
dash, a space, the words blocked by matched case-insensitively, a space, and
then a non-empty comma-separated list. The clause stays part of the task’s text,
like a tag and unlike a field, because it reads as part of the sentence and a plain
Markdown viewer has no other way to show it.
- [ ] 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
[[#^slug]]found- Resolves in this file, and lifts itself the moment the target task reaches
[x]or[-]. Keying on terminal rather than on done is what stops a dropped task silently deadlocking everything behind it. [[#^slug]]missingMDTO411. It names no block identifier in this file, so it can never lift; the repair is to fix the reference.[[Name#^slug]]- External, and out of validation scope in 0.1 deliberately. Validation is a single-file parse that performs no I/O, so a claim about a file it has not been given is recorded and reported, never diagnosed. Do not “fix” a cross-file blocker by deleting it.
- prose
- First-class and expected. “blocked by Marina’s illustration” is more honest than inventing a task to point at. It holds until an author edits it away.
- a cycle
- A permanent block, reported once per member as
MDTO415. A task that blocks itself is the one-member case.
A task is blocked when it has at least one blocker that is not satisfied. Nothing
is scanned inside code spans or fences, so a file can document this clause without
writing one. [[Name#^slug]] is always external even when
Name is this file’s own name: the envelope and never the filename
is authoritative, and a parser may have been handed a string rather than a path. Inside a
backlog write [[#^x]]; from other files write
[[backlog#^x]].
The shared conventions require a block identifier to be the final token of its
line, and that rule is load-bearing: it is how every implementation’s item parser
finds an id at all. So the canonical order is
text [fields] #tags — blocked by … ^id.
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 stops being an identifier and becomes literal text
(MDTO012), and then every reference pointing at it fails to resolve
(MDTO411) — one transposition, two families of diagnostic.
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 three hold.
- state
- It is
open. A task that isin-progressis active, not ready — and active work is offered first. - not blocked
- Every blocker it declares is satisfied. A broken relation counts as blocked, never as clear.
- band
- Its band is neither
SomedaynorDone. Everything else ranks:NowbeforeNextbeforeLater, and within a band, document order.
A child of an open parent is itself ready — decomposition means the children
are the work. And a parent that is terminal while a child is not is
MDTO420, a warning rather than an error: the file is unambiguous, it says
exactly what it says, and validation is advice. The verbs are the strict half.
finish and drop refuse rather than guess.
Read [email protected] in full — the normative text Shared conventions
§ 03
Verb reference
[email protected] owns the vocabulary add, start,
finish, drop, block, unblock,
promote, demote, reorder,
graduate — and spends it on the state machine and the relation that
[email protected] and [email protected] do not have. Where a name is shared the
contract still is not: backlog promote never writes a checkbox, while
kanban move writes one when it crosses that spec’s done role.
Read the verb reference of the spec the file declares.
add
mdto backlog add "<text>" --band <name> [--create-band]
[--priority p] [--due d] [--owner o]
[--blocked-by <ref>…] [--id <slug>]
[--top | --bottom | --before <ref> | --after <ref>]Inserts one line at the end of the target band, or
wherever the position flags say. --band is required — there
is no implicit band to fall back on. The bullet marker and indentation are copied from
the nearest sibling; fields land in the canonical order, then the blocker clause, then
the identifier. A band that does not exist is an error unless
--create-band, which also adds the name to a bands:
declaration if the file has one, because leaving them inconsistent would immediately
produce MDTO401.
start
mdto backlog start <ref>…Writes / into the checkbox on one line. Defined
from any state, so it doubles as the reopen path: start on a
[x] task writes [/]. 0.1 has no verb that writes
[ ] back onto an existing task — edit the line. That is
deliberate, and narrow enough to be honest about.
finish
mdto backlog finish <ref>… [--cascade]Writes x into the checkbox. Refuses
— non-zero exit, nothing written — when the task has any non-terminal
descendant, and says which lines. --cascade performs the batch; a
descendant that is already terminal keeps its marker, so cascading a
finish never overwrites a [-].
drop
mdto backlog drop <ref>… [--reason "<text>"] [--cascade]Writes - into the checkbox, and with
--reason appends [reason:: …] 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. Same
refusal as finish.
block
mdto backlog block <ref> --on <ref>…
[--prose "<text>"]…Rewrites one line’s clause, appending to an
existing list rather than replacing it. The one verb allowed to pin without being
asked: when --on names a task that has no identifier,
block pins one — a slug derived from that task’s text —
because a reference needs a referent, and it reports every id it pinned in
--json. An --on matching no task is an error, not a prose
blocker; write prose deliberately with --prose.
unblock
mdto backlog unblock <ref> [--on <ref>…]Removes the entire clause together with the
— that opened it, leaving the identifier where it was. With
--on it removes one element and its separating comma, and the whole clause
if that was the last one. An --on matching zero or several elements
refuses.
promote
mdto backlog promote <ref>… [--to <band>] [--create-band]
[--top | --bottom | --before <ref> | --after <ref>]Moves the task one band toward Now, skipping
bands the document does not contain, or straight to --to. Its lines and
its children’s are deleted and reinserted verbatim: same text, fields,
tags, identifier, blocker clause, markers and relative indentation. It never writes
a checkbox, whatever the destination band is called.
demote
mdto backlog demote <ref>… [--to <band>] [--create-band]
[--top | --bottom | --before <ref> | --after <ref>]The mirror, toward Done. The direction is
checked either way: promote --to a lower-priority band is an error and so
is its opposite, because a verb that silently did the reverse of its own name would be
a trap. Promoting a task already in the top band is a successful no-op with an empty
diff.
reorder
mdto backlog reorder <ref>
[--top | --bottom | --before <ref> | --after <ref>]The same delete-and-reinsert, constrained to the task’s
own band. Reprioritizing within a band is the most common edit a backlog gets, and
keeping it in a separate verb from promote means “move this
up” can never cross a band boundary by accident.
graduatedeferred
mdto backlog graduate <ref> --to <path>Writes a new note and rewrites the task’s text to link it — the edit agentsFS describes when a task accumulates enough state to deserve its own page. Specified, and deliberately unshipped. It is the only verb here that is not a patch-engine operation: it writes two files, and the engine’s whole safety story is one file, one source hash, all-or-nothing. Specifying the edit 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.
Addressing a task, and the common flags
<ref>- A pinned identifier with or without the caret (
offline-cache,^offline-cache); 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- Narrows the search on
start,finish,drop,block,unblockandreorder. Onadd,promoteanddemoteit names the destination instead and never narrows anything. --file- Defaults to the single
*.backlog.mdin the working directory; failing that, the single page whose frontmatter declaresmarkdownto: [email protected]— which is how an agentsFS backlog page is found without knowing its name. --expect <hash>- Every mutating verb requires a matching source hash before it writes. The
CLI computes one from the file it just read unless this flag supplies one from an
earlier read; a mismatch is a conflict and nothing is applied.
--forceoverrides the validation refusal, never this check. --dry-run- Prints the unified diff and writes nothing.
--json- Emits
{edits, pinned, addresses, hash}, for a caller that wants a value rather than a file. --pin,--id- Pins a minted identifier on the touched task, or pins this one instead
— refusing on collision (
MDTO011). No verb pins otherwise, with the single documented exception ofblockon a target that has none.
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·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·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·writes a file whose
source hash no longer matches the one it read.
§ 04
Diagnostics carry the repair
Three of the findings below exist because the failure they describe is silent: the author believes a task is blocked and a tool believes it is ready. A file can look completely ordinary and be wrong in exactly that way, so the messages quote the offending element back, name what it actually parsed as, and say which direction to repair it in.
--- markdownto: [email protected] title: Tidepool launch --- ## Now - [ ] 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 ## Done - [x] Pick a tide-data source — blocked by [[#^copy]]
-
MDTO410 error line 8
- [ ] Ship the home-screen widget — blocked byMalformed blocker clause:
— blocked byis followed by nothing. List at least one blocker, or remove the clause. -
MDTO411 error line 9
- [ ] Wire up the settings screen — blocked by [[#^onboarding]]Blocker
[[#^onboarding]]names no task in this file;#^targets are resolved within the document. -
MDTO414 warning line 10
- [ ] Write the App Store copy — blocked by [[#^screenshots]] and the new iconBlocker
[[#^screenshots]] and the new iconmixes a wikilink with prose, so it is one prose blocker and will never lift on its own. Separate blockers with commas. -
MDTO413 warning line 11
- [ ] Record the demo video, blocked by [[#^copy]] ^screenshotsblocked by [[#^copy]]looks like a blocker clause but is not introduced by—, so it is prose and this task is not blocked. -
MDTO415 warning line 12
- [ ] Draft the release notes — blocked by [[#^launch]] ^copyBlocker cycle:
^copyis blocked by^launch, which is blocked by^copy. Neither can ever become unblocked. -
MDTO415 warning line 13
- [ ] Book the launch tweet — blocked by [[#^copy]] ^launchBlocker cycle:
^launchis blocked by^copy, which is blocked by^launch. Neither can ever become unblocked. -
MDTO412 warning line 17
- [x] Pick a tide-data source — blocked by [[#^copy]]Blocker clause on a task that is already done; it has no effect.
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. So every recovery rule takes the second failure over the first: a malformed clause recovers to a single prose blocker holding the body verbatim, an unresolvable target is kept as unresolved, and a cycle leaves every member blocked.
MDTO413 is the near miss worth studying. …, blocked by
[[#^copy]] is not a clause — a comma is not an em dash — so the
parser is right to read it as prose, and the task genuinely is not
blocked. Reporting that as an error would be the validator overruling the
grammar; saying nothing would leave the author certain of something false. A warning
that names both readings is the only honest answer.
The codes this spec owns
MDTO400–MDTO499 belongs to [email protected], in
three families: bands in 401–409, the blocker relation in
410–419, state and hierarchy in
420–429. Codes are never reused, renumbered or
repurposed. Everything else a backlog file can get wrong is a shared code — and
MDTO031, the unrecognized marker, is shared and not owned here: the
same broken marker produces the same code in every spec, and only the message names this
spec’s enum.
bands.
# title and ## bands.
[[#^slug]] blocker names no block identifier
in this file.
Done band is not in a terminal
state, so a prune would destroy it.
When a bands: declaration cannot be read at all (MDTO007) it
is discarded — and the vocabulary check MDTO401 and the order check
MDTO403 are skipped with it rather than falling back to
the reserved names. A file that mistyped one frontmatter key would otherwise report an
unknown band on every heading it has, burying the one diagnostic that matters under a
list of its consequences.
§ 05
For agents
A backlog is the page an agent reads before it decides what to do, so it is the spec where reading and writing meet most often. The value over freehand editing is determinism: guaranteed-valid output, minimal diffs, no whole-file context required — and a relation that lifts itself instead of needing to be swept.
$ mdto spec backlog [email protected] — agent-facing specification 1. Purpose what the format is for 2. Grammar heading = band, item = task, — blocked by = a relation 3. Rationale why it is shaped that way 4. Examples valid + invalid + diagnostics 5. Verbs add · start · finish · drop · block · unblock · … $ mdto validate tidepool.backlog.md ok conforms to [email protected] · 16 tasks, 5 bands, 4 blocked $ mdto backlog start "station search" --dry-run --- tidepool.backlog.md +++ tidepool.backlog.md -- [ ] Station search that tolerates misspelled harbours [priority:: medium] #ux +- [/] Station search that tolerates misspelled harbours [priority:: medium] #ux note --dry-run: nothing written $ mdto backlog block submission --on "swell height" ok 1 line rewritten · pinned ^swell-height on the target
-
The rationale is the repair manual
Purpose, grammar, rationale, examples with their exact diagnostics, and the verb reference — versioned alongside the spec.
mdto spec backlogprints it. This spec’s rationale is unusually blunt about the wrong repairs, because they are all plausible: do not delete a cross-file blocker, do not check a box because a task moved intoDone, do not implementgraduateby writing the note first and hoping. -
Fetchable, not scrapeable
The same document lives at a stable URL beside an
llms.txtso an agent never has to scrape a human documentation site to author a conforming file.fetch
markdownto.ai/specs/backlog.mdfetch
markdownto.ai/llms.txt -
Fixtures are the contract
An independent implementation conforms if and only if it agrees with
specs/backlog/fixtures/. Valid fixtures must parse with no error and round-trip byte for byte; every invalid one carries a sibling.diagnostics.jsonlisting the exact codes, severities and lines it must produce. All fifteen codes above are fixtured.universal
validaterenderspec