# backlog-workspace@0.1

**Status:** normative. **Inherits:** [`../conventions.md`](../conventions.md) — the envelope, lazy
identifiers, inline fields, tags, wikilinks, preservation rules, the shared diagnostic codes
`MDTO001`–`MDTO099`, and §13, the multi-file transaction contract this spec's verbs are the first
to satisfy.
**Refines:** [`backlog@0.1`](../backlog/SPEC.md) — which does not change. A workspace adds relations
*between* conforming files and takes nothing away from any one of them.
**Declared by:** `markdownto: backlog@0.1` — the spine's envelope. This spec has none of its own; a
workspace is declared by containment (§2.1).
**Schema:** [`schema.json`](schema.json), the resolution over N backlog IRs.
**Fixtures:** [`fixtures/`](fixtures/), which are directories (§4.1). **Owns diagnostic codes:**
`MDTO500`–`MDTO599`.

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

---

## 1. Purpose

`backlog-workspace@0.1` describes a backlog that has outgrown one file: a directory holding exactly
one `backlog@0.1` spine, the detail notes its tasks have earned, an `archive/` for what closed, and
sub-backlogs a spine task delegates to. Every file in it is a conforming file of some other spec —
the spine is a `backlog@0.1` page a 0.1-only tool still reads whole — and this spec adds only what
one file cannot say: which page is the priority authority, which note belongs to which task, and
what a task in one file is waiting on in another.

---

## 2. Grammar

The unit is a **directory**. Everything below is a rule about *which files are in it* and *what
relations hold between them*; nothing here is a rule about the inside of a line. A spine is parsed
by `backlog@0.1` exactly as it would be alone on disk, and the workspace layer only ever **refines**
values that parse already produced.

| Piece | What it is |
|---|---|
| **spine** (§2.1) | The one file directly in the directory that declares `backlog@`. The priority authority. |
| **detail note** (§2.2) | Any other `.md` in the workspace. An ordinary document; never parsed for tasks. |
| **artifact** (§2.2) | Any other file. Never read, never linked-checked, never diagnosed. |
| **sub-backlog** (§2.5) | A subdirectory with a spine of its own. A workspace, recursively. |
| **`archive/`** (§2.3) | The one reserved path: closed notes and rollup pages. Never holds a spine. |

### 2.1 The unit, and the envelope it does not have

**A workspace is a directory containing exactly one file that declares `backlog@`** (any minor).
That file is the **spine**. There is no `markdownto: backlog-workspace@0.1` — no file declares this
spec, and none should: a spine that declared it would stop being readable by every tool that
implements `backlog@0.1` and nothing would be gained, because the fact a workspace adds is not a
property of any one file. The envelope in the directory is the spine's:

```yaml
---
markdownto: backlog@0.1     # the spine, and the only envelope the workspace requires
description: What Tidepool is doing next, and what it is waiting on.
agentsfs_role: backlog      # an unknown key: preserved verbatim, never validated
title: Tidepool
---
```

- **Containment is the declaration.** Conventions §2.4 says the envelope, never the filename, is
  authoritative, and this spec keeps that rule and extends it: the *directory*, never a filename,
  says which files belong together. agentsFS names its spine `INDEX.md`; a `.backlog.md` beside two
  notes is the same workspace. Both spellings are fixtures ([`minimal/`](fixtures/valid/minimal/),
  [`spine-and-notes/`](fixtures/valid/spine-and-notes/)).
- Containment is measured on **canonical paths**, not lexical spelling. A symlink explicitly chosen
  as the workspace root names its referent intentionally; a symlink below that root MUST NOT make a
  member resolve outside the canonical root when it is observed. Parent directories are created
  one component at a time, with the canonical parent rechecked immediately before each creation;
  every file replacement rechecks its target immediately before it changes the filesystem. This is
  portable path containment, not descriptor-level isolation from a hostile local process that can
  replace a component between a final userspace check and the kernel operation.
- A transaction MUST NOT follow a member that is itself a symbolic-link leaf for `create`, `write`,
  or `delete`, even when its referent is inside the workspace: deletion would remove the referent and
  leave a dangling member. This does not change the preceding explicit-root rule; `--dir` may name a
  symlink because that root is canonicalized before any member is addressed.
