Skills-Ansicht: Umbenennung + aktiv/inaktiv + Karten-Ergebnisse im Auftragsbuch

User-Feedback 15.07.:
1) 'Von allein' heisst jetzt 'Skills' (id #eigenleben bleibt, Links halten).
2) Jeder Skill traegt aktiv/inaktiv - Quelle ist Hermes' EIGENE Wahrheit
   (hermes_cli.banner.get_available_skills via Hermes-venv, dieselbe Logik
   wie das eingebaute WebUI: Plattform/Voraussetzungen/Config). Bewusst NUR
   die Datenquelle wiederverwendet, nicht deren UI - Skill-VERWALTUNG bleibt
   im Hermes-GUI-Tab, alles andere waere Bloat/Doppelbau.
3) Auftragsbuch: fertige Queue-Karten sind aufklappbar und zeigen das
   ERGEBNIS des Workers (kanban_complete-Summary, lazy via
   GET /api/ideen/{id}/ergebnis) - 'Dinge werden getestet, ich sehe aber
   das Ergebnis nicht' ist damit zu.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-15 14:21:46 +02:00
parent bf9fd808d8
commit fdabe1768e
30 changed files with 197 additions and 48 deletions
+5
View File
@@ -55,3 +55,8 @@ def archive(body: TaskIn) -> dict:
@router.get("/ideen/{id}/log")
def get_log(id: str) -> dict:
return ideen.log_of(id)
@router.get("/ideen/{id}/ergebnis")
def get_ergebnis(id: str) -> dict:
return ideen.ergebnis_of(id)
+38
View File
@@ -18,11 +18,15 @@ from __future__ import annotations
import json
import re
import subprocess
import time
from pathlib import Path
SKILLS_DIR = Path.home() / ".hermes" / "skills"
BUNDLED_DIR = Path.home() / ".hermes" / "hermes-agent" / "skills"
REPO_DIR = Path.home() / "mission-control-v2" / "deploy" / "skills"
HERMES_SRC = Path.home() / ".hermes" / "hermes-agent"
HERMES_PY = HERMES_SRC / "venv" / "bin" / "python"
USAGE_FILE = SKILLS_DIR / ".usage.json"
AUFTRAGSBUCH_FILE = Path("/srv/models/mc2-auftragsbuch.json")
ABLEHNUNGEN_FILE = Path("/srv/models/mc2-ablehnungen.jsonl")
@@ -56,6 +60,35 @@ def _norm(s: str) -> str:
return re.sub(r"[\s_-]+", "", (s or "").lower())
# aktiv/inaktiv: dieselbe Wahrheit wie Hermes selbst (und sein eingebautes WebUI) —
# hermes_cli.banner.get_available_skills() prüft Plattform/Voraussetzungen/Config und
# liefert, was der Agent WIRKLICH laden kann. Wir rufen sie im Hermes-venv auf (MC2
# läuft in eigenem Python) und cachen großzügig — der Skill-Satz ändert sich selten.
_aktiv_cache: dict = {"ts": 0.0, "namen": None}
_AKTIV_EVERY = 600.0 # s
def _aktive_namen() -> set[str] | None:
"""Normalisierte Namen aller ladbaren Skills; None = nicht ermittelbar (Dev-Modus)."""
if not HERMES_PY.is_file():
return None
now = time.time()
if _aktiv_cache["namen"] is not None and now - _aktiv_cache["ts"] < _AKTIV_EVERY:
return _aktiv_cache["namen"]
code = ("from hermes_cli.banner import get_available_skills;import json;"
"print(json.dumps(get_available_skills()))")
try:
r = subprocess.run([str(HERMES_PY), "-c", code], capture_output=True,
text=True, timeout=60, cwd=str(HERMES_SRC))
idx = r.stdout.find("{")
kategorien = json.loads(r.stdout[idx:]) if idx >= 0 else {}
namen = {_norm(n) for liste in kategorien.values() for n in liste}
except Exception:
return _aktiv_cache["namen"] # alter Stand ist besser als flackerndes None
_aktiv_cache.update(ts=now, namen=namen)
return namen
def list_skills() -> list[dict]:
"""Alle Skills mit Beschreibung, Herkunft und Nutzungszahlen (leer im Dev-Modus)."""
if not SKILLS_DIR.is_dir():
@@ -65,6 +98,7 @@ def list_skills() -> list[dict]:
except Exception:
usage_raw = {}
usage = {_norm(k): v for k, v in usage_raw.items()}
aktive = _aktive_namen()
def eintrag(md: Path, rel: str, kategorie: str | None) -> dict:
try:
@@ -79,10 +113,14 @@ def list_skills() -> list[dict]:
herkunft = "selbst"
kurz = rel.split("/")[-1]
u = usage.get(_norm(kurz)) or usage.get(_norm(fm.get("name", ""))) or {}
aktiv = None
if aktive is not None:
aktiv = _norm(kurz) in aktive or _norm(fm.get("name", "")) in aktive
return {
"id": rel,
"name": fm.get("name") or kurz,
"kategorie": kategorie,
"aktiv": aktiv,
"beschreibung": fm.get("description") or "",
"version": fm.get("version") or "",
"autor": fm.get("author") or "",
+41
View File
@@ -35,6 +35,10 @@ _LIST_EVERY = 15.0 # s
_log_cache: dict = {"ts": 0.0, "task_id": None, "data": None}
_LOG_EVERY = 4.0 # s
# Cache für Ergebnis (Abschluss-Zusammenfassung) — ändert sich nach done nie mehr
_erg_cache: dict = {}
_ERG_MAX = 40 # simple Deckelung gegen unbegrenztes Wachsen
def _available() -> bool:
return os.name == "posix" and HERMES.is_file()
@@ -213,6 +217,43 @@ def archive_task(task_id: str) -> dict:
return {"ok": True}
def ergebnis_of(task_id: str) -> dict:
"""Das ERGEBNIS einer fertigen Aufgabe — die Abschluss-Zusammenfassung des Workers.
User-Wunsch 15.07.: „im Auftragsbuch werden Dinge getestet, ich sehe aber das
Ergebnis nicht." Die Wahrheit liegt beim kanban_complete-Aufruf (summary/result)
im jüngsten `completed`-Event; `list --json` trägt sie nicht → lazy per `show`,
nur wenn der User die Karte aufklappt. Fertige Ergebnisse ändern sich nie → Cache.
"""
if not _available():
return {"available": False, "summary": "", "result": ""}
if not _TASK_ID_RX.match(task_id or ""):
return {"available": True, "summary": "", "result": ""}
if task_id in _erg_cache:
return _erg_cache[task_id]
try:
r = _hermes(["show", task_id, "--json"], timeout=30)
d = _parse_json(r.stdout)
except Exception:
log.warning("ideen: kanban show %s (ergebnis) fehlgeschlagen", task_id, exc_info=True)
return {"available": True, "summary": "", "result": ""}
summary, result = "", ""
if isinstance(d, dict):
if isinstance(d.get("result"), str):
result = d["result"].strip()
for ev in d.get("events") or []: # chronologisch → letztes completed gewinnt
if isinstance(ev, dict) and ev.get("kind") == "completed":
s = ((ev.get("payload") or {}).get("summary") or "").strip()
if s:
summary = s
data = {"available": True, "summary": summary[:4000], "result": result[:4000]}
if len(_erg_cache) >= _ERG_MAX:
_erg_cache.pop(next(iter(_erg_cache)))
_erg_cache[task_id] = data
return data
def log_of(task_id: str) -> dict:
"""Worker-Log einer Idee (hermes kanban log <id>), ANSI entfernt, letzte ~120 Zeilen."""
if not _available():