docs(requirements): add 004 radio set

The Agent Orchestrator radio as a minimalist marine VHF set: a server CPU gate for speech, a WebAudio noise burst, modes Auto, Quiet and Silence, procedure words that always sound, every radio setting in the panel and a log with Sent, Received and All plus filters. Operator decisions 2026-11-12.

🎯 Fix: 95 %
💥 Break: 1 %
💸 Cost: 100 %
⏱️ Time: 100 %
🛡️ Security: 100 %
🏆 Score: 94
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-12 16:19:16 +00:00
1 parent fd8fc08603
commit 8711c23a39
2 files changed
+155

No files matched your search

+154
View File
@@ -0,0 +1,154 @@
# 004 - Radio Set
## User Story
As the operator of many parallel agents, I want the Agent Orchestrator radio
to work like a minimalist marine VHF set, with every setting at hand, a
filterable log and speech only when the host has CPU to spare, so that I hear
that someone transmits without the speech engine loading a busy host.
## Decisions (operator-confirmed 2026-11-12; do not re-litigate)
1. **CPU gate on the server**: `/api/tts` renders an uncached line only while
the latest host CPU sample of the W4 resource feed (`samples.jsonl`, at
most 120 s old) is below the threshold; a cached line is always served;
otherwise it answers 503 with the reason `busy`, or `unknown` without a
fresh sample. Rejected: a gate only in the browser (another tab or an old
page still renders), browser `speechSynthesis` (same host CPU, other
voices), rendering held-back speech later (stale).
2. **Noise burst**: when no speech plays, the page plays a squelch-tail noise
burst made by WebAudio in the browser (band-passed white noise, about
0.6 s, at the tone's volume and squelch); no server request. Rejected: a
recorded sound file.
3. **Modes**: Auto (speech below the threshold, otherwise the burst), Quiet
(the burst only, never speech), Silence (no sound; bubbles and log only).
Default Auto.
4. **Procedure words**: for MAYDAY, PAN-PAN and SÉCURITÉ the procedure word,
spoken three times in the sender's voice, always sounds, in every mode and
under load; under load or in Quiet the burst replaces the rest of the
message. The word line is the same text every time, so after its first
render per voice it is a cache hit.
5. **Settings in the panel**: mode, volume per tone, squelch, read-aloud
filters (classes, agents) and the internet radio station are radio-set
buttons and controls in the radio panel; the radio section leaves the
settings dialog. They live per browser, without a passkey. Rejected: signed
hub settings (one passkey per button press).
6. **Threshold**: adjustable in the panel, 30 to 90 % in steps of 10, default
50 %; a signed hub setting `speech.threshold`, since the server enforces
it. Rejected: a fixed 50 %.
7. **Log**: tabs Sent (from the operator), Received (to the operator or to
all) and All (every radio row the hub logs); filter chips class, sender,
recipient and agent plus text search, combined with AND, newest first.
Rejected: Received as all agent traffic.
8. **Look**: one faceplate inspired by a marine VHF set, functional and
minimalist: a display line (class, mode, CPU, TX/RX lamp), a row of mode
buttons, sliders as knobs, a PTT send button; palette tokens, light and
dark, no textures. Rejected: a full VHF set with dual watch, scan and DSC
(high effort, no gain).
9. **Order**: builds on W4's resource feed; implementation starts once W4 is
on master.
## Approach
```mermaid
flowchart TD
subgraph P["🧭 0 · Preparation"]
P1["📋 Write requirement"] --> P2{"✅ Operator approves?"}
P2 -- "✏️ corrections" --> P1
P2 -- "🤖 yes" --> P3{"📊 W4 on master?"}
P3 -- "⏳ no: wait for the unify" --> P3
end
P3 -- "🟢 yes" --> A1
subgraph A["🛡️ A · Server gate"]
A1["🧩 CPU gate in /api/tts, gate state in /api/state, signed threshold"] --> A2{"🧪 Unit and HTTP tests green?"}
A2 -- "🔴 no: fix" --> A1
end
A2 -- "🟢 yes" --> B1
subgraph B["🔊 B · Sound"]
B1["🎚️ Noise burst, modes, procedure words"] --> B2{"🧪 Tests green?"}
B2 -- "🔴 no: fix" --> B1
B2 -- "🟢 yes" --> BF{"👂 Operator listens: fits?"}
BF -- "✏️ changes" --> B1
end
BF -- "👍 fits" --> C1
subgraph C["📻 C · Panel"]
C1["🎛️ Faceplate, settings into the panel, threshold control"] --> C2{"🧪 e2e at 1920, 1366, 390 green?"}
C2 -- "🔴 no: fix" --> C1
C2 -- "🟢 yes" --> CF{"🖼️ Screenshots: operator fits?"}
CF -- "✏️ changes" --> C1
end
CF -- "👍 fits" --> D1
subgraph D["📜 D · Log"]
D1["🗂️ Sent, Received, All, chips, search"] --> D2{"🧪 Tests green?"}
D2 -- "🔴 no: fix" --> D1
D2 -- "🟢 yes" --> DF{"🖼️ Screenshots: operator fits?"}
DF -- "✏️ changes" --> D1
end
DF -- "👍 fits" --> E1["🏁 Review, security scan, unify gate, commit and master on the operator's word"]
```
## Acceptance Criteria
### Server gate
- [ ] `POST /api/tts` with an uncached line answers 503 `{"reason": "busy"}`
while the latest `samples.jsonl` row has `cpu` >= the threshold, and the
fake Piper records no call.
- [ ] Without a sample younger than 120 s it answers 503
`{"reason": "unknown"}`, and the fake Piper records no call.
- [ ] A cached line answers 200 whatever the CPU.
- [ ] Below the threshold an uncached line renders as before.
- [ ] `GET /api/state` carries `speech.gate` with `threshold`, `cpu` and
`open`.
- [ ] `speech.threshold` accepts only 30 to 90 in steps of 10, default 50,
and changes only with a valid passkey signature.
### Sound
- [ ] Auto below the threshold: the page plays the spoken line; on a 503 it
plays the burst once and does not retry that row.
- [ ] Quiet: the page sends no `/api/tts` request except for procedure-word
lines, and every row plays the burst.
- [ ] Silence: no audio node starts except for a procedure-word line.
- [ ] The burst is WebAudio noise of 0.4 to 0.8 s at the tone's volume times
the squelch, and it sends no network request.
- [ ] A MAYDAY, PAN-PAN or SÉCURITÉ row requests its procedure-word line
first, in every mode; the second such line for the same voice is a
cache hit with no Piper call.
### Panel
- [ ] The display line shows class, mode, CPU % and a TX/RX lamp that is lit
while sound plays.
- [ ] Auto, Quiet and Silence form one button group (`aria-pressed`); a press
changes the mode without a passkey and survives a reload.
- [ ] Volume per tone, squelch, the read-aloud filters for classes and
agents, and the internet station sit in the panel; the settings dialog
has no radio section.
- [ ] The threshold control asks for the passkey once per change and shows
the server's value.
- [ ] At 1920, 1366 and 390 px the panel has no horizontal scroll, in the
light and the dark palette.
### Log
- [ ] Tabs Sent, Received and All show their counts; Received holds exactly
the rows addressed to the operator or to all.
- [ ] The chips class, sender, recipient and agent and the text search
combine with AND; one control clears them all.
- [ ] Rows run newest first with time, direction, class badge,
sender → recipient and text; a click opens the full message.
- [ ] Tab, chips and search survive a reload in the same browser.
### Tests
- **Unit**: gate busy, unknown, cache hit and open; threshold validation;
procedure-word lines from `phrasing.py`; every case with a negative
control.
- **Integration**: hub HTTP tests with a fake Piper and a fake
`samples.jsonl`: 503 and no Piper call under load, 200 for a cache hit,
`speech.gate` in `/api/state`.
- **E2E**: Playwright in Chromium and Firefox at 1920, 1366 and 390 px: mode
buttons, burst through a spy on the AudioContext, procedure word under
load, the settings dialog without a radio section, the log tabs and
filters.
+1
View File
@@ -7,3 +7,4 @@ skill defines the format.
- [001 - Agent Orchestrator](001-agent-orchestrator.md) - [001 - Agent Orchestrator](001-agent-orchestrator.md)
- [002 - Cosmos](002-cosmos.md) - [002 - Cosmos](002-cosmos.md)
- [003 - Operator Workbench](003-operator-workbench.md) - [003 - Operator Workbench](003-operator-workbench.md)
- [004 - Radio Set](004-radio-set.md)