- A directory with **no** file declaring `backlog@` is not a workspace: `MDTO500`.
- A directory with **two** is ambiguous, and which page ranks the work is exactly the question the
  unit exists to answer: `MDTO501`.
- The spine must be directly in the directory. A `backlog@` file one level down opens a
  **sub-backlog** (§2.5) instead — except under `archive/`, where it is `MDTO502`.

### 2.2 Membership: the nearest enclosing spine

> **Every file belongs to the nearest enclosing directory that has a spine.**

That one rule classifies a whole tree, and it is the only membership rule this spec has:

- the **spine** of a workspace is the `backlog@` file directly in its directory;
- **detail notes** are every other `.md` file the workspace owns, at any depth — including inside
  ordinary subdirectories that hold no spine, and inside `archive/`, where the reserved path gives
  them the two further roles §2.3 names (an **archived note**, or a **rollup page**);
- **artifacts** are the members that are not `.md`: screenshots, traces, a CSV of timings. They are
  members so that "which task owns this file" has an answer, and they are never opened;
- a subdirectory that has a spine is a **sub-backlog** and owns its own members, so the parent stops
  at its boundary.

**The workspace parses spines and nothing else.** A detail note is an ordinary document: usually
plain Markdown with a `description:`, sometimes another Markdown To spec's file, and either way its
body is never scanned for tasks, identifiers, fields or blockers. Every fact this spec derives about
a note comes from its **path** — where it sits, and what links to it. Two consequences worth
stating, because they are what keep the layer cheap: resolving a workspace reads exactly the spines,
and no note can ever contradict the spine about its own state.

### 2.3 The one reserved path: `archive/`

`archive/` directly inside a workspace is **reserved**. It holds what closed:

| Member | What it is |
|---|---|
| `archive/<name>.md` | An **archived note** — the detail note of a task that closed, moved here whole. |
| `archive/<year>.md` | A **rollup page**: one line per closed task, appended in date order. `2026.md`, `2027.md`. |
| any other file | An artifact that closed with its task. |

- `archive/` is **never scanned for spines**. A file in it that declares `backlog@` is `MDTO502`: its
  tasks would be invisible forever, which is worse than either reading them or refusing them.
- Archived notes are **exempt from `MDTO513`**: having no spine line pointing at them is what being
  archived means.
- A rollup page is an ordinary document. Its `- [x] …` lines look like tasks and are not parsed as
  any: a rollup is a record, not a backlog, and 0.1 does not read it back.
- The `archive` verb stamps `closed: YYYY-MM-DD` in an archived note's frontmatter (§5). It is a
  fact-date and a courtesy to the reader; **0.1 never reads it back**, and its absence is not a
  diagnostic — the path is what makes a note archived.
- Every workspace may have its own `archive/`, including a sub-backlog. Rarely useful, never wrong.

This and containment are the only two places this project has made a path mean something, and both
are stated loudly here because they are exceptions to how everything else in Markdown To works
(§3).

### 2.4 Refinement 1 — cross-file blockers

`backlog@0.1` classifies every `[[Name#^slug]]` blocker as `external` and never diagnoses it
(backlog §2.5), because a single-file parse has no right to open another file. A workspace has been
handed the other files. So it **refines** — and refines only:

| Target | Refines to | When |
|---|---|---|
| a member spine, slug found, target terminal | `satisfied` | The target task is `[x]` or `[-]`. |
| a member spine, slug found, target not terminal | `pending` | The target task is `[ ]` or `[/]`. |
| a member spine, no such slug in it | `unresolved` (`MDTO510`) | Never lifts. Fix the reference. |
| anything else | `external`, unchanged | The name is not a member of this tree. |

- The scope is the **workspace tree**: the workspace, its sub-backlogs, and theirs, recursively. A
  blocker naming a file outside it stays exactly what `backlog@0.1` made it —
  `[[handover#^dataset]]` is still `external`, still undiagnosed.
- Same-file `[[#^slug]]` blockers are untouched: they are resolved, and diagnosed, by `backlog@0.1`.
- This is a **strict addition**. A `backlog@0.1`-only tool reading the same spine sees precisely what
  it saw before, and a task the workspace calls `satisfied` is one the single-file tool calls
  `external` and therefore still blocked — the two disagree only in the safe direction (§3).
