# The board

The board is AI Maestro's source of truth. Two files plus a specs folder, and the plan the work
is scoped against:

- **`plan.json`** — the **project plan**: goal, scope, deliverables, use cases, functional and
  non-functional requirements, milestones, risks, and gaps other skills raised against it. Every
  item carries a stable id (`D-`, `UC-`, `FR-`, `NFR-`, `M-`, `OUT-`) that a ticket's
  `traces_to` points at. A large project may also group its epics under **initiatives**
  (`I-`) — see [Initiatives](#initiatives) below. Most do not.
  See [Plan and scope](#plan-and-scope).
- **`plan.md`** — a **generated** readable mirror of `plan.json`, rewritten on every plan write.
  Never edit it: your changes are discarded on the next write.
- **`data.json`** — live work: epics and unresolved tickets, with dependency order.
- **`archive.json`** — completed tickets and their verification evidence. Done work moves
  here so `data.json` only ever shows what's live.
- **`specs/`** — one file per ticket that needs more than its `desc`: `specs/<ticket-id>.md`.
  Reference it from the ticket (`Companion spec: board/specs/<id>.md`) so it's discoverable.
- **`board.schema.json`** / **`plan.schema.json`** — the canonical shapes. Validate with
  `node ../scripts/validate-board.mjs data.json`, which checks the board *and* its scope
  against the plan.

## Ticket lifecycle

```
backlog → todo → in-progress → review → done
                       │
                       └──► blocked  (needs a prerequisite or a human)
```

- A ticket is **eligible** only when every id in `depends_on` is `done`.
- `human_gate` tickets are never auto-picked — a human clears them first.
- When a ticket lands, move it from `data.json` to `archive.json` with its `evidence`.

## Writing the board

Use `maestro ticket` — not an editor, and not read-file/edit/write-file:

```sh
maestro ticket next-id --count 12             # allocate free ids (--epics for epic ids)
maestro ticket add --name "…" --desc "…" --traces-to FR-3
maestro ticket add-epic --name "…" --traces-to D-1
maestro ticket import plan.json --replace-sample   # a whole planned board, in one write
maestro ticket set-status T-010 in-progress   # optionally --agent-plan/--current-agent/…
maestro ticket set-epic T-010 --epic e3       # move a ticket to another epic
maestro ticket retrace T-010 --traces-to FR-3 | --scope-exception "…"
maestro ticket block T-010 --name "BLOCKER: …" --desc "…"
maestro ticket archive T-010 --evidence "merged abc123: …" --done-at 2026-08-09
maestro ticket drop T-010 --reason "superseded"    # leaves unfinished, via the archive
maestro ticket unarchive T-010 [--status todo] [--with-epic]  # undo a mistaken archive/drop
maestro ticket archive-epic e3                # retire an epic with no live tickets
maestro ticket version                        # the board's content version
```

`import` is the one op that takes a whole document, and it is safe for one reason: it **only
adds**. An id already live or archived is a hard error, never an overwrite — so it cannot modify
or delete existing work. Its single removal, `--replace-sample`, touches only items a starter
marked `"sample": true`, which is why it is safe to pass even on a project full of real tickets.

`drop` is the honest delete: the ticket leaves through `archive.json` carrying *why*, under
`wont-do` / `duplicate` / `archived`. It refuses when other live tickets depend on it — because
eligibility treats every archived id as a **satisfied** dependency, so dropping a prerequisite
silently makes its dependents runnable.

`unarchive` is the way back from a mistaken `archive` or `drop` — never a hand edit of both files.
It moves exactly one ticket from `archive.json` to `data.json` in one locked, validated write,
strips the archive-only fields (`evidence` — which also holds a drop's reason — and `done_at`),
and restores it as `review` unless `--status` names another live status (`done` and the terminal
states are refused). An id that is not archived, or is already live, fails with both files
untouched. If the ticket's epic survives only in `archive.epics`, it refuses unless `--with-epic`,
which copies that epic back to the live board in the same write. `--dry-run`, `--json`,
`--expect-version` and `--expect-archive-version` behave as for `archive`.

`archive-epic` retires an obsolete epic — one with **zero live tickets** — by moving it from
`data.json` to `archive.json` in one locked, validated write (archive.json is written first). It
refuses, with both files untouched, while any live ticket still belongs to the epic, or when the
id is not a live epic. Archiving a ticket already leaves a shadow copy of its epic in
`archive.epics`; `archive-epic` replaces that copy with the live one, so the archive holds a single
entry with the epic's latest fields (its `initiativeId` included). `--dry-run`, `--json`,
`--expect-version` and `--expect-archive-version` behave as for `archive`.

And `maestro plan` — never an editor — for the plan:

```sh
maestro plan status                           # completeness %, what's thin, open gaps
maestro plan questions                        # the next unanswered questions
maestro plan add functional --text "…" --verify "…"
maestro plan scope --in "…" --out "…"         # --out mints an OUT- id the gate enforces
maestro plan gap-add --need required --from atomic-report --text "…"
maestro plan coverage                         # plan items vs the tickets working them
```

Plan writes take the **same lock** as board writes — the lock guards the directory, not one
file — so a run that reads the plan and the board together always sees a coherent pair.

Every op is **declarative** — it names the change, not the resulting board — and is applied
to the file as it exists at write time, under a lock, validated, and written atomically.

That matters because the obvious alternative silently destroys work. Read the board, change
one ticket in memory, write the whole file back, and any change another writer made in
between is gone: no error, no conflict, and the result is valid JSON that passes this
directory's own validator. It happened on this repo's board on 2026-08-08 and cost a filed
ticket, which is why the tooling no longer offers that shape.

Exit codes are the contract: **0** written, **1** the request was wrong (do not retry),
**2** contended — the board moved or the lock was busy, so re-run the same command.

The web dashboard's save button and `maestro ticket` share one version token, so a UI tab and an
agent cannot disagree about whether the board changed underneath them.

## Fields that drive execution

| Field | Drives |
| --- | --- |
| `agent_plan` | The pipeline of agents that work the ticket |
| `model` | The model tier each stage runs on |
| `area` | Area defaults (model floor, test command) |
| `depends_on` | Eligibility ordering |
| `human_gate` | Whether the orchestrator may auto-pick it |
| `traces_to` | Whether the ticket is in the plan's scope at all |
| `touches` | Whether it can run in a lane parallel to another ticket |
| `dev_runtime` / `dev_model` | Cross-review only: the runtime adapter + model id/tier that implements the ticket |
| `reviewer_runtime` / `reviewer_model` | Cross-review only: the runtime adapter + model id/tier that reviews the PR |

Cross-review is opt-in and independent of `agent_plan`/`model` — see
[docs/CROSS-REVIEW.md](../docs/CROSS-REVIEW.md) for the fields, defaults, and the
`maestro run <ticket-id>` pipeline that dispatches them.

## Plan and scope

A ticket's `traces_to` names the plan items it serves. That one field answers two questions the
board couldn't answer before: *what is this ticket for?* and *is it in scope?*

The gate is deliberately split:

| Where | Behaviour |
| --- | --- |
| `maestro validate` / the web dashboard's save | **Warns.** The board stays valid — you must be able to jot a ticket before the plan covers it. |
| The orchestrator picking a ticket | **Blocks.** Nothing runs that the plan doesn't cover. |

A ticket is out of scope when it traces to nothing, to an id the plan doesn't define, or to an
`OUT-` id. Three ways out, in order of preference:

1. Add the requirement to the plan — `/plan-update` — and trace the ticket at it.
2. Decide the ticket shouldn't exist.
3. Have a human write a `scope_exception` explaining why it runs anyway. It clears the gate and
   stays visible in every report, so an exception can't quietly become the norm.

The gate is **off** until the plan names at least one deliverable, use case, or requirement — a
project one minute past `maestro setup` isn't refused everything.

## Initiatives

Optional, and absent from most projects. When a project holds several outcomes that are each
independently worth shipping and each need multiple epics, the plan can name **initiatives**:

```
Project plan   the boundary — nothing outside it may run
└── Initiative an independently valuable outcome, delivered by several epics
    └── Epic   a demonstrable delivery outcome, made of tickets
        └── Ticket  the executable, independently verifiable unit
```

An initiative states an outcome, a boundary and metrics, and **owns** plan items — the same
`D-`/`UC-`/`FR-`/`NFR-`/`M-` ids, tagged with `initiativeId`, never copied into a nested
structure. Items left untagged are **project-wide** and apply to every initiative.

| Rule | Why |
| --- | --- |
| An epic carries `initiativeId`; a ticket does not. | A ticket derives its initiative through its epic. Recording it twice is how the two come to disagree. |
| An epic or ticket may trace only to a project-wide item or one its own initiative owns. | Otherwise "which initiative delivered this?" has two answers. Cross-initiative traces are **errors**, and a `scope_exception` does not clear one — that is a decision about project scope, not about ownership. |
| An unassigned epic **warns**, and its tickets are **blocked at pick time**. | A board gains its first initiative with every epic unassigned. Erroring would make it invalid at the one moment nothing could fix it, since the plan CLI cannot write a board. |
| `depends_on` between initiatives is planning information only. | Nothing schedules from it. Lanes still come from the epic, the area, `depends_on` and `touches`. |
| Delivery per initiative is derived, never declared. | Covered when a ticket traces to an owned item; done when a landed one does. There is no percentage anyone can type. |

Write them through `maestro plan initiative-add|initiative-edit|initiative-remove`, and assign
epics with `maestro ticket edit-epic <id> --initiative <I-n>`, and move a ticket between epics
with `maestro ticket set-epic <id> --epic <e-n>` — both refuse a result that would leave any
trace crossing initiative ownership. `initiative-remove` refuses while
anything still references the initiative, and offers no `--force`.

**A project that defines no initiative behaves exactly as it did before they existed** — same
completeness percentage, same coverage, same scope verdicts, same lane assignment.

See [`../docs/METHOD.md`](../docs/METHOD.md) for the why, and
[`board.schema.json`](./board.schema.json) / [`plan.schema.json`](./plan.schema.json) for the
full field lists.
