Files
so-many-crows/EVENTS.md
T
SmokeyandClaude Opus 5.5 57e9fc2fd2 Crow personas and generated banter
- personas.go: backstory, prompt policy and output policy per crow;
  Hugin and Munin grounded in Grimnismal 20 and Gylfaginning 38;
  every working crow also follows a common output policy
- Personas go into each crow's instructions; Sentry keeps JSON-only
- banter.go: retrieve backstory, relationships and recent flock
  memory, then the speaking crow generates one line in character;
  max 2 per list against the list budget, never blocks work,
  unusable lines dropped
- crow.banter event, GET /api/crows, `crows personas` -> PERSONAS.md
- UI: epithets on tiles and messages, banter as quiet asides

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQnSw4RRWp8uqpj3nsnnXF
2026-10-27 17:31:53 -06:00

73 lines
4.8 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.
## 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`.