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:
1 parent
8e180be256
commit
4540494c30
53 files changed
+1821
-211
No files matched your search
+9
-1
@@ -3,7 +3,8 @@ import {mkdtempSync, readFileSync, rmSync, writeFileSync} from "node:fs";
|
||||
import {tmpdir} from "node:os";
|
||||
import {join} from "node:path";
|
||||
|
||||
// Args: <chromium binary> <plan.json>; the plan lists steps {width, height, url, wait, probe, shot, media} and an optional
|
||||
// Args: <chromium binary> <plan.json>; the plan lists steps {width, height, url, wait, keys, probe, shot, media}, keys
|
||||
// being real key presses (Enter, Tab) on the focused element before the probe, and an optional
|
||||
// authenticator {credentialId, privateKey, rpId}: base64 id and PKCS#8 key of a passkey the page may sign with.
|
||||
// Prints {results, errors}: one probe value per step and every uncaught page exception.
|
||||
const [binary, planFile] = process.argv.slice(2);
|
||||
@@ -14,6 +15,7 @@ const profile = mkdtempSync(join(tmpdir(), "cdp-"));
|
||||
const flags = ["--headless", "--no-sandbox", "--disable-gpu", "--use-angle=swiftshader", "--enable-unsafe-swiftshader", "--no-first-run", "--no-default-browser-check", "--hide-scrollbars", `--user-data-dir=${profile}`, "--remote-debugging-port=0", "about:blank"];
|
||||
const browser = spawn(binary, flags, {stdio: ["ignore", "ignore", "pipe"]});
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const KEYS = {Enter: [13, "\r"], Tab: [9, ""]};
|
||||
|
||||
const address = await new Promise((resolve, reject) => {
|
||||
let text = "";
|
||||
@@ -79,6 +81,12 @@ try {
|
||||
await page("Page.navigate", {url: step.url});
|
||||
}
|
||||
await sleep(step.wait);
|
||||
for (const key of step.keys || []) {
|
||||
const [code, text] = KEYS[key];
|
||||
await page("Input.dispatchKeyEvent", {type: "keyDown", key, code: key, windowsVirtualKeyCode: code, text});
|
||||
await page("Input.dispatchKeyEvent", {type: "keyUp", key, code: key, windowsVirtualKeyCode: code});
|
||||
await sleep(250);
|
||||
}
|
||||
let value = null;
|
||||
if (step.probe) {
|
||||
const {result, exceptionDetails} = await page("Runtime.evaluate", {expression: step.probe, awaitPromise: true, returnByValue: true});
|
||||
|
||||
@@ -19,11 +19,13 @@ sys.path.insert(0, str(HERE.parent))
|
||||
from hub_case import HubCase
|
||||
from hub_support import ASK, iso, verifier
|
||||
from test_hub_cleanup import DONE_TIP, FAKE_PKGMGR, host
|
||||
from test_hub_offices import COUNCIL_SCRIPT
|
||||
from test_hub_offices import host as office_host
|
||||
|
||||
OUT = Path(os.environ.get("E2E_OUT", "/tmp/orchestrator-phase4-shots"))
|
||||
WIDTHS = ((1920, 1080), (1366, 768), (390, 844))
|
||||
VIEWS = ("orbit", "chord", "swimlane", "tree", "3d")
|
||||
SCREENS = ["stream", "stream/inbox", "stream/rules", *(f"{tab}/{view}" for tab in ("agents", "artifacts") for view in VIEWS)]
|
||||
SCREENS = ["stream", "stream/inbox", "stream/rules", *(f"{tab}/{view}" for tab in ("agents", "artifacts") for view in VIEWS), "agents/hierarchy"]
|
||||
HELPERS = """
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const until = async (check, ms) => {
|
||||
@@ -78,9 +80,57 @@ PROBE = """(async () => {@HELPERS@
|
||||
marks: all("#main svg.chart .node").length,
|
||||
note: text(document.querySelector("#main .space-note")),
|
||||
canvas: Boolean(document.querySelector("#main .graph3d canvas")),
|
||||
org: all("#main .org-chart .org-row[data-office]").map((row) => [row.dataset.office, text(row.querySelector(".org-holder"))]),
|
||||
toggles: all("#main .org-toggle").map((button) => [name(button), button.getAttribute("aria-expanded")]),
|
||||
order: all("#main .agent-order li").map((item) => item.dataset.name),
|
||||
wide: document.documentElement.scrollWidth > window.innerWidth + 1,
|
||||
};
|
||||
})()""".replace("@HELPERS@", HELPERS)
|
||||
ORG_OPEN = """(async () => {@HELPERS@
|
||||
const row = await until(() => document.querySelector('#main .org-row[data-office="identity/oidc"]'), 8000);
|
||||
if (!row) {
|
||||
return {error: text(document.getElementById("main"))};
|
||||
}
|
||||
const toggle = all("#main .org-toggle").find((button) => name(button) === "Zuklappen: identity");
|
||||
toggle.focus();
|
||||
return {focused: document.activeElement.dataset.focus, offices: all("#main .org-row[data-office]").map((entry) => entry.dataset.office)};
|
||||
})()""".replace("@HELPERS@", HELPERS)
|
||||
ORG_FOLDED = """(async () => {@HELPERS@
|
||||
const toggle = document.activeElement;
|
||||
const folded = {focused: toggle.dataset.focus, expanded: toggle.getAttribute("aria-expanded"), label: name(toggle), hidden: document.getElementById(toggle.getAttribute("aria-controls")).hidden, seen: all("#main .org-row[data-office]").filter((entry) => entry.offsetParent !== null).map((entry) => entry.dataset.office)};
|
||||
toggle.click();
|
||||
await sleep(200);
|
||||
const before = all("#main .agent-order li").map((item) => item.dataset.name);
|
||||
all("#main .agent-order li")[1].querySelector('[data-focus^="up:"]').focus();
|
||||
return {...folded, again: toggle.getAttribute("aria-expanded"), before};
|
||||
})()""".replace("@HELPERS@", HELPERS)
|
||||
ORDER_MOVED = """(async () => {@HELPERS@
|
||||
const after = all("#main .agent-order li").map((item) => item.dataset.name);
|
||||
const focused = document.activeElement.dataset.focus || null;
|
||||
const save = all("#main .agent-order button").find((button) => text(button) === "Reihenfolge speichern");
|
||||
const disabled = save.disabled;
|
||||
save.click();
|
||||
const toast = await until(() => all("#toasts .toast").find((box) => text(box).startsWith("Priorität")), 8000);
|
||||
return {after, focused, disabled, toast: text(toast)};
|
||||
})()""".replace("@HELPERS@", HELPERS)
|
||||
OPEN_TAKEOVER = """(async () => {@HELPERS@
|
||||
document.querySelector('#main [data-focus="takeover:identity/oidc"]').click();
|
||||
await sleep(400);
|
||||
return {title: text(document.querySelector(".sheet-head .panel-title")), options: all(".sheet-box select option").map((option) => option.value), work: document.querySelector('.sheet-box input[aria-label="Arbeit"]').value, focused: document.activeElement.tagName};
|
||||
})()""".replace("@HELPERS@", HELPERS)
|
||||
CONFIRM_TAKEOVER = """(async () => {@HELPERS@
|
||||
all(".sheet-box button").find((button) => name(button) === "Übergeben").click();
|
||||
const done = await until(() => document.querySelector(".sheet-box [data-result]"), 30000);
|
||||
return {result: done ? done.dataset.result : null, text: text(done), toasts: all("#toasts .toast").map(text)};
|
||||
})()""".replace("@HELPERS@", HELPERS)
|
||||
AFTER_TAKEOVER = """(async () => {@HELPERS@
|
||||
document.querySelector(".sheet-head button").click();
|
||||
const row = await until(() => {
|
||||
const found = document.querySelector('#main .org-row[data-office="identity/oidc"] .org-holder');
|
||||
return found && !text(found).endsWith("Stage") ? found : null;
|
||||
}, 15000);
|
||||
return {holder: text(row)};
|
||||
})()""".replace("@HELPERS@", HELPERS)
|
||||
ACTIONS = """(async () => {@HELPERS@
|
||||
const press = async (key) => {
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", {key, bubbles: true}));
|
||||
@@ -174,6 +224,8 @@ class TestOrchestratorE2E(HubCase):
|
||||
self.claim("s1", "Skills session")
|
||||
self.write_council({
|
||||
"work/s2/claim.json": {"office": "work/s2", "session": "s2", "runtime": "claude", "name": "Stage", "since": iso(now - 7200), "beat": now - 2000, "work": "x"},
|
||||
"chair/claim.json": {"office": "chair", "session": "s1", "runtime": "claude", "name": "Skills session", "since": iso(now - 7200), "beat": now, "work": "allocate"},
|
||||
"identity/oidc/claim.json": {"office": "identity/oidc", "session": "s2", "runtime": "claude", "name": "Stage", "since": iso(now - 7200), "beat": now - 2000, "work": "oidc line"},
|
||||
"rasci.json": {"work/stage": {"title": "Stage line", "status": "running", "worker": "Stage (this session)", "roles": {"Stage (this session)": "RA"}}},
|
||||
"radio-silence.json": {"since": now - 60, "until": now + 3600, "by": "Docs", "text": "disk full"},
|
||||
"news.jsonl": self.jsonl([
|
||||
@@ -253,15 +305,20 @@ class TestOrchestratorE2E(HubCase):
|
||||
self.assertEqual([probe["chips"] for probe in probes if probe["hash"] == "#stream"], [["Für dich", "Funk", "Arbeit", "Transkripte"]] * len(WIDTHS))
|
||||
self.assertEqual((stream["menus"], stream["custom"]), (["Filter"], True))
|
||||
self.assertEqual(by_hash["#stream/inbox"]["inbox"], 2)
|
||||
for tab in ("agents", "artifacts"):
|
||||
for tab, own in (("agents", ["Hierarchie"]), ("artifacts", [])):
|
||||
with self.subTest(tab=tab):
|
||||
self.assertEqual(by_hash[f"#{tab}/orbit"]["views"], ["Orbit", "Chord", "Swimlane", "Tree", "3D"])
|
||||
self.assertEqual(by_hash[f"#{tab}/orbit"]["views"], ["Orbit", "Chord", "Swimlane", "Tree", "3D", *own])
|
||||
self.assertEqual(by_hash[f"#{tab}/3d"]["layouts"], ["Kraft", "Zeit", "Spawn-Tiefe"])
|
||||
self.assertTrue(by_hash[f"#{tab}/3d"]["canvas"], by_hash[f"#{tab}/3d"]["note"])
|
||||
self.assertRegex(by_hash[f"#{tab}/3d"]["note"], r"^\d+ Knoten · \d+ Kanten · Kraft$")
|
||||
agents = by_hash["#agents/orbit"]
|
||||
self.assertEqual(agents["toolbar"], ["Funk an alle", "Mayday aufheben", "Priorität", "Tag/Nacht"])
|
||||
self.assertEqual(agents["toAll"], [["newsletter", True], ["broadcast", False], ["pan-pan", False], ["security", False], ["mayday", False]])
|
||||
hierarchy = by_hash["#agents/hierarchy"]
|
||||
self.assertEqual([office for office, _ in hierarchy["org"]], ["chair", "identity/oidc", "work/s1", "work/s2"])
|
||||
self.assertEqual(hierarchy["toggles"], [["Zuklappen: Operator", "true"], ["Zuklappen: Chair", "true"], ["Zuklappen: identity", "true"], ["Zuklappen: Präsidenten", "true"]])
|
||||
self.assertEqual(hierarchy["toolbar"], agents["toolbar"])
|
||||
self.assertGreaterEqual(len(hierarchy["order"]), 2)
|
||||
self.assertEqual(by_hash["#agents/tree"]["spawn"][0], ["operator", None])
|
||||
self.assertIn(["twig", "true"], by_hash["#agents/tree"]["spawn"])
|
||||
self.assertIn(["twig", "false"], by_hash["#agents/tree"]["spawn"])
|
||||
@@ -363,6 +420,45 @@ class TestOrchestratorE2E(HubCase):
|
||||
["branch", "tree", "--format", "json", "--output", str(self.tree_file)],
|
||||
])
|
||||
|
||||
def test_chromium_folds_the_hierarchy_by_keyboard_reorders_agents_and_hands_an_office_over_with_a_signature(self):
|
||||
signer = self.pin()
|
||||
trust = verifier.Trust(approvers=self.approvers, replay=self.replay / "offices.json", origin=self.origin)
|
||||
runner = office_host.Offices(trust, self.council, COUNCIL_SCRIPT, self.outside / "home")
|
||||
jobs, results = self.offices / "jobs", self.offices / "results"
|
||||
self.beside(lambda: any(jobs.iterdir()) and office_host.cycle(jobs, results, runner))
|
||||
private = "".join(line for line in signer.pem.read_text(encoding="utf-8").splitlines() if not line.startswith("-----"))
|
||||
authenticator = {"credentialId": base64.b64encode(signer.raw_id).decode(), "privateKey": private, "rpId": "localhost"}
|
||||
local = f"http://localhost:{self.port}/"
|
||||
steps = [
|
||||
{"width": 1366, "height": 768, "url": f"{local}#agents/hierarchy", "wait": 1200, "probe": ORG_OPEN, "shot": self.shot("1366-hierarchy-open")},
|
||||
{"width": 1366, "height": 768, "url": None, "wait": 0, "keys": ["Enter"], "probe": ORG_FOLDED, "shot": None},
|
||||
{"width": 1366, "height": 768, "url": None, "wait": 0, "keys": ["Enter"], "probe": ORDER_MOVED, "shot": self.shot("1366-hierarchy-order")},
|
||||
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": OPEN_TAKEOVER, "shot": self.shot("1366-hierarchy-takeover")},
|
||||
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": CONFIRM_TAKEOVER, "shot": self.shot("1366-hierarchy-takeover-result")},
|
||||
{"width": 1366, "height": 768, "url": None, "wait": 0, "probe": AFTER_TAKEOVER, "shot": self.shot("1366-hierarchy-after")},
|
||||
]
|
||||
data = self.drive(steps, authenticator)
|
||||
(OUT / "chromium-hierarchy.json").write_text(json.dumps(data, indent=1), encoding="utf-8")
|
||||
self.assertEqual(data["errors"], [])
|
||||
opened, folded, moved, sheet, confirmed, after = data["results"]
|
||||
self.assertNotIn("error", opened)
|
||||
self.assertEqual(opened["focused"], "toggle:dimension:identity")
|
||||
self.assertEqual((folded["focused"], folded["expanded"], folded["label"], folded["hidden"], folded["again"]), ("toggle:dimension:identity", "false", "Aufklappen: identity", True, "true"))
|
||||
self.assertNotIn("identity/oidc", folded["seen"])
|
||||
before = folded["before"]
|
||||
self.assertEqual(moved["after"], [before[1], before[0], *before[2:]])
|
||||
self.assertEqual((moved["focused"], moved["disabled"]), (f"down:{before[1]}", False))
|
||||
self.assertTrue(moved["toast"].startswith("Priorität"), moved)
|
||||
self.assertIn(("rasci.py", ["agents", "order", ",".join(moved["after"])]), [(Path(script).name, args) for script, args in self.scripts.calls])
|
||||
self.assertEqual((sheet["title"], sheet["options"], sheet["work"], sheet["focused"]), ("identity/oidc: Übergeben", ["s1"], "oidc line", "SELECT"))
|
||||
self.assertEqual((confirmed["result"], confirmed["text"]), ("done", "identity/oidcerledigt"), confirmed)
|
||||
self.assertIn("Übergeben: erledigtidentity/oidc: erledigt", confirmed["toasts"])
|
||||
self.assertNotIn("error", after)
|
||||
claim = json.loads((self.council / "identity" / "oidc" / "claim.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual((claim["session"], claim["work"]), ("s1", "oidc line"))
|
||||
written = [json.loads(path.read_text(encoding="utf-8")) for path in results.glob("*.json")]
|
||||
self.assertEqual([[(item["office"], item["action"], item["status"]) for item in result["items"]] for result in written], [[("identity/oidc", "takeover", "done")]])
|
||||
|
||||
@unittest.skipUnless(shutil.which(os.environ.get("FIREFOX", "firefox")), "no Firefox")
|
||||
def test_firefox_walks_every_screen_at_three_widths(self):
|
||||
shots = [(width, height, screen, OUT / f"firefox-{width}-{screen.replace('/', '-')}.png") for width, height in WIDTHS for screen in SCREENS]
|
||||
|
||||
Reference in new issue
Block a user