5.3 KiB
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: <name> and data: <JSON>. 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": "<todo list>", "requester": "<id>"} |
{"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.
Nest messages (contract in progress — B1 owns the full payload tables)
Message kinds (CHATROOM_SPEC.md section 4): say, request, claim, deliver, refer, nudge (harness trigger), flag (Sentry only). Nest events stream over SSE keyed by nest_id.
Tagging [decided 2026-10-27]: Jev choice-tags every message (kind question, high band); Jev unsure → the crow's own structured header; disagreement → Jev wins, logged. Payload fields carrying this (author, kind, to, refers_to) are finalized in B1.
Settled details
item_idis<list_id>-<n>, numbered from 1 in list order.POST /api/listsreturns 202. Bad input returns 400 with{"message"}.POST /api/verdictsreturns 404 when nothing is waiting (including a second answer), 400 for a bad body.approveis 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.flyingandcrow.landedfire for the Sentry and Elder too.requester.prankedgoes 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 withLast-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.startedaddsnest_id.crow.flyingaddsborrowed(bool): true when the crow is not in the nest's core. Always false for Sentry, Elder, and thetodonest.
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.