feat(orchestrator): S5 Tasks with choices, Gantt, estimates and burn charts

Requirement 001 phase 6, S5:

- Tasks tab: every pending ask, rule request, operator row and choice
  card shows under "waits on Operator"; a choice request goes out on a
  fixed channel button, post and call with an agent multi-select, and a
  click on an option sends a passkey-signed answer the agent checks with
  operator_message. The unsigned /api/decisions/<id>/choose route is
  gone, and a decision, choice answer or choice recipient whose name
  holds "*" is refused, so a signed answer never reaches every agent.
- Gantt view: one bar per task from start to end (running to now), one
  arrow per explicit, branch-base and queue-order edge, hatched forecast
  bars (always, points only, real only) from the median duration or
  points times median hours per point.
- rasci.py set --after/--drop-after stores task dependencies and refuses
  unknown tasks and cycles.
- Estimation toggle (No estimate default, Story points) is stored in the
  hub settings; burndown and burnup per whole register, list filter or
  one agent or line, counting tasks or summing complexity with the
  unestimated tasks as their own line.
- New skill kvb-org-queue-estimate: "Request estimates" (story points
  mode, passkey-signed board action) or the skill sends exactly one
  newsletter listing every unestimated task with its RASCI parties; a
  party name that is no plain name shows as "unnamed party".
- Business value per task or in bulk is stored only with a verified
  passkey assertion over the exact ids and values; WSJF uses it instead
  of the median. The task list sorts by every decision-50 mode both ways.
- e2e: the walk's pending ask lives an hour instead of 60 s, and the
  CDP and BiDi drivers retry removing the browser profile while the
  browser exits (ENOTEMPTY).

Evidence: make test 709 OK, ruff and markdownlint clean
(/tmp/s5-make-test-fix-2.log); e2e 26 OK in Chromium and Firefox
(/tmp/s5-e2e-fix-2.log); MEDUSA 50 files, an independent skeptic
rejected four holes (unsigned choose route, unsigned estimates request,
free-text party names, "*" recipients incl. normalizer bypasses), all
fixed and re-verified, the 8 remaining pattern hits accepted, rescan 0
open.

🎯 Fix: 92 %
💥 Break: 4 %
💸 Cost: 100 %
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 88
🚦 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-09 06:53:55 +00:00
1 parent 5c8b69d53d
commit d9eb121d12
49 files changed
+2162 -148

No files matched your search

+30 -11
View File
@@ -5,12 +5,12 @@ import secrets
import time
from .council import HUMAN, agent, ordered, read_claims
from .errors import Missing, ToolError
from .errors import Conflict, 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 .signed import message_signed, signing
from .skill_scripts import NIGHT, RADIO, RASCI
from .skill_scripts import ESTIMATE, NIGHT, RADIO, RASCI
NIGHTS = {
"drain": (["drain"], "Operator ran kvb-org-drain in the Agent Orchestrator: run kvb-agent-drain; verify with night.py status."),
@@ -23,7 +23,7 @@ TO_ALL = {"newsletter": ["post"], **{cls: ["send", cls] for cls in ("broadcast",
EVERYONE = "*"
DENIED = "Denied by the operator."
MESSAGE_KEYS = ("to", "text", "upload", "class", "voice")
SIGNED_BOARD = ("clear-mayday", *NIGHTS)
SIGNED_BOARD = ("clear-mayday", *NIGHTS, "estimates")
def chosen(payload: dict, choices: dict[str, list[str]]) -> list[str]:
@@ -142,17 +142,31 @@ class Messaging:
item, _, _ = self.composed(payload, {**DIRECT, **TO_ALL})
return self.challenge_for([item])
def stored_message(self, item: dict, proof: dict, upload: dict | None) -> dict:
"""Args:
item: the message item a pinned key signed.
proof: that signature, kept so operator_message can prove it.
upload: the attached upload or None.
Returns:
The stored record, addressed to the radio address the claims
give the recipient.
"""
target = item["to"]
claims = read_claims(self.council)
address = next((claim["address"] for claim in claims if claim["agent"] == target and claim["address"]), target)
fields = {"to": target, "address": address, "text": item["text"], "upload": upload, "voice": item["voice"] or None, "voice_sha256": item["voice_sha256"], "class": item["class"]}
return self.messages.add({**fields, "proof": proof, "created_ts": time.time()})
def radioed(self, record: dict, command: list[str]) -> dict:
targets = [] if record["to"] == EVERYONE else ["--to", record["address"]]
return self.scripts.run(RADIO, [*command, *targets, "--session", HUMAN, f"[kvb-orchestrator msg {record['id']}] {self.relayed(record)}"])
def relay(self, payload: dict, choices: dict[str, list[str]]) -> dict:
with self.voice_notes.held(payload["voice"]):
item, command, upload = self.composed(payload, choices)
proof = self.operator_proven(payload["proof"], item)
target = item["to"]
claims = read_claims(self.council)
address = next((claim["address"] for claim in claims if claim["agent"] == target and claim["address"]), target)
fields = {"to": target, "address": address, "text": item["text"], "upload": upload, "voice": item["voice"] or None, "voice_sha256": item["voice_sha256"], "class": item["class"]}
record = self.messages.add({**fields, "proof": proof, "created_ts": time.time()})
targets = [] if target == EVERYONE else ["--to", address]
run = self.scripts.run(RADIO, [*command, *targets, "--session", HUMAN, f"[kvb-orchestrator msg {record['id']}] {self.relayed(record)}"])
record = self.stored_message(item, self.operator_proven(payload["proof"], item), upload)
run = self.radioed(record, command)
return {"id": record["id"], "ok": run["code"] == 0, "runs": [run]}
def message(self, body: bytes) -> dict:
@@ -191,6 +205,11 @@ class Messaging:
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 == "estimates":
estimation = self.settings.get()["estimation"]
if estimation != "points":
raise Conflict({"estimation": estimation, "reason": "estimates are requested only in story points mode"})
runs = [self.scripts.run(ESTIMATE, ["request", "--session", HUMAN])]
elif action in NIGHTS:
command, text = NIGHTS[action]
runs = [self.scripts.run(NIGHT, [*command, "--session", HUMAN])]