# Master Work Register — data contract

`register.json` in this folder drives `/register/` and is the ONE file
the register's surfaces read: the workbench page (the live surface),
the two-hour loop and exceptions engine (AF-94), and the daily pass's
register checks (AF-95). No second copy exists. It is **GENERATED,
never hand-edited** — with one declared exception: its `declared_rows`
section is written by desks, under the tracking rule below. Everything
else in the file is rewritten by the generator on every run.

Producer: `scripts/build_work_register.py` (in this repo), run by the
sweeper desk (generation duty of record, AF-93) from the daily pass's
fleet-sync board mirrors. The board list is configuration:
`scripts/work_register_boards.json` — a seventh board enrolls by
configuration alone. The boards remain the source of truth for
stories; the register is a rendered view plus an exceptions engine,
not a second record.

## Shape

```json
{
  "schema":        { "...": "the row schema of record (below, machine-readable)" },
  "generated_at":  "2026-10-20T16:40:00Z",
  "generator":      { "tool": "...", "boards_config": "...", "bases": {"AO": {"repo": "...", "base": "<mirror commit>"}} },
  "declared_rows": [ "layer-2 / backlog declarations, carried verbatim" ],
  "rows":          [ "the full register: layer 1 derived + declared, merged" ],
  "summary":       { "totals, per-board counts, rollup by sprint, exception row ids" }
}
```

## Row schema (of record — AF-93; the planner's groom, sha256
207ea04667480cccc696fc5bf744e304b3f312408b3afee7ed93d156e9ce1637)

| Field | Meaning |
| --- | --- |
| `row_id` | Stable id. Layer 1: the board story id. Layer 2 / backlog: an **MWR-###** sequence issued by the creating desk at the creation act; ids are never reused. |
| `layer` | `1` board stories · `2` in-between work · `"backlog"` standing non-project work. Layer 3 (owner action queue) is reserved by the owner's parameter — not in launch scope; the vocabulary leaves it room. |
| `kind` | `story` · `epic` · `consult` · `handoff` · `review` · `gate-in-formation` · `decision-question` · `backlog-item` — `epic` is layer 1 only: a board face of type epic, carried as a parent (grammar determination below); it is never a declared row. |
| `title` | The work, in one line. |
| `project` | Program or project the work belongs to. |
| `repo` | Repo the work's record lives in, where one does. |
| `holder` | Who holds it now: a desk id, `review-lane`, or `the owner`. |
| `state` | Board vocabulary: `backlog` · `ready` · `in-progress` · `in-review`. Done stories are not live work and carry no row. Layer 2 uses the same vocabulary where it fits. |
| `context` | Sprint id or parent context (epic / parent item). |
| `points` | Story points where priced, else null. Always an integer or null in this file — the generator coerces (integral floats and digit strings become ints; booleans and anything else become null), because the render interpolates the field directly (verdict 177, closed by AF-94). |
| `since` | When the current holder/state began. |
| `last_moved` | Latest evidence timestamp on the source. |
| `lease_until` | Layer 1: the live claim's lease expiry from the face, carried so AF-94's engine can evaluate the claim-liveness signal from this file alone. Null when no lease stands. (AF-94 schema extension.) |
| `waiting_on` | What the work waits on, in one line. |
| `stall_class` | `two-hour` · `build` · `gate` · `backlog` — the stall signal family the AF-94 engine evaluates this row under. Epic rows carry null: a parent stands outside stall evaluation by grammar. |
| `source_pointer` | **Mandatory.** `<repo>:<path>` for layer 1; the dispatch record for layer 2 / backlog. No row without a pointer. |
| `exception` | True when the row is malformed, pointer-less, or its source failed validation; the AF-94 loop also sets it for stalls and clears it when the row moves. |
| `exception_reason` | Why, when `exception` is true; else null. |

## Holder derivation (layer 1, mechanical — from the story's face)

