---
name: lettuce
description: Use lettuce, the filesystem-native work tracker and coverage store, correctly from a CLI or HTTP server — discover work, claim it with leases, transition status, record runs/artifacts, grade coverage cells, and validate. For agents and humans driving the `lettuce` binary.
---

# Using lettuce

`lettuce` is a filesystem-native work tracker **and coverage/quality store**.
Canonical state (projects, tasks, comments, runs, artifacts, cells) lives as
ordinary files inside a path-jailed store root; every mutation is validated and
recorded as an event. The same binary is a CLI and an HTTP server, and the same
CLI becomes an HTTP client when `LETTUCE_SERVER_URL` is set.

This document is the embedded, living agent guide — the **model-first entry
point**. It is printed by `lettuce skill` and served at `/` and `/SKILL.md` by
`lettuce serve`. Read it once top-to-bottom and you understand the whole tool;
for exhaustive per-command detail the binary documents itself (next section).
When this prose and the binary disagree, the binary wins.

## Why lettuce (and not just Markdown + a convention)

A Markdown board in your repo is only as honest as the agent editing it: an agent
can write `done` or `hardened` with nothing behind it, and the next reader
believes it. lettuce moves the guarantee from *instruction-following* to *code*.
State transitions are validated rather than requested; leases are atomic,
expiring, and audited rather than a naming convention; and a coverage cell
**cannot** reach `hardened` — or earn hardening *depth* — without linked evidence:
the tool refuses the write. The store is still ordinary text (greppable,
diffable, exportable), but what that text *claims* is enforced by the binary, not
by the goodwill of whoever wrote it. That is the whole point — **determinism over
"please follow the convention."** If a rule in `CLAUDE.md` reliably did the job
you would not need lettuce; you reach for lettuce when *verified* must mean
**proven**, not merely **asserted**.

## Contents

