feat(skills)!: S4 night commands become drain, suspend and resume

Requirement 001 phase 6, S4 (decision 29):

- Skills renamed: kvb-org-evening -> kvb-org-drain, kvb-org-sunset ->
  kvb-org-suspend, kvb-org-sunrise -> kvb-org-resume, kvb-agent-relax
  -> kvb-agent-drain, kvb-agent-sleep -> kvb-agent-suspend,
  kvb-agent-wakeup -> kvb-agent-resume; their text uses the new verbs and
  every reference in skills, README, orchestrator and tests follows.
- night.py takes drain, suspend, resume, resume --alone and status. A
  state file written before the rename keeps working: mode evening reads
  as drain, sunset as suspend, the awake list as resumed and is rewritten
  on the next save; the hub's council.night_mode maps the same values.
  mode: day and handover_night_<session>.md keep their names.
- Orchestrator: the Day/Night menu shows Drain, Suspend and Resume, each
  confirmed, signed through /api/board/sign with the passkey and run as
  the existing argv job; the old action names are refused.
- kvb-skill-cleanup: git rates two renames at 44 and 50 % similarity,
  under its default threshold, so legacy.py detects renames from 40 %.
- tests/test_night_names.py fails on any old name in the tree outside a
  rename record "old ->".

BREAKING CHANGE: the six old skill names and night.py's clear and wake
are gone. Run make install (it reinstalls the orchestrator too), then
kvb-skill-cleanup to remove the old installed copies; until the install,
the running hub calls the old night.py path.

Evidence: make test 679 OK, ruff and markdownlint clean
(/tmp/s4-make-test-1.log); e2e 22 OK in Chromium and Firefox
(/tmp/s4-e2e-3.log); MEDUSA 23 files, the 2 findings are the
byte-identical atomic state write that moved with night.py, already
accepted after a skeptic at its old path, rescan 0 open.

🎯 Fix: 93 %
💥 Break: 4 %
💸 Cost: 100 %
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 89
🚦 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-08 23:18:18 +00:00
1 parent 4d88296e47
commit 5c8b69d53d
24 files changed
+420 -176

No files matched your search