1. `blocked: true` → `blocked_owner` (the named owner of the block);
   `waiting_on` carries the face's `action_needed`.
2. `claimed_by` set → that desk (the holding desk).
3. `in-review` → `review-lane` (the §3 pair: code quality + security);
   `waiting_on` names the acceptance owner behind the review.
4. An `owner` assignee on the face → that assignee.
5. Otherwise → `unclaimed`.

`last_moved` (layer 1) is the latest of the face's `updated`,
`heartbeat_at`, `claimed_at`, `blocked_since`, `completed` stamps;
`since` is `claimed_at`, else `blocked_since`, else `updated`.

## Stall classes (classification — thresholds live in AF-94's engine)

- **two-hour** — handoffs, reviews, claims: work whose next act is
  another desk's prompt move (story states ready / in-review; layer-2
  consults, handoffs, reviews).
- **build** — stories in-progress under a live claim, evaluated under
  the owner's ruled dual signal (a build-stall signal + a
  claim-liveness signal) under the flat 24-hour outer bound: nothing
  unmoved crosses 24h unflagged, regardless of class (owner's ruling,
  2026-10-20).
- **gate** — gates in formation, decision questions, blocked work:
  age carried in the open, flagged on its own terms.
- **backlog** — backlog stories and backlog items: captured and aged
  in the open.

Declared quiet periods (the daily maintenance window, 01:00–02:00
America/Chicago) are evaluation context for the engine, not a class.

## The exceptions engine (AF-94) — evaluation of record

`scripts/stall_engine.py` runs after every generation in the
two-hour loop. Thresholds, quiet windows, and report routing are
CONFIGURATION — `scripts/work_register_engine.json`, sibling to the
board list; the engine carries no constants of its own. The engine
annotates this file in place: a stalled row's `exception` /
`exception_reason` (composed with, never erasing, a generator
validation reason) plus a per-row `stall` object (tripped, signals,
effective_age_hours, evaluated_at). The next generation rewrites
rows from the boards and the engine re-derives every annotation
from `last_moved` / `lease_until`, so nothing is lost between
cycles.

