feat(orchestrator): hierarchy view from council offices with a separate agent order

The Agents views gain a Hierarchie view: an org chart of the chair and
the line offices read from the council claims. The operator appoints,
hands over and recalls an office there; the page signs each change with
the passkey and a host unit (client/offices.py, offices path unit)
verifies the signature, removes a live holder itself and runs a plain
council.py claim for the successor. council.py loses the --operator
flag, so no agent can override a live holder.

The agent order is now separate from task priority: rasci.py agents
order stores it in agent-order.json beside the register through the
nofollow helpers and never writes a --prio pin. It only breaks ties of
equal task rank, also among the queue grants. The Hierarchie view edits
it as a drag list.

Host units run the installed skill scripts, so orchestrator.sh keeps the
offices unit off while the installed council.py, nofollow.py or rasci.py
differ from the tree. Job files, council files and hub records are read
with O_NOFOLLOW and O_NONBLOCK and size caps; nofollow.rewrite replaces
a claim without creating directories, so a beat never revives a released
office.

🎯 Fix: 92 %
💥 Break: 4 %
💸 Cost: 100 %
⏱️ Time: 79 %
🛡️ Security: 100 %
🏆 Score: 100
🚦 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-07 17:46:39 +00:00
1 parent 8e180be256
commit 4540494c30
53 files changed
+1821 -211

No files matched your search