1. [Mental model](#1-mental-model)
2. [Getting details — the binary is self-documenting](#2-getting-details--the-binary-is-self-documenting)
3. [Defaults — the shipped configuration](#3-defaults--the-shipped-configuration)
4. [Agent rules (non-negotiable)](#4-agent-rules-non-negotiable)
5. [Command map (every command, grouped)](#5-command-map-every-command-grouped)
6. [The core agent loop](#6-the-core-agent-loop)
7. [Reading and querying (FQL)](#7-reading-and-querying-fql)
8. [Cells: the coverage model](#8-cells-the-coverage-model)
   - [8.1. Running lettuce as an agentic development loop](#81-running-lettuce-as-an-agentic-development-loop)
9. [Graph authoring & enactment](#9-graph-authoring--enactment)
   - [9.1. Every work item is a ticket; every ticket runs a graph-run-case](#91-every-work-item-is-a-ticket-every-ticket-runs-a-graph-run-case)
10. [Output formats and exit codes](#10-output-formats-and-exit-codes)
11. [Modes](#11-modes)
12. [Invariants and gotchas](#12-invariants-and-gotchas)

## 1. Mental model

- **Files are the source of truth.** You can inspect, diff, and validate the
  store with normal tools. `lettuce validate --strict` and `lettuce doctor`
  tell you if it is healthy and how to repair it.
- **Mutations are explicit and attributed.** Always pass `--project` and
  `--author`. Never infer them from the OS, Git config, or store contents.
  Every mutation is recorded as an event (`task audit`, `query timeline`).
- **Concurrency is coordinated by leases and revisions.** Claim a task with a
  lease before working it; pass `--expect-revision N` when retrying or racing
  other agents. Writes are serialized by a single-writer generation lock.
- **Output is a stable envelope.** With `--format json|yaml` every result is
  `{ ok, data, warnings, meta }`; errors are `{ ok:false, error:{ code,
  message, diagnostics:[...] } }` with machine-readable diagnostic codes.
- **`meta.command` names the command you INVOKED, not the internal one it routed to
(LET-381).** `milestone close v1` reports `milestone close`, even though it delegates to
the registry updater — so an agent can match a reply to the request it sent. Two edges
this does *not* cover, both deliberate: on a **replay** the recorded bytes of the first
call are re-delivered, so `meta.command` names the route that made the **original**
call, not the replaying one; and the **idempotency record's own `Command` is a different
field that did not change** — both routes stay one operation so they keep deduplicating
against each other. Do not read the envelope's `command` as the idempotency key's.
- **`data` NESTS ONE LEVEL DEEPER ON MUTATIONS — read this before writing a parser.**
  The envelope is stable; the shape of `data` inside it is not the same for both
  command classes, and that catches every automated consumer once:

  | class | shape | example |
  |---|---|---|
  | **read** (`show`, `list`, `audit`, `query …`) | payload is `data` itself | `.data.tasks`, `.data.present` |
  | **mutation** (`create`, `set`, `add`, `transition`, `acquire`, …) | payload is `data.data`, sharing the level with the operation attribution | `.data.data.id`, alongside `.data.operation_id` |

  So reaching for the READ path on a mutation — one `data` short of the payload —
  yields **null**, not an error, and every later command that consumes it then runs
  against an empty value. (That wrong path is deliberately not spelled out here as a
  copyable token: SKILL.md is what agents copy from, and a literal example of the
  broken form gets pasted no matter what the surrounding prose says.) Censused
  empirically:
  6/6 mutations nest, 7/7 reads are flat, and the split follows the emitter, so it
  holds for commands not listed here.

  **Unwrapping both with one expression:** `.data.data // .data` (jq) works *today*
  because no read payload has its own `data` key — but treat that as an observation,
  not an invariant, but it is now guarded by a **census enumerated from the usage
  catalog** (LET-1217), not a hand list. `TestLET1217CensusPartitionsTheCatalog`
  requires every command the catalog classifies `read` or `mutation` to be either
  driven by the census or excluded — and **excluded only because its own usage line
  requires a positional argument**, never because it was awkward. A newly added
  command satisfies neither and fails BY NAME until someone classifies it; that
  tripwire, not the coverage number, is what a hand list structurally could not do.
  The **population is complete and pinned; coverage within it is partial and named**:
  of 164 classified commands it drives **41** (the other 123 need an argument
  fixture), and of those 41 it asserts payload shape on **29** — the remaining **12**
  return an error envelope on a bare store and are pinned by name in
  `let1217NotShapeChecked`, so that set cannot grow silently either. Do not read it
  as "the catalog is checked".

  The nesting is deliberate — `operation_id`, `event`, `kind`, `author`, `at` and the
  `revision_before`/`revision_after` pair are audit evidence a mutation must return and
  a read has none of. Flattening it would mean dropping that evidence or inventing it
  for reads; whether the operation fields should instead move to `meta` is open
  (LET-923), and would be a schema-version change, not a silent fix.
- **Two data planes:**

| Plane | Objects | Question it answers |
|---|---|---|
| Work | projects, tasks, workflow, leases, runs, artifacts, comments | what is being done, by whom, with what evidence |
| Coverage | packs, dimensions, members, cells, gates, boards | how *proven* each part of the product is |

## 2. Getting details — the binary is self-documenting

This guide teaches the model; the **full flag-by-flag reference lives in the
binary**. For any command, get the exhaustive, always-current contract with:

- `lettuce usage` — the complete CLI documentation (all commands, all flags).
- `lettuce usage <command-prefix>` — filtered, e.g. `lettuce usage query saved`.
- `lettuce <command> --help` — quick per-command help.
- `lettuce usage <cmd> --format json` — machine metadata per command: `kind`
  (read | mutation | maintenance | local | server), `requires_root`,
  `requires_project`, `requires_author`, `stable_snapshot`, `idempotency_key`,
  `external_input_keys`, flags, examples. **Trust this over any prose.**
- `lettuce dimension list --project <p>` — the project's effective coverage
  dimensions; `dimension show <slug> --project <p>` reads one axis in full.
- `lettuce doctor --format json` — when the store is unhealthy: diagnoses plus
  repair suggestions.

**Agent Playbook (do-this-now guides).** For step-by-step walkthroughs beyond
this reference, the embedded wiki carries a three-part playbook — read them with
`lettuce docs show <slug>` (or browse `lettuce docs`):

- `guide-first-contact` — a fresh agent's exact first moves (skill/docs/usage/status/board).
- `guide-project-setup-playbook` — prepare a project: scopes, dimensions (+ methodology), an evaluable DoD.
- `guide-agentic-loop-demo` — one dev loop end to end, showing what you GET and DO at each step.

## 3. Defaults — the shipped configuration

Precedence everywhere: **explicit flag → environment variable → `--config`
JSON file → built-in default**.

| Flag | Env var | Default |
|---|---|---|
| `--root PATH` | `LETTUCE_ROOT` | the nearest `.lettuce` directory found by walking up from the working directory (dedicated-git mode: current directory). **No home fallback — if no store is named or discoverable the command refuses.** |
| `--project NAME` | `LETTUCE_PROJECT` | none — explicit where required |
| `--author NAME` | `LETTUCE_AUTHOR` | none — **required for every mutation, never inferred** |
| `--mode` | `LETTUCE_MODE` | `filesystem` (other: `dedicated-git`) |
| `--format` | — | `table`. Others: `plain`, `json`, `yaml`; `markdown` only for `doctor`, `usage`, `skill` |
| `--server-url URL` | `LETTUCE_SERVER_URL` (`_FILE`) | none; setting it switches the CLI to HTTP-client mode (fallback URL `http://127.0.0.1:8727`) |
| `--bearer` / actor | `LETTUCE_BEARER` (`_FILE`), `LETTUCE_ACTOR` (`_FILE`) | none (client-mode auth + `X-Lettuce-Actor` identity) |
| organization admission | `LETTUCE_ORGANIZATION` (`_FILE`), `LETTUCE_ORGANIZATION_TOKEN` (`_FILE`) | none; optional pair used together for organization-gated services |
| `serve --listen` | `LETTUCE_LISTEN` | `127.0.0.1:8727` |
| `--config PATH` | `LETTUCE_CONFIG` | none |

Other `serve` flags (secret, IP allowlist, rate limit, authz policy,
auto-provision actors, push interval) map to `LETTUCE_API_SECRET`,
`LETTUCE_ALLOWED_IPS`, `LETTUCE_RATE_LIMIT`, `LETTUCE_AUTHZ_POLICY_FILE`,
`LETTUCE_AUTO_PROVISION_ACTORS`, `LETTUCE_PUSH_INTERVAL` — defaults: see
`lettuce usage serve`. GitHub bootstrap: `LETTUCE_GITHUB_PAT` (`_FILE`),
`LETTUCE_GITHUB_API_URL`.

**Content-Type on POST — mutations enforce JSON, three read routes do not.** A
mutation POST with a non-JSON `Content-Type` is refused `415`
`FW-API-UNSUPPORTED-MEDIA-TYPE`. `v1/query/run`, `v1/query/tasks` and
`v1/repair/check` are POSTs that only READ, and are exempt: they accept any
`Content-Type` and decode the body as JSON, so a genuinely wrong body fails at
decode rather than with a clean `415` — a worse error message, not a weaker
gate, since the decoder accepts JSON only.

The exemption is a consequence of read semantics, not laxity. Those three
routes are classified non-mutating so they take a stable snapshot, skip the
`X-Lettuce-Actor` impersonation check, and skip idempotency-header handling.

Role is decided separately, and earlier: `v1/query/run` and `v1/query/tasks`
need only reader, but `v1/repair/check` is a **maintenance** route and needs
**maintainer** — `authorizationMaintenanceRoute` matches `POST v1/repair/{check,
apply,automatic}` and returns before the mutation predicate is consulted at all.
A route that is content-type-exempt *and* gated harder on role is the clearest
evidence that the two are decided by different predicates. **If a uniform
Content-Type policy is ever wanted, it needs its own predicate over "this request
carries a body the server decodes" — widening the mutation predicate to reach the
content-type gate would silently change all of those behaviours instead.**

**Internal `.runtime/` paths appear in diagnostic `path` fields.** A caller with
any authenticated role can see them: with a write lock present, `GET v1/doctor`
returns `"path":".runtime/locks/write.lock"` alongside
`FW-RUNTIME-WRITER-ACTIVE`; on a healthy store no such path appears. This is
intended, not a leak — the paths are relative, operator-oriented, and carry no
secrets, and a diagnostic that named no path would tell the operator to repair
something without saying what. Documented so that the exposure is a stated
property rather than a surprise to anyone auditing the surface.

**What every new project ships with (the `default` workflow and registries):**

- **Workflow `default`** — states `open` (initial) → `ready` → `active` →
  `review` → `done`, plus `blocked`, `needs-human`, and terminals
  `done`/`failed`/`canceled` (hard sinks — only `task reopen` leaves them).
  Happy path: `mark-ready` → `start-work` (**requires an active lease**) →
  `complete` (or `submit-review` → `approve`). Detours:
  `block`/`unblock`, `request-human`/`human-resolved`, `request-changes`,
  `cancel`, `fail` — every non-happy-path action requires `--reason`.
  Terminals are hard sinks; leave them only via `task reopen`. Full edge
  list: `lettuce workflow show default`.
- **Severities**: `low`(rank 10), `medium`(20), `high`(30), `critical`(40).
- **Task types**: epic, story, task, subtask, feature, bug, research, design,
  ops, doc, incident, chore. Priority is an integer `0..100`.
- **Artifact types**: patch, test-report, screenshot, design, benchmark,
  migration, api-spec, log-excerpt, review-report, deployment-receipt, other.
- **Coverage pack**: `coverage` (a project with no pack pointer is on
  `coverage`); an untouched cell coordinate reads as the pack default state
  (`untested`) with `stored=false`.
- **Lease token**: generated when omitted on `acquire`
  (form `lease-YYYYMMDD-HHMMSS-<4..32 alnum>`).
- **Archived TASKS are hidden** from every list/query surface by default;
  `--include-archived` opts in. An archived **project** is a different contract:
  it hides from `project list` and refuses new task create, but `query tasks`
  still returns its tasks and `--include-archived` does not change that — the
  rows were never withheld. Scope a read with `--project`; do not infer scope
  from a project's archived state.

## 4. Agent rules (non-negotiable)

- Prefer `--format json` for automation.
- Pass `--project` and `--author` explicitly on every mutation.
- Use fully qualified references like `project/TASK-1` when context is ambiguous.
- Use `--expect-revision` on event-bearing mutations when retrying/coordinating.
- Use `--idempotency-key` only on commands that advertise `idempotency_key=true`
  (see `lettuce usage <cmd>`).
- Run `validate --strict` and `doctor --format json` after repair, import, or
  conflict work.

## 5. Command map (every command, grouped)

`M` = mutation/maintenance (mutations need `--author`), `R` = read, `L` = local
(no store needed). Flags and examples: `lettuce usage <command>`.

**Core & runtime** — `usage`(L) print CLI docs · `skill`(L) print this guide ·
`okf`(L) emit the OKF command-reference bundle (`usage --format okf --out DIR`) ·
`version`(L) · `init`(M) create a store (`--bootstrap-project`,
`--bootstrap-author`, `--idempotent`; add `--yes` when `--bootstrap-project`
would add a SECOND project to a store that already holds one) · `status`(R)
store/runtime/Git health ·
`validate`(R) (`--scope store|project|task|runtime`, `--strict|--loose`) ·
`doctor`(R) diagnosis + repair guidance · `recover`(M) heal interrupted
operations / a stale writer lock (`--abandon` to force-clear an unprovable
owner) · `cleanup`(M) drop released runtime data.

**Authors & projects** — `author add|list` · `project create|list|show`
(**one store = one project.** `project create` refuses an ADDITIONAL project with
`FW-PROJECT-ADDITIONAL-UNCONFIRMED`, naming what is already there, unless you
pass `--yes`/`--force` — over HTTP, `"confirm": true`. The board, cell coverage,
run-cases and most queries are project-scoped, so a store split across projects
has no single view able to relate its tickets, cells and run-cases. It is a
confirmation, not a ban) ·
`project set-name` (display name) · `project rename OLD NEW` (change the slug:
moves `projects/OLD`→`projects/NEW` and rewrites every stored OLD-qualified
reference so the store stays strict-valid; whole-store, atomic; preserves the
display name and all data; refuses with `FW-PROJECT-REF-REWRITE-UNSAFE` if a ref
to OLD sits inside a content-addressed identity) ·
`project merge SRC DST` (consolidate: folds SRC
into DST and removes SRC — every task/cell/artifact/comment/run/saved-query/
graph object moves in, shared registry vocabulary is reconciled, every stored
`SRC/…` reference is rewritten; whole-store, atomic, nothing overwritten or
dropped — it REFUSES with `FW-PROJECT-MERGE-CONFLICT` if an id/coordinate exists
in both projects, a shared registry slug differs semantically, or a ref to SRC
is folded into a CONTENT-ADDRESSED identity) ·
`project archive|unarchive` (soft, reversible;
archived projects hide from `project list` and refuse new tasks) ·
`project delete` (irreversible; `--yes`, `--cascade` for non-empty) ·
`project author add|list` (project membership).

**Registries & workflow** — `registry create|update|list|show KIND SLUG` with
KIND ∈ label, component, milestone, workflow, task-type, artifact-type,
severity, custom-field · `milestone create|list|show|set-stage|close|reopen` (sugar over the
milestone registry; `show` includes task-completion progress; `set-stage`
progresses the first-class hypothesis-ladder stage) ·
`workflow list|show|revise`, `workflow transition list`.

**The workflow is versioned, and a judgement pins the version it was made under
(LET-727).** A workflow is the bar a task is judged against, so it is recorded
append-only under `workflows/<slug>/spec/<n>/` and a transition into a TERMINAL
state stamps `workflow-version` + `workflow-effective-hash` onto its event. This
does not stop anyone lowering a gate — a store is a directory tree — it stops
them lowering it *retroactively*: the old version is still on disk and the closed
task still names it. `workflow show` reports which recorded version the live
policy matches; `doctor` raises `FW-WF-POLICY-UNVERSIONED` when it matches none,
which is what an out-of-band edit looks like. `validate --strict` deliberately
stays quiet — a hand-edited workflow is still well-formed, and this is a semantic
finding, not a grammar one. A workflow with no recorded version (any store
predating LET-727) is reported as unversioned, never as a violation.

**`workflow revise` is how you CHANGE that policy — including on a store that
already has tasks (LET-904).** `registry create workflow` cannot: `--idempotent`
refuses a changed definition (`FW-CMD-IDEMPOTENCY-CONFLICT` — idempotent means
no-op, not overwrite) and a task cannot be moved between workflows, so before
this a gated workflow could be authored but never applied.

```bash
# Dry run FIRST — without --yes it refuses and lists every task the new gates
# would block, and writes NOTHING.
lettuce workflow revise default --workflow-file ./gated.json \
  --root .lettuce --project lettuce --author agent-1 --format json
# Then commit to it.
lettuce workflow revise default --workflow-file ./gated.json --yes \
  --root .lettuce --project lettuce --author agent-1 --format json
```

`--workflow-file PATH` / `--workflow-json JSON` carry the COMPLETE new
definition (same schema as `registry create`); there is no default, because
defaulting would silently reset a customised policy to stock. The definition
file must live OUTSIDE the store root. It appends `spec/{N+1}`, re-pins
`effective-hash`, and updates the live `states/`+`transitions/` subtree; prior
versions are never touched. An **identical definition is a no-op** (`revised:
false`, no version minted) so re-running a deploy script cannot inflate the
history. An unsound definition is refused with nothing written. Local +
dedicated-git only; it refuses against `--server-url`. Note `requires_lease` /
`requires_reason` are never reported as breakage — the caller satisfies those at
transition time, so they strand nobody.

**Tasks** — `task create|show|list|exists` · `task set|unset` (scalar fields:
title, priority, severity, type, assignee, reporter, component, milestone,
estimate, due-at, parent, workflow) · `task set-list` (labels, depends-on, blocks, watchers) ·
`task set-where` (bulk set via FQL `--where`; `--confirm` for >1 match,
`--dry-run` previews) · `task transition REF ACTION` · `task clone` ·
`task reopen` (terminal → initial) · `task archive|unarchive` (soft) ·
`task delete` (hard; `--yes`, `--cascade` for children) · `task body add`
(bodies are versioned, never edited in place) · `task audit` (event history) ·
`task graph` (`--relation depends-on|blocks|parent|children`, `--depth`) ·
`custom set|clear` (custom-field values; the field must exist in the registry).

**Task-local objects** — `comment add|edit|status|list|show|archive|unarchive|delete` ·
`lease acquire|renew|release|steal|show` (task-scoped) and `lease list`
(project-scoped; `--holder`, `--status active|expired`) · `run start|finish`,
`run log add|list|show`, `run summary add|list|show`, `run list|show` ·
`artifact add|replace|list|show|archive|unarchive|delete`, `artifact file get`
(payloads are immutable after creation; `replace` makes a new revision) ·
`version add|list|show` (named, versioned documents under a task, grouped by
CATEGORY/NAME).

**Queries** — `query run FQL` · `query tasks` (structured filters; `--fields`,
`--refs-only`) · `query search TEXT` (full-text;
`--scope tasks|comments|runs|artifacts|versions|registry|all`) · `query audit REF` ·
`query timeline` (merged events; `--task --author --kind --since --until`) ·
`query graph` (dependency DAG: cycles + critical path) ·
`query saved create|update|archive|list|show|run` (stored FQL).

**Import/export/repair/sync** — `export --bundle PATH` (faithful whole-store
backup) · `import --bundle PATH` · `repair plan|check|apply|automatic`
(plan-driven; `--dry-run`; `--allow-high-intrusion` gates risky actions) ·
`sync status|push|pull` (dedicated-git mode) · `conflict bundle`.

**Server & GitHub** — `serve` (HTTP API; `--listen`, `--secret`, `--allow-ip`,
`--rate-limit`, `--authz-policy`, `--auto-provision-actors`, `--push-interval`;
`GET /` and `GET /SKILL.md` serve this guide unauthenticated, API under
`/v1/…`) · `github init-repo` (bootstrap a GitHub-backed store repo).

The four public document routes — `/`, `/SKILL.md`, `/docs`, `/docs/flat.md` —
carry an `ETag`. **If you poll them, send `If-None-Match` and you get `304` with
an empty body instead of the full artifact** (`/docs` is ~601 KB, this guide
~112 KB). The tag is derived from the content, so it is stable across restarts
and changes only when the binary's embedded copy does. These routes accept
`GET`, `HEAD` and `OPTIONS`; `OPTIONS` answers `200` with an `Allow` header, and
any other method answers `405` with the same header.

**Cells & coverage** — `dimension list|show`, `dimension member list`
(effective dimensions = the shipped coverage convention ⊕ project layer) ·
`dimension declare`, `dimension member add|update` (project runtime layer,
additive-only) · `dimension rename OLD NEW` (change a runtime dimension's slug:
moves the definition + its own event ledger AND re-addresses every cell whose
coordinate names it — a coordinate IS the cell's identity, so each affected cell
directory moves to its new coordinate hash carrying state/note/evidence/ledger
across; whole-store, atomic; refuses `FW-DIMENSION-RENAME-UNSAFE`/`-CONFLICT`
rather than shipping a partial rename) · `defaults show` (read the effective LADDER — states/transitions/
gates each source-tagged `default|project`, plus hidden states) ·
`defaults state declare|set-default|hide`, `defaults transition declare`,
`defaults gate declare`, `defaults reset` (tune the convention per project —
additive-only; `hide` subtracts a base state, `reset` clears the tweak layer) ·
`cell set|show|clear|list|transition`, `cell set-where|clear-where` (bulk via FQL
`--where`), `cell note`, `cell import` ·
`cell evidence add|remove|list` · `cell gate check` · `cell rollup --by DIM` ·
`cell verify|affirm` (FRESH-2 confirmation ledger — re-confirm a cell's current
grade; distinct-rev hardening DEPTH, verify=independent re-check vs affirm=restate;
never moves the ratio. The STRONG depth `depth_verify` — the one a DoD depth floor
keys off — additionally requires a DISTINCT `--evidence` ref PER CELL: re-citing the
same ref on the same cell records as affirm-tier even at a new store revision, so one
proof cited N times earns 1, not N. The de-duplication is per-cell, so one ref shared
across DIFFERENT cells — the `--coords-file` pattern — still deepens each of them. The
call returns `ok:true` either way; read `depth_verify` back rather than infer it) · `cell reconcile` (regress stale-green cells) ·
`board export` (BoardExport
JSON data contract) · `board next` (the ranked next-actions feed) · `board render` (self-contained HTML board with a
CSS-only light/dark switch, `--theme`; rendered natively from the store — no
external generator; pure-CSS except the grc replay simulator's one inline
script, present only when the project has graph-run-cases) · `grid scope add-unit|add-dim|show`, `grid show`
(Coverage-Grid: the scope × unit × dim grid) · `dod set|show|clear`
(Definition-of-Done policy — the grade/depth/recency floors).

**Graph authoring & enactment** (see §9) — `graph-def create|revise|show|list|lint|viz|compile`
(author a named, versioned process graph as a first-class object; `create` folds
in the soundness gate — an unsound spec is refused `FW-GRAPH-DEF-UNSOUND` and
never stored; `lint` reports advisory design smells at exit 0; `viz` renders it as
a mermaid/DOT diagram to eyeball; `compile`
canonicalizes to an order-invariant effective-hash and resolves the `uses`
composition closure — a cycle is `FW-GRAPH-DEF-CYCLE`, a missing used def
`FW-PATH-NOT-FOUND`) · `graph-def catalog list|show` + `graph-def use` (browse the
shipped named-pattern catalog and materialize a pattern by name into a project —
usage by name; the materialized def compiles to the catalog's effective-hash) ·
`graph-run-case open|advance|close|abandon|show|list|viz|refs-to` (open a run-case
enacting a graph-def, walk it across guarded edges, close it — event-sourced,
`state == f(events)`; `abandon` ends a run that cannot be completed, recording why;
`list` enumerates them (`--open-only`, `--state`); `viz` draws the run with its live
state overlaid; `refs-to`
inverts the effect edge — from an object, list the run-cases that touched it) ·
`graph-run-case conform` (CONFORMANCE REPLAY: does the walk match the graph-def it
PINNED at open? The runtime is graph-def-free — `advance` reads no spec — so this is
the read-time counterpart that makes the pinned hash a binding rather than a label.
Reports `FW-GRAPH-CONFORM-UNDECLARED-EDGE`, `-QUORUM-BELOW-DECLARED`, `-UNKNOWN-NODE`,
`-DEF-MISSING`, `-PIN-UNRESOLVABLE`; a REPORT not a refusal, but **exit 1** when the
walk does not conform, so a script can gate on it without parsing JSON. Note it replays
RECORDED TRANSITIONS: a branch OPENING records no traversal and is not checked, but a
`branch-advanced` event does record one, and LET-1224 extended the undeclared-edge and
unknown-node checks to reach those — so a fork/join graph is judged on its BRANCHES as
well as its main line) ·
`carrier produce|list|show` (the write-once, content-addressed
evidence an edge emits, read back by `advance`'s `--produces` post-check; `list`/`show`
read them back — a declared edge `carrier` is NOT auto-enforced, the caller must pass
`--produces KEY`).

## 6. The core agent loop

```bash
ROOT=.lettuce            # name the store explicitly; there is no home-directory default
P=myproject; A=my-agent

# 1. One-time: create/seed a store (idempotent). ONE store holds ONE project:
#    a second one must be confirmed with --yes (see §5, project create).
lettuce init --root "$ROOT" --author "$A" \
  --bootstrap-project "$P" --bootstrap-author --idempotent --format json

# 2. Discover work — at two levels.
#    (a) Concrete items: open tasks.
lettuce task list --project "$P" --root "$ROOT" --format json
lettuce query run "from tasks select task,title,status" --root "$ROOT" --format json
#    (b) Higher-order: the coverage board read FORWARD is a work MAP (see §8). The
#        rollup/board surface WHERE effort is owed — untested/gap cells, weak
#        dimensions — at aggregate scale, before any single task exists.
lettuce board next --project "$P" --root "$ROOT" --format json      # frontier: weakest cell first (works on an empty board)
lettuce board render --project "$P" --root "$ROOT" --format html   # the human view

# 3. Create work (if needed).
lettuce task create TASK-1 --root "$ROOT" --project "$P" --author "$A" \
  --title "Short title" --body "What to do." --format json

# 4. Claim it: acquire a lease, then start work.
lettuce lease acquire "$P/TASK-1" --expires-at 2099-12-31T00:00:00Z \
  --root "$ROOT" --project "$P" --author "$A" --format json
lettuce task transition "$P/TASK-1" start-work \
  --root "$ROOT" --project "$P" --author "$A" --format json

# 5. Record progress as you work.
lettuce comment add "$P/TASK-1" --body "Investigated X." --root "$ROOT" --project "$P" --author "$A" --format json
lettuce run start "$P/TASK-1" --root "$ROOT" --project "$P" --author "$A" --format json
lettuce run log add "$P/TASK-1" 1 --type progress --message "Tests running." \
  --root "$ROOT" --project "$P" --author "$A" --format json
lettuce artifact add "$P/TASK-1" --type test-report --primary-file report.md \
  --file ./report.md:report.md --root "$ROOT" --project "$P" --author "$A" --format json
lettuce run finish "$P/TASK-1" 1 succeeded --root "$ROOT" --project "$P" --author "$A" --format json

# 6. Finish: complete the transition, then release the lease.
lettuce task transition "$P/TASK-1" complete --reason "Done + verified" \
  --root "$ROOT" --project "$P" --author "$A" --format json
lettuce lease release "$P/TASK-1" --root "$ROOT" --project "$P" --author "$A" --format json
```

### Transitions need an active lease

`start-work` (and other lease-gated actions) fail with
`FW-WF-REQUIREMENT-UNSATISFIED` unless you hold the lease. Acquire first.
Transition syntax is positional: `lettuce task transition REF ACTION [--reason TEXT] [--expect-revision N]`.

## 7. Reading and querying (FQL)

```bash
lettuce task show "$P/TASK-1" --with-body --root "$ROOT" --format json   # or --full
lettuce task audit "$P/TASK-1" --root "$ROOT" --format json              # event history
lettuce query run "from tasks where status = active select task,title" --root "$ROOT" --format json
lettuce validate --root "$ROOT" --strict --format json
lettuce doctor --root "$ROOT" --format json   # diagnoses + repair guidance
```

**FQL** shape: `from SOURCE [where EXPR] [select f1,f2] [order by f [desc]]
[limit N] [offset N]`. Sources: `tasks`, `events`, `registry`, `authors`,
`projects`, `dimensions` (active pack's declared dimensions), `cells` (stored
cell assertions; sparse defaults are NOT rows). Strings use **double quotes**
(single quotes are rejected); `contains` matches case-insensitively unless
`--case-sensitive`. `--group-by FIELD` (tasks source only) buckets by
assignee, component, milestone, severity, status, type, or workflow.
Soft-archived tasks are hidden from every query/list surface by default;
pass `--include-archived`.

**Pagination is a TWO-TIER contract, and the tiers are intentional (LET-380).**
FQL's `limit`/`offset` above are clauses of the query language. The CLI flags
`--limit` / `--offset` are a separate thing, and exactly **four** commands accept
them:

| tier | commands |
|---|---|
| **paginated** | `task list`, `lease list`, `query search`, `query tasks` |
| **unpaginated** | every other `… list` surface — 23 of them |

An unpaginated surface REFUSES the flags with `FW-CMD-UNKNOWN-FLAG`; it does not
accept-and-ignore them. That refusal is the design: these enumerate bounded
registries (authors, projects, dimensions, workflows, a task's own comments and
versions), where the whole set is the useful answer and a partial page would
invite a reader to mistake it for one. The four paginated surfaces are the ones
that range over an unbounded population.

So do NOT expect an `offset` field in an unpaginated envelope — advertising a
flag the command refuses is the LET-39 no-op-flag defect wearing a response
field, and it is why this is documented rather than "fixed". If you need a subset
from an unpaginated surface, filter it or query it; do not page it.

**Querying coverage with `cells`** — the `cells` source lets you interrogate
the coverage plane the same way you query tasks. Stored fields: `coordinate`,
`state`, `hash` (plus `cell`/`project`/`pack` identity). It ALSO carries the
DOD-4 derived axes: `freshness` (`fresh|aging|stale`), `depth` (distinct-rev
confirmation count, an int), `dod` (`blocking|met`), and `dod-reason`
(`grade|depth|recency|unknown` for a blocking cell). These make "what is not
yet done?" a query:

```bash
# every cell still blocking the Definition of Done (grade/depth/recency too weak)
lettuce query run "from cells where dod = blocking select coordinate,state" --project "$P" --root "$ROOT" --format json
# cells whose evidence has gone stale (freshness = fresh|aging|stale)
lettuce query run "from cells where freshness = stale select coordinate" --project "$P" --root "$ROOT" --format json
# cells confirmed at depth >= 2 distinct revisions
lettuce query run "from cells where depth >= 2 select coordinate" --project "$P" --root "$ROOT" --format json
# cells blocked specifically because their grade is below the DoD floor
lettuce query run "from cells where dod-reason = grade select coordinate" --project "$P" --root "$ROOT" --format json
```

FQL has no dedicated `scope`/`unit`/`dimension` *filter operator* today — slice by a
coordinate member instead, e.g. `where coordinate contains "layer=api"` or
`where coordinate contains "scope=login"`. (These ARE modeled dimensions: `scope` in
particular is the reserved partition the DoD/board evaluate by — see §8/§11. A cell
that declares no `scope=` is bucketed under the synthetic scope **`unscoped`** at
evaluation time, so it is never silently outside the DoD frame; you cannot assert
`scope=unscoped` on a cell — the name is reserved for that bucket.)

## 8. Cells: the coverage model

The coverage plane grades *how proven* each part of a product is — and, read
the other way, *what work remains*. Vocabulary:

- **Convention** — coverage, the convention-as-data lettuce ships: states,
  transitions, gates, dimensions, families. Bundled in the binary and active on
  every project by default; you inspect it (`defaults show` for the effective
  ladder — states/transitions/gates each source-tagged, plus hidden states;
  `dimension list` for the axes, `dod show`, `grid show`, `board render`) and
  tune it per project with the `defaults` layer rather than swapping it.
- **Dimension** — one quality axis (e.g. `test-coverage`,
  `input-validation`). Grouped into **families**; each has an
  **applicability** (`universal` | `conditional`) and is **closed** (fixed
  member enumeration) or **open** (members minted ad-hoc in coordinates). A
  project's *effective* dimensions = the pack's ⊕ an additive project runtime
  layer (`dimension declare`; never shadows pack vocabulary). Each dimension
  also carries a **`methodology`** — how to *apply* the axis to a unit
  (the 6-facet form: Procedure · Best practices · Tools · Current approaches ·
  Evaluation · Hardened-evidence bar), distinct from its `description`
  (what the axis *is*). **Read a dimension's methodology before grading any of
  its cells** — `dimension show <slug> --project <p>` (or `dimension list
  --project <p> --format json` for the whole set) surfaces it, and it is what
  tells you what a *smoke* vs a *hardened* grade actually requires.
  A runtime dimension's slug is renameable with `dimension rename OLD NEW
  --project <p>`: it preserves the family/closed-ness/members/ledger and rewrites
  the `OLD=` key in every stored cell coordinate (which re-keys those cells' hash
  directories). It refuses — writing nothing — when a reference cannot be
  rewritten faithfully (a content-addressed run-case/carrier ref, a stored saved
  query naming the dimension, or a declared grid/DoD keyed off a reserved axis).
- **Member** — one enumerated value of a dimension, first-class
  `{slug, name, description, rank}` (empty name renders as the slug; rank 0 =
  unranked, sorts by slug). `dimension member add|update` edit only
  project-declared dimensions.
- **Cell** — one sparse, evidence-asserted point: a canonical **coordinate**
  (`dim=member;dim=member` — pairs sorted by dimension, duplicates rejected,
  slugs validated) carrying one **state** from the active pack. An untouched
  coordinate is not stored; it reads as the pack default with `stored=false`.
- **State / grade** — pack states carry a category (`open`, `in-progress`,
  `review`, `done`, `excluded`, `flagged`) that drives engine-enforced honesty
  invariants: only the `done` state counts as green; an `excluded` state
  leaves the denominator; review states never count as done; machine-only
  transitions (e.g. `regress`) fire only via the sanctioned path
  (`cell reconcile`).
- **Gate** — authorizes a gated transition. Built-in evaluators:
  `guard-bite` (the cell has ≥1 AUTHORISING evidence link: `--kind task`, citing a
  task that is `done` and carries `custom/grc`) and `consistency` (store predicates
  recompute clean). A `--kind url` link ANNOTATES a cell and is stored, listed and
  displayed as before, but it never authorises a gated state — an opaque string
  nobody checked must not be what unlocks `hardened`. `cell transition`
  refuses a gated move that fails its gate (`FW-WF-GATE-UNSATISFIED`) unless
  `--facilitate` (records the verdict but allows the move).
- **Gate-entry-only state** — a state the active pack lets you enter ONLY
  through gated transitions (`hardened`: every `harden` edge carries
  `guard-bite`). A *direct* assert into one — `cell set`, `cell set-where`,
  `cell import` — must satisfy at least ONE of those gates, or it is refused
  `FW-WF-GATE-UNSATISFIED` naming the state, the gates and why each failed.
  `--facilitate` applies it anyway and records the bypass on the cell-set
  event, in the SAME shape `cell transition --facilitate` writes, so a forced
  set and a forced transition are indistinguishable in the ledger. A state
  with ANY ungated way in (or none at all, like the pack default) is
  unaffected — the guard never fires on honest work.
- **Evidence** — links on an asserted cell (`--kind task` = validated task
  ref, `--kind url` = opaque string). `cell reconcile` machine-regresses
  stale-green cells whose cited evidence no longer resolves.

### The coverage convention (concrete)

lettuce ships one convention, **coverage**, active on every project by default.
Its grade vocabulary:

| Grade vocabulary (default → … → done) | Meaning |
|---|---|
| untested* (never exercised) → planned → in_progress → gap (exercised, defect known) → evidence_linked → smoke (exercised, shallow proof) → **hardened** (a committed guard provably bites under fault-injection); blocked, regressed (flagged); excluded, waived (excluded, leave the denominator) | lettuce's own 88-dimension / 15-family quality taxonomy; `harden` is guard-bite-gated (requires evidence) |

`*` = the default an untouched coordinate reads as (a fresh project reads
`untested`). You do not swap the convention; tune it per project with `defaults`.

**Two readings: proof and roadmap.** The same cells serve a backward and a
forward reading. Backward, a cell is *proof* — evidence of quality already
earned. Forward, each coordinate is a point in the product's quality space and
every thin cell is work waiting to happen: `untested` (or sparse/unstored) =
not yet discovered or decided, `gap` = a known deficiency (a decided TODO),
`smoke` = shallow proof needing deepening, `hardened` = done, `excluded` =
decided out of scope. The grade ladder is thus also a work-discovery ladder,
and the board is a roadmap: an agent picks its next target from the frontier
of weak coverage (`cell rollup` for the weakest axes/members, `board export`
for the whole frontier), does the work as a task on the work plane, then
closes the loop by linking that task as evidence and grading the cell. Work
hardens cells; cells reveal work.

The `coverage` convention ships **88 dimensions in 15 families** (letter slugs;
inspect any with `lettuce dimension list --project <p>`):

| Family | Dimensions |
|---|---|
| correctness | A functional correctness · C spec-impl fidelity · N domain-model completeness · O compat/versioning/migration · Q release-compat contract · U internal consistency |
| usability | B UX/DX · S self-documentation · T error-handling & messaging · M docs & examples · AY accessibility (a11y) · BI i18n/l10n · VD visual & product design · AX agent experience · KB knowledge-base & self-doc quality · WL white-label & theming · BC browser/client compatibility |
| interface | H HTTP/API surface · J import/export round-trip · BX agent-readiness · CB multichannel parity · AP agent-protocol interoperability · DP data portability & anti-lock-in |
| security | I attack surface · V privacy · AO rate-limiting/abuse · AZ bounded resources · BD auditability · RT red teaming · PT purple teaming · TM threat modeling · RC regulatory compliance & enforcement |
| reliability | BZ temporal-correctness · D data integrity/atomicity · E resilience/recovery · F concurrency/race-safety · FP cross-process concurrency & lock-safety · EL service liveness & crash-isolation · AM idempotency · CC resource-cleanup · AN backup/DR · AT determinism · OP operations & incident response |
| distributed | G multi-node/sync · CA storage-backend equivalence |
| performance | L performance/efficiency · P observability · SC scalability patterns · DT distributed tracing & trace ownership |
| maintainability | K code-quality · AA maintainability · AB testability · R configurability · BL architecture · CS code smells & anti-patterns |
| delivery | AC licensing · AR supply-chain/SBOM · AS deploy/release · BM CI-CD health · BN packaging · BY stack maturity · BP cross-platform portability · LG legal, IP & terms · CN cloud-native / kubernetes deployment · RG reproducible generation & asset provenance |
| process | TD TDD discipline · GQ quality-gate quality · TK tracking & board discipline · QA QA procedure (full) · RV adversarial/peer review · AL agentic development-loop quality · PM engineering-process maturity & artifacts · RQ research & inquiry quality |
| product | PA product analytics & north-star instrumentation · XP experimentation & A/B testing · PD product discovery & prioritization |
| growth | PR pricing & packaging · MN revenue, billing & unit economics |
| customer | ON onboarding & activation · SU customer support & service · CX success, retention & churn · FB feedback & voice-of-customer |
| market | PO positioning & messaging · DG demand generation & campaigns · SE content, SEO & GEO/AEO discoverability · IR investor & stakeholder communications |
| lifecycle | FO cloud cost & FinOps efficiency · SN deprecation, EOL & sunset |

```bash
lettuce cell set "area=auth;layer=api" --state in_progress --project "$P" --author "$A"
lettuce cell evidence add "area=auth;layer=api" --ref "$P/TASK-1" --kind task --project "$P" --author "$A"
lettuce cell transition "area=auth;layer=api" link-evidence --project "$P" --author "$A"   # in_progress → evidence_linked
lettuce cell transition "area=auth;layer=api" harden --project "$P" --author "$A"   # gate must pass
# `cell set --state hardened` is NOT a shortcut past that gate: hardened is
# gate-entry-only, so a direct set must pass guard-bite too (or --facilitate,
# which records the bypass on the event).
lettuce cell rollup --by area --project "$P"    # per-member floor state + hardened ratio, N/A-excluded
lettuce board export --project "$P" --format json   # BoardExport v0.1 for a renderer
```

### Scope ≠ subsystem ≠ shape (board structure)

A **scope** is a top-level partition of the board (S1 Feature, S2 Component,
S3 Product, S4 Ecosystem) scored **separately and never blended** — blending
scopes hides gaps. Each scope has a **row-axis** (S1→command, S2→subsystem,
S3→none: its rows are the dimensions, S4→milestone) and a **shape** — how it
holds and renders data: `grid` (rows × dimensions × grade), `scalars` (one
grade per dimension), `ladder` (ordered milestone stages, no ratio). A
*subsystem* is specifically S2's row-axis entity, not a synonym for scope.
Today scope/subsystem are modeled as declared dimensions whose members appear
as coordinate axes; a cell's row lives under the `unit` axis for S1/S2
(`dim=a;scope=s1;unit=<command>`, `dim=am;scope=s2;unit=<subsystem>`) and
under `dim` alone for S3. Shape is renderer-side — its
promotion to first-class data is an approved design that has **not shipped
yet**.

**No-scope cells → the `unscoped` bucket.** A cell whose coordinate declares no
`scope=` member is not silently outside the per-scope frame: the DoD and `board next`
bucket it under a synthetic reserved scope named **`unscoped`** at evaluation time (the
stored coordinate is untouched — no rewrite, no hash change). This keeps the honesty
invariant intact — a grid of nothing but un-scoped untested cells reads as **unmet**,
never vacuously "done". Because the bucket is evaluation-only, the name is **reserved**:
`cell set`/`cell transition` refuse an explicit `scope=unscoped` (`FW-NAME-RESERVED`) so
a real cell can never collide with the bucket. Omitting `scope=` is the supported way
to land there; give a cell a real `scope=` to pull it into its own partition.

### Standing up a coverage board from scratch (worked example)

**The scoped-cell coordinate convention — learn this first.** A board cell's
coordinate names three axes that the board reads structurally:

    dim=<quality-dim> ; scope=<s1|s2|s3|s4> ; unit=<row>

- `dim=` — WHICH quality axis (a coverage dimension by its slug lower-cased:
  `dim=a` functional correctness, `dim=am` idempotency & delivery semantics; see
  `lettuce dimension list --project <p>`). The board upper-cases it to match the
  dimension.
- `scope=` — WHICH assessment tier (see "Scope ≠ subsystem" above). The slugs
  `s1`/`s2`/`s3`/`s4` are magic: they carry the shapes Feature-grid / Component-grid /
  Product-scalars / Ecosystem-ladder. A cell with no `scope=` lands in the reserved
  `unscoped` bucket.
- `unit=` — the row within an S1/S2 grid (a command / subsystem). **S3 scalar cells
  omit `unit=` entirely** (their rows ARE the dimensions): `dim=am;scope=s3`.

Coordinate axes are minted freely — you do NOT have to declare `dim`/`scope`/`unit` as
dimensions just to assert a cell (only a *closed* declared dimension enforces its member
list). Declaring `scope`/`unit` is optional but recommended: it gives the board real row
names, ordering, and per-scope shapes instead of the built-in s1..s4 fallback.

Copy-paste recipe (filesystem mode):

```bash
ROOT=.lettuce ; P=myproject ; A=my-agent

# 1. Store + project (coverage is the shipped convention — active by default,
#    nothing to enable).
lettuce init --root "$ROOT" --author "$A" \
  --bootstrap-project "$P" --bootstrap-author --idempotent --format json

# 2. (Recommended) declare the partition + row axes so the board reads richly.
#    --family must be one of the coverage convention's families (interface is apt here).
lettuce dimension declare scope --family interface --applicability universal \
  --name "Scope" --project "$P" --root "$ROOT" --author "$A" --format json
lettuce dimension member add scope s2 --name "Component" \
  --project "$P" --root "$ROOT" --author "$A" --format json
lettuce dimension member add scope s3 --name "Product" \
  --project "$P" --root "$ROOT" --author "$A" --format json

lettuce dimension declare unit --family interface --applicability universal \
  --name "Command / unit" --project "$P" --root "$ROOT" --author "$A" --format json
lettuce dimension member add unit store --name "store" \
  --project "$P" --root "$ROOT" --author "$A" --format json

# 3. Assert the FIRST scoped cell — an S2 component-grid cell (has a unit row)…
lettuce cell set "dim=am;scope=s2;unit=store" --state gap \
  --reason "durability not yet proven" \
  --project "$P" --root "$ROOT" --author "$A" --format json

# …and an S3 product-scalars cell (NO unit — its row is the dimension itself).
lettuce cell set "dim=a;scope=s3" --state untested \
  --project "$P" --root "$ROOT" --author "$A" --format json

# 4. Orient — the board and its frontier now have real content.
lettuce board next --project "$P" --root "$ROOT" --format json
lettuce board render --project "$P" --root "$ROOT" --format html > board.html
```

To later HARDEN a cell, walk it up the pack ladder and link the proving task as
evidence — the `guard-bite` gate refuses `hardened` without it. The link must be
`--kind task` and the cited task must be `done` and carry `custom/grc`; a `--kind url`
link is an annotation and will not authorise the move. The gap cell above goes
`gap → smoke` via `exercise-gap` (from `untested` the action is `exercise`); a fresh
`untested` cell goes `untested → smoke → hardened`. The proving task must already exist:

```bash
lettuce task create TASK-1 --title "Prove durability" --project "$P" --author "$A"
lettuce cell transition "dim=am;scope=s2;unit=store" exercise-gap --project "$P" --author "$A"   # gap → smoke
lettuce cell evidence add "dim=am;scope=s2;unit=store" --ref "$P/TASK-1" --kind task --project "$P" --author "$A"
lettuce cell transition "dim=am;scope=s2;unit=store" harden --project "$P" --author "$A"          # smoke → hardened
```

### BoardExport v0.1 (the data contract)

`board export` emits a stable, versioned, read-only projection for dashboard
renderers — data strictly separated from visuals: `schema_version`,
`generated_at` + `source_rev` (provenance, not body content — `generated_at`
tracks the store's latest event and advances only when the store does, never a
wall clock; the body is deterministic), `families[]`, `axes[]` (with first-class `members[]`),
`dimensions[]`, `states[]` (the legend), `cells[]` (each with evidence links),
`rollups.by_axis` keyed by real axis names (`by_axis.scope`,
`by_axis.command`, …), `rollups.headline` (the ONE blended
Σhardened/Σnon-excluded ratio — for honest per-scope figures read
`by_axis.scope`), `milestones[]`, `tickets[]`, `registry[]`, `activity[]`.
It is **not** a backup — `lettuce export --bundle` is the faithful whole-store
bundle; the two share no schema.

### Definition of Done — the north-star "are we there yet?"

A project's **Definition of Done** is a tunable, stored policy — the objective
bar that answers "are we there yet, and what's blocking?". Lettuce is the
authoritative keeper of that answer; **an agent orienting on a project should
read the DoD verdict FIRST**, before picking any target.

- **Set it** — `lettuce dod set --grade STATE [--depth N] [--recency fresh|aging]
  [--scope SCOPE] --project "$P"`. `--grade` is the required grade floor (a pack
  state, e.g. `hardened`); `--depth` is an optional minimum distinct-rev
  confirmation depth (FRESH-2); `--recency` an optional minimum freshness bucket
  (FRESH-3). Without `--scope` you set the project defaults; with `--scope` you
  override one scope's floors (unset fields inherit the default).
- **Read it** — `lettuce dod show --project "$P"` prints the policy AND the
  current verdict: per-scope `met/unmet` with a k/n count (and any `unknown`),
  plus project-level `met_scopes/total_scopes`. The verdict is a strict
  per-scope **AND-gate** over applicable (non-excluded) cells — a scope is done
  only when EVERY cell clears the bar, never a blended percentage. Setting the
  DoD NEVER moves the hardened ratio: a scope can read 100% hardened yet be DoD
  **unmet** (stale or shallow evidence).
- **`board next` leads with it** — the orientation report's first line is the
  DoD verdict (`DoD: NOT DONE (0/1 scopes met)`), then the DoD-blocked scopes
  and the single "start here" pointer. So a bare `lettuce board next --project
  "$P"` both answers "are we there yet?" and hands you the next target. DoD is
  opt-in: a project that has declared no grade floor reports `declared=false`
  and the board renders unchanged.

The same tri-state verdict block also rides in `board export` under `dod` and in
the query surface (`from cells where dod = blocking`, above).

### Milestones and the S4 hypothesis ladder

A milestone is a first-class registry object carrying a **hypothesis-ladder STAGE**
(and optional CONFIDENCE) — real scalar data, not parsed from prose. `milestone
set-stage` progresses it:

```bash
lettuce milestone create sellable --title "Sellable to first customer" \
  --project "$P" --author "$A" --format json
lettuce milestone set-stage sellable --stage hypothesis --confidence low \
  --project "$P" --author "$A" --format json
# …later, once instrumented and evidenced…
lettuce milestone set-stage sellable --stage instrumented --confidence high \
  --project "$P" --author "$A" --format json
```

The board's built-in ladder rungs, weakest → strongest, are
**`hypothesis → validated → instrumented → proven`**. `--stage` is a free-form 1–64-char
label (NOT restricted to those four — the four are the default rung ordering the S4 panel
renders against; a project may declare its own rungs via a scope member's `stages`
attribute). Stage/confidence are each an evented mutation; they surface in `milestone
show`, `board render` (the S4 ladder badge), and `board export` under `milestones[]`.

### Wiring the work plane to the coverage plane (the S4 bridge)

**The S4 scope is the bridge between the two data planes.** S1/S2/S3 are graded from
*cells* (the proof plane). **S4 is different: its rows come from the milestone registry,
not from `scope=s4` cells.** Each milestone renders as one ladder rung (its
`stage`/`confidence`) plus a delivery bar (its N/M task completion). So the same
milestone is simultaneously a *work-plane* object (tasks point at it via `task set …
milestone <slug>`, and `query … --group-by milestone` buckets by it) and a *coverage-plane*
row (the S4 ecosystem ladder) — no `scope=s4` cell is needed or read.

```bash
# Work plane: a milestone with tasks attached.
lettuce milestone create beta --title "Public beta" --project "$P" --author "$A"
lettuce task set "$P/TASK-1" milestone beta --project "$P" --author "$A"
lettuce query tasks --group-by milestone --project "$P" --format json   # delivery progress

# Coverage plane: the SAME milestone as an S4 ladder rung.
lettuce milestone set-stage beta --stage validated --confidence medium \
  --project "$P" --author "$A"
lettuce board next --project "$P" --format json     # S4 shows beta's rung + N/M bar
```

This is why a project's overall Definition of Done layers two outer gates on top of the
per-scope cell rollup: DONE requires the S1–S3 coverage met AND every committed milestone
reached (hypothesis-stage milestones are excluded as bets, not commitments) AND zero
non-terminal tickets. Grid/scalar scopes are hardened by cells; the ecosystem scope is
progressed by advancing milestones.

## 8.1. Running lettuce as an agentic development loop

Lettuce is not primarily an autonomy tool — everything above works one command
at a time, human or agent. But one powerful way to use it: if you are an agent
with standing goals and a store, run lettuce **as your loop**. The store read
forward is the plan; the store written backward is the proof — no external
planner needed. Here is how it shines.

**`board next` requires — and what makes it *useful*.** `board next` (like `board
export`/`render`) is a pure projection of the store, so it needs: **a store and a valid
`--project`** (a missing/unknown project is refused — there is no default and no
cross-project scan); **at least one *stored* cell to be useful** (the frontier is built
from asserted cells only — sparse pack-defaults are never rows; an empty board is not an
error, every scope meter reads `n/a`); and **a declared DoD to get the verdict line**
(the leading `DoD: …` line appears only once a project has declared a grade floor with
`dod set`). Assert cells first (see "Standing up a coverage board from scratch" above).
`--depth` accepts only `0`, `1`, or `2`; `--scope <name>` naming a scope no cell declares
returns an honest error listing the available scopes, never a silent empty report.

**The cycle** — orient → act → reflect → file, repeat:

```bash
# ORIENT — ask the store what matters most right now.
lettuce board next --project "$P" --root "$ROOT" --format json
#   → every scope's meter weakest-first, shape-aware suggestions, concrete
#     gap/untested coordinates — each with the exact drill command
#     (--depth 0|1|2 sets detail). Pick ONE target: gap (known defect)
#     before untested (undiscovered), weakest scope first. Act on the
#     coordinate it printed; don't hand-compose one.

# ACT — make the work claimed, attributed, and visible (§6 steps 3-5).
lettuce task create TASK-N ... --title "Harden <coord>"    # if no ticket exists yet
lettuce lease acquire "$P/TASK-N" ...
lettuce task transition "$P/TASK-N" start-work ...
# ...do the real work; narrate with `run log add` / `comment add`...

# REFLECT — record what happened WITH evidence, then close the cell (§6 step 6).
lettuce artifact add "$P/TASK-N" --type test-report ...    # the proof itself
lettuce task transition "$P/TASK-N" complete --reason "guard bites" ...
lettuce cell evidence add "<coord>" --ref "$P/TASK-N" --kind task ...
lettuce cell transition "<coord>" harden ...               # guard-bite gate must pass

# FILE — organize everything you discovered before looping.
lettuce task create ...                            # one ticket per unfixed finding
lettuce cell set "<coord>" --state gap --reason "<defect>" ...  # defects get addresses
lettuce dimension member add <dim> <member> ...    # new surface → new map row
lettuce milestone set-stage <slug> --stage <s> ... # progress the hypothesis ladder
lettuce cell reconcile ...                         # regress stale greens
lettuce validate --strict ...
```

Then run `board next` again. The frontier has changed — partly because you
hardened a cell, partly because you filed what you found. That is the whole
method: **work hardens cells; cells reveal work.** FILE is not bookkeeping —
it is how the map stays truthful enough to steer the next iteration.

**The invariant that keeps the loop honest.** A `gap`/`untested` cell spawns a
task (the cell is the work's address); the COMPLETED task — `done`, carrying its
`custom/grc` run-case pin, linked as `--kind task` evidence — is what authorizes
hardening (both doors are guard-bite gated: `cell transition
harden`, and `cell set --state hardened`, which is gate-entry-only); and if
the cited evidence later unresolves, `cell reconcile` machine-regresses the
cell. What that establishes is PROVENANCE — the green traces to a completed,
graph-backed ticket — not proof that the cell's subject was checked: the gate does
not test whether the ticket is RELEVANT to the cell, how MANY cells one ticket
backs, or whether the ticket was filed to justify the very cell it now authorises. Neither direction can lie to the next iteration — which is why you never
`--facilitate` past a failed gate to "make progress": one unproven green
poisons every future ORIENT, and the bypass is on the ledger forever.

**Self-organizing, not preconfigured.** The shipped defaults (§3) are a
starting point, not the map. Discover the project's *existing* plans, specs,
and milestones and reflect them into the store — milestones with stages,
scope/unit members for the real surfaces, dimensions the project actually
needs — coordinating with your operator per whatever authority you have been
granted over map structure. An unorganized project is not an obstacle; it is
the other entry point: lettuce is how it becomes organized. One rule either
way: goals, milestones, and actual work MUST be reflected in the store — work
that lives only in your context dies with your context.

**Direction: goals and subgoals.** Today direction is carried entirely by
declared data — milestones (whose stage ladder is the hypothesis being
progressed), scopes, severities — and `board next` surfaces progress toward
it. Each scope already implies a subgoal by its shape: a grid wants its
hardened ratio at 100%, a scalars scope wants every dimension green, a ladder
wants its final stage. A first-class *project goal* field (with explicit
per-scope subgoals) is the stated intent of this design but is **not a store
primitive yet** — until it ships, put the goal in milestone descriptions and
let the shapes carry the rest.

**Survive your own restarts.** Whatever harness runs you, scheduled wakes and
crons typically die on context compaction — and your CLAUDE.md (or equivalent
always-injected instruction file) is usually the only thing that comes back
every session. Anchor the loop there, not in memory: the store coordinates
(root/project/author), the instruction to re-register the loop's wake/cron on
every cycle (a cron-guard), and the loop itself. Then each iteration: read
your own state first (`board next`, `query tasks --status active`, your held
leases) — the store, not your context, is your memory; notice when you are
churning (the same coordinates cycling without hardening means stuck); and
when stuck, widen the map (the FILE-phase moves: new members, a deeper
ladder, the next milestone stage) instead of spinning. Nothing is hardcoded —
everything the loop needs after a restart is declared store data.

**This is the basics — deeper aid is on demand.** This section is
deliberately a hint, not a manual. For richer per-command guidance,
`lettuce usage <cmd>` (and `--format json` for machine metadata); for the
loop at full depth, work the cycle and let the store teach you. If you want
standing, harness-specific loop instructions, write them into your own
CLAUDE.md — the tool stays harness-agnostic by design.

## 9. Graph authoring & enactment

Beyond one-off tasks, lettuce lets you encode a **process** once — as a sound,
named, versioned graph — and enact it repeatably. Two objects:

- A **graph-def** is the reusable authored DATA: nodes (typed by *concern*)
  wired by edges, with a start node. Stored at
  `projects/<p>/graph-defs/<slug>/`, versioned + content-addressed like a task
  body. It is the "keeper": every stored def is *sound by construction*.
- A **graph-run-case** (grc) is ONE enactment of a def against the real ledger.
  It is event-sourced — `state == f(events)` — so it replays exactly.
  **Carriers** are the write-once, content-addressed evidence its edges emit.

The loop is **author → verify → compose → enact → replay**. `--project` and
`--author` apply as everywhere (§4); `--root` as in §6.

### Author — write a spec, `create` gates it

A spec is JSON: a `start` node, `nodes` (each a `concern` ∈
`producer|reviewer|router|gate|human|verifier|terminal|fork|join`), and `edges`
(`from`/`to`/`edge`; a loop back-edge must carry a positive `cap`; a `router`'s
out-edge may carry a `when` guard). Keep the spec file **outside the store root**
— a stray file under `$ROOT` trips `FW-PATH-UNKNOWN`.

**Loop iteration (`min_times`/`until_stable`/`until_drain`/`circuit_breaker`).**
A capped loop back-edge (`cap > 0`) MAY refine *how* it iterates and exits:
`min_times: K` (a floor — at least K iterations before it may exit),
`until_stable: N` (converge — exit after N consecutive clean/stable iterations),
`until_drain: true` (exit when the work-queue drains), and `circuit_breaker: M`
(trip/fail the loop after M unproductive iterations). Each is valid **only** on a
`cap > 0` back-edge — on a `cap == 0` edge it is `invalid-iteration` — and every
bound must satisfy `0 ≤ bound ≤ cap` so the exit conditions can never outlast the
hard cap. All fold into the `effective-hash` (changing `until_stable` moves it).
Like fork/join, this is authored, canonicalized and hashed here — and **enacted at
runtime** by `graph-run-case` (ENACT-3): a run-case really does close waves, count
consecutive clean ones, drain a queue and trip the breaker, refusing an early exit
with `FW-GRAPH-ITERATION-FLOOR` / `FW-GRAPH-LOOP-NOT-CONVERGED`.

**Structured parallelism (`fork`/`join`/`quorum`).** A `fork` splits control
into concurrent branches (it must branch — out-degree ≥ 2, else
`degenerate-fork`); a `join` merges them (it must merge — in-degree ≥ 2, else
`degenerate-join`). A `join` may carry `"quorum": K` — a K-of-M threshold over
its M inputs (`0` or omitted = an AND-join requiring **all** M). `K` must satisfy
`0 ≤ K ≤ M`, and a non-zero `quorum` is valid **only** on a join — anything else
is `invalid-quorum`. The quorum folds into the `effective-hash` (changing K moves
the hash). It is authored, canonicalized and hashed here — and **enacted at
runtime** by `graph-run-case` (ENACT-1/2): a fork really does open concurrent
branches and a join fires only once K of M have arrived, else
`FW-GRAPH-JOIN-UNSATISFIED`.

**Firing a join: `--edge` is the ARRIVING BRANCH edge, never `<fork>-><join>`.**
A fire is not an edge traversal — it *collapses* the main state from the fork
(`--from`) onto the join (`--to`) — so `--edge` names one of the edges a branch
came in on (`v1->join`). The value `--from`/`--to` make obvious, `fork->join`, is
the one value that is always wrong, and it is now refused
`FW-GRAPH-JOIN-EDGE-UNARRIVED` **before** anything is written, naming the edges
branches did arrive on (LET-914). The refusal reads THIS run-case's branch ledger,
not the graph-def — so it also refuses a *declared* in-edge no branch arrived on
(`--edge v3->join` when v3 never arrived), which `conform` cannot catch:

```bash
lettuce graph-run-case advance "$GRC" --from fork --to join --edge 'v1->join' \
  --fire-join --quorum 3 --root "$ROOT" --project "$P" --author "$A" --format json
```

**...but WHICH arrived edge you name is not part of the fire's identity (LET-918).**
The join-fired event is content-addressed over the fork→join transition alone —
`grc·fork·visit·join` — independent of the arrived-set, the quorum *and* `--edge`.
That matters the moment two clones fire the same join: with three branches arrived
there are three equally TRUE edges, and a clone that saw `{v2,v3}` arrive **cannot**
name `v1->join` at all (LET-914 refuses it), so the divergence is forced rather than
avoidable. Folding it minted two event ids for one revision step, and the merged
store came back `FW-REVISION-CHAIN-BROKEN` → consistency-gate → *every* mutation
refused. Now both clones mint the same event and the chain stays contiguous.

The recorded `--edge` remains **provenance, and accountable**: the derived check
requires a fire to cite an edge one of its recorded arrived branches genuinely came
in on, so a hand-edited edge is `FW-STORE-MERGE-CONFLICT` at heal even though it is
no longer inside the hash. Note what "de-dupe" does and does not promise: the
directory *name* converges, which is what keeps the chain intact — but `at`,
`operation-id` and now `edge` may still differ between two clones' copies of that
one event, and git stops on those for a human to resolve. Either resolution leaves a
valid store.

**Conditional routing (`when` guards on a `router`).** A `router` node's out-edges
MAY each carry a `"when": GUARD` — the condition, over data the run-case actually
*records*, under which that branch is taken. The grammar is deliberately tiny,
total and deterministic (no CEL, no new dependency):

```
REF == "VALUE" | REF != "VALUE"      combined with  and / or / not / ( )
REF ::= carrier.<key>                # a carrier value this run-case produced (this visit)
      | outcome.<clean|progress|level-up|queue-remaining>   # the LATEST closed wave
      | effect.<cell-hardened|task-opened|artifact-added|dod-progressed|transition>
```

Every fact is a **string** and comparison is byte equality; an **unrecorded** fact
reads as `""` (a produced carrier value is never empty, so `carrier.x == ""` means
exactly "not recorded"). No clock, no randomness, no map order — the same events
always route the same way.

Semantics are **exactly-one-match**: at most one out-edge guard may hold, and a
guarded router may declare at most **one unguarded** out-edge as its **default
(else)** route. A `when` on a non-router out-edge, a malformed/unknown-vocabulary
guard, or a second default edge is refused `invalid-guard` at **authoring** — you
never learn about a bad guard mid-run. The guard folds into the `effective-hash`,
so a stored def can never route differently under the same hash. Runtime
enactment is live (ENACT-5, below).

**SHAPE vs BEHAVIOR — two orthogonal vocabularies (`concern` vs `instruction`).**
A node has **two** independent attributes, and lettuce keeps their vocabularies
strictly separate:

- **SHAPE vocabulary** (`concern`) — the *structure/geometry*: what the node **IS**.
  Terms: `producer`, `reviewer`, `verifier`, `gate`, `human`, `router`, `fork`,
  `join`, `terminal` (+ a `join`'s K-of-M `quorum`, a loop `cap`). The **runtime
  contracts enforce STRUCTURE** from these (fork branches, quorum-join, until_stable,
  gate). A shape term is a role label only — it must **not** imply behavior.
- **BEHAVIOR vocabulary** (`instruction` + the pattern **name**) — what is **DONE**
  on the shape: the node's behavioral contract, in prose. e.g. an interrogator's
  *"generate probing questions over edge cases, failure modes, and assumptions;
  require a satisfactory answer to each"*. The **instruction enforces MEANING** — it
  is folded into the `effective-hash`, so **behavior is part of identity**. Two defs
  of identical shape but different instructions hash **differently**; a pattern named
  `question-based-verification` is *unable* to not carry its behavior.

So: two names, two guarantees. `question-based-verification` is a **behavior** that
happens to sit on a `verifier`→`gate` **shape**. `instruction` is a **leaf-node**
attribute (a `uses:` subgraph node inherits behavior from the referenced def and
carries none) and is **optional** — a leaf def without one still validates; only the
advisory `missing-instruction` lint warns on a **behavioral** node (concern ∈
`producer`/`reviewer`/`verifier`/`gate`/`human`/`router`) that lacks one. The
structural nodes (`fork`/`join`/`terminal`) carry no behavior and are exempt. Every
shipped catalog pattern carries a real instruction on each behavioral node — the
catalog holds itself to that higher bar (a guard test enforces it). `graph-def show`
renders both fields; `graph-def viz` shows the shape plus a labelled `behavior: …`
line so you can SEE what a node does.

**EXECUTION — the third vocabulary (`exec_policy`).** Beside SHAPE (what a node
IS) and BEHAVIOR (what it DOES), a leaf node MAY declare **how it is RUN**:

```jsonc
{"id": "reviewer", "concern": "reviewer", "instruction": "…",
 "exec_policy": {"model": "opus-5", "effort": "high", "residency": "resident"}}
```

- **`model`** — the model *or* capability-tier the agent at this node should use.
  Deliberately an **open** vocabulary (only grammar-checked: 1–64 chars, starting
  with a letter/digit, then letters/digits/`. _ - : / + @`) — model names are
  vendor- and time-varying, so closing the set would rot. Tier words
  (`frontier`/`balanced`/`fast`) are equally valid by convention.
- **`effort`** — reasoning effort, from the **closed** ladder
  `low | medium | high`.
- **`residency`** — the **closed** actor lifecycle `resident | ephemeral`:
  whether the actor **keeps its context across loop iterations** (`resident` —
  it remembers the previous wave) or is **spawned fresh each time**
  (`ephemeral` — it re-reads from scratch). This is the **load-bearing** knob
  for loops: the same topology behaves differently depending on it.

Every knob is independently optional (declare only `residency` if that is all
you mean), but an **empty** `"exec_policy": {}` is refused — it would move the
hash while saying nothing. Bad values are refused at authoring with
`FW-GRAPH-DEF-UNSOUND` / `invalid-exec-policy`, naming the node *and* the closed
set. Like `instruction` it is a **leaf-node** attribute (a `uses:` subgraph node
inherits its atoms' policies and carries none of its own) and it **folds into the
`effective-hash`** — so *how* a node runs is part of the content-addressed
identity: flipping `resident` → `ephemeral` mints a **new version** rather than
silently mutating a pinned one, and composition carries each atom's policy onto
the inlined children. An advisory **`missing-residency`** lint warns when a
*behavioral* node **inside a declared loop** leaves its residency implicit
(exit 0 — advice, not a gate). `graph-def show` renders `exec_policy`;
`graph-def viz` adds a labelled `exec: model=… effort=… residency=…` line, so the
three vocabularies stay visually distinct on the diagram.

```bash
# review-linear.json (author it anywhere but inside $ROOT):
# {
#   "start": "producer",
#   "nodes": [
#     {"id": "producer", "concern": "producer", "instruction": "produce the artifact to be admitted"},
#     {"id": "gate",     "concern": "gate",     "instruction": "admit only if it meets the criteria; else send it back"},
#     {"id": "terminal", "concern": "terminal"}
#   ],
#   "edges": [
#     {"from": "producer", "to": "gate",     "edge": "producer->gate"},
#     {"from": "gate",     "to": "terminal", "edge": "gate->terminal"}
#   ]
# }
lettuce graph-def create review-linear --spec-file ./review-linear.json \
  --root "$ROOT" --project "$P" --author "$A" --format json
```

`create` folds in the **soundness gate** (liveness ∧ boundedness): every edge
must reference a declared node, every node must be reachable from the start,
there must be a declared start, and every loop back-edge must be capped. An
unsound spec is refused `FW-GRAPH-DEF-UNSOUND` (the diagnostic names the
offending node/edge) and **nothing is written** — so a run-case can never enact
a broken topology. `create` also pins the compiled `effective-hash`.

**Unknown keys are WARNED, not refused (LET-734) — read the advisory.**
A spec is stored **verbatim** and its bytes are folded into `effective-hash`, so
a key the schema does not recognise does the opposite of vanishing: `graph-def
show`'s `spec` field reads it back, the hash covers it, and a run-case pins that
hash — so the phantom declaration looks *pinned and immutable* while **nothing
reads it**. `create` and `revise` therefore scan the raw JSON and report every
unrecognised key with its path (`nodes[<id>].<key>`,
`edges[<from>-><to>].<key>`, `nodes[<id>].exec_policy.<key>`, or a bare
top-level key) on **both** surfaces (LET-913): the human line on **stderr**, and
the same diagnostic in the success envelope's **`warnings`** array under
`--format json|yaml`. If you automate this command, parse `.warnings` — a
non-empty array is the only signal that a key you wrote means nothing.
The same is true of `registry create workflow` / `workflow revise` (LET-718).
Exit stays **0** and the def is still stored: unknown-field
tolerance is deliberate, and refusing would make every already-stored spec
unrewritable. The truth is always in the **parsed** view — the `nodes`/`edges`
arrays of the same payload — and it shows by *absence*, which is the hardest
thing to notice. If you meant the key to do something, it does not.

### Verify — `lint` (advisory) and `compile` (canonical hash)

```bash
lettuce graph-def lint    review-linear --root "$ROOT" --project "$P" --format json
lettuce graph-def compile review-linear --root "$ROOT" --project "$P" --author "$A" --format json
lettuce graph-def show    review-linear --root "$ROOT" --project "$P" --format json
lettuce graph-def list                  --root "$ROOT" --project "$P" --format json
```

- `lint` reports **advisory design smells** (a fan-out with no join, a terminal
  with an out-edge, a gate/verifier that dead-ends, a review edge with no
  evidence carrier, a behavioral node with no `instruction`, and a behavioral
  node **inside a loop** with no `exec_policy.residency`) as warnings with
  **exit 0** — a smelly def is still sound and still runs. Distinct from
  soundness, which is fatal.
- `compile` canonicalizes the def (nodes sorted by id, edges by tuple,
  whitespace normalized) and content-hashes it → the `effective-hash`. It is
  deterministic and order-invariant (any spelling of the same topology → the
  same sha256) and idempotent (re-compiling an unchanged def rewrites the same
  hash). A grc pins this hash at open, so a running case is reproducible even if
  the def is edited mid-flight. A rename does not move the hash; a topology
  change does.

### Revise — append a new spec version (`revise`)

```bash
lettuce graph-def revise review-linear --spec-file ./review-linear-v2.json \
  --root "$ROOT" --project "$P" --author "$A" --format json
```

`revise` is the in-band spec-version authoring path — the counterpart to `create`:
`create` mints a NEW slug (an existing slug is refused), `revise` **appends** a
new spec version to a slug that already exists (a missing slug is refused
`FW-PATH-NOT-FOUND`). It runs the **same soundness gate** as `create` and — only
if sound — appends the new version and **re-pins the effective-hash from it**, so
after `revise` `graph-def show`/`compile` reflect the LATEST spec and its new
hash. An UNSOUND revise is refused `FW-GRAPH-DEF-UNSOUND` and **nothing is
appended** (the latest version is unchanged).

The append is **version-safe**, and this is the composition payoff: a
`<slug>@<version>` reference **pins** a specific spec version. A floating
`<slug>@latest` (or an omitted version) is **resolved and pinned to the current
highest version AT AUTHOR TIME** — the stored spec always holds a version-exact
ref, so an immutable `spec/{N}` is reproducible and its `effective-hash` can
never move when a child later gains a version. So a parent that pins
`uses: review-linear@1` still compiles to the OLD expansion after a revise;
to pick up a newer child a consumer **re-authors** (`revise`) and its floating
`@latest` re-pins to the new highest at that moment. (A `@latest`/omitted ref to
a child that does not exist yet cannot be pinned and is refused
`FW-GRAPH-DEF-UNPINNABLE` — name an explicit `<slug>@N` or author the child first.)

### Compose — subgraphs by name (`uses` / `bind`)

A node may be a SUBGRAPH that inlines another def instead of carrying a
`concern`: `uses: "<slug>@<version>"`, with `bind` wiring the subgraph's open
ports (`start` / `terminal`) to nodes in the PARENT. The subgraph is entered via
the parent edge that targets the subgraph node.

```bash
# pipeline.json — inline review-linear@1 as one node, bind its exit to `done`:
# {
#   "start": "intake",
#   "nodes": [
#     {"id": "intake", "concern": "producer"},
#     {"id": "review", "uses": "review-linear@1", "bind": {"terminal": "done"}},
#     {"id": "done",   "concern": "terminal"}
#   ],
#   "edges": [{"from": "intake", "to": "review", "edge": "intake->review"}]
# }
lettuce graph-def create  pipeline --spec-file ./pipeline.json \
  --root "$ROOT" --project "$P" --author "$A" --format json
lettuce graph-def compile pipeline --root "$ROOT" --project "$P" --author "$A" --format json
```

`compile` is the composition ENFORCEMENT point: it resolves the transitive
`uses` closure, inlines every referenced def (namespacing inlined node ids), and
hashes the WHOLE expansion. Because every stored ref is version-exact (floating
`@latest` was pinned at author time), recompiling a parent is DETERMINISTIC —
improving a child does NOT move a parent's `effective-hash`; a consumer picks up
a child improvement only by **re-authoring** (`revise`), which re-pins its
`@latest` to the new highest version. A composition cycle is
refused `FW-GRAPH-DEF-CYCLE`; a missing used def `FW-PATH-NOT-FOUND`; an unbound
port or unsound expansion `FW-GRAPH-DEF-UNSOUND`. (A forward/cyclic `uses` ref is
tolerated at `create` — the hash is pinned provisionally — and enforced at
`compile`.)

### The shape vocabulary — say which topology you are building

A graph-def is assembled from a small set of named **shapes**. The terms below are
the whole vocabulary; every one of them is a real primitive with a real refusal
behind it, not a diagram word. Say the shape out loud when you author, and reach for
the catalog pattern that already ships it rather than re-deriving the topology.

**A shape without its guardrail teaches the shape and not the discipline.** Each row
names the refusal that makes that shape honest, because the topology alone is
decorative: the enactment runtime is graph-def-FREE (`advance` reads no spec), so a
def never enforces anything *by itself* — what refuses is the primitive the enactor
drives it with, and `graph-run-case conform` replays the walk against the pinned def
afterwards.

<!-- SHAPE-VOCABULARY: every backticked token in this table is guarded against the
     code by TestSkillShapeVocabularyMatchesBehavior (LET-917). Do not hand-edit a
     term here without checking it still names something real. -->

| shape | built from | the refusal that makes it honest | ships as |
|---|---|---|---|
| **fan-out** | a `fork` node + `--open-branch` per out-edge | `FW-GRAPH-FROM-MISMATCH` — a `--branch` advance is guarded on THAT branch's own tip, so a branch cannot be walked from a state it never reached | `review-panel@v1` |
| **fan-in at a barrier** | a `join` node + `--fire-join --quorum` | `FW-GRAPH-JOIN-UNSATISFIED` below K, and `FW-GRAPH-JOIN-EDGE-UNARRIVED` if the fire names an edge no branch arrived on | `claim-verification@v1` |
| **the diamond** | fan-out then fan-in on one `fork`/`join` pair | as above — the house shape, and the one to reach for by default | `red-team@v1` |
| **routing** | a `router` node + `--route-edge` guards over recorded evidence | `FW-GRAPH-ROUTE-UNSATISFIED` / `FW-GRAPH-ROUTE-AMBIGUOUS` / `FW-GRAPH-ROUTE-NO-MATCH` — the caller cannot overrule the guards | `escalating-review@v1` |
| **verification** | a `verifier` or `gate` node + `--produces` | `FW-GRAPH-PRODUCES-UNSATISFIED` — you cannot leave a station claiming a carrier the ledger does not hold | `question-based-verification@v1` |
| **converging cycle** | a capped back-edge + `--wave-close` then `--loop-exit` | `FW-GRAPH-LOOP-CAP-EXCEEDED` / `FW-GRAPH-LOOP-NOT-CONVERGED` / `FW-GRAPH-CIRCUIT-TRIPPED` — the exit is a fold over the wave log, not a claim | `audit-wave@v1` |
| **tournament** | a `fork` of competitors, a quorum `join` as the match barrier, and a `router` with **no** default edge | `FW-GRAPH-ROUTE-NO-MATCH` — every crown edge is guarded on the recorded verdict AND on both entries existing, so a winner that beat nobody is a dead-end, not a fall-through | `tournament@v1` |
| **generate-and-filter** | a `gate` that pins the rubric first, a `fork` of candidates, an AND-`join`, and a `router` whose UNGUARDED edge lands on the empty terminal | `FW-GRAPH-ROUTE-UNSATISFIED` — reaching the shortlist needs a recorded **keep** verdict; rejecting every candidate is the DEFAULT route, so the filter can always return zero | `generate-and-filter@v1` |

<!-- /SHAPE-VOCABULARY -->

The supporting terms, in the order you meet them: a **carrier** is the write-once,
content-addressed datum an edge produces (`carrier produce`), and it is what makes an
edge REAL — order is not an edge and proximity is not an edge; only data crossing is.
A **visit** is the loop-iteration index a carrier and an edge traversal are keyed by. A
**branch coordinate** is the content-addressed id `--open-branch` mints. A **wave** is
one closed pass round a cycle. **Quorum** is K-of-M — a *partial* barrier, so a join
is evidence that K independent things agreed rather than a mere synchronisation point.
**Residency** (`exec_policy`) says whether a station's actor is `resident` across
iterations or spawned fresh (`ephemeral`). A **`uses`-composite** is a def that inlines
another by version-exact reference, and **`cost_hint`** is a pattern's ordinal
worst-case band — catalog metadata that the runtime never reads.

### Usage by name — the pattern catalog

Beyond authoring a def from scratch, lettuce ships a **catalog** of canonical,
versioned, VERIFIED graph-def **patterns** embedded in the binary — the org
authors a sound process once and every consumer pulls it in *by name*
(mission-graph-engineering SCH-5a). Every shipped pattern is sound by
construction and compiles to a stable `effective-hash`.

```bash
# Browse the shipped patterns (name@vN + one-line description + effective-hash).
lettuce graph-def catalog list --format json
# Inspect one pattern's spec + nodes/edges + hash.
lettuce graph-def catalog show review-panel@v1 --format json
# MATERIALIZE it as a new def in your project, then compose/compile/enact it.
lettuce graph-def use review-panel@v1 --as my-review \
  --root "$ROOT" --project "$P" --author "$A" --format json
lettuce graph-def compile my-review --root "$ROOT" --project "$P" --author "$A" --format json
```

Shipped patterns — browse them with `catalog list` rather than trusting this list to
stay complete: `gate@v1` (producer→gate→terminal), `tracer-bullet@v1`
(producer→verifier→terminal), `review-panel@v1` (fork→3 reviewers→join
`quorum=2`→terminal) and `review-panel@v2` (5 reviewers, 4-of-5 supermajority),
`claim-verification@v1` (claim→fork→3 verifiers→join `quorum=2`→terminal) and
`claim-verification@v2` (unanimous 3-of-3), `red-team@v1` (an adversarial panel whose
unanimous join gives every branch a VETO), `question-based-verification@v1` (an
interrogator feeding a must-answer gate), `audit-wave@v1` (a review panel looped back
through a gate until N consecutive clean waves — the convergence QA loop, `cap=5
until_stable=2 min_times=1`), `harden-loop@v1` (a producer→work→verify cycle with a
capped `circuit_breaker=3` back-edge — iterate a fix→verify until it trips or
converges), `external-audit@v1` (a convergence loop signed off by a distinct
out-of-loop auditor), `tournament@v1` (pairwise competition whose crown router has no
default edge, so a winner must have BEATEN someone), `generate-and-filter@v1` (many
candidates against one pre-declared bar, where an EMPTY survivor set is the default
route), `deep-review@v1` (an 8-stage `uses:`-composite of the atoms above), and
`escalating-review@v1`/`@v2` (the cheap-first cost gradient — routers that escalate
only on positively recorded evidence).
`catalog list`/`catalog show` are **read-only and project-independent** (the
catalog is embedded, not stored per-project); an unknown `name@version` is
`FW-PATH-NOT-FOUND`.

`use` is a **straight materialize**: it stores the pattern's spec as a new def
via the same path as `create`, so the soundness gate + effective-hash pin apply
and the new def compiles to the **same** hash `catalog show` reports (a faithful
reproduction). The result is an ordinary def — composable (a `uses` node),
compilable, enactable. A slug already in use is refused exactly as `create`.
**Deferred:** `--bind` re-wiring of a pattern's open ports at use time
(specialize a materialized pattern the normal way — a parent def that `uses` it
and binds its ports).

### Enact — open a run-case and walk it

Open a grc at the def's start node, then advance edge by edge. Before writing a
`case-advanced` event, `advance` runs a **produces post-check**: each
`--produces KEY` must already exist as a carrier on the leaving edge×visit, else
`FW-GRAPH-PRODUCES-UNSATISFIED` and state is unchanged. So produce the carrier
first.

**The post-check is CALLER-ELECTIVE — read this before relying on it.** It validates
the keys the caller names on `--produces`, and nothing else. An edge does **not**
carry a `produces` declaration: `GraphDefEdge` has no such field, so writing
`"produces": ["artifact"]` on an edge in a spec declares nothing (it survives
verbatim in the stored raw `spec` and is covered by `effective-hash`, which makes it
*read back* as declared). Omit the flag and the advance succeeds regardless. Treat
`--produces` as the caller ASSERTING what it produced, never as the graph REQUIRING
it. Since LET-734, `graph-def create`/`revise` **warn** on that key rather than
accepting it in silence, so the mistake is caught where it is made.

**This is not a `produces` quirk — the whole `advance` runtime is graph-def-free,
and that is the design.** `advance` reads NO stored def: the join `--quorum`, the
loop `--cap`/`--min-times`/`--until-stable`/`--circuit-breaker`/`--until-drain`
bounds, and the router's `--route`/`--route-else` guards are ALL resolved by the
CALLER from the def and passed as flags (see `AdvanceGraphRunCaseOptions`). Verified
live: an edge declaring `cap: 1` is walked twice with exit 0 when `--cap` is omitted,
and refused when it is supplied — exactly the shape `produces` has. So every authored
edge attribute is elective in the same way.

LET-914's join-fire refusal does **not** break this: it checks `--edge` against the
run-case's OWN branch ledger (which edges branches actually arrived on), never against
the stored def. `advance` still reads no spec, so a def revised mid-run can never
strand an in-flight case.

The READ-time counterpart is `graph-run-case conform` (LET-720), which replays the
ledger against the pinned def — but know **exactly** what it covers before leaning on
it. It reports undeclared edges, unknown nodes, a join fired **below** the declared
quorum, a missing def, an unresolvable pin, and a finished-but-empty walk. It does
**not** check the loop bounds or the router guards: verified live, the run that walked
a `cap: 1` back-edge twice reports `conforms: true`. So do not read a spec's attributes
as runtime guarantees. Read them as the process a run is *claimed* to have followed,
check the claim with `conform`, and know that the caps and guards are outside what it
checks.

**"Against the pinned def" became true only in LET-919 — it was documented here
before it was implemented.** A run-case pins `graph-effective-hash` at open, but
nothing could resolve that hash back to a `spec/<n>/` dir, so `conform` read the
def's **latest** spec. `hash_drifted` honestly reported that the def had moved, yet
the **verdict** was computed against the wrong text: `graph-def revise` silently
changed the basis of every prior run-case's verdict — the walk did not move, the
yardstick did (the LET-727 moved-goalpost shape, one object over). `conform` now
resolves the pinned hash by recomputing each stored version's effective-hash
newest-first and judging against the version that mints it, reporting
`pinned_version` and `latest_version` so the basis is never inferred:

- `pinned_version: 1, latest_version: 3` — judged against v1; the def has since moved on.
- `pinned_version: 0` **with** a `pinned_hash` — `FW-GRAPH-CONFORM-PIN-UNRESOLVABLE`.
  No stored version mints that hash, so the topology walked cannot be recovered and
  **no verdict is computed**. Falling back to the latest here is exactly the defect.
- `pinned_version: 0` with **no** `pinned_hash` — a run-case older than the pin.
  Judged against the latest and labelled as such; absence is history, not guilt.

The per-version hash is **recomputed, never stored**. A stored per-version hash would
be an unverifiable claim: edit `spec/2/content.md`, leave the stored hash alone, and a
pin would still "resolve" to v2 while `conform` judged the walk against text that
version no longer holds. Recomputing makes the version's own bytes the only evidence.

**`open` is gated (LET-733) — the def must EXIST and `--start` must be its
declared start.** `--graph` is a **reference to a stored def's slug**, not a
free-text label: a slug the project does not declare is refused
`FW-GRAPH-DEF-UNKNOWN` (naming the defs it *does* declare), and a `--start` that
is not the def's declared start is refused `FW-GRAPH-START-UNDECLARED` (stating
the start that works). This is what makes the reproducibility pin a **binding**
— an unresolvable `--graph` pins nothing while still minting a well-formed
`grc_…` id that passes every downstream grammar check. Note the second refusal
also blocks opening at a *real but later* node: from there every edge walked is
a declared edge, so `conform` would report `conforms:true` over a run that
skipped the whole process. The gate is on **new opens only** — already-recorded
run-cases keep reading, advancing, closing and validating even if their def is
later revised, renamed or deleted, and `advance` remains graph-def-free.
(A composite whose declared start is a `uses` node accepts either the authored
start or the inlined effective start `compile` enters at.)

```bash
# Open — mints a grc id (grc_…) and records the start node (revision 1).
# The id is in the JSON envelope at .data.data.id — note the DOUBLE data.
# MUTATION envelopes nest the command payload one level under an operation wrapper;
# READ envelopes (list/show) are flat. Reaching for the flat path on a mutation yields
# null, and every later command in this section then runs against an empty id.
lettuce graph-run-case open --graph review-linear --start producer \
  --root "$ROOT" --project "$P" --author "$A" --format json
# -> {"data":{"operation_id":"op-…","data":{"id":"grc_…","state":"producer","revision":1}}}
#    GRC=$(… --format json | jq -r .data.data.id)
GRC=grc_…                            # the minted id from the line above

# Produce the evidence the producer->gate edge emits (write-once, content-addressed).
lettuce carrier produce --graph-run-case "$GRC" --node producer \
  --edge 'producer->gate' --key artifact --value 'art:GATE-1/1' \
  --root "$ROOT" --project "$P" --author "$A" --format json

# Advance across the guarded edge — the produces post-check reads the carrier back.
lettuce graph-run-case advance "$GRC" --from producer --to gate --edge 'producer->gate' \
  --produces artifact --root "$ROOT" --project "$P" --author "$A" --format json

# Advance to the terminal (optionally record what this node changed with --effect,
# and what the step actually COST with --duration-ms / --cost-usd).
lettuce graph-run-case advance "$GRC" --from gate --to terminal --edge 'gate->terminal' \
  --duration-ms 1250 --cost-usd 0.42 \
  --root "$ROOT" --project "$P" --author "$A" --format json

# Inspect derived state (graph, state, revision) and close at the terminal.
lettuce graph-run-case show  "$GRC" --root "$ROOT" --project "$P" --format json
lettuce graph-run-case close "$GRC" --from terminal --outcome completed \
  --root "$ROOT" --project "$P" --author "$A" --format json
```

**Routing out of a `router` (ENACT-5).** When the node being left is a `router`,
pass its **whole** out-edge guard set with repeatable `--route-edge 'EDGE=WHEN'`
(plus `--route-else EDGE` for the single unguarded default). The engine resolves
the guards' facts from **this run-case's own ledger** and decides — the caller does
not get to pick:

```bash
# The producer records the decision as a carrier on the edge into the router.
lettuce carrier produce --graph-run-case "$GRC" --node producer \
  --edge 'producer->route' --key decision --value approved \
  --root "$ROOT" --project "$P" --author "$A" --format json

# REFUSED: the guards select route->approve, so the reject branch is barred.
lettuce graph-run-case advance "$GRC" --from route --to reject --edge 'route->reject' \
  --route-edge 'route->approve=carrier.decision == "approved"' \
  --route-edge 'route->reject=carrier.decision == "rejected"' \
  --root "$ROOT" --project "$P" --author "$A" --format json   # FW-GRAPH-ROUTE-UNSATISFIED

# PERMITTED: the same command along the edge the evidence selects.
lettuce graph-run-case advance "$GRC" --from route --to approve --edge 'route->approve' \
  --route-edge 'route->approve=carrier.decision == "approved"' \
  --route-edge 'route->reject=carrier.decision == "rejected"' \
  --root "$ROOT" --project "$P" --author "$A" --format json
```

Three refusals, each with a different repair, and **all** leave state unchanged:
`FW-GRAPH-ROUTE-UNSATISFIED` (this edge is not the selected one — the diagnostic
names the edge that *is*, plus the facts it read), `FW-GRAPH-ROUTE-AMBIGUOUS` (two
guards true at once, or a carrier fact recorded with two divergent values — never
a silent pick), and `FW-GRAPH-ROUTE-NO-MATCH` (nothing matched and no
`--route-else` — a loud dead-end, not an arbitrary fall-through). The decision and
the evidence it consumed are written onto the `case-advanced` event
(`route-taken`, `route-guard`, and the `route/` + `route-facts/` groups) and folded
into its content-addressed id — so routing is auditable, replay-stable, de-dupes
across clones, and a **fabricated** route is refused at merge-heal (Class-B).

`advance` also takes `--visit N` (loop iteration; default 0),
`--effect 'KIND|REF[|CARRIER]'` (record a mutation this node performed — e.g.
`cell-hardened|<cell-ref>|<carrier-ref>`), and a gate trio (`--gate-task
REF --gate-action ACTION --gate-reason TEXT`) that fires a real task-workflow
transition as a PRE-gate (a missing requirement bites
`FW-WF-REQUIREMENT-UNSATISFIED`). `carrier produce` takes `--ref
'ROLE|REF[|NOTE]'` to attach typed enrichment (evidence/subject/tool/…). A
refused advance or a divergent double-produce leaves the ledger unchanged — the
post-checks are the contract's teeth.

**Run telemetry — what a step actually cost (`--duration-ms` / `--cost-usd`).**
An advance MAY record the **observed** cost of the step it just performed:
`--duration-ms N` (wall-clock milliseconds) and `--cost-usd 0.42` (a
non-negative decimal, at most 6 decimal places — converted **exactly** to
integer micro-USD, so sums never drift and `0.42` / `0.420000` are the same
evidence). Both are stored **verbatim** on the `case-advanced` /
`branch-advanced` event. The engine reads **no clock** for this — *you* supply
the numbers, it records what it is told.

The one rule: **telemetry is EVIDENCE, never CONTROL INPUT.** No derived-state
reader folds it, so routing, joins, loops, the terminal and replay are all
untouched by what a step cost — driving the same transitions with wildly
different telemetry folds to an *identical* state, revision and transition
sequence. It *is* folded into the content-addressed event id (exactly like
`--effect` and the routing decision), so an identical re-record de-dupes while
two clones recording **different** readings for the same transition fork the
chain (Class-B refuse) rather than silently unioning. Because it describes *a
step an actor performed*, it is **refused** (never silently dropped) on the
control-bookkeeping intents `--open-branch` / `--fire-join` / `--wave-close` /
`--loop-exit` — a wave's cost is already recorded by the advances that made up
the wave. A **routed** advance (above) *is* a step an actor performed, so
`--route-edge`/`--route-else` and the telemetry flags compose freely: a router
step records both *why* it went that way and *what it cost*.

`graph-run-case show` reports the per-run totals — refolded from the event log
(main chain **and** every branch), never a stored cache:

```jsonc
"telemetry": {"steps": 2, "duration_ms": 2000,
              "cost_micro_usd": 500000, "cost_usd": "0.5"}
```

Absent entirely for a run that recorded nothing. Pair it with the node
`exec_policy` above and a run answers both *how it was meant to be staffed* and
*what that actually cost*.

### Visualize — `viz` (eyeball the graph)

Render a graph-def — or a running run-case — as a **mermaid** diagram (or DOT) so
you can *see* the topology. mermaid renders natively on GitHub, mobile, and
artifacts and converts cleanly to PNG. Node **shape encodes the concern**
(producer = stadium, gate = diamond, human = parallelogram, router = hexagon,
terminal = subroutine box, fork/join = trapezoids, reviewer/verifier = rectangle;
a `uses` subgraph node = a single cylinder labelled with its ref). A join's label
shows its **quorum** (`join (2 of 3)`), and a **loop back-edge** (cap>0) renders
dashed with its cap + iteration primitives (`cap=5 min_times:1 until_stable:2`).
A node carrying a behavior or an exec-policy gets extra labelled lines
(`behavior: …`, `exec: model=… effort=… residency=…`), so SHAPE, BEHAVIOR and
EXECUTION never blur together. Output is deterministic (sorted) so it diffs
cleanly.

```bash
# A graph-def as mermaid (default). --format mermaid|dot print the raw diagram to
# stdout (pipe it to a .mmd/.dot file or paste into a GitHub/artifact block);
# --format json|yaml wrap it in the envelope's `diagram` field.
lettuce graph-def viz review-linear --root "$ROOT" --project "$P" --format mermaid > graph.mmd
lettuce graph-def viz review-linear --root "$ROOT" --project "$P" --format dot   > graph.dot

# A run-case with its live state OVERLAID: the CURRENT node is highlighted and
# every VISITED node is styled distinctly (classDef current / visited).
lettuce graph-run-case viz "$GRC" --root "$ROOT" --project "$P" --format mermaid
```

`viz` is a pure read. An unknown slug / run-case is `FW-PATH-NOT-FOUND`. The
default view shows a composite (`uses`) node as ONE node — a `--expand` flag that
inlines the referenced subgraph is deferred; DOT is graph-def-only (the run-case
overlay is mermaid-specific).

**An unknown graph ref tells you how to repair it (LET-946).** Every
`FW-PATH-NOT-FOUND` on a graph-def slug or a `grc_…` id — from `graph-def
show`/`compile`/`lint`/`viz`/`revise` and `graph-run-case
show`/`viz`/`conform`/`advance`/`close` — now carries `expected`, `actual` and a
non-empty `suggested_actions[]`, and it distinguishes the two ways a reference can
be unusable. A **malformed** ref answers with the GRAMMAR (`lowercase-letter start
then a-z/0-9/-…` for a slug, `grc_ followed by 32 lowercase hex characters` for a
run-case id), because no stored object could carry that name. A **well-formed but
absent** ref answers with the discovery command (`graph-def list --project P` /
`graph-run-case list --project P`) plus, for a graph-def only, how to author it —
a grc id is minted by the store, so opening a new run-case would produce a
different id and repair nothing. Two refs get no discovery command on purpose: a
missing `uses: <slug>@N` spec **version** is answered by ENUMERATING the versions
that exist (`graph-def show` reports the object revision, not the version count),
and a `--branch` coordinate is answered by `graph-run-case show`'s
`branches[].coordinate`. Read `suggested_actions[]`, not `expected` — the actions
are the machine-readable half.

**Which spec version a run-case viz draws (LET-921).** The topology is the version
the run-case **PINNED** at open — resolved from its `graph_effective_hash` by the
same resolver `graph-run-case conform` uses — *not* the graph-def's latest. So
`graph-def revise` never redraws a run that walked the earlier text, and the picture
agrees with the conform report about the same run. Every diagram **states its basis**
on both surfaces: the envelope carries `basis` (`pinned` | `latest-unpinned` |
`trail-pin-unresolvable` | `trail-def-missing`) with `pinned_version` /
`latest_version`, and the diagram itself carries a mermaid `%%` comment on the line
under `flowchart TD`, so redirecting the raw output to a `.mmd` file keeps the
provenance:

```text
flowchart TD
%% basis: PINNED spec version 1 of 2 of graph-def tcv — the topology this run-case actually walked
```

A run-case with **no** pin (the pre-pin shape) is drawn against the latest spec and
says so — absence is history, not an error. If the def is missing, or the pin matches
none of its stored spec versions (so the latest is a topology the run provably did
*not* walk), viz falls back to the run's own visited trail and names that basis.

### Replay

Because a grc is event-sourced, its whole history is the audit trail: `case-
opened` → `case-advanced`… → `case-closed`, plus the carrier events. `graph-run-
case show` returns the derived scalars (a faithful projection of the fold, heal-
forward-completable after a crash mid-write). The event chain replays the walk
exactly.

### Navigate back-references — `refs-to` (from a cell/object → the run-cases that touched it)

`advance --effect 'KIND|OBJECT-REF[|CARRIER]'` records a FORWARD edge: an effect
whose ObjectRef points at the object the node mutated (a hardened cell, an opened
task). `graph-run-case refs-to OBJECT-REF` **inverts** it (INT-15) — from any
object, find the run-cases that referenced it. So a cell (or an evidence artifact)
answers *which decisions touched me*.

```bash
# A cell is referenced by its coordinate HASH (INT-13): lettuce/cells/<hash>.
# Get the hash from `cell show <coord>` (or it is the effect ref you recorded).
lettuce graph-run-case refs-to lettuce/cells/9f3a1c2b4d5e6f70 --root "$ROOT" --project "$P" --format json
# -> {"data":{"references":[{"graph_run_case":"grc_…","from":"gate","to":"terminal",
#      "edge":"gate->terminal","kind":"cell-hardened","ref":"lettuce/cells/9f3a…",
#      "carrier":"lettuce/carriers/car_…"}]}}
```

Each hit carries the run-case, the transition the effect was recorded on
(`from`/`to`/`edge`), the effect kind, the exact stored ref, and the optional
carrier back-ref to the justifying evidence — every hop a navigable ObjectRef. The
reverse index is **derived = f(events)** (recomputed from the durable event log,
never a cache) with deterministic ordering (grc id, then event order). Matching is
on object identity (`project/kind/id`): querying `lettuce/cells/<hash>` matches an
effect that pinned `lettuce/cells/<hash>@4` (the revision pin narrows a citation,
not the object). It works for **any** object kind, not just cells. An object
nothing references returns an **empty `references[]` with `ok=true`** — "no
back-references" is a valid answer, like an empty query, not a not-found. A
malformed OBJECT-REF is `FW-CMD-USAGE` with the ref grammar. Read-only.

The **cell-facing** view of this reverse index is surfaced inline by `cell show
--with-references` (INT-15b) — so a cell viewer sees *its own* provenance (which
run-case decisions hardened/touched it) without a separate `refs-to` call:

```bash
lettuce cell show area=auth;layer=api --with-references --root "$ROOT" --project "$P" --format json
# -> {"data":{"coordinate":"area=auth;layer=api","state":"hardened",…,
#      "referenced_by":[{"graph_run_case":"grc_…","kind":"cell-hardened",
#      "from":"gate","to":"terminal","edge":"gate->terminal","ref":"lettuce/cells/cf141a…"}]}}
```

`referenced_by` reuses the same `refs-to` reverse index and record shape (same
deterministic order). The flag is **opt-in**: default `cell show` stays cheap and
byte-unchanged (no `referenced_by` key, no grc scan); with the flag the field is
always present in the machine formats (json/yaml) and the human table — an empty
`[]` when nothing references the cell. Under `--format plain` (the pipeable format)
the flag is a no-op and the bare cell renders — read provenance via json/yaml/table.

### Deferred, honestly

**Shipped end-to-end** (authored *and* enacted at runtime): the linear walk,
composition (`uses`/`bind`), structured parallelism (`fork`/`join`/`quorum` →
ENACT-1/2: real concurrent branches and K-of-M join firing), loop iteration
(`min_times`/`until_stable`/`until_drain`/`circuit_breaker` → ENACT-3: real waves,
convergence, drain and breaker trips), and **conditional routing**
(`when` guards on a router's out-edges → ENACT-5: the run-case picks its own
branch from recorded evidence). Each is validated + hashed at author/compile time,
guarded by a precise refusal at runtime, and materialized as catalog patterns
(`audit-wave@v1`, `harden-loop@v1`).

**Not yet shipped:** routing INSIDE an open concurrent branch (`--route-edge` is
mutually exclusive with `--branch`/`--open-branch`); the remaining schema surface
(carrier-*types*, io-contracts); a `graph-def validate` subcommand (soundness is
folded into `create` + `compile`, there is no separate verb);
the named-pattern catalog's `--bind` re-wiring at `use` time; and any HTTP route —
graph commands are **local + dedicated-git only**. Prefer `lettuce usage graph-def`
(and `usage graph-run-case`, `usage carrier`) for the always-current flag
contract.

## 9.1. Every work item is a ticket; every ticket runs a graph-run-case

This is the chain the DoD's **ticket gate** (§8, "Definition of Done") actually
measures: `dod show` is met only at **zero** non-terminal tickets, counting
every task in the store — not the ones an agent remembers to mention. Work
that is never filed is invisible to that count; a run-case opened and then
abandoned proves nothing either. Both halves are required, in order:

**1. File a ticket first.** Before touching anything:

```bash
lettuce task create TASK-N --title "Short title" --body-file ./body.md \
  --root "$ROOT" --project "$P" --author "$A" --format json
```

**2. Open a run-case BEFORE the work, not after.** Open at the def's start
node — the def must exist and `--start` must be its declared start, else
`FW-GRAPH-DEF-UNKNOWN` / `FW-GRAPH-START-UNDECLARED` (§9, "Enact") — then
record the ticket as an effect on the FIRST advance. The ref is
`project/kind-plural/id` — `myproject/tasks/TASK-N`, **never**
`myproject/TASK-N` (that shorter `$P/TASK-N` shorthand is only for
`task`/`lease`/`comment`-style commands, not for object refs like `--effect`
or `refs-to`; the wrong shape is refused with a suggestion naming the fix):

```bash
# review-linear must already be a graph-def in $P, and `producer` must be ITS declared
# start — author it once (§9, "Author") or `graph-def use` a shipped pattern. Check with
# `graph-def list --project "$P"`; the wrong slug or start is refused, not recorded.
lettuce graph-run-case open --graph review-linear --start producer \
  --root "$ROOT" --project "$P" --author "$A" --format json   # -> grc_…
GRC=grc_…
lettuce carrier produce --graph-run-case "$GRC" --node producer \
  --edge 'producer->gate' --key artifact --value 'art:TASK-N/1' \
  --root "$ROOT" --project "$P" --author "$A" --format json
lettuce graph-run-case advance "$GRC" --from producer --to gate --edge 'producer->gate' \
  --produces artifact --effect "task-opened|$P/tasks/TASK-N" \
  --root "$ROOT" --project "$P" --author "$A" --format json
```

**3. Drive it — do the real work between advances, all the way to the
terminal.** An opened-and-abandoned run-case is exactly as unproving as no
run-case at all. Fork nodes: `--open-branch` to open a branch, `--branch
COORD` to advance within one, `--fire-join --quorum K` to reconverge (see
"Enact" above); a plain linear walk just keeps calling `advance` then `close`:

```bash
lettuce graph-run-case advance "$GRC" --from gate --to terminal --edge 'gate->terminal' \
  --root "$ROOT" --project "$P" --author "$A" --format json
lettuce graph-run-case close "$GRC" --from terminal --outcome completed \
  --root "$ROOT" --project "$P" --author "$A" --format json
```

**3b. If the run will NOT be finished, ABANDON it — do not leave it open, and
never delete it (LET-915).** An unfinished run must still get a terminal
disposition, because the ledger has to be able to answer "what happened to this
run" with a name, a time and a reason rather than with silence. `abandon`
resolves the node the case is parked on itself (there is no `--from` — an
abandonment claims nothing about where the run got to, so it bypasses no gate)
and requires `--reason`:

```bash
lettuce graph-run-case abandon "$GRC" --reason 'superseded by a rerun; never walked' \
  --root "$ROOT" --project "$P" --author "$A" --format json
```

It writes the same `case-closed` event as `close --disposition abandoned` —
no second terminal, no second event kind — and REFUSES a run-case that is
already closed (`FW-GRAPH-CASE-CLOSED`): terminal is terminal, and two
dispositions is a fork in the record. **Deleting the directory is not an
alternative**; it destroys the one record that says the process was not
followed, which is the tamper shape this whole family exists to refuse.

Two consequences worth knowing:

- An abandoned run is **EXCLUDED from the conformance denominator and VISIBLE
  in the census**. `graph-run-case conform` reports `excluded: true`,
  `disposition: abandoned` and exits `0` — it never walked, so "did it conform"
  has no answer, and answering "no" would make honest closure cost more than
  leaving the case open. `failed` is NOT excluded: it asserts the process DID
  run, so its walk stays evidence and stays checked.
- Every READ surface now names it (LET-1100). `graph-run-case list` projects a
  per-case `disposition` and counts `abandoned` beside `open`; `show` adds
  `disposition` plus the `outcome` carrying the abandonment's required
  `--reason`; `viz` puts it in the DIAGRAM BYTES as a `%%` comment beside the
  basis, not only in the envelope. Before that, an abandoned case was
  byte-identical to a completed one on all three, so this doctrine was stated
  and unmeasurable — and the required reason, whose whole purpose is to answer
  "what happened to this run", could only be read by `cat`-ing a file inside a
  content-addressed event directory. **Read `open` as "never closed", not as
  the unproving population: that is `open + abandoned`.**
- Abandoning a run with concurrent branches still open is allowed and WARNS
  (`FW-GRAPH-RUN-CASE-BRANCHES-UNRESOLVED`), naming each leftover tip. A branch
  has no disposition of its own: the abandonment collapses the MAIN state and
  leaves every tip where it stood, because stamping a terminal onto a branch
  nobody walked would invent an event that was never enacted. `show` keeps
  listing those branches — after a close they are the record of where each one
  stopped, not work in flight.

**4. The ticket already carries the grc — query it back from either side.**
The `--effect` at step 2 recorded the reference, so the evidence is queryable
from the work item with no extra write:

```bash
lettuce graph-run-case refs-to "$P/tasks/TASK-N" --root "$ROOT" --project "$P" --format json
```

**4b. Make the chain ENFORCED, not merely conventional.** Declare a custom field
of `value-kind graph-run-case` and gate the transition on it. The gate then
requires **both** that the cited run-case EXISTS and that its ledger records a
produced-effect on *this* task — the reverse of the `refs-to` above, read from
the same index. Existence alone is not enough: any real grc id can be copied
from another ticket:

```bash
lettuce registry create custom-field grc --value-kind graph-run-case \
  --root "$ROOT" --project "$P" --author "$A" --format json
# …then a workflow transition carrying requires_field: ["custom/grc"], and:
lettuce custom set TASK-N grc "$GRC" --root "$ROOT" --project "$P" --author "$A" --format json
```

Refusals name which half failed, and the code — never the prose — is the
contract: `FW-WF-REQUIREMENT-REFERENT-MISSING` (no such run-case),
`FW-WF-REQUIREMENT-REFERENT-UNLINKED` (a real run-case that never touched this
task), and `FW-WF-REQUIREMENT-REFERENT-INCOMPLETE` (a real, linked run-case whose
own graph-def declares carriers it never produced). All exit `1`, like every other
refused transition.

**What this proves is LINKAGE plus DECLARED EVIDENCE.** Linkage alone was
forgeable (LET-1446): one `advance` carrying a `transition` effect that names the
ticket satisfied the gate exactly as well as a completed walk, leaving the
run-case parked at its start node with zero branches. The gate now also reads the
run-case's *own* graph-def and requires a carrier for every edge that def declares
one on — def-driven, so a graph declaring no carriers is never asked for any, and
a run-case whose def or pin cannot be resolved is reported unjudged rather than
refused.

It is still a narrower claim than conformance: that the walk *conformed* to the
def is `graph-run-case conform`; that the workflow policy itself was not edited
under you is the workflow policy pin (`workflow show`). And carrier production is
not constrained by the def, so a run-case may hold carriers on edges its def never
declared — this gate does not examine those.

**5. DoD is a strict AND of three gates, never a blend** —
`lettuce dod show --project "$P"`: every scope's coverage cells clear the
grade/depth/recency floor, every **committed** milestone is reached
(`hypothesis`-stage and canceled milestones are excluded as bets, not
commitments), and the ticket gate above reads zero-open. Each gate is a
**count**, never a percentage; one weak gate blocks the whole verdict even if
the other two read 100%.

## 10. Output formats and exit codes

`--format table` (human default), `plain`, `json`, `yaml` (machine envelopes),
and `markdown` (only `doctor`, `usage`, and `skill`). Two more formats are
command-specific: `okf` (the usage-bundle export — `lettuce usage --format okf
--out DIR`) and `html` (`lettuce board render --format html`). Errors exit non-zero with
a distinct code so automation can branch on the process exit code: `1`
validation or invalid input, `2` object not found, `3` expected-revision
mismatch, `4` lock acquisition failure, `5` Git mode failure, `6` interrupted
operation requires recovery, `7` internal error (success is `0`). Human output
prints `lettuce: <message>` plus `-> <suggested action>` lines.
`lettuce --help`/`-h` prints a short hint; `lettuce usage` prints everything.
Supported commands also accept a structured input envelope via
`--from-json PATH` / `--from-yaml PATH` (see each command's
`external input keys` in `lettuce usage`).

## 11. Modes

- **Local** (default): operate on a filesystem store via `--root`.
- **Dedicated Git**: `--mode dedicated-git` makes each mutation a Git commit
  and **fetches the upstream before every mutation** — a strongly-consistent
  shared store that requires connectivity by design (not an offline mode);
  requires a clean, synchronized worktree. `sync status|push|pull` manage it.
- **HTTP client**: set `LETTUCE_SERVER_URL` (+ `LETTUCE_BEARER`/`_FILE`,
  `LETTUCE_ACTOR`) or pass `--server-url`, and the identical CLI dispatches
  over HTTP. The binary name is cosmetic — symlink it to anything.

**What works over the HTTP client.** Almost everything — the exceptions are narrow. The
authoritative per-command answer is the `kind` field of `lettuce usage <cmd> --format
json`.

| Command / group | HTTP client | Notes |
|---|---|---|
| Work plane: `project`, `task` (incl. `transition`), `comment`, `lease`, `run`, `artifact`, `author`, `version`, `custom set` | ✓ | full CRUD |
| Registry + milestones: `registry create/update`, `milestone create/close/set-stage` | ✓ | |
| Coverage cells: `cell set` · `clear` · `transition` · `evidence add/remove` · `gate check` · `list` · `show` · `rollup` | ✓ | |
| Dimensions: `dimension declare` · `rename` · `member add/update` · `list`/`show` | ✓ | |
| Definition of Done: `dod set` · `dod show` | ✓ | |
| Board: `board export` · `render` · `next` | ✓ | frontier/HTML computed client-side from the fetched export |
| Queries: `query run`/`tasks` (incl. `--group-by`) · `search` · `audit` · `timeline` · `graph` · saved queries | ✓ | |
| Maintenance: `init` · `status` · `validate` · `doctor` · `import` · `export --bundle` · `recover` (incl. `--abandon`) · `reconcile` · `cleanup` · `repair` | ✓ | |
| `cell set-where` · `clear-where` · `note` · `import` · `verify` · `affirm` | — | **local / dedicated-git only** (bulk & confirmation ops; a single cell note is still reachable over HTTP via `cell set --note`) |
| `serve` · `sync status/push/pull` · `github init-repo` | — | **local / server-only** — refused in client mode (`serve` starts a local process; `sync`/`github` act on the local working copy + Git remote) |

## 12. Invariants and gotchas

- **Path jail.** All references are validated slugs/paths inside the store
  root; traversal and malformed components are refused (`FW-PATH-*`). Input
  *files* (e.g. `--body-file`) follow symlinks; store-internal guards do not.
- **Faithful round-trip.** Adversarial Unicode (bidi, homoglyphs, zero-width)
  is stored and returned byte-faithfully; input rejects only format-breaking
  control characters. "Accepts suspicious Unicode" is by design.
- **Single writer.** One mutation at a time per store. A killed writer can
  leave the lock held (`FW-RUNTIME-WRITER-ACTIVE`); `lettuce recover` heals it
  when the owner is provably gone (`--abandon` when liveness is unknowable).
  Exit code 6 = run recovery.
- **Soft vs hard removal.** `archive` (task/project/comment/artifact) is
  reversible and history-preserving (hidden from lists, still searchable);
  `delete` is permanent and needs `--yes` (+ `--cascade` for
  children/non-empty).
- **Forced overrides need a reason.** `lease release --force`, `lease steal`,
  `run finish --force` all require `--reason` — they override another holder.
- **Bodies and artifacts are immutable.** New body/comment/summary versions
  are appended; artifact payloads are never edited (`artifact replace` makes a
  new revision).
- **Additive dimension layer.** A project's runtime dimensions can never
  shadow or edit pack vocabulary; refusals happen before any write.
- **Diagnostics are the API.** Every error carries `code` (e.g.
  `FW-WF-REQUIREMENT-UNSATISFIED`), `expected`/`actual`, `repairability`, and
  `suggested_actions`. Branch on codes, not message text — they tell you the
  fix. After any repair, import, or conflict resolution, re-run
  `validate --strict` and `doctor`.
