feat(quill): assistant crow Quill — addressable service role (#38) #46

Merged
jacisjake merged 3 commits from task/38-quill-role into main 2026-10-28 13:52:36 +00:00
4 changed files with 61 additions and 12 deletions
Showing only changes of commit 9744904645 - Show all commits

No files matched your search

+1 -1
View File
@@ -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. Each step: show it, adjust, then the next.
## 13. Open questions ## 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`). 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). 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. 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.
+4 -4
View File
@@ -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/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. - `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. - 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. - `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. - 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. - The server listens on localhost only by default. There is no login yet, and requester names are self-asserted.
## Nests ## 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". - 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. - 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 | | 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/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 | | 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: Event changes:
- `list.started` adds `nest_id`. - `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 ## Personas and banter
+21 -7
View File
@@ -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. - 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** **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** **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.
+35
View File
@@ -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)
}
}