Files
so-many-crows/EVENTS.md
T

150 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. 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`. Posting a flag also quarantines the crow until the owner lifts it (the `ask_owner` escalation lands with #45); a quarantined crow's POSTs get 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. 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`), `"rejected"` (an `ask_owner` unanswered past the 10-minute approval window — the same fail-safe window as a list approval). |
### 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."}`, and it is skipped by the open-, conspiracy- and directed-response selectors. Quarantine is fail-safe: it sticks until the owner lifts it (`ask_owner` escalation lands with #45).
- 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; everything 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 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`.