frame 0.1.7

A markdown task tracker with a terminal UI for humans and a CLI for agents
Documentation
# Markdown File Format

Frame uses plain markdown files as its data store. This document specifies the exact syntax recognized by the parser.

## Track Files

Each track is a single `.md` file with this structure:

```markdown
# Track Title

> Optional description line.

## Backlog

- [ ] `ID-001` Task title #tag
  - added: 2025-05-10
- [>] `ID-002` Active task

## Parked

- [~] `ID-010` Parked task

## Done

- [x] `ID-003` Completed task
  - resolved: 2025-05-14
```

### Sections

Three section headers are recognized (case-insensitive):

- `## Backlog` — todo, active, and blocked tasks
- `## Parked` — intentionally paused tasks
- `## Done` — completed tasks

Any content that isn't a section header or task is treated as literal passthrough text and preserved verbatim.

### Title and Description

The first `# Title` line sets the track's display title. An optional `> Description` line can follow.

## Task Syntax

A task line has this format:

```
INDENT- [STATE] `ID` Title text #tag1 #tag2
```

**Indentation**: 0 spaces for top-level, 2 for subtasks, 4 for sub-subtasks (3 nesting levels max).

**Checkbox states**:

| Char | State |
|------|-------|
| ` ` (space) | Todo |
| `>` | Active |
| `-` | Blocked |
| `x` | Done |
| `~` | Parked |

**ID** (optional): Enclosed in backticks after the checkbox. Format: `PREFIX-NNN` for top-level, `PREFIX-NNN.N` for subtasks, `PREFIX-NNN.N.N` for sub-subtasks.

The ID grammar is:

```
task_id  = prefix "-" segment ("." segment)*
segment  = token? number
token    = one or more lowercase letters   (omitted = "null" / default namespace)
number   = digits
prefix   = the track's configured prefix (e.g. EFF)
```

