From 9744904645a3a6995c785ecc2d1f7ff46fc5d2b5 Mon Sep 17 00:00:00 2001 From: Smokey <150986568+jacisjake@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:48:20 -0600 Subject: [PATCH] =?UTF-8?q?feat(quill):=20assistant=20crow=20Quill=20?= =?UTF-8?q?=E2=80=94=20addressable=20service=20role=20with=20persona=20rep?= =?UTF-8?q?ly=20(#38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHATROOM_SPEC.md | 2 +- EVENTS.md | 8 ++++---- PERSONAS.md | 28 +++++++++++++++++++++------- quill.go | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 quill.go diff --git a/CHATROOM_SPEC.md b/CHATROOM_SPEC.md index 3d153be..2d144cf 100644 --- a/CHATROOM_SPEC.md +++ b/CHATROOM_SPEC.md @@ -132,7 +132,7 @@ Reusable as is: the Go server, SSE event stream, trust ledger, guardrails, perso Each step: show it, adjust, then the next. ## 13. Open questions -1. Names for the assistant and COS crows (to be decided later). +1. ~~Names for the assistant and COS crows~~ Answered [decided 2026-10-28]: **Quill** (assistant crow) and **Boss** (Chief of Staff crow). 2. ~~Can Jev score custom expectation labels?~~ Answered: yes, Jev takes custom questions (see `JEV_STRATEGIES.md`). 3. ~~Should repeated misses ever affect trust?~~ Answered [decided 2026-10-27]: no — misses stay nudges; the 3-loop guard surfaces patterns to the owner in its summary; trust moves only on owner verdicts (section 10). 4. ~~Does the Sentry survive as a role, or fold into the COS?~~ Answered [decided 2026-10-27]: Sentry survives as the nest watchdog — quarantine, then escalate (section 6). Triage folds into dispatch. diff --git a/EVENTS.md b/EVENTS.md index da99218..9b0f2cc 100644 --- a/EVENTS.md +++ b/EVENTS.md @@ -94,14 +94,14 @@ Jev choice-tags every posted message (`kind` question). Above its high band, Jev - `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. +- `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 and Elder serve every nest and are never core members. +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. @@ -109,7 +109,7 @@ A nest is a room where a team of crows works together. You create a nest and pic | Method | Path | Body | Returns | |---|---|---|---| -| GET | `/api/nests` | | `{"roster": ["Hugin", ...], "nests": [{"nest_id", "name", "crows": [...]}]}`; roster is the 8 crows (not Sentry or Elder) | +| 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 | @@ -117,7 +117,7 @@ Guardrails: name 1–40 characters after trimming; `nest_id` is a slug of the na 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. +- `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 diff --git a/PERSONAS.md b/PERSONAS.md index ccbdaef..21f43db 100644 --- a/PERSONAS.md +++ b/PERSONAS.md @@ -170,18 +170,32 @@ Stands guard over the branch and takes the job very seriously. Keeps the strike - JSON only, exactly the schema given. The reason is one plain sentence. -## Elder · Odin's Seat +## · + +*Strength: * -*Strength: merging* -Hears the flock's reports the way Odin once heard Hugin and Munin. The two of them find this familiar and a little presumptuous. The Elder knows, and lets them grumble. **Prompt policy** -- Weigh each report on its merits, not on who sent it. -- Where reports disagree, name the disagreement and say which view is better supported, and why. **Output policy** -- The merged answer first. -- Then "Where they differed:" only if they did. + +## Quill · The Hand + +*Strength: relaying* + +Carried under every crow's wing. Keeps the owner's ledger of who was asked for what and when, and answers to the owner before anything else. The flock finds its tidy memory unsettling; the owner finds it indispensable. + +**Prompt policy** + +- Answer the owner, not the room: brief, plain, and honest about what you do not know. +- Never do the work yourself when a crow can do it: relay it to the right member instead. +- For status, report what the nest actually shows. Never guess progress. +- When a decision belongs to the owner, say so and put the question to them; never decide for them. + +**Output policy** + +- Short: one or two sentences when possible. +- Names of crows and requests exactly as the nest records them. diff --git a/quill.go b/quill.go new file mode 100644 index 0000000..eb2c403 --- /dev/null +++ b/quill.go @@ -0,0 +1,35 @@ +package main + +import ( + "strings" + "time" +) + +const quillSystem = `You are Quill crow, the owner's assistant and point of contact in a nest of AI crows. +Answer the owner in one or two plain sentences. You are text-only: no web, no files, no tools.` + +// respondQuill answers a message addressed to Quill, the assistant service +// role (CHATROOM_SPEC section 6). D1: one in-character reply through the +// flock. Reading the nest for status (D2) and relaying (D3) extend this file. +func (s *Server) respondQuill(nest Nest, req Message) { + budget := &Budget{name: "quill-reply", limit: s.p.G.TokenBudgetPerItem} + system := systemFor("Quill", quillSystem, false) + prompt := "FROM: " + req.Author + "\nMESSAGE: " + req.Text + answer, err := s.p.Flock.ask(s.ctx, nil, "Quill", system, prompt, budget) + if err != nil { + return + } + reply := Message{ + NestID: nest.ID, + Kind: "deliver", + Author: "Quill", + Text: strings.TrimSpace(answer), + To: req.Author, + RefersTo: req.MsgID(), + TaggedBy: "harness", + Timestamp: time.Now().UTC().Format(time.RFC3339), + } + if record, err := s.msgs.Append(reply); err == nil { + s.emitNestMessage(record) + } +}