From de8c5f3116cf85b443b483a066580a7a5967fa1f Mon Sep 17 00:00:00 2001 From: Kublai Agent Date: Sun, 10 May 2026 22:10:35 -0400 Subject: [PATCH] Add buildroom control room cron loop --- tests/kurultai/test_buildroom_foundation.py | 55 +++++ .../control-room-attention-items.json | 74 +++++++ .../buildroom/control-room-kanban-drafts.json | 33 +++ tools/kurultai/buildroom/control-room.html | 4 +- tools/kurultai/buildroom/control-room.json | 2 +- tools/kurultai/buildroom/control-room.md | 2 +- .../buildroom/scripts/control_room_cron.py | 208 ++++++++++++++++++ 7 files changed, 374 insertions(+), 4 deletions(-) create mode 100644 tools/kurultai/buildroom/control-room-attention-items.json create mode 100644 tools/kurultai/buildroom/control-room-kanban-drafts.json create mode 100644 tools/kurultai/buildroom/scripts/control_room_cron.py diff --git a/tests/kurultai/test_buildroom_foundation.py b/tests/kurultai/test_buildroom_foundation.py index 5f48d6df..7d02ac74 100644 --- a/tests/kurultai/test_buildroom_foundation.py +++ b/tests/kurultai/test_buildroom_foundation.py @@ -466,3 +466,58 @@ def test_control_room_report_flags_missing_evidence(tmp_path: Path) -> None: assert payload["rooms"][0]["has_missing_evidence"] is True assert "missing" in html_output.read_text() + + + +def test_control_room_cron_emits_attention_and_kanban_drafts(tmp_path): + room = copy_demo_room(tmp_path) + trust_path = room / "trust/trust-report.json" + trust = json.loads(trust_path.read_text()) + trust["state"] = "investigate" + trust_path.write_text(json.dumps(trust)) + rooms_root = room.parent + attention = tmp_path / "attention.json" + drafts = tmp_path / "drafts.json" + markdown = tmp_path / "control-room.md" + payload = tmp_path / "control-room.json" + html = tmp_path / "control-room.html" + + result = run_script( + "tools/kurultai/buildroom/scripts/control_room_cron.py", + "--rooms-root", rooms_root, + "--markdown-output", markdown, + "--json-output", payload, + "--html-output", html, + "--attention-output", attention, + "--kanban-drafts-output", drafts, + ) + + assert result.returncode == 0, result.stderr + attention_payload = json.loads(attention.read_text()) + draft_payload = json.loads(drafts.read_text()) + assert attention_payload["summary"]["room_count"] == 1 + assert attention_payload["summary"]["critical_count"] == 1 + assert attention_payload["summary"]["actionable_item_count"] == 1 + assert attention_payload["items"][0]["category"] == "trust" + assert draft_payload["draft_count"] == 1 + assert draft_payload["drafts"][0]["priority"] == "high" + assert "Buildroom follow-up" in draft_payload["drafts"][0]["title"] + + +def test_control_room_cron_fail_on_actionable_exit_code(tmp_path): + room = copy_demo_room(tmp_path) + trust_path = room / "trust/trust-report.json" + trust = json.loads(trust_path.read_text()) + trust["state"] = "watch" + trust_path.write_text(json.dumps(trust)) + result = run_script( + "tools/kurultai/buildroom/scripts/control_room_cron.py", + "--rooms-root", room.parent, + "--attention-output", tmp_path / "attention.json", + "--kanban-drafts-output", tmp_path / "drafts.json", + "--markdown-output", tmp_path / "control-room.md", + "--json-output", tmp_path / "control-room.json", + "--html-output", tmp_path / "control-room.html", + "--fail-on-actionable", + ) + assert result.returncode == 2 diff --git a/tools/kurultai/buildroom/control-room-attention-items.json b/tools/kurultai/buildroom/control-room-attention-items.json new file mode 100644 index 00000000..624d1574 --- /dev/null +++ b/tools/kurultai/buildroom/control-room-attention-items.json @@ -0,0 +1,74 @@ +{ + "actionable_items": [ + { + "category": "operator_decision", + "headline": "gkisokay Auto Think Auto Build", + "id": "2026-05-10-gkisokay-auto-think-auto-build:operator-decisions", + "kanban_recommended": true, + "operator_decisions_needed": [ + "Run validate_room", + "Review operator summary" + ], + "recommended_action": "Resolve or route the operator decision queue.", + "room_id": "2026-05-10-gkisokay-auto-think-auto-build", + "severity": "warning", + "summary": "Room has 2 operator decision(s) queued." + }, + { + "category": "trust", + "headline": "gkisokay Auto Think Auto Build", + "id": "2026-05-10-gkisokay-auto-think-auto-build:trust-watch", + "kanban_recommended": true, + "recommended_action": "Refresh the operator summary and decide whether the room is clean, needs more evidence, or needs investigation.", + "room_id": "2026-05-10-gkisokay-auto-think-auto-build", + "severity": "warning", + "summary": "Room is in watch state." + } + ], + "generated_at": "2026-05-11T02:09:12Z", + "items": [ + { + "category": "operator_decision", + "headline": "gkisokay Auto Think Auto Build", + "id": "2026-05-10-gkisokay-auto-think-auto-build:operator-decisions", + "kanban_recommended": true, + "operator_decisions_needed": [ + "Run validate_room", + "Review operator summary" + ], + "recommended_action": "Resolve or route the operator decision queue.", + "room_id": "2026-05-10-gkisokay-auto-think-auto-build", + "severity": "warning", + "summary": "Room has 2 operator decision(s) queued." + }, + { + "category": "trust", + "headline": "gkisokay Auto Think Auto Build", + "id": "2026-05-10-gkisokay-auto-think-auto-build:trust-watch", + "kanban_recommended": true, + "recommended_action": "Refresh the operator summary and decide whether the room is clean, needs more evidence, or needs investigation.", + "room_id": "2026-05-10-gkisokay-auto-think-auto-build", + "severity": "warning", + "summary": "Room is in watch state." + }, + { + "category": "monitor", + "headline": "Install a typed buildroom foundation for autonomous improvement work", + "id": "demo-room:clean-monitor", + "kanban_recommended": false, + "recommended_action": "No immediate action; include in retention sweep.", + "room_id": "demo-room", + "severity": "info", + "summary": "Room is clean and complete." + } + ], + "summary": { + "actionable_item_count": 2, + "attention_item_count": 3, + "critical_count": 0, + "error_count": 0, + "info_count": 1, + "room_count": 2, + "warning_count": 2 + } +} diff --git a/tools/kurultai/buildroom/control-room-kanban-drafts.json b/tools/kurultai/buildroom/control-room-kanban-drafts.json new file mode 100644 index 00000000..bc69cd1f --- /dev/null +++ b/tools/kurultai/buildroom/control-room-kanban-drafts.json @@ -0,0 +1,33 @@ +{ + "draft_count": 2, + "drafts": [ + { + "body": "Room: 2026-05-10-gkisokay-auto-think-auto-build\nHeadline: gkisokay Auto Think Auto Build\nCategory: operator_decision\nSeverity: warning\nSummary: Room has 2 operator decision(s) queued.\nRecommended action: Resolve or route the operator decision queue.", + "labels": [ + "buildroom", + "control-room", + "operator_decision", + "warning" + ], + "priority": "normal", + "room_id": "2026-05-10-gkisokay-auto-think-auto-build", + "source": "buildroom-control-room-cron-v0", + "title": "Buildroom follow-up: 2026-05-10-gkisokay-auto-think-auto-build \u2014 operator_decision" + }, + { + "body": "Room: 2026-05-10-gkisokay-auto-think-auto-build\nHeadline: gkisokay Auto Think Auto Build\nCategory: trust\nSeverity: warning\nSummary: Room is in watch state.\nRecommended action: Refresh the operator summary and decide whether the room is clean, needs more evidence, or needs investigation.", + "labels": [ + "buildroom", + "control-room", + "trust", + "warning" + ], + "priority": "normal", + "room_id": "2026-05-10-gkisokay-auto-think-auto-build", + "source": "buildroom-control-room-cron-v0", + "title": "Buildroom follow-up: 2026-05-10-gkisokay-auto-think-auto-build \u2014 trust" + } + ], + "generated_at": "2026-05-11T02:09:12Z", + "source": "buildroom-control-room-cron-v0" +} diff --git a/tools/kurultai/buildroom/control-room.html b/tools/kurultai/buildroom/control-room.html index 5a0cf0a1..5cf4b43b 100644 --- a/tools/kurultai/buildroom/control-room.html +++ b/tools/kurultai/buildroom/control-room.html @@ -31,7 +31,7 @@

