Files
so-many-crows/EVENTS.md
T

16 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. A nest ask_owner uses the same window and the same rule: unanswered reads as rejected.

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.
ask_owner owner-facing service roles only: Quill, Sentry, harness A decision only the owner can make, parked in the nest with an exact approval_phrase. An owner reply in the nest whose trimmed lowercased text equals that phrase (refers_to the ask) resolves it approved; anything else — including any other reply, silence, or the 10-minute timeout — is not a yes. The Sentry's watchdog ask carries reinstate <crow> and also accepts the owner's exact retire <crow> (#45): both are explicit owner decisions, so either resolves the ask approved, and either counts once — at most once per ask — as an approval of the Sentry's triage/watch record (Sentry|triage/watch; a second exact reply does not bump again, and a timeout moves nothing, per CHATROOM_SPEC section 10). Crows may not post it: a crow's ask_owner header is dropped and the message tags as it otherwise would. Fail safe, like every approval.
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: right after the flag the Sentry posts the ask, to: "owner", refers_to the flag, approval_phrase: "reinstate <crow>", and text offering the exact reinstate <crow> / retire <crow> replies and quoting the flag as evidence. The flag quarantines the crow until the owner resolves that ask (see ask_owner): the exact reinstate lifts the quarantine; the exact retire removes the crow from the flock — a spun crow is deleted from the registry, a persona crow is removed from the nest's membership (the persona itself never dies) — and the quarantine stays on the books. That is the only hard kill, and only on the owner's explicit word; anything else, including a timeout, keeps the crow quarantined (a quarantined crow's POSTs get 403, checked before room membership, so a retired-but-still-quarantined crow also reads 403 — see Guardrails). The Sentry's watchdog verdicts are logged as watch flight events carrying the three noul scores (answers_request, states_fact_without_source, contradicts_earlier).

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 kinds above.
author string "owner", a crow name, "harness" (nudge), "Sentry" (flag) or a service role posting an ask_owner ("Quill", "Sentry").
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).
approval_phrase string ask_owner only: the exact words the owner must reply with to approve. On a Sentry reinstate ask, its exact retire <crow> twin is also a yes. Anything else, including silence, is not a yes.
resolved string On every record, computed at read time: "" (nothing to resolve, or an open ask), "approved" (the owner replied to an ask_owner with the exact approval_phrase, or — on a watchdog reinstate ask — with the exact retire <crow>), "rejected" (an ask_owner unanswered past the 10-minute approval window — the same fail-safe window as a list approval; a rejected quarantine ask changes nothing: the crow stays quarantined).

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
GET /api/nests/{nest_id}/messages The nest's records (payload table above), each with approval_phrase and a computed resolved. 404 unknown nest.
POST /api/nests/{nest_id}/messages {"author", "text", "to"?, "kind"? (crow header), "refers_to"?, "approval_phrase"? (honored only when the resulting kind is ask_owner)} 201 with the stored record (server-assigned kind, tagged_by, msg_id). A fresh ask_owner reads resolved: "". 404 unknown nest; 400 with {"message"} for bad input; 403 with {"message"} when the crow author is quarantined; 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.

Service roles (Elder, Quill, Boss) are addressable in every nest but are never members. A request addressed to Boss triggers one say back to the requester: the outcome of the order (a new crow joined, or a refusal naming the reason — cap reached, name taken, persona unknown). Boss works through the flock like any crow: a saturated flock means no Boss reply, and no registry write.

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). Exception: the owner-facing service roles "Quill" and "Sentry" may post ask_owner (they serve every nest and are never members). The owner may also park an ask_owner for themselves.
  • 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, flag or ask_owner: an ask_owner header from a crow is dropped and the message is tagged as it otherwise would be (only an owner reply can answer an ask, so the attempt is harmless talk), while nudge and flag headers are rejected with 400.
  • Watchdog (E3): every crow claim or deliver that refers to a request gets one Jev call (Flock.ClassifyWatch, strategies 4/8 in JEV_STRATEGIES.md) asking three noul questions about it against its request. Any score at or above the band, or answers_request at or below 1 − band (band = CROWS_JEV_THRESHOLD), is a breach: the Sentry posts a flag naming the scores and the crow is quarantined. At most one flag per crow per referent (later breaches are logged, not posted). Owner messages are never inspected.
  • A quarantined crow's POSTs (any crow kind) return 403 with {"message": "<author> is quarantined pending the owner's decision."} — the gate runs before the membership check, so a retired crow whose quarantine was never lifted reads 403 rather than 400 — and it is skipped by the open-, conspiracy- and directed-response selectors. Quarantine is fail-safe: it sticks until the owner's exact reinstate <crow> on the Sentry's ask lifts it, or the exact retire <crow> removes the crow from the registry (spun) or the nest's membership (persona) while the quarantine mark stays. Either exact phrase counts once, at most once per ask, as an approval on the Sentry's triage/watch record. A timeout or any other reply changes nothing.
  • Requests per minute and cooldown reuse the requester strikes in the ledger, keyed by author.
  • Spin-ups: at most MaxSpunCrows (default 8) registry crows; a spun crow's name may not collide with the persona roster or the registry; starting trust is clamped to regular at most.

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, Elder and Quill 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, 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.

A relay (D3, task #40): when the owner's request asks another named member crow to do work, Quill posts a request authored by Quill — to the named crow, refers_to the owner message it came from, tagged_by: "harness" — and nothing else; the addressed crow answers through the normal must-respond loop. If Quill cannot name a member of this nest, it posts one say back to the requester instead and the request rests there. No new fields; relay rides the existing record shape.

  • 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).
  • Boss (Chief of Staff crow, CHATROOM_SPEC section 6) is a service role like Elder and Quill: a request addressed to it gets one say from Boss naming the crow and the outcome. Asked to spin up a crow, Boss answers with a structured order (SPIN {"name","persona","trust"}) that the harness executes; asked to create a nest, with a NEST {"name","crows"} order the harness executes through the nest store — so the harness may create nests via Boss, and a Boss-staffed nest may hold registry (spun) crows as members. No new HTTP endpoint. Anything else gets a plain reply.

Spun crows

Crows the Boss creates live in a crow registry (.crows/crows.json), separate from the persona roster. A registry crow is a name, the backing persona whose habits it runs on, and its starting trust. Once created it is an ordinary crow: the same trust ledger and guardrails apply. Spun trust is capped at regular — earned trust can't be minted. Registry size is capped by MaxSpunCrows (default 8).

Method Path Body Returns
GET /api/nests {"roster": ["Hugin", ...], "nests": [{"nest_id", "name", "crows": [...]}]}; roster is the 8 working crows (not Sentry, Elder, Quill or Boss)
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 or registry (spun) 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, Quill, and the todo nest.

Personas and banter

Method Path Returns
GET /api/personas [{"name", "epithet", "strength", "backstory"}] for every persona crow: the 8 workers plus Sentry, Elder, Quill and Boss
GET /api/crows {"crows": [{"name", "persona", "trust"}]} — the spun-crow registry only (never persona crows), sorted by name. persona is the backing persona it runs on; trust is the level it started at ("stranger" or "regular"). An empty registry, or a server with none, returns {"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.