- `MDTO510` is a **warning**, where the same mistake inside one file is the error `MDTO411`. Both
  files conform on their own; a stale reference between two conforming files is a finding about the
  pair, and only `error` makes a file non-conforming (conventions §9.1). It is also the rule
  conventions §9.2 states for a code arriving after the construct it checks: `backlog@0.1` published
  the cross-file blocker as accepted-and-undiagnosed, so a code added over it now may be a warning
  and may not be an error. See
  [`fixtures/valid/blockers/`](fixtures/valid/blockers/), which carries all four resolutions.

### 2.5 Refinement 2 — delegation

A subdirectory with a spine is a **sub-backlog**, and its work is invisible until the root points at
it. A spine task **delegates** to a sub-backlog when its text contains a wikilink resolving to that
sub-backlog's spine:

```markdown
- [/] Voice v3 lanes → [[voice/INDEX]] ^voice-v3
```

- **The root spine is the sole priority authority.** Nothing in a sub-backlog is ready on its own.
  The delegating task is the sub-spine's tasks' parent, one file up: this is
  nesting-as-decomposition (backlog §2.6) extended across a file boundary.
- **Ready** (backlog §2.7) extends accordingly: a task in a sub-backlog is ready when it is ready by
  backlog §2.7 *within its own spine* **and** every delegating ancestor is non-terminal and
  unblocked.
- **Ordering:** the delegating line's position ranks the whole subtree — its band, then its position
  within that band. Inside the subtree, the sub-spine's own bands and document order apply. No band
  is ever merged across files and no order is invented between two sub-backlogs that no single line
  ranks.
- A sub-backlog no task delegates to is `MDTO511` (info): parsed, listed by a tool asked for
  everything, never offered as ready work. Parked workstreams are legitimate, so it is not a
  warning.
- A **terminal** task delegating to a sub-spine that still has non-terminal tasks is `MDTO512`
  (warning) — the cross-file `MDTO420`, and a warning for the same reason: the file says what it
  says, and validation is advice.
- The `→` is agentsFS's punctuation, not grammar. What the workspace reads is the wikilink.

### 2.6 Refinement 3 — ticket links, and how a member reference resolves

A wikilink on a spine task that names a member is a **ticket link**: this task's state lives here.

- A member reference resolves by **filename without extension**, as conventions §5.1 says, against
  every member of the workspace tree. `[[offline-cache]]` finds `offline-cache.md` wherever it sits.
- Ambiguity is resolved by path suffix, exactly as §5.1 prescribes: `[[voice/INDEX]]`. A reference
  matching **more than one** member resolves to none of them and is `MDTO515` (warning). This is not
  a corner: a tree of agentsFS spines has an `INDEX.md` at every level, so `[[INDEX]]` is ambiguous
  by construction.
- A reference matching no member is not a finding of any kind. Wikilinks are inert in the core
  (conventions §5.1) and a spine may reasonably link a file that is not in the workspace.
- A detail note **outside `archive/`** that no task on any member spine links is `MDTO513` (info):
  either it was left behind when its line was archived, or it is a note nobody can reach.
- A **non-terminal** task whose ticket link lands **inside `archive/`** is `MDTO514` (warning): the
  work is open and its note is filed closed. See [`fixtures/valid/gardening/`](fixtures/valid/gardening/).

### 2.7 What the workspace does not change

- **Identifiers stay file-scoped.** Conventions §3.1's "unique within the file" is unchanged: two
  sibling notes may both own `^lanes`, and a cross-file reference always carries the filename, which
  is what disambiguates it. There is no workspace-wide identifier namespace and no diagnostic for a
  slug reused in two files.
- **No band is merged, no state is derived, no order is invented.** Every rule above reads values
  the member parses already produced.
- **Diagnostics are per-file.** Every finding — the workspace's own and every member's — carries the
  optional `path` field conventions §9.1 already allows, naming the member it is about, and `line`
  stays 1-based **within that file**. A finding about the directory itself carries the directory as
  its `path` (`.` for the workspace root) and line 1. No new required diagnostic field, so §9 is
  untouched.