The ruled rule (owner, 2026-10-20; AF-94's face): **two-hour**
class trips at >2h unmoved · **build** trips on build-stall (>48h
unmoved) or claim-liveness (heartbeat past `lease_until` + 2h,
unmoved since the lease) · the **flat 24-hour outer bound** trips
first over both classes — nothing unmoved in them crosses 24h
unflagged · **gate** and **backlog** carry NO hour bound: their
ages are computed every cycle and carried in the open (the daily
digest lists gate rows aged).

**Quiet-period treatment (of record): EXCLUDED FROM AGE.** The
overlap between a row's unmoved span and a declared quiet window
is subtracted before any threshold applies — unmoved-ness accrued
inside the 01:00–02:00 maintenance pause never trips a flag.

Loop state lives at the sweeper desk, never in this repo:
`~/workspace/sweeper-register/state/` (`loop-state.json` — the
previous cycle's stall exception set + digest state; `events.jsonl`
— every raise/clear event, the daily digest's source;
`cycles.jsonl` — one line per cycle). Reporting: a cycle reports
its stall delta (raised / cleared) to the chief **on change only**
— an unchanged exception set is silence. Once daily, at the first
cycle of the local day at/after 06:00 America/Chicago (the cycle
nearest the 06:37 daily pass; the anchor is configuration), the
day's exception events + every gate row's age route to the
escalation chat as ONE digest entry.

## The tracking rule (standing, the owner's)

All work gets tracked: commissioning or dispatching anything creates
its register row **in the same act**. A desk declares a layer-2 or
backlog row by adding it to `declared_rows` in this file (guarded
publish, in the same act as the dispatch), issuing the next MWR-###
id. Unregistered work is a defect — the daily pass's check (AF-95)
is the backstop, never an auto-created row: the register stays
generated-and-declared, never hallucinated.

Declared-row required fields: `row_id` (MWR-###), `layer` (2 or
"backlog"), `kind`, `title`, `project`, `holder`, `state`,
`waiting_on`, `source_pointer`, `created`, `last_moved`. Optional:
`repo`, `context`, `points`, `stall_class` (derived from kind when
absent).

## Declaring and closing rows — the cured path (2026-10-20)

Desks declare, update, and close declared rows ONLY through
`scripts/declare_register_row.py` — the canonical declaration
path, landed after the MWR-035 integrity event (a declared row
dropped by a whole-file publish composed from a stale local
copy). The helper seals every write the same way: it fetches the
live file in the same minute as the publish, composes exactly
one named change on the live bytes, asserts the declared census
before and after publishing, and refuses loudly rather than
overwrite. Whole-file publishes of this file composed from
local copies — however fresh the publisher's base sha looks —
are outside the discipline: gh_file.py's staleness guard covers
kanban story files only and does not seal this path.

- **Ids** issue against the live file read within the same
  minute as the publish (the chief's issuance guard) AND against
  the register's commit history. The live maximum is not a
  history: a row closed by removal vanishes from it, and an id
  issued, closed, and re-issued in one evening then denotes two
  work items (the MWR-039 denotation break, 2026-10-20 — no desk
  in violation; the guard as first operable could not see it).
  The helper computes `NEXT` over every id ever issued in the
  file's history ∪ the live set (operationalized as a
  watermarked ledger derived from that history), refuses any id
  history shows as issued — standing or not — and fails closed
  when the history cannot be read. A never-issued gap at or
  below the live maximum needs `--allow-reissue` to name the act.
- **Tombstones.** The file carries its own issued-id memory: a
  top-level `issued_ids` array holding every id ever issued,
  never shrinking. The helper unions it forward on every write
  and adds each new id at issuance; the generator unions it from
  its inputs under the same seal as the declared set; the loop
  guard's verify refuses a candidate whose tombstone set shrinks
  the live one. Closure removes a row — never its tombstone. The
  commit history remains the derivation of record; the tombstone
  set is its carried, checkable form.
- **Closure is removal.** The state vocabulary carries no
  closed state; a declared row closes by being REMOVED entire,
  by its holder desk, through the helper's `close` operation,
  which requires `--confirm-close` to name the act. No other
  write path may remove a row: generation and the two-hour loop
  change NO declared row — neither drop, edit, nor resurrect.
- **Generation** takes its declared set from the live head:
  the generator's `--preserve-from` (a fresh live read) carries
  the live declared rows verbatim and names every divergence a
  stale working copy would have caused; the loop's
  `scripts/register_guard.py` prepare/verify wraps the cycle the
  same way, with a stand-down-unpublished path instead of an
  overwrite.

## Validation (the generator's, every run)

- Every live story (status ≠ done) across the configured boards
  appears exactly once; a duplicate `row_id` is an exception on each
  duplicate row — never a silent drop.
- A story file whose frontmatter fails strict YAML parsing is
  recovered by a tolerant parse and carried with `exception: true`
  naming the failure — the board's defect is the register's
  exception, and the row stays traceable to its source pointer.
- A row missing `row_id`, `title`, or `state` after parsing is
  carried with `exception: true` naming the missing fields.
- A declared row missing a required field, carrying an id outside
  the MWR-### convention, an unknown kind, or no source pointer is
  carried with `exception: true`. Declared rows are validated, never
  edited and never dropped by the generator.
- If this file's JSON itself is unparseable, generation fails loud
  and publishes nothing — the last good register stands.

## The durable record + the daily pass's checks (AF-95)

The register's durable record is a docsite page —
`docs/03-projects/agent-fleet-runtime/master-work-register.md` in
jknash/docsite — rendered from THIS file by
`scripts/build_work_register_snapshot.py` and re-landed daily by
the sweeper daily pass through the docsite prepublish gate: the
day's register as generated + annotated, one page updated in
place. The workbench render is the live surface; the snapshot is
the durable record; the two derive from the same data home state,
and any difference is the cadence itself (the loop republishes
the live data up to every two hours; the snapshot lands once a
day).

The daily pass also runs this file's two standing checks:

- **Data validation** — `scripts/validate_work_register.py`
  reads this file against the contract above: sections present,
  every row carrying its full field set with a non-empty
  `source_pointer`, points int-or-null, layer / kind / state /
  stall_class inside the vocabulary, exception rows carrying
  their reason, row ids unique, declared rows complete and
  carried exactly once, the summary agreeing with the rows.
  Violations surface as named findings in the pass report. An
  exception flag itself is NOT a violation — generator and engine
  exceptions are data the register carries on purpose.
- **Unregistered-work check** — `scripts/check_unregistered_work.py`
  compares the work items the pass observed in its window
  (dispatches, consults, handoffs, claims) against this file's
  rows (row-id or source-pointer match; a backlog row counts as
  tracked). Work with no row raises a DQ entry in the pass's
  ordinary form, decider the chief. The check never creates a
  row — it is the tracking rule's backstop, not a second
  declaration path.

Grammar determinations (the chief's rulings of 2026-10-20,
landed by AF-95's composite closing act, 2026-10-21):

**Epics carry as parents, not work items.** A board face of
`type: epic` derives a register row of kind `epic` with NO stall
class: the row renders and groups on the register (its children
carry the epic's id as their context where no sprint does), is
aged in the open, and stands OUTSIDE stall evaluation — the
engine computes its age and never trips it. Its children derive
their own kinds and classes and are fully bound by them.
Program-level attention for an epic's lane rides a declared
layer-2 gate or decision row, which flags in its own class; a
program whose children are all terminal sits quiet under its
parent's row — a true state. First application: KL-00's standing
outer-bound flag (first observed at 230.5h while the question
stood open) resolved BY RECLASSIFICATION at the first generation
under this grammar — a reclassification, never a cure; and, as
the checks were built, neither the daily pass's structural check
nor its unregistered-work check manufactures a finding from
epic/child structure in either direction, and no desk is asked
to cure an epic row.

**Cross-board supersession is admitted in board-qualified
form.** A story face's `superseded_by` may carry
`<BOARD>:<story id>` (for example `IT:IT-08`), naming the
delivering story on another board; the board vocabulary is this
register's boards of record (AO, KL, IT, WB, CM, RR —
`scripts/work_register_boards.json`). The single-board validator
(`kanban/kanban.py`, one identical tool in every board repo)
checks a QUALIFIED reference for FORM only — a known board
prefix and a well-formed story id — and a BARE reference for
existence in its repo's board family, as before; a qualified
target's existence is verified where all boards are visible
(the register loop's mirrors), never from one board's
filesystem. First application: WB-003, cured by qualifying its
reference to IT-08 on the IT board — the substance its face
recorded at its close in 2026-10 (the workbench launch tile,
which IT-08 genuinely delivered).

## Summary field: reports_this_week (WB-013, 2026-10-28)

`summary.reports_this_week` is the motion formation's record-port
feed: the number of commits to `jknash/docsite` that touch `docs/`
whose committer date falls in the trailing 7 days at generation
time — the window (generated_at − 7 days, generated_at],
start-exclusive and end-inclusive. The generator reads the GitHub
commits API (path-filtered to `docs`) in the same generation act,
against the docsite head pinned once at the act's start, and
re-filters by committer date on the client, so the count is
exact at the window edges. The value is an integer ≥ 0 (a quiet
week is a true 0), or `null` when the API read fails at
generation: null means "not counted", never zero, and the next
generation retries. No page fetches this cross-repo — pages read
the field from this file, like every other summary value.

Keep secrets and compartmented material out — this file renders on
the workbench (behind Access, but still).
