Files
so-many-crows/EVENTS.md
T

9.0 KiB
Raw Blame History

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

  • text 1–2,000 characters after trimming (same cap as a list item).
  • author must 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 post nudge or flag.
  • Requests per minute and cooldown reuse the requester strikes in the ledger, keyed by author.

Settled details

  • item_id is <list_id>-<n>, 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.