# Events: the shared contract The Go server emits these over Server-Sent Events (SSE). The web UI turns them into Rive state machine inputs. Both sides build against this file; change it first, then the code. Each SSE message: `event: ` and `data: `. Every payload includes `list_id` and `ts` (RFC 3339). | Event | Payload (besides list_id, ts) | Meaning | |---|---|---| | `list.started` | `items: [{item_id, text}]`, `requester` | Flock accepted a list | | `item.triaged` | `item_id`, `mode` (solo/split/murder), `task_type`, `subtasks[]`, `reason`, `needs_approval` (bool) | Sentry made its call | | `crow.flying` | `item_id`, `crow` | A crow (or Sentry, Elder) took off | | `crow.landed` | `item_id`, `crow`, `ok` (bool), `tokens`, `error?` | A crow came back, or didn't | | `item.answered` | `item_id`, `answer`, `crows[]`, `needs_approval` (bool) | Result ready | | `verdict` | `item_id`, `target` (triage/output), `verdict` (approved/rejected/auto/timeout) | Judgment recorded | | `trust.changed` | `holder` (crow name or "flock"), `task_type`, `from`, `to` (0 stranger, 1 regular, 2 trusted) | Level up or down | | `item.skipped` | `item_id`, `reason` | Item dropped (rejected triage, budget, failure) | | `requester.pranked` | `message`, `cooldown_s` | Abuse response | | `list.finished` | `tokens_used`, `token_budget` | Done | ## HTTP API | Method | Path | Body / query | Returns | |---|---|---|---| | POST | `/api/lists` | `{"text": "", "requester": ""}` | `{"list_id"}`, or 429 with `{"message","cooldown_s"}` when pranked | | GET | `/api/events` | `?list_id=` | SSE stream | | POST | `/api/verdicts` | `{"item_id", "target": "triage"\|"output", "approve": bool}` | 204 | | GET | `/api/trust` | | ledger JSON | Pending approvals that get no answer within 10 minutes count as rejected (`verdict: timeout`). Fail safe. ## Settled details - `item_id` is `-`, numbered from 1 in list order. - `POST /api/lists` returns 202. Bad input returns 400 with `{"message"}`. - `POST /api/verdicts` returns 404 when nothing is waiting (including a second answer), 400 for a bad body. `approve` is required, so a missing value is never read as a no. - When trust skips approval, the server still emits `verdict: "auto"`, so every target gets exactly one verdict. - `crow.flying` and `crow.landed` fire for the Sentry and Elder too. - `requester.pranked` goes to every list of that requester's still flying; if none, only the 429 is sent. - Each SSE message carries an `id:` line, so clients can reconnect with `Last-Event-ID`. Heartbeat comment every 15 seconds. - The server listens on localhost only by default. There is no login yet, and requester names are self-asserted. ## Nests A nest is a room where a team of crows works together. You create a nest and pick its core crows; when a job needs more crows than the core has, the Sentry borrows from the rest of the flock. The Sentry and Elder serve every nest and are never core members. - The default nest is `todo`, with an empty core: its crows come from the whole flock, so nothing there is ever "borrowed". - Crows are picked core-first (rotating within the core), then borrowed from outside it (rotating), still capped by the flock guardrails. - Nests persist on the server (`.crows/nests.json`). | Method | Path | Body | Returns | |---|---|---|---| | GET | `/api/nests` | | `{"roster": ["Hugin", ...], "nests": [{"nest_id", "name", "crows": [...]}]}`; roster is the 8 crows (not Sentry or Elder) | | POST | `/api/nests` | `{"name": "Launch copy", "crows": ["Pepper", "Inky"]}` | 201 with the nest; 400 with `{"message"}` if invalid | | POST | `/api/lists` | adds optional `"nest_id"` (default `todo`) | 404 with `{"message"}` for an unknown nest | Guardrails: name 1–40 characters after trimming; `nest_id` is a slug of the name (lowercase, `a-z0-9-`), unique (409 with `{"message"}` on a clash); `crows` must be roster names, no duplicates, 1–8 of them; at most 20 nests. Event changes: - `list.started` adds `nest_id`. - `crow.flying` adds `borrowed` (bool): true when the crow is not in the nest's core. Always false for Sentry, Elder, and the `todo` nest. ## Personas and banter | Method | Path | Returns | |---|---|---| | GET | `/api/crows` | `[{"name", "epithet", "strength", "backstory"}]` for all ten crows | | Event | Payload (besides list_id, ts) | Meaning | |---|---|---| | `crow.banter` | `item_id`, `crow` (speaker), `to` (the crow it's addressed to), `text` (one line) | A generated aside between crows. Not part of any answer. | Banter is generated: the server retrieves the involved crows' backstories, their relationship and recent flock events, then the speaking crow writes one line in character. At most 2 per list, counted against the list budget; banter never blocks work, and a failed or unusable line is dropped. `list.finished` waits up to 20 seconds for in-flight banter. See `PERSONAS.md`.