Kurultai / Buildroom

Control Room

-

Generated 2026-05-11T01:58:38Z. Static, public-safe dashboard for buildroom trust and evidence.

+

Generated 2026-05-11T02:09:12Z. Static, public-safe dashboard for buildroom trust and evidence.

@@ -239,7 +239,7 @@

Install a typed buildroom foundation for autonomous improvement work

} ], "summary": { - "generated_at": "2026-05-11T01:58:38Z", + "generated_at": "2026-05-11T02:09:12Z", "room_count": 2, "rooms_needing_decision": 1, "rooms_with_missing_evidence": 0, diff --git a/tools/kurultai/buildroom/control-room.json b/tools/kurultai/buildroom/control-room.json index 4af1d873..fae9375e 100644 --- a/tools/kurultai/buildroom/control-room.json +++ b/tools/kurultai/buildroom/control-room.json @@ -135,7 +135,7 @@ } ], "summary": { - "generated_at": "2026-05-11T01:58:38Z", + "generated_at": "2026-05-11T02:09:12Z", "room_count": 2, "rooms_needing_decision": 1, "rooms_with_missing_evidence": 0, diff --git a/tools/kurultai/buildroom/control-room.md b/tools/kurultai/buildroom/control-room.md index 2b7bb14e..17d55d7c 100644 --- a/tools/kurultai/buildroom/control-room.md +++ b/tools/kurultai/buildroom/control-room.md @@ -1,6 +1,6 @@ # Kurultai Buildroom Control Room -Generated: 2026-05-11T01:58:38Z +Generated: 2026-05-11T02:09:12Z ## Summary diff --git a/tools/kurultai/buildroom/scripts/control_room_cron.py b/tools/kurultai/buildroom/scripts/control_room_cron.py new file mode 100644 index 00000000..e35d897d --- /dev/null +++ b/tools/kurultai/buildroom/scripts/control_room_cron.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import subprocess +from pathlib import Path +from typing import Any + +from common import BUILDROOM_ROOT, utc_now +from control_room import DEFAULT_HTML, DEFAULT_JSON, DEFAULT_MARKDOWN, ROOMS_ROOT, build_payload, write_outputs + +DEFAULT_ATTENTION = BUILDROOM_ROOT / "control-room-attention-items.json" +DEFAULT_KANBAN_DRAFTS = BUILDROOM_ROOT / "control-room-kanban-drafts.json" +SEVERITY_ORDER = {"info": 0, "warning": 1, "error": 2, "critical": 3} + + +def severity_rank(severity: str) -> int: + return SEVERITY_ORDER.get(severity, 0) + + +def room_attention_items(room: dict[str, Any]) -> list[dict[str, Any]]: + room_id = str(room.get("room_id") or "unknown-room") + headline = str(room.get("headline") or room_id) + trust_state = str(room.get("trust_state") or "watch") + retention = str(room.get("retention") or "unknown") + phase = str(room.get("phase") or "unknown") + missing = [str(item) for item in room.get("missing_artifacts", [])] + decisions = [str(item) for item in room.get("operator_decisions_needed", [])] + items: list[dict[str, Any]] = [] + + if trust_state == "investigate": + items.append({ + "id": f"{room_id}:trust-investigate", + "room_id": room_id, + "headline": headline, + "category": "trust", + "severity": "critical", + "summary": "Trust report requires investigation.", + "recommended_action": "Open the trust report, classify the failure mode, and create a repair or quarantine follow-up.", + "kanban_recommended": True, + }) + elif trust_state == "watch": + items.append({ + "id": f"{room_id}:trust-watch", + "room_id": room_id, + "headline": headline, + "category": "trust", + "severity": "warning", + "summary": "Room is in watch state.", + "recommended_action": "Refresh the operator summary and decide whether the room is clean, needs more evidence, or needs investigation.", + "kanban_recommended": True, + }) + + if missing: + items.append({ + "id": f"{room_id}:missing-evidence", + "room_id": room_id, + "headline": headline, + "category": "missing_evidence", + "severity": "error", + "summary": f"Room is missing {len(missing)} required artifact(s).", + "missing_artifacts": missing, + "recommended_action": f"Fill or explicitly receipt the first missing artifact: {missing[0]}", + "kanban_recommended": True, + }) + + if decisions: + items.append({ + "id": f"{room_id}:operator-decisions", + "room_id": room_id, + "headline": headline, + "category": "operator_decision", + "severity": "warning", + "summary": f"Room has {len(decisions)} operator decision(s) queued.", + "operator_decisions_needed": decisions, + "recommended_action": "Resolve or route the operator decision queue.", + "kanban_recommended": True, + }) + + if phase == "complete" and retention in {"improve", "park", "prune"}: + severity = "critical" if retention == "prune" else "warning" + items.append({ + "id": f"{room_id}:retention-{retention}", + "room_id": room_id, + "headline": headline, + "category": "retention", + "severity": severity, + "summary": f"Retention review recommends {retention}.", + "recommended_action": "Create the retention follow-up or record an explicit no-op receipt.", + "kanban_recommended": retention == "improve", + }) + + if not items and phase == "complete" and trust_state == "clean": + items.append({ + "id": f"{room_id}:clean-monitor", + "room_id": room_id, + "headline": headline, + "category": "monitor", + "severity": "info", + "summary": "Room is clean and complete.", + "recommended_action": "No immediate action; include in retention sweep.", + "kanban_recommended": False, + }) + + return items + + +def build_attention_payload(control_payload: dict[str, Any]) -> dict[str, Any]: + rooms = control_payload.get("rooms", []) + items: list[dict[str, Any]] = [] + for room in rooms: + if isinstance(room, dict): + items.extend(room_attention_items(room)) + actionable = [item for item in items if item.get("kanban_recommended")] + items.sort(key=lambda item: (-severity_rank(str(item.get("severity", "info"))), str(item.get("room_id", "")), str(item.get("category", "")))) + actionable.sort(key=lambda item: (-severity_rank(str(item.get("severity", "info"))), str(item.get("room_id", "")), str(item.get("category", "")))) + return { + "generated_at": utc_now(), + "summary": { + "room_count": len(rooms), + "attention_item_count": len(items), + "actionable_item_count": len(actionable), + "critical_count": sum(1 for item in items if item.get("severity") == "critical"), + "error_count": sum(1 for item in items if item.get("severity") == "error"), + "warning_count": sum(1 for item in items if item.get("severity") == "warning"), + "info_count": sum(1 for item in items if item.get("severity") == "info"), + }, + "items": items, + "actionable_items": actionable, + } + + +def kanban_draft_for_item(item: dict[str, Any]) -> dict[str, Any]: + room_id = str(item.get("room_id") or "unknown-room") + category = str(item.get("category") or "attention") + severity = str(item.get("severity") or "info") + return { + "title": f"Buildroom follow-up: {room_id} — {category}", + "priority": "high" if severity in {"critical", "error"} else "normal", + "labels": ["buildroom", "control-room", category, severity], + "source": "buildroom-control-room-cron-v0", + "room_id": room_id, + "body": "\n".join([ + f"Room: {room_id}", + f"Headline: {item.get('headline', room_id)}", + f"Category: {category}", + f"Severity: {severity}", + f"Summary: {item.get('summary', '')}", + f"Recommended action: {item.get('recommended_action', '')}", + ]), + } + + +def build_kanban_drafts(attention_payload: dict[str, Any]) -> dict[str, Any]: + actionable = attention_payload.get("actionable_items", []) + drafts = [kanban_draft_for_item(item) for item in actionable if isinstance(item, dict)] + return { + "generated_at": utc_now(), + "source": "buildroom-control-room-cron-v0", + "draft_count": len(drafts), + "drafts": drafts, + } + + +def write_json(path: Path, payload: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def git_has_changes(paths: list[Path]) -> bool: + result = subprocess.run(["git", "status", "--porcelain", "--", *[str(path) for path in paths]], text=True, capture_output=True, timeout=30) + return bool(result.stdout.strip()) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Cron-ready buildroom Control Room regeneration and attention extraction.") + parser.add_argument("--rooms-root", type=Path, default=ROOMS_ROOT) + parser.add_argument("--markdown-output", type=Path, default=DEFAULT_MARKDOWN) + parser.add_argument("--json-output", type=Path, default=DEFAULT_JSON) + parser.add_argument("--html-output", type=Path, default=DEFAULT_HTML) + parser.add_argument("--attention-output", type=Path, default=DEFAULT_ATTENTION) + parser.add_argument("--kanban-drafts-output", type=Path, default=DEFAULT_KANBAN_DRAFTS) + parser.add_argument("--fail-on-actionable", action="store_true", help="Exit 2 when actionable attention items exist.") + parser.add_argument("--fail-on-changed", action="store_true", help="Exit 3 when generated outputs changed the working tree.") + args = parser.parse_args() + control_payload = build_payload(args.rooms_root) + write_outputs(control_payload, args.markdown_output, args.json_output, args.html_output) + attention_payload = build_attention_payload(control_payload) + kanban_payload = build_kanban_drafts(attention_payload) + write_json(args.attention_output, attention_payload) + write_json(args.kanban_drafts_output, kanban_payload) + print(json.dumps({ + "room_count": attention_payload["summary"]["room_count"], + "attention_item_count": attention_payload["summary"]["attention_item_count"], + "actionable_item_count": attention_payload["summary"]["actionable_item_count"], + "attention_output": str(args.attention_output), + "kanban_drafts_output": str(args.kanban_drafts_output), + }, sort_keys=True)) + if args.fail_on_actionable and attention_payload["summary"]["actionable_item_count"]: + return 2 + if args.fail_on_changed and git_has_changes([args.markdown_output, args.json_output, args.html_output, args.attention_output, args.kanban_drafts_output]): + return 3 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())