- **A workspace is only as conforming as its members.** The union of every member spine's own
  diagnostics is part of the workspace's report, unchanged except for the added `path`
  ([`fixtures/invalid/member-invalid/`](fixtures/invalid/member-invalid/)).

### 2.8 Diagnostics owned by this spec

**The unit (500–509)**

| Code | Severity | Meaning |
|---|---|---|
| `MDTO500` | error | No spine: no file directly in this directory declares `backlog@`, so it is not a workspace. |
| `MDTO501` | error | Two spines in one workspace directory; which page ranks the work is ambiguous. |
| `MDTO502` | error | A file inside `archive/` declares `backlog@`; the archive is never scanned for spines. |

**Relations (510–519)**

| Code | Severity | Meaning |
|---|---|---|
| `MDTO510` | warning | A cross-file blocker names a member spine but no such `^slug` in it; it can never lift. |
| `MDTO511` | info | A sub-backlog no task delegates to: parsed, listed, never ready. |
| `MDTO512` | warning | A terminal task delegates to a sub-backlog that still has non-terminal tasks. |
| `MDTO513` | info | A detail note outside `archive/` that no task on any member spine links. |
| `MDTO514` | warning | A non-terminal task links a note inside `archive/`. |
| `MDTO515` | warning | A member reference matches more than one member, so it resolves to none of them. |

`MDTO503`–`MDTO509` and `MDTO516`–`MDTO599` are unallocated; `520`–`529` is held for the findings a
transacting verb will need. Codes are never reused, renumbered, or repurposed (conventions §9.2).

**Diagnostic anchors.** `MDTO500` reports the directory, line 1. `MDTO501` reports the **second**
spine in sorted path order and names the first, the shape `MDTO402` uses for a duplicate band rather
than one finding per file. `MDTO502` reports the offending file's own `markdownto` line. `MDTO511`
reports the sub-backlog's spine as a whole, which is its `markdownto` line (conventions §9.1).
`MDTO513` reports the note, line 1 — a note declares no envelope, so §9.1's fallback applies.
Everything else reports the spine line the relation is written on.

### 2.9 Recovery

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

- **A workspace that does not resolve produces exactly one finding.** `MDTO500` and `MDTO501` are
  the envelope errors of this layer: with no spine, or no way to choose one, nothing else can be
  said, so nothing else is. No member is classified and no relation is refined.
- **`MDTO502` does not stop the workspace.** The root still resolves; the file in `archive/` is
  classified as an archived note and its tasks are not read.
- **A broken relation stays blocking, never ready** — backlog §2.10's rule, applied one level up. An
  `unresolved` cross-file blocker (`MDTO510`) leaves its task blocked; an ambiguous ticket link
  (`MDTO515`) resolves to no member, so the note it meant to name may also report `MDTO513`, and the
  sub-backlog it meant to delegate to may also report `MDTO511`. Both consequences are reported:
  they are how a reader finds the one edit that fixes all three.
- **A member that fails its own spec is reported, not skipped.** Its tasks stay in the resolution
  with whatever `backlog@0.1` recovered them to, so a tree-wide report is complete rather than
  truncated at the first bad file. Verbs are the strict half: they refuse to write into a workspace
  with any `error` (§5).

---

## 3. Rationale

*Read this before repairing a workspace, or before proposing that any of it move into
`backlog@0.1`.*

**Why `backlog@0.1` does not change, and could not.** Conventions §2.3 makes a minor version
strictly additive: every file valid under `backlog@0.1` stays valid under `backlog@0.2`, and older
tools keep reading. A spec whose *unit* becomes a tree is not additive — a spine whose meaning
depends on its siblings is exactly the "file that is not the complete state of a small application"
the project exists to avoid, and §1's "exactly one spec per file" refuses the mode flag that would
otherwise have to appear in the envelope. The single-file grammar is genuinely complete for any one
spine; a sub-backlog's spine is just another `backlog@0.1` file. What the directory adds is
*relations between files*, which is new meaning, and conventions §5.1 says new meaning is a spec's
to assign. So it is assigned here.

