"""Simbiot Harness — command side (CQRS): валидация + события. Задачи подаются через CLI/Telegram/файл-инбокс; каждая команда валидируется и только потом пишет событие. """ import os import secrets from datetime import datetime, timezone from . import es from .levels import size_class def submit_task(title: str, details: str = "", minutes: int = 5, source: str = "cli"): """SubmitTask: класс по оценке длительности. Идемпотентно по id.""" task_id = f"t-{datetime.now(timezone.utc).strftime('%y%m%d%H%M%S')}-{secrets.token_hex(3)}" size = size_class(minutes) es.append_event("task", task_id, "TaskSubmitted", { "id": task_id, "title": title, "details": details, "size": size, "est_minutes": minutes, "source": source, }) return task_id, size def promote_task(task_id: str, reason: str = "manual promote"): """Промоушен ready-задачи на уровень выше (TaskPromoted → ready, size выше).""" order = ["S", "M", "L"] model = es.run_projections() t = model["tasks"].get(task_id) if t is None: raise ValueError(f"задача {task_id} не найдена") if t["status"] != "ready": raise ValueError(f"задача {task_id} в статусе {t['status']} — promote только из ready") i = order.index(t["size"]) if t["size"] in order else 0 if i >= len(order) - 1: raise ValueError(f"задача {task_id} уже размера {t['size']} — выше некуда") new_size = order[i + 1] es.append_event("task", task_id, "TaskPromoted", {"to": new_size, "from": t["size"], "reason": reason}) return task_id, new_size def requeue_task(task_id: str, reason: str = "manual requeue"): """Вернуть failed/прерванную задачу в очередь (TaskPromoted → ready).""" model = es.run_projections() t = model["tasks"].get(task_id) if t is None: raise ValueError(f"задача {task_id} не найдена") if t["status"] not in ("failed", "done"): raise ValueError(f"задача {task_id} в статусе {t['status']} — requeue не нужен") es.append_event("task", task_id, "TaskPromoted", {"to": t["size"], "from": t["size"], "reason": reason}) return task_id def set_policy(identity: str = None, cpu_alert_threshold: int = None): """PolicyChanged: идентичность + пороги. Мержим поверх текущей политики.""" model = es.run_projections() current = dict(model.get("policy") or {}) patch = {} if identity is not None: patch["identity"] = identity if cpu_alert_threshold is not None: patch["cpu_alert_threshold"] = cpu_alert_threshold current.update(patch) es.append_event("harness", "policy", "PolicyChanged", current) return current def list_tasks(): model = es.run_projections() out = [] for t in model["tasks"].values(): out.append(t) return out def status(): model = es.run_projections() return { "cycle_state": model["cycle_state"], "tasks_ready": sum(1 for t in model["tasks"].values() if t["status"] == "ready"), "tasks_done": sum(1 for t in model["tasks"].values() if t["status"] == "done"), "digests": len(model["digests"]), "spend": model.get("spend", [])[-8:], }