+7 -1
View File
@@ -7,6 +7,7 @@ from .asking import ask_state
from .cleanup import cleanup_request
from .errors import Conflict, Missing, ToolError
from .fields import decode, expect, field
from .offices import office_request
from .records import digest, shown
from .register import REGISTER, operator_rows, read_register, runnable
from .rules import rule_matches
@@ -144,7 +145,12 @@ class Approving:
payload = decode(body)
expect(payload, ("items",))
raw = payload["items"]
signed = self.deletable(raw) if cleanup_request(raw) else [item["signed"] for item in self.checked(raw) if item["signed"] is not None]
if cleanup_request(raw):
signed = self.deletable(raw)
elif office_request(raw):
signed = self.appointable(raw)
else:
signed = [item["signed"] for item in self.checked(raw) if item["signed"] is not None]
hashes = [signing.item_hash(item) for item in signed]
credentials = [key["id"] for key in self.keys()]
if not hashes:
+37 -4
View File
@@ -18,6 +18,10 @@ MODES = ("evening", "sunset")
RADIO_LIMIT = 5000
GRANT_LIMIT = 50
MAYDAY_ACTIVE = 7200
CHAIR_OFFICE = "chair"
GROUPS = ("work", "cluster")
CLAIM_FILES = ("*/claim.json", "*/*/claim.json")
OFFICE = re.compile(r"[a-z0-9][a-z0-9._-]*(?:/[a-z0-9][a-z0-9._-]*)?")
def agent(name: object) -> str:
@@ -74,10 +78,33 @@ def radio_rows(rows: list[dict], begin: float, end: float) -> list[dict]:
return sorted(found, key=lambda row: row["ts"])[-RADIO_LIMIT:]
def claim_row(data: dict) -> dict:
def office_kind(office: str) -> tuple[str, str, str]:
"""Args:
office: an office of one or two segments.
Returns:
(kind, dimension, line): kind is chair, line for <dimension>/<line>,
work or cluster for their groups, other for any other single
segment; dimension and line are empty unless the kind is line.
"""
head, _, rest = office.partition("/")
if office == CHAIR_OFFICE:
return "chair", "", ""
if not rest:
return "other", "", ""
if head in GROUPS:
return head, "", ""
return "line", head, rest
def claim_row(office: str, data: dict) -> dict:
name = data.get("name")
kind, dimension, line = office_kind(office)
return {
"office": shown(data.get("office")),
"office": office,
"kind": kind,
"dimension": dimension,
"line": line,
"agent": agent(name),
"address": name.strip() if isinstance(name, str) else "",
"session": shown(data.get("session")),
@@ -89,8 +116,14 @@ def claim_row(data: dict) -> dict:
def read_claims(council: Path) -> list[dict]:
paths = sorted([*council.glob("cluster/*/claim.json"), *council.glob("work/*/claim.json")])
return [claim_row(data) for data in map(read_json, paths) if isinstance(data, dict)]
found = []
for pattern in CLAIM_FILES:
for path in council.glob(pattern):
office = path.parent.relative_to(council).as_posix()
data = read_json(path)
if OFFICE.fullmatch(office) and isinstance(data, dict):
found.append(claim_row(office, data))
return sorted(found, key=lambda row: row["office"])
def sessions(claims: list[dict]) -> dict[str, str]:
+19 -7
View File
@@ -2,13 +2,23 @@ from __future__ import annotations
from collections import Counter
from .council import HUMAN, agent, ordered, subagent
from .council import CHAIR_OFFICE, HUMAN, agent, ordered, subagent
from .names import Directory
from .orbits import affinity
from .records import shown
from .register import ENDED, parties, status_of
CHAIR = "Maintainer"
def chair_of(claims: list[dict], directory: Directory, end: float) -> str:
"""Returns:
The node key of the chair office's holder, empty while it is vacant.
"""
claim = next((claim for claim in claims if claim["office"] == CHAIR_OFFICE), None)
if claim is None:
return ""
if claim["session"] != "-":
return claim["session"]
return directory.key(claim["agent"], end)
def parent_of(roles: dict[str, str], known: set[str], fallback: str) -> str:
@@ -36,13 +46,14 @@ def edges(radio: list[dict], grant_rows: list[dict], chair: str) -> list[dict]:
def graph(radio: list[dict], grant_rows: list[dict], tasks: dict[str, dict], claims: list[dict], end: float, directory: Directory) -> dict:
current = {task: row for task, row in sorted(tasks.items()) if status_of(row) not in ENDED}
chair = directory.key(CHAIR, end)
chair = chair_of(claims, directory, end)
top = chair or HUMAN
workers = {task: agent(row.get("worker")) for task, row in current.items()}
keys = {row["source"] for row in radio} | {key for row in radio for key in row["targets"]}
keys |= {claim["session"] for claim in claims if claim["agent"] and claim["session"] != "-"}
keys |= {directory.key(name, end) for name in workers.values() if not subagent(name)}
found = sorted(keys - {"", HUMAN, chair}, key=lambda key: (directory.label(key, end).lower(), key))
known = {chair, *found}
known = {chair, *found} - {""}
moons: dict[str, tuple[str, str]] = {}
holders: dict[str, dict[str, dict]] = {}
for task, row in current.items():
@@ -53,14 +64,15 @@ def graph(radio: list[dict], grant_rows: list[dict], tasks: dict[str, dict], cla
worker = workers[task]
holder = directory.key(worker, end)
if subagent(worker):
parent = parent_of(roles, known, chair)
parent = parent_of(roles, known, top)
holder = f"{worker} @ {parent}"
moons[holder] = (parent, worker)
if holder:
duties(holder, task, row, holders)["worker"] = True
for key, letters in roles.items():
duties(key, task, row, holders)["letters"] += letters
plan = [(HUMAN, "operator", None, HUMAN), (chair, "chair", None, directory.label(chair, end))]
plan = [(HUMAN, "operator", None, HUMAN)]
plan += [(chair, "chair", None, directory.label(chair, end))] if chair else []
plan += [(key, "session", None, directory.label(key, end)) for key in found]
plan += [(moon, "subagent", *moons[moon]) for moon in ordered(set(moons))]
nodes = [
@@ -77,4 +89,4 @@ def graph(radio: list[dict], grant_rows: list[dict], tasks: dict[str, dict], cla
for key, node_kind, parent, label in plan
]
layout = affinity(nodes, radio, tasks, claims, end, lambda name: directory.key(name, end))
return {"nodes": nodes, "edges": edges(radio, grant_rows, chair), "layout": layout}
return {"nodes": nodes, "edges": edges(radio, grant_rows, top), "layout": layout}
+8 -1
View File
@@ -36,10 +36,12 @@ SIGN_ROUTE = "/api/sign"
RULE_SIGN_ROUTE = "/api/rules/sign"
CLEANUP_ROUTE = "/api/cleanup"
CLEANUP_STATE = re.compile(r"/api/cleanup/([0-9a-f]{16})")
OFFICES_ROUTE = "/api/offices"
OFFICE_STATE = re.compile(r"/api/offices/([0-9a-f]{16})")
VENDOR_ROUTE = re.compile(r"/vendor/([a-z0-9.-]{1,64})")
VENDOR = {"3d-force-graph.min.js": "3d-force-graph/3d-force-graph.min.js", "three.module.js": "three/three.module.js", "three.core.js": "three/three.core.js"}
SCRIPT_TYPE = "text/javascript; charset=utf-8"
ACTION_POSTS = ("/api/rules", "/api/nl", SETTINGS_ROUTE, TTS_ROUTE, STT_ROUTE, SIGN_ROUTE, RULE_SIGN_ROUTE, CLEANUP_ROUTE)
ACTION_POSTS = ("/api/rules", "/api/nl", SETTINGS_ROUTE, TTS_ROUTE, STT_ROUTE, SIGN_ROUTE, RULE_SIGN_ROUTE, CLEANUP_ROUTE, OFFICES_ROUTE)
class Handler(Exchange):
@@ -59,6 +61,7 @@ class Handler(Exchange):
proof = PROOF_ROUTE.fullmatch(path)
vendored = VENDOR_ROUTE.fullmatch(path)
cleanup = CLEANUP_STATE.fullmatch(path)
office = OFFICE_STATE.fullmatch(path)
if self.host() is None:
self.send_text(HTTPStatus.FORBIDDEN, "forbidden host")
elif path == "/":
@@ -67,6 +70,8 @@ class Handler(Exchange):
self.vendored(VENDOR[vendored.group(1)])
elif cleanup:
self.answer(lambda: hub.cleanup_state(cleanup.group(1)), HTTPStatus.OK)
elif office:
self.answer(lambda: hub.office_state(office.group(1)), HTTPStatus.OK)
elif proof:
self.answer(lambda: hub.review_proof(proof.group(1)), HTTPStatus.OK)
elif path == "/api/state":
@@ -125,6 +130,8 @@ class Handler(Exchange):
return lambda body: self.answer(lambda: hub.sign_rule(body), HTTPStatus.OK)
if path == CLEANUP_ROUTE:
return lambda body: self.answer(lambda: hub.cleanup(body), HTTPStatus.ACCEPTED)
if path == OFFICES_ROUTE:
return lambda body: self.answer(lambda: hub.appoint(body), HTTPStatus.ACCEPTED)
if inbox:
return lambda body: self.answer(lambda: hub.mark(body), HTTPStatus.OK)
if undo:
+5 -1
View File
@@ -22,6 +22,7 @@ from .ledger import Ledger, Marks, Reviews
from .mcp import Mcp
from .messaging import Messaging
from .names import Names
from .offices import Offices
from .reviewing import Reviewing
from .rules import Rules
from .settings import Settings
@@ -40,6 +41,7 @@ class Places:
timers: Path
vendor: Path
cleanup: Path
offices: Path
def environment(templates: Path) -> jinja2.Environment:
@@ -55,7 +57,7 @@ def environment(templates: Path) -> jinja2.Environment:
@dataclass(frozen=True)
class Hub(Mcp, Reviewing, Asking, Messaging, Approving, Cleaning, Inbox, Rules, Jobs, Sources, Timeline, View, Language):
class Hub(Mcp, Reviewing, Asking, Messaging, Approving, Cleaning, Offices, Inbox, Rules, Jobs, Sources, Timeline, View, Language):
council: Path
pages: Path
public_port: int
@@ -84,6 +86,7 @@ class Hub(Mcp, Reviewing, Asking, Messaging, Approving, Cleaning, Inbox, Rules,
vendor: Path
icons: Markup
spool: Spool
office_spool: Spool
touched: Touched
def hosts(self) -> tuple[str, str]:
@@ -101,6 +104,7 @@ def open_hub(council: Path, pages: Path, public_port: int, state: Path, host_sta
vendor=places.vendor,
icons=sheet(places.vendor, templates),
spool=Spool(places.cleanup).ready(),
office_spool=Spool(places.offices).ready(),
touched=Touched(reviews.stored),
approvers=approvers,
speech=Speech(engines, state / "tts"),
+2 -2
View File
@@ -11,7 +11,7 @@ from .errors import ToolError
from .events import VIEWS
from .fields import bounded, decode, expect, message_text
from .records import read_jsonl
from .register import REGISTER, priority, read_register
from .register import REGISTER, agent_order, read_register
NL_LIMIT = 2000
NL_ACTION_LIMIT = 20
@@ -183,6 +183,6 @@ class Language:
directory = self.directory(claims, read_jsonl(self.council / NEWS), now)
self.watch.see(tasks, now)
items = self.inbox(now, tasks, directory)
agents = ordered({claim["agent"] for claim in claims if claim["agent"]} | set(priority(tasks)))
agents = ordered({claim["agent"] for claim in claims if claim["agent"]} | set(agent_order(tasks, [], [])))
shown_items = [{"id": item["id"], "kind": item["kind"], "title": item["title"][:200]} for item in items]
return nl_actions(self.model.complete(nl_messages(text, shown_items, agents)), items, agents)
+6 -1
View File
@@ -8,6 +8,7 @@ from .council import HUMAN, ordered, read_claims
from .errors import Missing, ToolError
from .fields import NAME_LIMIT, ORDER_NAME, decode, expect, field, message_text, single
from .records import write_atomic
from .register import REGISTER, agent_order, read_register
from .skill_scripts import NIGHT, RADIO, RASCI
NIGHTS = {
@@ -111,7 +112,11 @@ class Messaging:
raise ToolError("agents must be a non-empty list of agent names without commas")
if len(set(names)) != len(names):
raise ToolError("agents must be distinct")
runs = [self.scripts.run(RASCI, ["order", ",".join(names)])]
known = agent_order(read_register(self.council / REGISTER), read_claims(self.council), [])
unknown = [name for name in names if name not in known]
if unknown:
raise ToolError(f"unknown agents: {', '.join(unknown)[:300]}; the register and the council know {', '.join(known)[:600] or 'none'}")
runs = [self.scripts.run(RASCI, ["agents", "order", ",".join(names)])]
elif action in NIGHTS:
command, text = NIGHTS[action]
runs = [self.scripts.run(NIGHT, [*command, "--session", HUMAN])]
+89
View File
@@ -0,0 +1,89 @@
from __future__ import annotations
import secrets
import time
from .council import HUMAN, office_kind, read_claims
from .errors import Conflict, Missing, ToolError
from .fields import decode, expect
from .records import read_json, stamp
from .signed import signing
ITEM_LIMIT = 20
JOB_VERSION = 1
LABELS = {"appoint": "besetzt", "takeover": "übergeben", "recall": "abberufen"}
def office_request(raw: object) -> bool:
return isinstance(raw, list) and bool(raw) and all(isinstance(entry, dict) and entry.get("kind") == "office" for entry in raw)
class Offices:
def office_sessions(self, claims: list[dict]) -> set[str]:
named = {entry["session"] for entry in self.names.listed()} | {claim["session"] for claim in claims}
return {session for session in named if signing.SESSION.fullmatch(session) and session != HUMAN}
def appointable(self, raw: object) -> list[dict]:
"""Args:
raw: the orders the page chose, each {"kind": "office", "action",
"office", "holder", "session", "work"}.
Returns:
The signed items: each order well formed, its holder the office's
current one, its successor a known session that represents no
other line of the same dimension.
"""
if not isinstance(raw, list) or not 1 <= len(raw) <= ITEM_LIMIT:
raise ToolError(f"items must be a list of 1 to {ITEM_LIMIT} objects")
claims = read_claims(self.council)
holders = {claim["office"]: claim["session"] for claim in claims}
sessions = self.office_sessions(claims)
chosen = []
for entry in raw:
if not isinstance(entry, dict):
raise ToolError("every item must be an object with kind, action, office, holder, session and work")
expect(entry, ("kind", *signing.OFFICE_KEYS))
if entry["kind"] != "office":
raise ToolError("kind must be office")
order = {name: entry[name] for name in signing.OFFICE_KEYS}
problem = signing.office_problem(order)
if problem:
raise ToolError(problem)
if order["holder"] != holders.get(order["office"], ""):
raise Conflict({"office": order["office"], "holder": holders.get(order["office"], "")})
if order["action"] != "recall":
self.successor(order, sessions, claims)
item = signing.office_item(**order)
if item in chosen:
raise ToolError("items must be distinct")
chosen.append(item)
return chosen
def successor(self, order: dict, sessions: set[str], claims: list[dict]) -> None:
if order["session"] not in sessions:
raise ToolError("session must be a session the Agent Orchestrator knows")
kind, dimension, _ = office_kind(order["office"])
others = [claim["office"] for claim in claims if claim["session"] == order["session"] and claim["dimension"] == dimension and claim["office"] != order["office"]]
if kind == "line" and others:
raise ToolError(f"{order['session']} represents {others[0]} already: one line per dimension")
def appoint(self, body: bytes) -> dict:
payload = decode(body)
expect(payload, ("items", "proof"))
items = self.appointable(payload["items"])
proof = self.covered(payload["proof"], items, time.time())
job_id = secrets.token_hex(8)
job = {"v": JOB_VERSION, "id": job_id, "created": stamp(), "items": [{name: item[name] for name in signing.OFFICE_KEYS} for item in items], "proof": proof}
self.office_spool.put(job_id, job)
names = ", ".join(f"{item['office']} {LABELS[item['action']]}" for item in items)
lines = "\n".join(f"{item['action']} {item['office']}: {item['holder'] or '-'} -> {item['session'] or '-'}" for item in items)
self.log.add({"kind": "run", "title": f"Amt signiert: {names}"[:200], "text": lines, "ref": {"offices": job_id, "status": "queued"}})
return {"id": job_id, "status": "queued", "items": job["items"]}
def office_state(self, job_id: str) -> dict:
result = read_json(self.office_spool.results / f"{job_id}.json")
if isinstance(result, dict) and isinstance(result.get("items"), list):
return {**result, "id": job_id, "status": "done"}
if (self.office_spool.jobs / f"{job_id}.json").is_file():
return {"id": job_id, "status": "queued", "items": []}
raise Missing(job_id)
+25 -2
View File
@@ -1,12 +1,17 @@
from __future__ import annotations
import datetime
import errno
import hashlib
import json
import os
import stat
import tempfile
from pathlib import Path
READ_FLAGS = os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC
JSON_LIMIT = 1 << 24
def stamp() -> str:
return datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds")
@@ -20,9 +25,27 @@ def write_atomic(path: Path, data: bytes) -> None:
os.replace(handle.name, path)
def read_regular(path: Path, limit: int | None) -> str:
"""Args:
path: the file; a link, FIFO or other non-regular file raises OSError
without blocking.
limit: the most bytes to accept, more raise OSError; None for any size.
Returns:
The file's text, UTF-8 decoded.
"""
with open(os.open(path, READ_FLAGS), "rb") as handle:
if not stat.S_ISREG(os.fstat(handle.fileno()).st_mode):
raise OSError(errno.EINVAL, "not a regular file", str(path))
data = handle.read() if limit is None else handle.read(limit + 1)
if limit is not None and len(data) > limit:
raise OSError(errno.EFBIG, f"larger than {limit} bytes", str(path))
return data.decode("utf-8")
def read_json(path: Path) -> object:
try:
return json.loads(path.read_text(encoding="utf-8"))
return json.loads(read_regular(path, JSON_LIMIT))
except (OSError, ValueError, RecursionError):
return None
@@ -37,7 +60,7 @@ def parse_line(line: str) -> dict | None:
def read_jsonl(path: Path) -> list[dict]:
try:
lines = path.read_text(encoding="utf-8").splitlines()
lines = read_regular(path, None).splitlines()
except (OSError, ValueError):
return []
return [row for row in map(parse_line, lines) if row is not None]
+26 -4
View File
@@ -2,11 +2,12 @@ from __future__ import annotations
from pathlib import Path
from .council import agent
from .council import agent, ordered
from .fields import ORDER_NAME
from .records import read_json, shown
REGISTER = "rasci.json"
AGENT_ORDER = "agent-order.json"
OPERATOR = "operator/"
ENDED = ("done", "stopped")
@@ -98,10 +99,31 @@ def register_rows(tasks: dict[str, dict]) -> list[dict]:
return [register_row(task, row) for task, row in sorted(tasks.items(), key=pinned) if is_open(task, row, False)]
def priority(tasks: dict[str, dict]) -> list[str]:
def stored_order(path: Path) -> list[str]:
data = read_json(path)
names = data.get("agents") if isinstance(data, dict) else None
if not isinstance(names, list) or not all(isinstance(name, str) for name in names):
return []
return names
def agent_order(tasks: dict[str, dict], claims: list[dict], stored: list[str]) -> list[str]:
"""Args:
tasks: the register.
claims: every office claim; its holder is an agent too.
stored: the operator's agent order, as `rasci.py agents order` wrote it.
Returns:
The agents `rasci.py agents order` accepts: the stored ones it still
knows first, in their order, then the workers of open tasks by pin,
then the remaining office holders by name.
"""
names: list[str] = []
for _, row in sorted(tasks.items(), key=pinned):
worker = row.get("worker")
if status_of(row) not in ENDED and isinstance(worker, str) and ORDER_NAME.fullmatch(worker) and worker not in names:
if status_of(row) not in ENDED and isinstance(worker, str) and worker not in names:
names.append(worker)
return names
names += [name for name in ordered({claim["address"] or claim["session"] for claim in claims}) if name not in names]
known = [name for name in names if ORDER_NAME.fullmatch(name)]
listed = [name for name in dict.fromkeys(stored) if name in known]
return [*listed, *(name for name in known if name not in listed)]
+10 -2
View File
@@ -22,7 +22,15 @@ from .messaging import message_row
from .orbits import TRACK_STEPS, track
from .phrasing import phrase
from .records import epoch, iso_epoch, read_jsonl
from .register import REGISTER, operator_rows, priority, read_register, register_rows
from .register import (
AGENT_ORDER,
REGISTER,
agent_order,
operator_rows,
read_register,
register_rows,
stored_order,
)
from .reviewing import review_row
from .settings import media_origins
from .skill_scripts import OUTPUT_LIMIT
@@ -117,7 +125,7 @@ class View:
"messages": [message_row(message) for message in self.messages.listed() if begin <= message["created_ts"] <= end],
"operator": operator_rows(tasks),
"register": register_rows(tasks),
"priority": priority(tasks),
"agent_order": agent_order(tasks, claims, stored_order(self.council / AGENT_ORDER)),
"radio": radio,
"claims": claims,
"grants": grant_rows,