# Fleet Backlog Conventions

The **backlog** is Mosaic's native backlog-of-record for fleet work. It is built
end-to-end on Mosaic's own storage layer (`@mosaicstack/db`, drizzle/Postgres)
and surfaced as mosaic fleet backlog <sub> --json.

> **Mosaic-native, no Hermes.** This backlog REPLACES the former Hermes adapter.
> There is **no** runtime dependency on Hermes, hermes kanban, or ~/.hermes
> anywhere in this feature. Anything previously delegated to Hermes is recreated
> here on Mosaic's own Postgres storage layer.

## Storage tier — PGlite by default, Postgres by config

The backlog uses the existing Mosaic storage layer; there is **no** new database
engine (no sqlite, no raw client).

| Condition                          | Tier                 | Data location                                                    |
| ---------------------------------- | -------------------- | ---------------------------------------------------------------- |
| `DATABASE_URL` injected at runtime | Full server Postgres | the verified runtime database; it never authorizes migration/DDL |
| `PGLITE_DATA_DIR` set (no URL)     | Embedded PGlite      | that directory                                                   |
| neither (default)                  | Embedded PGlite      | ~/.config/mosaic/fleet/backlog                                   |

PGlite is real Postgres semantics in-process — including the row locks the atomic
claim relies on — so the **same code** runs on a laptop (embedded, single-host
default) and on a full Postgres deployment. Switching tiers is config-only.

For embedded PGlite only, the local backlog routine may prepare its local schema on first use. **Current operator behavior is PGlite-only.** The PostgreSQL path is held until KBN-101 activation; no current PostgreSQL CLI route, runner, or first-use migration is available or authorized. A future activated PostgreSQL runtime may connect only after its separately certified readiness gate.

### Update safety

The embedded PGlite store lives under ~/.config/mosaic/fleet/backlog, which is
listed in `PRESERVE_PATHS` in `packages/mosaic/framework/install.sh`. This means
mosaic update (which runs the framework sync with rsync --delete) will **not**
wipe the operator's backlog — same protection as the roster, per-agent env, and
heartbeat run dir.

## Card schema

A card is one row in the `backlog` table:

| Column              | Type                | Notes                                                         |
| ------------------- | ------------------- | ------------------------------------------------------------- |
| `id`                | text (PK)           | Stable, caller-supplied id (e.g. `A4`, `fleet-001`).          |
| `title`             | text                | Required.                                                     |
| `body`              | text (nullable)     | Free-form description.                                        |
| `phase`             | text (nullable)     | Board/phase grouping (see below).                             |
| `priority`          | int (default 0)     | **Higher = sooner.** Claim picks the max-priority ready card. |
| `status`            | enum                | `ready` \| `claimed` \| `blocked` \| `done`.                  |
| `depends_on`        | jsonb string[]      | DAG edges — ids of cards this one depends on.                 |
| `claim_owner`       | text (nullable)     | Owner token of the active claim.                              |
| `claim_ttl_seconds` | int (nullable)      | TTL of the active claim.                                      |
| `claimed_at`        | timestamptz (null)  | When the claim was taken. claimed_at + ttl = expiry.          |
| `attempts`          | int (default 0)     | Incremented each time the card is claimed.                    |
| `idempotency_key`   | text (unique, null) | Dedups `create`; NULLs are distinct in Postgres.              |
| `acceptance`        | jsonb (nullable)    | Acceptance criteria (array of strings or object).             |
| `created_at`        | timestamptz         |                                                               |
| `updated_at`        | timestamptz         |                                                               |

`depends_on` is modeled as a `jsonb` array column rather than a separate edge
table. Justification: it matches the repo's existing style (e.g. `tasks.tags`,
`agents.skills`, `routing_rules.conditions` are all jsonb arrays), keeps a card
self-contained, and the DAG is small (per-card dependency lists), so a join table
would add ceremony without benefit.

### Board / phase convention

`phase` is a free-form grouping string used as the board column / milestone label
(e.g. `M1`, `fleet`, `infra`). list --phase <phase> filters to one board lane.
`priority` orders cards **within** the ready pool regardless of phase.

## Status lifecycle

```text-diagram
            create
              │
              ▼
   ┌──────► ready ───── claim ─────► claimed ───── complete ─────► done
   │          │                         │
   │       block                  reclaim (TTL expiry or --id)
   │          ▼                         │
   │       blocked                      └──────────────────────────┘ (back to ready)
   └──────────┘  (reclaim / re-create can return a card to ready)
```

- **ready** — eligible to be claimed once every `depends_on` card is `done`.
- **claimed** — a worker holds it; `claim_owner` + `claimed_at` set.
- **blocked** — explicitly parked; never auto-claimed.
- **done** — completed; satisfies dependents.

## Atomic claim (FOR UPDATE SKIP LOCKED) + TTL

`claim` is atomic. Inside a single transaction it locks candidate `ready` rows
with SELECT ... FOR UPDATE SKIP LOCKED (via the drizzle `sql` operator), picks
the highest-priority deps-satisfied card, and flips it to `claimed`. Because a row
already locked by a concurrent claimer is **skipped**, two claimers can **never**
both win the same card — the loser falls through to the next candidate or gets
`null`. (Proven by the concurrency tests in `packages/db/src/backlog.spec.ts`.)

- **Deps gate:** a card is only claimable when every id in `depends_on` is `done`.
- **TTL:** claim --ttl <sec> (default **900s**) records `claim_ttl_seconds`.
- **reclaim:** releases claims whose claimed_at + ttl is in the past (expired)
  back to `ready`, clearing the claim fields. reclaim --id <id> force-releases a
  specific card regardless of expiry. This is how a crashed worker's card returns
  to the pool.

## CLI — mosaic fleet backlog <sub> --json

All subcommands support `--json`.

| Subcommand                                                                                  | Purpose                                                                                   |
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| create --id --title [--body --phase --priority --depends-on --acceptance --idempotency-key] | Create a card; `idempotency_key` dedups (repeat returns the existing card).               |
| list [--status --phase --ready-only]                                                        | List cards. `--ready-only` = status `ready` AND all deps `done`.                          |
| claim --owner [--ttl <sec> --id <id>]                                                       | Atomically claim the highest-priority ready card (or `--id`). Returns the card or `null`. |
| reclaim [--id <id>]                                                                         | Release expired claims (or a specific card) back to `ready`.                              |
| link --from --to                                                                            | Add a `depends_on` edge (`--from` depends on `--to`).                                     |
| `stats`                                                                                     | Counts by status, oldest-ready age, expired-claim count.                                  |
| block --id                                                                                  | Set a card to `blocked`.                                                                  |
| complete --id                                                                               | Set a card to `done` (releases any claim).                                                |

### Example

Seed two cards; the second depends on the first. Because A2 is gated on A1, claim returns A1 first. Finish A1, then list A2 as ready. Recover stalled work.

```fleet-command
mosaic fleet backlog create --id A1 --title "schema" --priority 5
mosaic fleet backlog create --id A2 --title "service" --depends-on A1 --priority 9

mosaic fleet backlog claim --owner worker-1 --ttl 600 --json

mosaic fleet backlog complete --id A1
mosaic fleet backlog list --ready-only --json

mosaic fleet backlog reclaim --json
```