+11 -1
View File
@@ -14,7 +14,8 @@ CLASSES = ("post", "call", "newsletter", "broadcast", "mayday", "pan-pan", "secu
RECEIPT = "receipt"
ALARMS = ("mayday", "pan-pan", "security")
VERSION = 2
MODES = ("evening", "sunset")
MODES = ("drain", "suspend")
RENAMED_MODES = {"evening": "drain", "sunset": "suspend"}
RADIO_LIMIT = 5000
GRANT_LIMIT = 50
MAYDAY_ACTIVE = 7200
@@ -154,8 +155,17 @@ def grants(rows: list[dict], window: tuple[float, float], claims: list[dict], ta
def night_mode(council: Path) -> str:
"""Args:
council: the council directory; night.json in it may still hold a
mode from before the rename, read as its successor.
Returns:
One of MODES, or day.
"""
data = read_json(council / "night.json")
mode = data.get("mode") if isinstance(data, dict) else None
if isinstance(mode, str):
mode = RENAMED_MODES.get(mode, mode)
return mode if mode in MODES else "day"
+3 -3
View File
@@ -13,9 +13,9 @@ from .signed import message_signed, signing
from .skill_scripts import NIGHT, RADIO, RASCI
NIGHTS = {
"evening": (["set", "evening"], "Operator ran kvb-org-evening in the Agent Orchestrator: run kvb-agent-relax; verify with night.py status."),
"sunset": (["set", "sunset"], "Operator ran kvb-org-sunset in the Agent Orchestrator: run kvb-agent-sleep; verify with night.py status."),
"sunrise": (["clear"], "Operator ran kvb-org-sunrise: run kvb-agent-wakeup; verify with night.py status."),
"drain": (["drain"], "Operator ran kvb-org-drain in the Agent Orchestrator: run kvb-agent-drain; verify with night.py status."),
"suspend": (["suspend"], "Operator ran kvb-org-suspend in the Agent Orchestrator: run kvb-agent-suspend; verify with night.py status."),
"resume": (["resume"], "Operator ran kvb-org-resume: run kvb-agent-resume; verify with night.py status."),
}
UNSAFE = re.compile(r"[^A-Za-z0-9._-]+")
DIRECT = {"post": ["post"], "call": ["send", "call"]}
@@ -11,7 +11,7 @@ OUTPUT_LIMIT = 4000
SAFE_PATH = "/usr/local/bin:/usr/bin:/bin"
RADIO = "kvb-org-radio/scripts/radio.py"
RASCI = "kvb-org-rasci/scripts/rasci.py"
NIGHT = "kvb-org-sunset/scripts/night.py"
NIGHT = "kvb-org-suspend/scripts/night.py"
OPERATOR_SCRIPT = "kvb-org-operator/scripts/operator.py"
+1 -1
View File
@@ -18,7 +18,7 @@
--host-state: the state directory's path on the host, named in the radio
rows that carry an upload or a voice recording.
--skills: installed skills directory holding the kvb-org-radio,
kvb-org-rasci and kvb-org-sunset scripts.
kvb-org-rasci and kvb-org-suspend scripts.
--transcripts: directory the Claude Code transcripts (*.jsonl) lie under.
--templates: directory holding index.html.j2 and the *.js modules it
includes.
+1 -1
View File
@@ -65,7 +65,7 @@
const clear = iconBtn("bi-life-preserver", "Cancel mayday", "btn small danger", () => confirmAct("clear-mayday"));
const mode = h("span", {});
const night = h("details", {class: "menu"}, h("summary", named("Day/Night"), icon("bi-moon-stars"), caret()), h("div", {class: "menu-box"}, mode,
h("p", {class: "muted"}, "Evening: agents finish their task. Sunset: they sleep. Sunrise: all wake up."),
h("p", {class: "muted"}, "Drain: agents finish their task and take nothing new. Suspend: they park their work and idle. Resume: all continue from the handover. Your passkey signs each."),
h("div", {class: "actions"}, ACTIONS.slice(1).map(([id, label]) => btn(label, "btn small", () => confirmAct(id))))));
return {bar: h("div", {class: "toolbar", role: "toolbar", "aria-label": "Controls"}, toAll(), clear, iconBtn("bi-sort-down", "Priority", "btn small", openAgentOrder, "Priority: agent order in the hierarchy"), night), clear, mode};
}
+2 -2
View File
@@ -9,8 +9,8 @@
const KIND_TONES = {radio: "teal", review: "info", ask: "warn", decision: "violet", operator: "bad", register: "neutral", rename: "info", cron: "teal", rule: "ok", approval: "ok", run: "info", transcript: "neutral"};
const VIEW_ICONS = {"fuer-dich": "bi-person-check", funk: "bi-broadcast", arbeit: "bi-briefcase", transkripte: "bi-chat-left-text"};
const ITEM_LABELS = {review: "Review", ask: "Ask", decision: "Decision", operator: "Operator", rule: "Rule request"};
const NIGHT = {day: ["Day", "ok"], evening: ["Evening", "warn"], sunset: ["Night", "violet"]};
const ACTIONS = [["clear-mayday", "Cancel mayday", "Lift the radio silence of the active mayday?"], ["evening", "Evening", "Send all agents into the evening (kvb-agent-relax)?"], ["sunset", "Sunset", "Put all agents to sleep (kvb-agent-sleep)?"], ["sunrise", "Sunrise", "Wake all agents (kvb-agent-wakeup)?"]];
const NIGHT = {day: ["Day", "ok"], drain: ["Draining", "warn"], suspend: ["Suspended", "violet"]};
const ACTIONS = [["clear-mayday", "Cancel mayday", "Lift the radio silence of the active mayday?"], ["drain", "Drain", "Drain all agents: finish the current task, accept nothing new (kvb-agent-drain)?"], ["suspend", "Suspend", "Suspend all agents: park work, write the handover, idle (kvb-agent-suspend)?"], ["resume", "Resume", "Resume all agents from their handover (kvb-agent-resume)?"]];
const UPLOAD_LIMIT = 50 * 1024 * 1024;
const STREAM_KEEP = 5000;
const CLASSES = {post: "Post", call: "Call", newsletter: "Newsletter", broadcast: "Broadcast", mayday: "Mayday", "pan-pan": "Pan-Pan", security: "Sécurité", receipt: "Read", grant: "Grant"};