Files
skills/orchestrator/kvb_orchestrator/sources.py
T
Kevin Veen-BirkenbachandClaude Opus 5.5 6f940b4415 feat(orchestrator): rename kvb-hub to Agent Orchestrator with the phase 2 surface
Surface (requirement 001 phase 2): navigation Stream (Inbox, Regeln) / Agents / Artifacts with keys 1-3 and hash routes (#graph and #tree redirect), header with title, command palette and settings only, server-side badges (unsnoozed inbox, beats within 900 s, open jobs), a Mayday banner with "aufheben", four stream chips with agent:/art:/seit: search, Filter and time menus, the inbox in main, an agents toolbar (Funk an alle with Newsletter, Broadcast, Pan-Pan, Security, Mayday; Priorität; Tag/Nacht), Post or Call in the agent panel, Ambiente in the settings and read-aloud over the seven classes with Mayday locked.

Rename (decision 9): hub/ is orchestrator/, the package kvb_orchestrator, the unit kvb-orchestrator-runner.service, scripts/orchestrator.sh, environment KVB_ORCHESTRATOR_*, make targets orchestrator-*, the MCP server, image and compose project kvb-orchestrator, the guard header X-Kvb-Orchestrator and the message marker [kvb-orchestrator msg <id>]. install runs migrate first: it stops the old container, removes its image and code, moves state, approvals and pinned keys, swaps the MCP registration, the PermissionRequest hook and the runner unit, and changes nothing on a second run (scratch-HOME test with stub docker, claude and systemctl). Saved views fall back to the old localStorage key once.

MEDUSA acceptances carried to the renamed paths: 82 rows whose lines are unchanged (reason prefixed "renamed from <old path>, line unchanged"), and 7 rows whose text changed only in the product name, the package name, a temp prefix or a new class argument, each upheld by one kvb-think-criticize skeptic (approval.py:60, messaging.py:53, server.py:56, tests/hub_support.py:64, passkey.py:205, page-agent.js:75, language.py:46).

Cost 101 %: the longer name adds 15 tokens of skill text; the operator approved it with the commit.

🎯 Fix: 92 %
💥 Break: 4 %
💸 Cost: 101 %
⏱️ Time: –
🛡️ Security: 100 %
💰 Value: ?
🏆 Score: 87
🚦 Gate: ⚠️ cost 101 % > 100: ask the operator before the commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 22:20:06 +00:00

54 lines
2.7 KiB
Python

from __future__ import annotations
import time
from .council import HUMAN, agent, read_claims
from .crons import cron_state, queue_rows, timer_rows
from .errors import ToolError
from .fields import single
from .lifetimes import edge_times, lifetime, session_gone, subagent_rows
from .names import PSEUDO, SESSION_ID, Directory, title_sightings
from .records import epoch, iso_epoch
from .register import REGISTER, read_register
from .tree import assign
class Sources:
def subagents(self, query: dict[str, list[str]]) -> dict:
session = single(query, "session") or ""
if not SESSION_ID.fullmatch(session):
raise ToolError("session must be a session id")
now = time.time()
beats = [claim["beat"] for claim in read_claims(self.council) if claim["session"] == session and claim["beat"] is not None]
try:
path = next(iter(sorted(self.transcripts.root.glob(f"*/{session}.jsonl"))), None)
except OSError:
path = None
times = edge_times(path) if path is not None else (None, None, None)
life = lifetime(times, session_gone(max(beats, default=None), times[2], now))
return {"session": session, **life, "subagents": subagent_rows(self.transcripts.root, session)}
def directory(self, claims: list[dict], news: list[dict], now: float) -> Directory:
claimed = {claim["session"] for claim in claims}
sightings = title_sightings(self.transcripts.root, claimed, now) if self.names.due(now) else []
sightings += [(claim["session"], claim["agent"], iso_epoch(claim["since"]), "claim") for claim in claims if claim["agent"] and claim["session"] != "-"]
self.names.observe(sorted(sightings, key=lambda sighting: (sighting[2] is None, sighting[2] or 0.0)), now)
known = {entry["name"] for entry in self.names.listed()}
radio = []
for row in news:
name, ts = agent(row.get("from")), epoch(row.get("ts"))
if name and name != HUMAN and name not in known and ts is not None:
radio.append((f"{PSEUDO}{name}", name, ts, "radio"))
known.add(name)
self.names.observe(radio, now)
return Directory(self.names.listed())
def repositories(self) -> dict:
tasks = read_register(self.council / REGISTER)
return assign(self.tree.get(), tasks, read_claims(self.council), self.reviews.listed())
def schedules(self, claims: list[dict], now: float) -> list[dict]:
beats = {claim["session"]: claim["beat"] for claim in claims if claim["beat"] is not None}
rows = [cron_state(cron, beats, now) for cron in self.transcripts.schedules()]
return rows + timer_rows(self.timers) + queue_rows(self.council)