A segment is a maximal run of lowercase letters (the optional token) followed by
a maximal run of digits (the number); because letters and digits are disjoint no
delimiter is needed between them. The token names the
[actor-token namespace](concepts.md#actors) the segment was minted in: the
primary working copy mints tokenless (null-namespace) IDs like `EFF-014` and
`EFF-014.2`, while a clone holding token `a` mints `EFF-a14`, and a subtask added
there under someone else's task reads `EFF-014.a1`.

**A subtask's ID extends its parent's**: every segment but the last matches the
parent exactly, whatever namespace the last one carries. `EFF-014.2` and
`EFF-014.a1` are both children of `EFF-014`; `EFF-020` nested under it is not, and
neither is `EFF-014.2.1`. `fr check` reports a subtask that breaks the rule and
`fr check --fix` renumbers it back under its parent.

**Literal passthrough**: any backtick-wrapped ID that does not match this grammar
(including legacy or hand-written IDs) is preserved verbatim on round-trip and is
ignored when frame computes the next ID to mint, so it never perturbs numbering.
Zero-padding in the number is preserved (`EFF-014` stays `EFF-014`). Such an ID
carries no parent/child relationship either, so a hand-written hierarchy is never
reported as broken.

**Tags** (optional): `#word` tokens at the end of the line. Parsed right-to-left from the end; only trailing `#word` sequences are recognized as tags.

Examples:

```markdown
- [ ] Task with no ID
- [>] `EFF-014` Implement inference #cc
  - [ ] `EFF-014.1` First subtask
    - [ ] `EFF-014.1.1` Deep subtask
```

## Metadata Lines

Metadata lines are indented under their task (task indent + 2 spaces) and start with `- key: value`:

```markdown
- [>] `EFF-014` Task title
  - added: 2025-05-10
  - dep: EFF-003, INFRA-007
  - ref: doc/design.md, src/parser.rs
  - spec: doc/spec.md#section
  - note: Short note text
```

### Metadata Types

**`added: YYYY-MM-DD`** — Creation date.

**`resolved: YYYY-MM-DD`** — Completion date.

**`dep: ID1, ID2`** — Comma-separated dependency task IDs.

**`ref: path1, path2`** — Comma-separated file paths (relative to project root).

**`spec: path#section`** — Single spec file path with optional `#anchor`.

**`note: text`** — Single-line note, or multi-line block:

```markdown
- [>] `EFF-014` Task title
  - note:
    First line of note.

    Second paragraph.

    ```rust
    fn example() {}
    ```

    Text after code block.
```

Multi-line notes: continuation lines are indented under the `note:` key. Blank lines within the note are preserved.

**A note's extent is set by indentation alone.** Every line indented to the note's block indent (or deeper) is note content — including lines that look like tasks or metadata, so a `- [ ]` or `- dep:` inside a note stays note text. The first line indented *less* than that ends the note, with no exceptions: code fences (`` ``` ``) are not tracked, and an unbalanced fence therefore cannot extend a note past its own indentation. The serializer re-indents every note line to the block indent, which is what makes the rule symmetric and the round-trip safe.

Two consequences worth knowing:

- A fenced block inside a note cannot contain flush-left lines — the note ends at the first one. Indent the whole block with the rest of the note.
- An unclosed fence is preserved verbatim and parses fine, but breaks downstream markdown rendering. [`fr check`](cli.md#fr-check) warns about it.

**Content frame cannot place is kept, not read.** An indented line that is neither metadata, nor a task, nor inside a `- note:` block — most often prose that lost its indentation, or a metadata key that lost its colon — is preserved byte for byte and written back where it was found. Frame does not interpret it: a `dep:` stranded this way creates no dependency, and stranded subtask text is not a task. [`fr check`](cli.md#fr-check) reports it as a stranded line so the indentation can be fixed.

## Nesting

Tasks nest up to 3 levels deep. Each level adds 2 spaces of indentation:

```markdown
- [>] `N-001` Top level (depth 0)
  - added: 2025-05-10
  - [ ] `N-001.1` Subtask (depth 1)
    - note: Details here
    - [ ] `N-001.1.1` Sub-subtask (depth 2)
    - [ ] `N-001.1.2` Another sub-subtask
  - [>] `N-001.2` Second subtask
```

Metadata always comes before subtasks for a given task. Subtask IDs follow the pattern `PARENT.N`.

## Blank Lines

- Non-task lines (including blank lines) **terminate** task parsing — the track parser handles inter-section blank lines.
- Blank lines between the section header and first task are preserved.
- Blank lines after the last task in a section are preserved.
- Blank lines within multi-line notes are preserved.

## Inbox File

`inbox.md` uses a simpler format — list items with no checkboxes or IDs:

```markdown
# Inbox

- Parser crashes on empty blocks #bug
  Stack trace points to parser.rs line 142.

- Think about expression vs statement design #design
  #research
  If it's an expression, we get composability:
  ```
  let x = perform Ask() + 1
  ```
  But it makes the type system more complex.

- Quick idea for later #todo
```

### Inbox Item Structure

**Title line**: `- Title text #tag1 #tag2`

**Tag-only continuation lines**: Indented lines where every word starts with `#` are parsed as additional tags, not body text:

```markdown
- Some idea
  #design #research
```

**Body text**: Any other indented continuation lines (1+ spaces). Body text is stripped of 2 leading spaces when present.

**Item separation**: Blank lines between items.

## Selective Rewrite

Frame uses a selective rewrite strategy for round-trip preservation:

- Each parsed task stores its original source lines (`source_text`) and a `dirty` flag.
- `source_text` contains only the task's own lines (task line + metadata), **not** subtask lines.
- On serialization:
  - **Clean tasks** (not modified): emit `source_text` verbatim, preserving exact formatting.
  - **Dirty tasks** (modified): regenerate in canonical format.
  - Subtasks are always serialized independently regardless of parent's dirty state.

This means editing one task never reformats its parent, siblings, or unrelated tasks. If no mutations occur, parse-then-serialize produces byte-identical output.