**Why the name is `backlog-workspace` and not `workspace`.** A domain-neutral `workspace@0.1` that
other specs could later join is an attractive shape and an unproven one: nothing in this spec is
written against anything but a backlog spine, and the two things a kanban directory would need —
what a column means across files, what a card's note is — are unanswered. Generality that is
guessed at is generality that gets renamed, and a rename costs a major version and every URL. So the
name says what it covers. If a second spec ever wants the same shape, the honest move is a shared
document beneath both, the way conventions §12 sits beneath three item models — not this spec
stretched over a case it was not written for.

**Why the workspace refines and never re-parses.** `parse(text)` is pure: no fs, no context, no
network (conventions §8), and that purity is what makes a browser playground, a CLI and an MCP
server run the same code. A layer that re-parsed a spine "in workspace mode" would fork that. So the
layer is a function over N `ParseResult`s that changes no value a parse produced and adds only
resolutions a parse could not have reached. The test of it: delete the workspace layer and every
file still means exactly what it meant.

**Why a cross-file `unresolved` is a warning when the same mistake inside one file is an error.**
`[[#^slug]]` is a claim about a document the validator is holding, so a claim that fails is an error
about that document. `[[voice/INDEX#^silence-trim]]` is a claim about the *pair*, and both halves of
the pair are conforming files that a tool may be reading one at a time. Making it an error would
mean a file's conformance depended on its neighbours — and then moving a file would change whether
another file validates, which is not a property this project is willing to have.

**Why the archive is a path and containment is a path, in a project that has never made a path
mean anything.** Both are deliberate, and both are stated in the grammar rather than assumed by a
tool. Containment is unavoidable: a directory model whose membership was declared in file contents
would need a manifest, and a manifest is the generated preamble conventions §0 rejects. `archive/`
is avoidable and was chosen anyway, because the alternative — marking closed work in frontmatter —
means the gardener's sweep rewrites every note it touches and a reader cannot see what is cold by
looking. One reserved name, in one position, is a smaller tax than either.

**Why the spine is not allowed to declare this spec.** It is the whole point. agentsFS's spines are
`backlog@0.1` pages today and stay conforming `backlog@0.1` pages after adopting workspaces; every
verb, renderer, board and fixture that reads one keeps working unchanged. A spec that required a new
envelope would have made adoption a migration of every file rather than the creation of a directory.

**On timing.** Adopting workspaces presumes the spine already conforms to `backlog@0.1` — in
particular that its blocker clauses sit **before** the block identifier, which is the clause order
backlog §3 explains and which files written under the older agentsFS convention have the other way
round. Until that migration lands, a workspace resolves over spines whose identifiers are literal
text (`MDTO012`) and whose references therefore do not resolve (`MDTO411`), which is a great deal of
noise for one mechanical reorder. The migration is tracked in both repositories and is not this
spec's to perform.

**Why `sweep` ships in 0.1 with `archive`.** The argument for holding it back was that batch
semantics might not fit the manifest. They fit exactly: conventions §13 is written over N files and
N hashes from its first sentence, so a sweep of eleven tasks is one manifest with more rows, not a
different contract. Holding it back would have meant shipping the gardener a loop over `archive`,
which is precisely the thing §13 exists to prevent — a sequence of transactions that can stop in the
middle with no report.

**Deliberate non-features of 0.1**, each a plausible minor addition: cross-workspace aggregation
(the owner is the ranking function across projects, not a tool); a live board over a workspace (a
board session is one text and one hash by construction, and N sessions is a design, not an
extension); reading `closed:` back; parsing rollup pages; owner/agent conventions such as agentsFS's
`— blocked by owner:` channel, which is that contract's, not this spec's; any lease, claim or WIP
semantics; a workspace-wide identifier namespace; and a `prune` verb.

---

## 4. Examples

### 4.1 The fixtures are directories

Conventions §10 fixes the fixture contract for a spec whose unit is a file. A workspace's unit is a
directory, so its fixtures are directories — and the shape is meant to be obvious on sight:

```
specs/backlog-workspace/fixtures/
  valid/<case>/                     a real workspace: a directory tree, nothing else in it
  valid/<case>.expected.json        what resolving <case>/ must produce — normative
  invalid/<case>/
  invalid/<case>.expected.json
```

- **The case directory holds only what a real workspace holds.** No metadata file, no marker, no
  README explaining itself. What a conforming implementation is handed is a path, which is what a
  person hands it too.
