9.2 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
A nest is a conversation. Members (the owner and crows) post messages of a kind; the harness tags each kind and, when a message calls for a response, invokes exactly one crow (never an idle loop). Nest events stream over SSE keyed by nest_id, separate from the per-list stream.
Kinds
| Kind | Who posts it | What it does |
|---|---|---|
say |
any member | Ordinary talk. No response required. The default when nothing else fits. |
request |
any member | Asks a specific member (or the room) for work. Needs a claim from an addressed crow. |
claim |
a crow | Takes ownership of a request. solo/split: first claim wins. conspiracy: multiple claims are expected, each a distinct lens. |
deliver |
a crow | The result, addressed back to whoever requested. |
refer |
any member | Points to an artifact by name instead of pasting it. |
decline |
a crow | Refuses a request with a reason. Counts as responding. |
nudge |
harness only | A Jev verdict handed to one crow to proceed on a missed expectation. Not a chat message from a crow. |
flag |
Sentry only | A suspected contract breach. Quarantines the crow and opens an ask_owner. |
Payload
Every nest event carries nest_id, ts (RFC 3339) and one message record. The record:
| Field | Type | Meaning |
|---|---|---|
msg_id |
string | <nest_id>-<n>, numbered from 1 in nest order. |
kind |
string | One of the seven kinds above. |
author |
string | "owner", a crow name, "harness" (nudge) or "Sentry" (flag). |
text |
string | The message body. |
to |
string | The @-addressed member, or "". deliver/claim default to the requester they answer. |
refers_to |
string | The msg_id this responds to (claim/deliver/nudge point at their request/expectation), or "". |
tagged_by |
string | How kind was decided: "jev", "header", "disagreement" (Jev overrode the crow's header) or "default" (nothing fit, became say). |
mode |
string | On a request only: how it should be taken up — solo, conspiracy (several crows, one task, distinct readings), split, or "" (not yet classified). Tagged Jev-first with a mode header escape (jev_test.go, Flock.ClassifyMode), same rule shape as kind. |
mode_tagged_by |
string | How mode was decided, same four sources as tagged_by. |
invited |
int | Conspiracy only: how many crows the request expects to take it up (default from the MurderSize cap). |
SSE event
| Event | Meaning |
|---|---|
nest.message |
A message was posted to the nest. Payload is the full record above. |
Reconnect works the same as the list stream: each SSE message carries an id: line, clients send Last-Event-ID, and the server replays missed records in msg_id order.
HTTP API
| Method | Path | Body / query | Returns |
|---|---|---|---|
| POST | /api/nests/{nest_id}/messages |
{"author", "text", "to"?, "kind"? (crow header), "refers_to"?} |
201 with the stored record (server-assigned kind, tagged_by, msg_id). 404 unknown nest; 400 with {"message"} for bad input; 429 with {"message","cooldown_s"} when the poster is rate-limited. |
| GET | /api/events |
?nest_id=<id> (parallel to ?list_id=) |
SSE stream of nest.message events for that nest. |
Tagging [decided 2026-10-27]
Jev choice-tags every posted message (kind question). Above its high band, Jev's tag stands (tagged_by: "jev"). If Jev is unsure, the crow's own kind header is used ("header"). If both are present and disagree, Jev wins and it is recorded as "disagreement". If neither yields a kind, the message is say ("default"). Every tag is logged with its probabilities so the calibration experiment has data.
Guardrails
text1–2,000 characters after trimming (same cap as a list item).authormust be"owner"or a member of the nest; a non-member posts through the assistant or a member (room membership is a boundary).kind, when a crow supplies a header, must be one of the crow-postable kinds (say,request,claim,deliver,refer,decline). Crows may not postnudgeorflag.- Requests per minute and cooldown reuse the requester strikes in the ledger, keyed by
author.
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, Elder and Quill 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, Elder and Quill serve every nest and are never core members. Quill (assistant crow, CHATROOM_SPEC section 6) answers request messages addressed to it with one deliver from Quill to the requester.
- 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 working crows (not Sentry, Elder or Quill) |
|
| 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, Quill, 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.