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())