- **The expectations file is a sibling, not a member**, for the same reason: a file inside the
  directory would be a member of the workspace it describes.
- **One shape for valid and invalid alike.** A workspace's normative facts are not only its
  diagnostics — which file is the spine, which member each note is, how each cross-file blocker
  refined — so the sibling carries all of them, with `diagnostics` shaped exactly as conventions
  §10.2 requires plus §9.1's optional `path`. `valid/` cases are those with no `error`; the file
  format does not change between them.

```json
{
  "note": "What this case is, in one or two sentences.",
  "workspaces": [{ "path": ".", "spine": "INDEX.md", "delegatedBy": null }],
  "members": [{ "path": "INDEX.md", "role": "spine", "workspace": "." }],
  "blockers": [{ "spine": "INDEX.md", "line": 16, "text": "[[voice/INDEX#^lanes]]", "resolution": "satisfied" }],
  "links": [{ "spine": "INDEX.md", "line": 15, "text": "[[offline-cache]]", "member": "offline-cache.md", "resolution": "resolved" }],
  "diagnostics": [{ "code": "MDTO510", "severity": "warning", "path": "INDEX.md", "line": 18, "message": "…" }]
}
```

| Key | What it holds |
|---|---|
| `workspaces` | Every workspace in the tree, sorted by path, `.` first. `spine` is null when the workspace does not resolve. `delegatedBy` names the task that delegates to it, or null. |
| `members` | Every file in the tree, sorted by path, each with its `role` — `spine`, `note`, `archived-note`, `rollup`, `artifact` — and the workspace that owns it (§2.2). |
| `blockers` | Every **cross-file** blocker on a member spine and what it refined to (§2.4). Same-file blockers belong to `backlog@0.1` and are not listed. |
| `links` | Every wikilink on a spine task that is not part of a blocker clause, with the member it names (§2.6). |
| `diagnostics` | Every finding: this spec's own and every member's own, in ascending `path`, then `line`, then `code` order. |

**The contract.** An implementation conforms if, handed the case directory, it produces `workspaces`,
`members`, `blockers` and `links` equal to the file, and every listed diagnostic matching on `code`,
`severity`, `path` and `line` — with no unlisted `error`. Extra warnings and info are permitted;
`message` wording is informative, exactly as in §10.2. Paths are relative to the case directory,
POSIX-separated, and sorted byte-wise. **Every member that declares an envelope must additionally
satisfy its own spec's fixture contract** — parse with no `error` and round-trip byte for byte —
which is what makes a workspace fixture also a bundle of `backlog@0.1` fixtures.
`invalid/member-invalid/` is the one case where that is deliberately false, and its expectations say
so. A detail note declares no spec and therefore owes no such contract; it is a member because of
where it sits, and the resolver never opens it (§2.2).

**There is no `.ir.json` here.** Conventions §10.5's reference snapshot is *derived* and explicitly
never hand-written; deriving one needs an implementation, and this spec ships its normative layer
first (§5 notes what the engine still owes). The expectations file carries the normative facts in
the meantime, and it is hand-written on purpose: every number in it was read off the fixture.

### 4.2 Valid

| Case | What it shows |
|---|---|
| [`minimal/`](fixtures/valid/minimal/) | The floor: one file. A `.backlog.md` spine and nothing else — a workspace before it needed to be one. |
| [`spine-and-notes/`](fixtures/valid/spine-and-notes/) | An `INDEX.md` spine, two earned notes, one artifact. Two tasks link their notes; the others are one-liners, because notes are earned and never default. |
| [`delegation/`](fixtures/valid/delegation/) | The showpiece: three sub-backlogs in three states — live, closed-too-early (`MDTO512`), and orphaned by an ambiguous `[[INDEX]]` (`MDTO515`, and the `MDTO511` it causes). Plus an archive with a rollup. |
| [`gardening/`](fixtures/valid/gardening/) | A workspace caught mid-sweep: a note filed closed under a task that reopened (`MDTO514`), and a note left behind when its line was archived (`MDTO513`). |
| [`blockers/`](fixtures/valid/blockers/) | All four cross-file resolutions in one spine: `satisfied`, `pending`, `unresolved` (`MDTO510`), and an `external` that stays external. |

