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

+9 -4
View File
@@ -4,6 +4,7 @@ import secrets
import time
from .asking import ask_state
from .choosing import answer_signed
from .cleanup import cleanup_request
from .denying import DENIABLE, DENY_KEYS, denial_note, request_status, request_words
from .errors import Conflict, Missing, ToolError
@@ -35,7 +36,8 @@ def inbox_item(raw: dict, label: str, title: str, gate: str, subject: dict | Non
label: the chosen option of a decision, else empty.
title: what the stream logs for the item.
gate: the review gate, else empty.
subject: what an approval signs; None for a decision.
subject: what an approval signs; for a decision the answer post to
the asking agent.
requester: the requesting agent's name for a denial, else empty.
Returns:
@@ -105,14 +107,15 @@ class Approving:
Returns:
The proof to relay, None when nothing needs a signature. The hub
verifies the signature of a denial itself, since it acts on it;
of an approval it checks the shape, and the executors verify.
of an approval it checks the shape, and the executors verify,
the hub itself for a choice answer when it sends it.
"""
if not signed:
if proof is not None:
raise ToolError("proof must be null when no item needs a signature")
return None
if not isinstance(proof, dict):
raise ToolError("not signed: an approval of an ask, review or operator command needs the passkey signature")
raise ToolError("not signed: an approval of an ask, review, choice or operator command needs the passkey signature")
expect(proof, ("payload", "assertion"))
try:
payload = signing.checked_approval(proof["payload"], now)
@@ -162,6 +165,8 @@ class Approving:
record, changed = self.asks.decide(item["id"], {"status": "allowed" if approve else "denied", "proof": proof})
elif item["kind"] == "review":
record, changed = self.reviews.decide(item["id"], {"status": "approved" if approve else "rejected", "proof": proof})
elif item["signed"] is not None:
return self.answer_choice(item, proof)
else:
record, changed = self.decisions.decide(item["id"], {"status": "decided", "choice": item["label"]})
if record is None:
@@ -218,7 +223,7 @@ class Approving:
if kind_name == "rule":
return inbox_item(raw, "", f"Rule {request_words(record)}", "", request_signed(record), self.requester(raw, record["agent"]))
title = {"review": record.get("title"), "ask": f"{record.get('tool')} {record.get('command')}", "decision": record.get("question")}[kind_name]
subject = {"review": review_signed, "ask": ask_signed}[kind_name](record) if kind_name != "decision" else None
subject = {"review": review_signed, "ask": ask_signed}[kind_name](record) if kind_name != "decision" else answer_signed(record, label)
return inbox_item(raw, label, title, record.get("gate", ""), subject, self.requester(raw, record["session"] if kind_name == "ask" else ""))
def checked(self, raw: object) -> list[dict]: