Files
mission-control-v2/backend/routers/skills.py
T
HitonabiandClaude Opus 5 84dc34c0a3 fix(stack): Mem0 restlos ausgebaut + vier stille Defekte behoben
Systemaudit vom 27.08.2026. Alle Befunde gemessen, nicht vermutet.

VIER STILLE DEFEKTE

1. mc2-steward startete seit Wochen nicht (live: 207.609 Neustarts).
   steward.py importierte services.memory, das beim Mem0-Ausbau geloescht
   wurde -> ImportError bei jedem Start. Re-Warm- und Health-Waechter
   waren damit tot.

2. Jedes Hermes-Update wurde automatisch zurueckgerollt.
   hermes-postcheck.sh prueft vier Dinge, die es seit dem 07.08. nicht mehr
   gibt (Sidecar :8765, /api/memory, memory.provider, mc2-memory-Plugin).
   Die Checks konnten nicht gruen werden -> autoupdate.sh wertete jedes
   Update als rot und rollte es zurueck. Checks ersatzlos entfernt; der
   Tool-Smoke laeuft ohnehin durch den echten Agenten.

3. 7 von 12 Skills waren per Knopfdruck nicht startbar.
   deploy.sh kopiert Skills mit tr '-' '_' nach ~/.hermes/skills/,
   routers/skills.py gab Hermes aber den Ordnernamen MIT Bindestrich.
   Der Knopf meldete Erfolg, ausgefuehrt wurde nichts. Neu: _hermes_name().

4. deploy.sh warf bei jedem Deploy die Live-Modellkonfiguration weg.
   MC2 schreibt /etc/llama-swap/config.yaml selbst; die Repo-Datei ist nur
   ein Abzug (ihm fehlt u.a. kritiker/Devstral). Jetzt: erst sichern, Diff
   zeigen, dann kopieren. MC_DEPLOY_SKIP_SWAP_CONFIG=1 ueberspringt.

MEM0-AUSBAU VOLLENDET (Kriterium 3: 17 -> 0 Dateien)
- mem0_service/, mcp/mcp_memory.py und hermes/plugins/mc2-memory entfernt;
  das Plugin schickte bei JEDEM Turn zwei 404-Requests an tote Routen.
- MEMORY_DB/MEM0_SERVICE_URL, _mem0_reachable(), MC_MEMORY_DB und
  MC_MEM_DEDUPE_ENABLED aus Config/Router/Unit entfernt.
- mem0_ms war strukturell tot (park("retrieve") wird nirgends mehr
  aufgerufen) -> aus Backend, API-Typ und Latenzkarte entfernt.
- Verbinden-Tab: tote Gedaechtnis-MCP-Leitung raus, Status-Kachel bleibt.
- AGENTS.md beschrieb Mem0 noch als aktiv - korrigiert.

GATEWAY-ROBUSTHEIT
- _proxy gab bei ungueltigen Payloads HTTP 500 (gemessen 5/5: Rohtext,
  leerer Body, JSON-Liste, JSON-String, null) -> jetzt 5/5 HTTP 400.
- Bild-Weiche ohne Deckel: 10 Bilder x 2 Versuche x 240 s hielten den
  Client bis zu 80 min. Neu: MC_CODER_IMAGE_MAX (4), Rueckfall auf die
  Vision-Umleitung.
- /v1/models: nicht-JSON von der Engine gab 500 -> jetzt 502.

UNITS UND DEPLOY
- mc2-steward.service, dessen warmset-Drop-in und voice-service.service
  fehlten im Repo, obwohl maintenance.py und stack-postcheck.sh sie
  voraussetzen. 1:1 von der laufenden Box uebernommen.
- deploy.sh startete mc2-steward nie neu; restore.sh liess mc2-gateway und
  mc2-steward mit alter Config weiterlaufen. Beide ergaenzt.

FRONTEND
- useEigenleben rief /api/eigenleben - existiert im Backend nicht und wurde
  nirgends genutzt. Samt Typen entfernt.
- Anleitung beschrieb einen Gedaechtnis-Tab, den es nicht gibt.
- Abgeglichen: alle uebrigen 63 Frontend-Aufrufe treffen echte Routen, alle
  5 SSE-Invalidation-Keys sind gemappt, keine ungefangenen Promises.

Gates: compileall gruen - ruff "All checks passed" - tsc gruen - vite build
gruen (dist aktualisiert) - Importe app/steward/gateway_app gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 14:58:42 +02:00

103 lines
4.7 KiB
Python

import os
import subprocess
import psutil
from fastapi import APIRouter
from pydantic import BaseModel
router = APIRouter(prefix="/api")
def _hermes_name(ordner: str) -> str:
"""Repo-Ordnername -> Hermes-Skillname.
deploy/deploy.sh kopiert `deploy/skills/<name>` nach `~/.hermes/skills/<name mit _>`
(`tr '-' '_'`), weil Hermes keine Bindestriche in Skillnamen mag. Ohne dieselbe
Umschreibung hier bekam Hermes den Ordnernamen MIT Bindestrich und fand den Skill
nicht - betroffen waren 7 der 12 Skills (betrieb-playbook, konzept-fliessband,
llm-wiki, morning-report, pc-pfad-cache, projekt-start, trend-radar): der Startknopf
meldete Erfolg, ausgefuehrt wurde nichts. deploy.sh bleibt die Quelle der Wahrheit.
"""
return ordner.replace("-", "_")
class RunSkillRequest(BaseModel):
skill_name: str
@router.get("/skills")
def list_skills():
"""Listet alle verfügbaren Skills aus dem deploy/skills Verzeichnis auf."""
# Pfad relativ zu dieser Datei (backend/routers/skills.py)
current_dir = os.path.dirname(os.path.abspath(__file__))
repo_root = os.path.dirname(os.path.dirname(current_dir))
skills_dir = os.path.join(repo_root, "deploy", "skills")
skills = []
# Check currently running hermes skill processes
running_skills = set()
for p in psutil.process_iter(['name', 'cmdline']):
try:
cmdline = p.info.get('cmdline')
if cmdline and any("hermes" in arg for arg in cmdline):
for arg in cmdline:
if "Führe den " in arg and " Skill aus" in arg:
# Extract skill name from "Führe den 'skill_name' Skill aus"
import re
match = re.search(r"Führe den '(.+?)' Skill aus", arg)
if match:
running_skills.add(match.group(1))
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
if os.path.exists(skills_dir) and os.path.isdir(skills_dir):
for entry in os.scandir(skills_dir):
if entry.is_dir():
# Suche nach SKILL.md für Metadaten
skill_md_path = os.path.join(entry.path, "SKILL.md")
description = ""
if os.path.exists(skill_md_path):
try:
with open(skill_md_path, "r", encoding="utf-8") as f:
# Lese erste Zeilen für Description (YAML Frontmatter)
lines = f.readlines()[:10]
for line in lines:
if "description:" in line.lower():
description = line.split(":", 1)[1].strip().strip('"\'')
break
elif "author:" in line.lower() and not description:
description = line.strip()
except Exception:
pass
skills.append({
"name": entry.name,
# Der Name, unter dem Hermes den Skill kennt - auch fuer den Laufend-
# Vergleich, weil im Prozess-cmdline genau dieser Name steht.
"hermes_name": _hermes_name(entry.name),
"description": description or "Keine Beschreibung verfügbar.",
"running": _hermes_name(entry.name) in running_skills
})
return {"skills": skills}
@router.post("/skills/run")
def run_skill(req: RunSkillRequest):
"""Startet einen autonomen Skill via Hermes im Hintergrund."""
hermes_bin = os.path.expanduser("~/.local/bin/hermes")
if not os.path.exists(hermes_bin):
# Fallback falls lokal (auf Windows) entwickelt wird
return {"ok": False, "err": "Hermes CLI nicht gefunden (~/.local/bin/hermes fehlt). Bist du lokal unterwegs?"}
# Entspricht: hermes -z "Führe den 'X' Skill aus" chat - mit dem Namen, den Hermes
# nach dem Deploy-Sync wirklich kennt (Bindestriche -> Unterstriche, s. _hermes_name).
skill = _hermes_name(req.skill_name)
cmd = [hermes_bin, "-z", f"Führe den '{skill}' Skill aus", "chat"]
try:
log_path = os.path.expanduser(f"~/.hermes/logs/skill_{skill.replace('/', '_')}.log")
os.makedirs(os.path.dirname(log_path), exist_ok=True)
with open(log_path, "a", encoding="utf-8") as f:
f.write(f"\n--- Starting Skill: {skill} ---\n")
subprocess.Popen(cmd, stdout=f, stderr=subprocess.STDOUT)
return {"ok": True, "msg": f"Skill '{req.skill_name}' erfolgreich angestoßen."}
except Exception as e:
return {"ok": False, "err": str(e)}