The showpiece tree, in full:

```
delegation/
  INDEX.md                      spine — backlog@0.1
  offline-cache.md              detail note
  dst-timings.csv               artifact
  voice/INDEX.md                sub-backlog spine — delegated from INDEX.md:17
  voice/lanes.md                detail note, owned by voice/
  harbours/INDEX.md             sub-backlog spine — delegated from a done task  → MDTO512
  pricing/INDEX.md              sub-backlog spine — delegated from nothing      → MDTO511
  archive/2026.md               rollup page
  archive/station-picker.md     archived note
```

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

**[`no-spine/`](fixtures/invalid/no-spine/)** — two ordinary notes, no spine.

→ `MDTO500` error, `.` line 1: *No spine: no file directly in this directory declares
`markdownto: backlog@…`, so it is not a workspace.*

**[`two-spines/`](fixtures/invalid/two-spines/)** — `INDEX.md` and `roadmap.backlog.md` both declare
the envelope.

→ `MDTO501` error, `roadmap.backlog.md` line 2: *Two spines in one workspace: `INDEX.md` already
declares `backlog@0.1`. Move one into a subdirectory of its own, or merge them.*

**[`spine-in-archive/`](fixtures/invalid/spine-in-archive/)** — a whole sub-backlog was filed into
`archive/`.

→ `MDTO502` error, `archive/voice.md` line 2: *`archive/voice.md` declares `backlog@0.1`, but
`archive/` is never scanned for spines, so its tasks would never be read.*

The root still resolves, and `archive/voice.md` is classified as an archived note: `archive/` decides
the role, envelope and all (§2.9).

**[`member-invalid/`](fixtures/invalid/member-invalid/)** — the workspace adds no finding of its own;
the sub-backlog's spine fails `backlog@0.1`.

→ `MDTO411` error, `voice/INDEX.md` line 9: *Blocker `[[#^normalizer]]` names no task in this file.*
→ `MDTO401` error, `voice/INDEX.md` line 11: *Unknown band `Icebox`.*

Both are the member's own diagnostics, reported unchanged with `path` naming the file. That naming is
the whole of what the workspace contributes here, and it is what makes a tree-wide report readable.

---

## 5. Verb reference

`backlog-workspace@0.1` owns the vocabulary **`graduate`, `archive`, `sweep`** — the three edits that
move work *between* files. **Every single-file verb stays `backlog@0.1`'s and is unchanged**: `add`,
`start`, `finish`, `drop`, `block`, `unblock`, `promote`, `demote` and `reorder` edit one spine, and
this namespace neither wraps nor replaces them. `mdto backlog-workspace` resolves *which spine* and
hands off; `mdto backlog start "tide tables"` inside a workspace is still the right command and still
the same edit.

Every verb here writes more than one file, so every verb here is a **conventions §13 multi-file
operation**: a manifest declared up front, every hash checked before any byte is written, writes in
declared order, rollback attempted and partiality reported. §13 is the contract; these three are its
first satisfiers, which is the sequencing backlog §5 asked for when it specified `graduate` and
refused to ship it.

`graduate` keeps the name `backlog@0.1` specified it under, deliberately. A shared verb name is not a
shared contract (conventions §11), and here it is the same edit shipping from the namespace whose
unit can hold the transaction: `mdto backlog graduate` stays "not implemented in this build", because
a `backlog@0.1` file alone does not know what directory it is in.

### Addressing, and which spine

`<ref>` is `backlog@0.1`'s: a pinned identifier with or without the caret (`offline-cache`,
`^offline-cache`), otherwise a case-insensitive substring of the task's normalized text. A ref
matching zero or several tasks fails with a non-zero exit and changes nothing.

**Which spine** is resolved before anything else: `--dir <path>` names the workspace directory, and
without it the workspace is the nearest ancestor of the working directory — the working directory
itself first — that contains exactly one file declaring `backlog@`. `MDTO500` and `MDTO501` are
refusals here, not findings: a verb that cannot say which page is the authority does not write.
A `<ref>` is matched against the **root spine only** unless `--sub <path>` names a sub-backlog, so a
slug that exists in two spines is never ambiguous by accident.

### Common flags

