# 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 --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` set | Full server Postgres | the configured database | | `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. The schema (`backlog` table) is created automatically on first CLI use: `runMigrations()` for Postgres, `runPgliteMigrations()` for embedded PGlite. ### 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 ` filters to one board lane. `priority` orders cards **within** the ready pool regardless of phase. ## Status lifecycle ``` 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 ` (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 ` force-releases a specific card regardless of expiry. This is how a crashed worker's card returns to the pool. ## CLI — `mosaic fleet backlog --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 --id ]` | Atomically claim the highest-priority ready card (or `--id`). Returns the card or `null`. | | `reclaim [--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 ```sh # Seed two cards, the second depends on the first. mosaic fleet backlog create --id A1 --title "schema" --priority 5 mosaic fleet backlog create --id A2 --title "service" --depends-on A1 --priority 9 # A2 is gated on A1, so claim returns A1 first. mosaic fleet backlog claim --owner worker-1 --ttl 600 --json # Finish A1; now A2 is ready. mosaic fleet backlog complete --id A1 mosaic fleet backlog list --ready-only --json # Recover stalled work. mosaic fleet backlog reclaim --json ```