feat(orchestrator): W2 Kanban board with swimlanes and forecast

Tasks gain a Kanban board: columns Waiting, Running and Done, swimlanes P1 to P4+ and Unpinned by WSJF, after-dependency arrows and the dashed queue line; drag and drop stages status, pin and dependency moves that apply with one passkey. The burn chart gets the trend and a forecast. Committed below the bar on the operator's word 2026-11-12 (no further per-bundle tests, one gate after the merge): the W2 e2e never ran.

🎯 Fix: 80 %
💥 Break: 10 %
💸 Cost: 100 %
⏱️ Time: 100 %
🛡️ Security: 100 %
🏆 Score: 72
🚦 Gate: ⛔ fix 80 % < 90, break 10 % > 5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-12 16:14:36 +00:00
1 parent fd8fc08603
commit 8e63b21e21
18 files changed
+1375 -76

No files matched your search

+28 -2
View File
@@ -28,9 +28,13 @@ def kind_of(task: str) -> str:
return "hotfix" if task.startswith(HOTFIX) else "task"
def editable(task: str, row: dict) -> bool:
def movable(task: str) -> bool:
shown_whole = len(task) <= TEXT_SHOWN and signing.TASK.fullmatch(task) is not None
return shown_whole and kind_of(task) == "task" and status_of(row) not in ENDED
return shown_whole and kind_of(task) == "task"
def editable(task: str, row: dict) -> bool:
return movable(task) and status_of(row) not in ENDED
def fibonacci(value: object) -> bool:
@@ -131,6 +135,7 @@ def task_row(task: str, row: dict, trees: list[dict]) -> dict:
"kind": kind_of(task),
"ended": ended,
"editable": editable(task, row),
"movable": movable(task),
"prio": prio_of(row),
"estimates": estimates,
"wsjf": wsjf(estimates),
@@ -138,6 +143,7 @@ def task_row(task: str, row: dict, trees: list[dict]) -> dict:
"points": points(estimates),
"start": iso_epoch(row.get("start")),
"end": iso_epoch(row.get("end")) if ended else None,
"updated": iso_epoch(row.get("updated")),
"after": after_of(row),
"base": shown(row.get("base"))[:TEXT_SHOWN],
"roles": roles_of(row),
@@ -163,6 +169,7 @@ def queue_row(entry: dict, created: float, trees: list[dict]) -> dict:
"kind": "queue",
"ended": False,
"editable": False,
"movable": False,
"prio": None,
"estimates": {},
"wsjf": None,
@@ -170,6 +177,7 @@ def queue_row(entry: dict, created: float, trees: list[dict]) -> dict:
"points": None,
"start": (epoch(entry.get("granted")) or created) if entry.get("status") == "active" else None,
"end": None,
"updated": None,
"after": [],
"base": "-",
"roles": {},
@@ -250,6 +258,24 @@ def queue_chain(queue: list[tuple[dict, float]], tasks: dict[str, dict], agents:
return [(shown(entry.get("id"))[:TEXT_SHOWN], entry.get("status") == "waiting") for entry, _ in [*active, *waiting]]
def queue_line(chain: list[tuple[str, bool]], queue: list[tuple[dict, float]]) -> list[str]:
"""Args:
chain: queue_chain().
queue: the kvb-org-queue entries with their enqueue time.
Returns:
The tasks holding a live queue entry, each once, in the grant order
of their first entry.
"""
owners = {shown(entry.get("id"))[:TEXT_SHOWN]: entry.get("task") for entry, _ in queue}
line: list[str] = []
for job, _ in chain:
task = owners.get(job)
if isinstance(task, str) and len(task) <= TEXT_SHOWN and task not in line:
line.append(task)
return line
def branch_of(row: dict) -> str:
return row["branch"] if row["branch"] != "-" else row["worktree_branch"]