`--dir <path>` (the workspace; default: the nearest ancestor of the working directory that is one) ·
`--sub <path>` (address a sub-backlog's spine instead of the root's) · `--dry-run` (print the
manifest and every file's diff, write nothing) · `--json` (emit the `mdtoJson: 1` payload with §13's
`files` array; the singular `file`, `hash` and `edits` describe the spine) · `--expect <path>=<hash>`
(repeatable; the expected source hash of one **existing** manifest file; a `create` has none and is
refused, conventions §13.1) · `--force` (override the validation refusal, never a hash check)

**Every verb here refuses before it writes** when any member of the workspace reports an `error`,
when any manifest hash has moved (exit 23, nothing written), or when a file the manifest expects to
create already exists. On a failure part-way through the sequence it attempts rollback and reports,
per file, what was restored and what was left changed (exit 24) — §13.4, and the reason this spec
never claims atomicity it cannot have.

### The verbs

| Verb | Signature | Source edit | Manifest |
|---|---|---|---|
| `graduate` | `mdto backlog-workspace graduate <ref> --to <path> [--sub <path>]` | Creates the note at `<path>`, then rewrites the task's text so it links it. Marker, fields, blocker clause and identifier are preserved exactly. | note (create) → spine (write) |
| `archive` | `mdto backlog-workspace archive <ref>… [--year <YYYY>] [--sub <path>]` | Moves the task's note into `archive/`, stamps `closed:`, appends one line to the year's rollup page, and deletes the task's lines from the spine. | note (create) → rollup (create or write) → note (delete) → spine (write) |
| `sweep` | `mdto backlog-workspace sweep [--band <name>] [--sub <path>]` | `archive`, batched over every terminal task on the spine, as one transaction. | the union of each task's, in task order |

**`graduate <ref> --to <path>`** — the edit `backlog@0.1` specified and declined to ship (backlog §5).
The new note carries frontmatter with a `description:` derived from the task's text; the task's
children become the note's own list and are **left on the spine as well**, because moving them is a
second decision an author may not want. The task's line keeps its marker, its fields, its blocker
clause and its identifier, and its text gains the wikilink — `- [/] Voice v3 lanes → [[voice-lanes]]
^voice-v3`. `<path>` must be inside the workspace and must not exist; a `<path>` that would land in
`archive/` is refused, since graduating is the opposite of closing.

**`archive <ref>…`** — the gardener's single move. **Refuses on a non-terminal task**: archiving is
filing what closed, and there is no `--force` for that (`--force` overrides validation, never
meaning). Every requested task block must be distinct: naming one twice, or naming a parent together
with one of its descendants, refuses before a manifest is built because filing the parent already
files its whole nested block. The rollup line is `- YYYY-MM-DD — <marker> <the task's text> ^<id>`, appended to
`archive/<year>.md`, where the year is `--year` or the current one, and the page is created with a
`description:` if it does not exist. A task with no note contributes only the rollup line. A task
whose note is already inside `archive/` is a successful no-op for the move and still deletes the
spine line. On a host that exposes permission modes, the archived note keeps its source mode; a
rollback that recreates the source keeps it too (conventions §13.4). `closed:` is stamped as a
fact-date; nothing ever reads it back (§2.3).

**`sweep`** — `archive` over every terminal task on the spine, in document order, in **one**
transaction: one manifest, every hash checked before the first write, and a failure part-way through
rolled back and reported per file. `--band <name>` narrows it to one band, which is how a workspace
that keeps a `Done` band sweeps only that. `--dry-run` prints the manifest and the diffs, and is what
a gardening prompt should run first. Sweeping a spine with no terminal task is a successful no-op
with an empty diff.

### What no verb here ever does

Writes a byte before every manifest hash has been checked · claims atomicity, or reports success
after a partial write · touches a member the manifest did not declare · reformats a file it opened
only to hash · parses a detail note · edits a spine line it was not asked to edit · archives a
non-terminal task · derives a task's state from the state of the sub-backlog it delegates to ·
resolves an ambiguous `<ref>` by picking the first match · rewrites the `markdownto` version of any
member · writes into a workspace with an `error`-severity finding unless `--force` says so — and
never against a moved hash, whatever `--force` says.
