feat: complete MC2 Kahlschlag (remove governor, memory, eigenleben, obsolete deploy scripts and frontend widgets)
Ampel / ampel (push) Successful in 24s
Ampel / ampel (push) Successful in 24s
This commit is contained in:
@@ -1,104 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Chef-Gutachter-Feed (Orchestrator-Finale, User-Idee 06.07.): gpt-oss-120b als NÄCHTLICHER
|
||||
# Chef-Gutachter über die autonome Arbeit der Box — bewusst NIE im heißen Pfad (Kaltladen
|
||||
# ~2-5 min ist um 04:30 egal; tagsüber bleibt Lucy schnell). Muster wie dreaming-feed.sh:
|
||||
# liegt in ~/.hermes/scripts/, stdout wird dem Cron-Agenten als Prompt eingespeist.
|
||||
# Der eigentliche RICHTER läuft als Ein-Schuss-Completion direkt gegen llama-swap (:8080,
|
||||
# fremdblick-Architektur — umgeht Hermes' Delegations-Floor und braucht keinen Subagenten).
|
||||
# Richter-Kette: gpt-oss-120b; wirft der beim Laden "exited prematurely" (neben dem Warm-Set
|
||||
# live gesehen, E2E 07.07.), übernimmt GLM-4.6V-Flash als gekennzeichnete VERTRETUNG.
|
||||
set -uo pipefail
|
||||
|
||||
ENDPOINT="${CHEF_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
|
||||
CHEF="${CHEF_MODEL:-gpt-oss-120b}"
|
||||
VERTRETUNG="${CHEF_FALLBACK:-GLM-4.7-Flash}"
|
||||
MC="$HOME/mission-control-v2"
|
||||
VAULT="$HOME/wissens-vault"
|
||||
|
||||
# ---------- Versionierter Auftrag für den Boten-Agenten ----------
|
||||
cat "$MC/deploy/chef-gutachter-prompt.md"
|
||||
echo
|
||||
|
||||
# ---------- Beweismaterial der letzten 24 h (hart begrenzt, erst sammeln, dann kürzen) ----------
|
||||
EVID="$(
|
||||
echo "## BEWEISMATERIAL (automatisch erhoben am $(date '+%d.%m.%Y %H:%M'))"
|
||||
echo
|
||||
echo "### Vorschlags-Branches (Werkstatt/Orchestrator, jüngste zuerst):"
|
||||
git -C "$MC" fetch -q origin 2>/dev/null || true
|
||||
git -C "$MC" for-each-ref --sort=-committerdate refs/remotes/origin \
|
||||
--format='%(committerdate:relative) | %(refname:short) | %(subject)' 2>/dev/null | head -10
|
||||
echo
|
||||
echo "### main-Commits der letzten 24 h:"
|
||||
git -C "$MC" log --since='24 hours ago' --oneline origin/main 2>/dev/null | head -12
|
||||
echo
|
||||
echo "### Wissens-Vault (neue Traum-Notizen, 24 h):"
|
||||
git -C "$VAULT" log --since='24 hours ago' --name-only --pretty='— %s' 2>/dev/null | head -20
|
||||
echo
|
||||
echo "### Ideen-Queue (natives Kanban, Stand jetzt — offene Ideen des Commanders):"
|
||||
bash -lc 'hermes kanban stats' 2>/dev/null | head -12
|
||||
echo
|
||||
echo "### Ideen-Queue: zuletzt bewegte Aufgaben:"
|
||||
bash -lc 'hermes kanban list --sort updated' 2>/dev/null | head -10
|
||||
echo
|
||||
echo "### Betriebs-Warnungen (journal, 24 h, je Dienst gezählt):"
|
||||
journalctl --user --since '24 hours ago' -p warning --no-pager 2>/dev/null \
|
||||
| grep -oE 'hermes-gateway|mission-control-2|mem0-service|voice-service|hermes-builtin-ui' \
|
||||
| sort | uniq -c | sort -rn | head -6
|
||||
echo
|
||||
echo "### Aktivitäts-Insights (1 Tag):"
|
||||
bash -lc 'hermes insights --days 1' 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g' | head -30
|
||||
)"
|
||||
|
||||
RASTER='Du bist der CHEF-GUTACHTER über die autonome Arbeit einer lokalen KI-Box (Hermes-Agent, Werkstatt, Orchestrator, Traum-Kreislauf). Du bekommst BEWEISMATERIAL der letzten 24 Stunden. Urteile AUSSCHLIESSLICH auf Basis dieses Materials — kein Lob und keine Behauptung ohne konkreten Beleg darin; fehlt dir zu einem Punkt Material, sage das offen. Antworte auf DEUTSCH, kompakt, in exakt dieser Struktur:
|
||||
VERDIKT: <ein Satz Gesamturteil über die autonome Arbeit der Nacht/des Tages>
|
||||
RISIKEN: <0-3 Punkte, je mit Beleg aus dem Material; "keine erkennbar" wenn leer>
|
||||
TOP-3 FUER DEN COMMANDER: <maximal 3 priorisierte, konkrete Empfehlungen — weniger ist ok, erfinde keine>'
|
||||
|
||||
# Ein-Schuss-Richter: $1=Modell-ID $2=curl-Timeout $3=max_tokens
|
||||
judge() {
|
||||
local req
|
||||
req="$(jq -n --arg m "$1" --arg sys "$RASTER" --arg usr "$EVID" --argjson mt "$3" \
|
||||
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
|
||||
max_tokens:$mt, temperature:0.2}')"
|
||||
curl -s -m "$2" "$ENDPOINT" -H 'Content-Type: application/json' --data-binary "$req" \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
msg = d["choices"][0]["message"]
|
||||
out = (msg.get("content") or msg.get("reasoning_content") or "").strip()
|
||||
print(out)
|
||||
except Exception:
|
||||
pass'
|
||||
}
|
||||
|
||||
RICHTER="$CHEF"
|
||||
VERDIKT="$(judge "$CHEF" 420 2600)"
|
||||
if [ -z "${VERDIKT// /}" ]; then
|
||||
RICHTER="$VERTRETUNG (Vertretung — $CHEF hat nicht geladen)"
|
||||
VERDIKT="$(judge "$VERTRETUNG" 240 1200)"
|
||||
fi
|
||||
|
||||
echo "$EVID"
|
||||
echo
|
||||
if [ -z "${VERDIKT// /}" ]; then
|
||||
echo "## CHEF-VERDIKT: AUSGEFALLEN"
|
||||
echo "Beide Richter ($CHEF, $VERTRETUNG) haben nicht geantwortet — das dem Commander ehrlich melden."
|
||||
else
|
||||
echo "## CHEF-VERDIKT (Richter: $RICHTER)"
|
||||
echo "$VERDIKT"
|
||||
fi
|
||||
|
||||
# Morgenlage für die Zentrale: das Verdikt STILL in den Briefkasten spiegeln (priority=silent —
|
||||
# Lucy soll die Wand Text nicht vorlesen; die Telegram-Botschaft macht der Cron-Agent selbst).
|
||||
# Cockpit („Morgenlage"-Karte) und Chronik lesen es dort mit source=chef-gutachter heraus.
|
||||
if [ -n "${VERDIKT// /}" ]; then
|
||||
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(python3 - "$RICHTER" "$VERDIKT" <<'PY'
|
||||
import json, sys
|
||||
print(json.dumps({"text": sys.argv[2], "subject": f"Morgenlage (Richter: {sys.argv[1]})",
|
||||
"source": "chef-gutachter", "priority": "silent"}))
|
||||
PY
|
||||
)" >/dev/null 2>&1 || true
|
||||
fi
|
||||
@@ -1,15 +0,0 @@
|
||||
# Auftrag: Nacht-Gutachten überbringen (Chef-Gutachter-Cron)
|
||||
|
||||
Du bist Lucy. Unten stehen (1) BEWEISMATERIAL der letzten 24 Stunden und (2) das
|
||||
CHEF-VERDIKT eines großen Gutachter-Modells darüber. Deine Aufgabe ist NUR das Überbringen
|
||||
an den Commander per Telegram — kurz, in deiner Stimme, ohne Werkzeuge.
|
||||
|
||||
Regeln:
|
||||
1. EIN einleitender Satz von dir (z. B. wie die Nacht lief), dann das Verdikt.
|
||||
2. Gib VERDIKT, RISIKEN und TOP-3 des Chef-Gutachters TREU wieder — nicht weichspülen,
|
||||
nicht kürzen bei Risiken, NICHTS dazuerfinden. Du bist die Botin, nicht die Richterin.
|
||||
3. Nenne am Ende in Klammern den Richter (steht über dem Verdikt: gpt-oss-120b oder
|
||||
die GLM-Vertretung).
|
||||
4. Steht dort „CHEF-VERDIKT: AUSGEFALLEN", melde genau das ehrlich — kein Ersatz-Urteil
|
||||
von dir.
|
||||
5. Telegram-tauglich: kompakt, keine Markdown-Tabellen, echte Umlaute.
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# curator-archive-watchdog.sh
|
||||
# Archiviert Skills nach 14 Tagen Inaktivität (nächster Durchlauf um 3:00 Uhr)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PYTHON_SCRIPT="$SCRIPT_DIR/curator_archive_logic.py"
|
||||
|
||||
# Python-Skript ausführen
|
||||
python3 "$PYTHON_SCRIPT"
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/bin/bash
|
||||
# curator-idle-watchdog.sh
|
||||
# Prüft den Curator-Idle-Status und triggert Reaktivierung nach 14 Tagen
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
LOG_DIR="/home/hitonabi/.hermes/logs/curator"
|
||||
LOG_FILE="$LOG_DIR/idle-watchdog-$(date +%Y%m%d).log"
|
||||
THRESHOLD_DAYS=14
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Curator-Status auslesen
|
||||
STATUS_OUTPUT=$(hermes curator status 2>&1) || {
|
||||
log "FEHLER: hermes curator status fehlgeschlagen"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Prüfen, ob Curator ENABLED ist
|
||||
if ! echo "$STATUS_OUTPUT" | grep -q "^curator: ENABLED$"; then
|
||||
log "INFO: Curator ist nicht ENABLED → überspringe Prüfung"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Letzten Lauf extrahieren (Format: "last run: 17d ago")
|
||||
LAST_RUN_LINE=$(echo "$STATUS_OUTPUT" | grep "^ last run:" | head -1)
|
||||
if [[ -z "$LAST_RUN_LINE" ]]; then
|
||||
log "FEHLER: Konnte 'last run' nicht aus Status extrahieren"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LAST_RUN_TEXT=$(echo "$LAST_RUN_LINE" | sed 's/^ last run: *//')
|
||||
log "Letzter Curator-Lauf: $LAST_RUN_TEXT"
|
||||
|
||||
# Prüfen, ob "d ago" vorhanden ist
|
||||
if [[ "$LAST_RUN_TEXT" =~ ([0-9]+)d\ ago ]]; then
|
||||
DAYS_SINCE_LAST_RUN="${BASH_REMATCH[1]}"
|
||||
else
|
||||
log "INFO: Kein 'd ago' gefunden (möglicherweise <1d oder nie gelaufen) → überspringe"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "Tage seit letztem Lauf: $DAYS_SINCE_LAST_RUN"
|
||||
|
||||
# Threshold prüfen
|
||||
if (( DAYS_SINCE_LAST_RUN < THRESHOLD_DAYS )); then
|
||||
log "INFO: Curator ist innerhalb des erlaubten Intervalls ($DAYS_SINCE_LAST_RUN < $THRESHOLD_DAYS Tage) → kein Trigger nötig"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Reaktivierung triggern
|
||||
log "WARNUNG: Curator ist für $DAYS_SINCE_LAST_RUN Tage idle (>= $THRESHOLD_DAYS) → triggere 'hermes curator run'"
|
||||
if hermes curator run 2>&1 | tee -a "$LOG_FILE"; then
|
||||
log "ERFOLG: Curator-Reaktivierung gestartet"
|
||||
else
|
||||
log "FEHLER: Curator-Reaktivierung fehlgeschlagen"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,90 +0,0 @@
|
||||
#!/bin/bash
|
||||
# curator-monitor.sh
|
||||
# Prüft den Curator-Idle-Status mit 7-Tage-Schwellenwert.
|
||||
# GIBT AUS: PASS oder FAIL mit Detaillierung.
|
||||
# EXIT 0 = OK (Curator aktiv innerhalb von 7 Tagen)
|
||||
# EXIT 1 = FAIL (Curator >7 Tage idle) –Monitoring-Skript erkannt Problem
|
||||
#
|
||||
# --dry-run DAYS Simuliert eine >DAYS-alte Curator-Auszeit (für Tests)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
LOG_DIR="/home/hitonabi/.hermes/logs/curator"
|
||||
LOG_FILE="$LOG_DIR/monitor-$(date +%Y%m%d).log"
|
||||
THRESHOLD_DAYS=7
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# --- Argumente parsen ---
|
||||
DRY_RUN=false
|
||||
DRY_RUN_DAYS=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
if [[ -n "${2:-}" && "$2" =~ ^[0-9]+$ ]]; then
|
||||
DRY_RUN_DAYS="$2"
|
||||
shift
|
||||
else
|
||||
DRY_RUN_DAYS=8 # Default-Testwert
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- LIVE-PRÜFUNG oder DRY-RUN ---
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log "DRY-RUN: Simuliere Curator-Auszeit von $DRY_RUN_DAYS Tagen"
|
||||
DAYS_SINCE_LAST_RUN="$DRY_RUN_DAYS"
|
||||
else
|
||||
# Live-Status abfragen
|
||||
STATUS_OUTPUT=$(hermes curator status 2>&1) || {
|
||||
log "FEHLER: hermes curator status fehlgeschlagen"
|
||||
echo "FAIL: hermes curator status fehlgeschlagen"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Prüfen, ob Curator ENABLED ist
|
||||
if ! echo "$STATUS_OUTPUT" | grep -q "^curator: ENABLED$"; then
|
||||
log "INFO: Curator ist nicht ENABLED → überspringe Prüfung"
|
||||
echo "PASS: Curator ist nicht ENABLED (keine Aktivität nötig)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Letzten Lauf extrahieren
|
||||
LAST_RUN_LINE=$(echo "$STATUS_OUTPUT" | grep "^ last run:" | head -1)
|
||||
if [[ -z "$LAST_RUN_LINE" ]]; then
|
||||
log "FEHLER: Konnte 'last run' nicht aus Status extrahieren"
|
||||
echo "FAIL: Konnte 'last run' nicht extrahieren"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LAST_RUN_TEXT=$(echo "$LAST_RUN_LINE" | sed 's/^ last run: *//')
|
||||
log "Letzter Curator-Lauf: $LAST_RUN_TEXT"
|
||||
|
||||
# Tage extrahieren
|
||||
if [[ "$LAST_RUN_TEXT" =~ ([0-9]+)d\ ago ]]; then
|
||||
DAYS_SINCE_LAST_RUN="${BASH_REMATCH[1]}"
|
||||
else
|
||||
log "INFO: Kein 'd ago' gefunden (möglicherweise <1d oder nie gelaufen)"
|
||||
echo "PASS: Curator ist aktuell aktiv oder <1 Tag idle"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Tage seit letztem Lauf: $DAYS_SINCE_LAST_RUN"
|
||||
|
||||
# --- MONITORING-LOGIK (keine Reaktivierung!) ---
|
||||
if (( DAYS_SINCE_LAST_RUN < THRESHOLD_DAYS )); then
|
||||
log "INFO: Curator ist innerhalb des erlaubten Intervalls ($DAYS_SINCE_LAST_RUN < $THRESHOLD_DAYS Tage)"
|
||||
echo "PASS: Curator ist aktiv ($DAYS_SINCE_LAST_RUN Tage idle)"
|
||||
exit 0
|
||||
else
|
||||
log "WARNUNG: Curator ist für $DAYS_SINCE_LAST_RUN Tage idle (>= $THRESHOLD_DAYS)"
|
||||
echo "FAIL: Curator-Cron ist für $DAYS_SINCE_LAST_RUN Tage stumm (>= $THRESHOLD_DAYS-Tage-Schwellenwert)"
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,198 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Curator Auto-Archivierung: Archiviert Skills nach 14 Tagen Inaktivität.
|
||||
|
||||
Liest `.curator_state` und `.usage.json` aus ~/.hermes/skills/, prüft
|
||||
auf Inaktivität (>14 Tage last_used_at), schließt aus (pinned, system)
|
||||
und verschiebt in .archive/<Kategorie>/.
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
HERMES_SKILLS_DIR = Path.home() / ".hermes" / "skills"
|
||||
CURATOR_STATE_FILE = HERMES_SKILLS_DIR / ".curator_state"
|
||||
USAGE_JSON_FILE = HERMES_SKILLS_DIR / ".usage.json"
|
||||
ARCHIVE_DIR = HERMES_SKILLS_DIR / ".archive"
|
||||
LOG_DIR = Path.home() / ".hermes" / "logs" / "curator"
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict:
|
||||
"""JSON-Datei laden."""
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def save_json(path: Path, data: dict) -> None:
|
||||
"""JSON-Datei speichern (mit Pretty-Print)."""
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
def is_stale(skill_usage: dict, threshold_days: int = 14) -> bool:
|
||||
"""Prüft, ob ein Skill älter als threshold_days ist."""
|
||||
last_used = skill_usage.get("last_used_at")
|
||||
if not last_used:
|
||||
return False
|
||||
|
||||
# ISO-Format mit optionaler Zeitzone
|
||||
last_used_str = last_used.replace("Z", "+00:00")
|
||||
try:
|
||||
last_used_dt = datetime.fromisoformat(last_used_str)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
delta = now - last_used_dt
|
||||
return delta.days >= threshold_days
|
||||
|
||||
|
||||
def is_excluded(skill_name: str, excluded: list) -> bool:
|
||||
"""Prüft, ob ein Skill ausgeschlossen ist."""
|
||||
return skill_name in excluded
|
||||
|
||||
|
||||
def is_pinned(skill_usage: dict) -> bool:
|
||||
"""Prüft, ob ein Skill angepinnt ist."""
|
||||
return skill_usage.get("pinned", False) is True
|
||||
|
||||
|
||||
def get_skill_category(skill_name: str) -> str:
|
||||
"""
|
||||
Ermittelt die Kategorie eines Skills basierend auf dem Verzeichnisnamen.
|
||||
Gibt 'unknown' zurück, wenn keine Kategorie gefunden wird.
|
||||
"""
|
||||
# Versuche, die Kategorie aus dem Verzeichnis zu ermitteln
|
||||
# z.B. 'betrieb-playbook' → 'betrieb-playbook' (direkt unter skills/)
|
||||
# 'apple' (in .archive/apple/) → 'apple'
|
||||
# Falls der Skill direkt in skills/ liegt ohne Unterverzeichnis:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def ensure_archive_category(category: str) -> Path:
|
||||
"""Erstellt das Archiv-Verzeichnis für eine Kategorie, falls nötig."""
|
||||
category_path = ARCHIVE_DIR / category
|
||||
category_path.mkdir(parents=True, exist_ok=True)
|
||||
return category_path
|
||||
|
||||
|
||||
def move_skill_to_archive(skill_name: str, category: str) -> Path:
|
||||
"""
|
||||
Verschiebt ein Skill-Verzeichnis in das Archiv.
|
||||
Gibt den neuen Pfad zurück.
|
||||
"""
|
||||
source = HERMES_SKILLS_DIR / skill_name
|
||||
target_category = ensure_archive_category(category)
|
||||
target = target_category / skill_name
|
||||
|
||||
if source.exists():
|
||||
shutil.move(str(source), str(target))
|
||||
|
||||
return target
|
||||
|
||||
|
||||
def archive_skill(skill_name: str, usage_data: dict) -> dict:
|
||||
"""
|
||||
Archiviert einen Skill:
|
||||
- Setzt state auf 'archived'
|
||||
- Setzt archived_at auf aktuelle Zeit
|
||||
- Verschiebt das Verzeichnis nach .archive/
|
||||
Gibt das aktualisierte Usage-Dict zurück.
|
||||
"""
|
||||
usage_data[skill_name]["state"] = "archived"
|
||||
usage_data[skill_name]["archived_at"] = datetime.now(timezone.utc).isoformat()
|
||||
return usage_data
|
||||
|
||||
|
||||
def log_archive_action(skill_name: str, category: str, success: bool, message: str) -> None:
|
||||
"""Protokolliert eine Archivierungsaktion."""
|
||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
log_file = LOG_DIR / f"archive-{datetime.now(timezone.utc).date().isoformat()}.log"
|
||||
|
||||
timestamp = datetime.now(timezone.utc).isoformat()
|
||||
status = "SUCCESS" if success else "FAILED"
|
||||
log_line = f"[{timestamp}] {status} | {skill_name} ({category}) | {message}\n"
|
||||
|
||||
with open(log_file, "a", encoding="utf-8") as f:
|
||||
f.write(log_line)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Hauptfunktion: Archiviert stale Skills."""
|
||||
# 1. Config laden
|
||||
if not CURATOR_STATE_FILE.exists():
|
||||
print(f"Fehler: {CURATOR_STATE_FILE} nicht gefunden.")
|
||||
return
|
||||
|
||||
curator_state = load_json(CURATOR_STATE_FILE)
|
||||
archivierung = curator_state.get("archivierung", {})
|
||||
if not archivierung.get("enabled", False):
|
||||
print("Archivierung ist nicht aktiviert.")
|
||||
return
|
||||
|
||||
threshold_days = archivierung.get("threshold_days", 14)
|
||||
excluded_skills = archivierung.get("excluded_skills", [])
|
||||
|
||||
# 2. Usage-Daten laden
|
||||
if not USAGE_JSON_FILE.exists():
|
||||
print(f"Fehler: {USAGE_JSON_FILE} nicht gefunden.")
|
||||
return
|
||||
|
||||
usage_data = load_json(USAGE_JSON_FILE)
|
||||
|
||||
# 3. Skills prüfen
|
||||
archived_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
for skill_name, skill_usage in usage_data.items():
|
||||
# Ausschlusskriterien prüfen
|
||||
if is_excluded(skill_name, excluded_skills):
|
||||
print(f"Übersprungen (Exkludiert): {skill_name}")
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
if is_pinned(skill_usage):
|
||||
print(f"Übersprungen (Gepinnt): {skill_name}")
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Inaktivität prüfen
|
||||
if not is_stale(skill_usage, threshold_days):
|
||||
print(f"Übersprungen (Aktiv): {skill_name}")
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# Archivierung durchführen
|
||||
category = get_skill_category(skill_name) or "unknown"
|
||||
print(f"Archiviere: {skill_name} → {category}")
|
||||
|
||||
try:
|
||||
# Usage aktualisieren
|
||||
usage_data = archive_skill(skill_name, usage_data)
|
||||
|
||||
# Verzeichnis verschieben
|
||||
move_skill_to_archive(skill_name, category)
|
||||
|
||||
# Erfolg protokollieren
|
||||
log_archive_action(skill_name, category, True, "Skill archiviert")
|
||||
|
||||
archived_count += 1
|
||||
except Exception as e:
|
||||
log_archive_action(skill_name, category, False, str(e))
|
||||
print(f"Fehler beim Archivieren von {skill_name}: {e}")
|
||||
|
||||
# 4. Usage-Daten speichern
|
||||
save_json(USAGE_JSON_FILE, usage_data)
|
||||
|
||||
# 5. Archivierungs-Zeitstempel aktualisieren
|
||||
curator_state["archivierung"]["last_archive_run_at"] = datetime.now(timezone.utc).isoformat()
|
||||
save_json(CURATOR_STATE_FILE, curator_state)
|
||||
|
||||
print(f"\nArchivierung abgeschlossen: {archived_count} archiviert, {skipped_count} übersprungen.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+13
-291
@@ -1,298 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Deploy AUF DER BOX als systemd-USER-Dienst — KEIN sudo, KEIN /opt, KEIN Passwort.
|
||||
# Erstinstallation + Updates in einem. Läuft als User hitonabi.
|
||||
#
|
||||
# Erstinstallation (einmalig):
|
||||
# git clone https://git.tobisniceshomelab.ddnsfree.com/Hitonabi/mission-control-v2 ~/mission-control-v2
|
||||
# bash ~/mission-control-v2/deploy/deploy.sh
|
||||
set -euo pipefail
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SRC="${MC2_SRC:-$HOME/mission-control-v2}"
|
||||
echo "Starte MC2 Deployment..."
|
||||
|
||||
cd "$SRC"
|
||||
git fetch -q origin && git reset -q --hard origin/main
|
||||
# 1. Neuesten Code holen
|
||||
git pull origin main
|
||||
|
||||
# ★ Selbst-Reset-Falle (15.07. beim P1-Deploy live gebissen): der reset oben ersetzt auch
|
||||
# DIESE Datei, und bash liest laufende Skripte häppchenweise von der Platte weiter — ab
|
||||
# hier liefe ein Zeilen-Mix aus alter+neuer Version (beim P1-Deploy fielen so daemon-reload
|
||||
# und der mc2-gateway-Start unter den Tisch). Darum: nach dem Reset genau EINMAL die
|
||||
# frische Version neu ausführen; der zweite Durchlauf überspringt dank Env-Marke nichts.
|
||||
if [ "${MC2_DEPLOY_REEXEC:-0}" != "1" ]; then
|
||||
export MC2_DEPLOY_REEXEC=1
|
||||
exec bash "$SRC/deploy/deploy.sh"
|
||||
fi
|
||||
# 2. Abhängigkeiten prüfen (falls sich was geändert hat)
|
||||
echo "Aktualisiere Python Abhängigkeiten..."
|
||||
backend/.venv/bin/pip install -r backend/requirements.txt
|
||||
|
||||
# venv + Abhängigkeiten
|
||||
if [ ! -d "$SRC/backend/.venv" ]; then
|
||||
python3 -m venv "$SRC/backend/.venv"
|
||||
fi
|
||||
"$SRC/backend/.venv/bin/python" -m pip install -q --upgrade pip
|
||||
"$SRC/backend/.venv/bin/python" -m pip install -q -r "$SRC/backend/requirements.txt"
|
||||
|
||||
# Mem0-Sidecar (auto-lernendes Gedächtnis) — eigenes Python-3.12-venv (~/.mem0/venv),
|
||||
# weil mem0+chromadb unter dem 3.14-Backend-venv nicht laufen. mem0ai/chromadb sind dort
|
||||
# bereits installiert; hier nur den HTTP-Server nachziehen + Alt-DB einmalig migrieren.
|
||||
if [ -x "$HOME/.mem0/venv/bin/python" ]; then
|
||||
# ~/.mem0/venv ist uv-managed (kein pip) → uv pip nutzen.
|
||||
UV="$(command -v uv || echo "$HOME/.local/bin/uv")"
|
||||
"$UV" pip install -q --python "$HOME/.mem0/venv/bin/python" -r "$SRC/mem0_service/requirements.txt"
|
||||
( cd "$SRC/mem0_service" && "$HOME/.mem0/venv/bin/python" migrate.py ) || true
|
||||
else
|
||||
echo "WARN: ~/.mem0/venv fehlt — Mem0-Sidecar wird nicht gestartet (siehe Plan A1)."
|
||||
fi
|
||||
|
||||
# Voice-Sidecar (lokales STT + gestuftes TTS für „Mit Hermes reden") — eigenes Python-3.12-venv
|
||||
# (~/.voice/venv), weil torch/chatterbox/faster-whisper nicht ins 3.14-Backend-venv passen.
|
||||
# install.sh ist idempotent (venv + Deps + Piper-Stimmen). Best-effort: schlägt es fehl, läuft
|
||||
# der restliche Stack weiter (der Voice-Tab meldet den Sidecar dann als offline).
|
||||
bash "$SRC/voice_service/install.sh" || echo "WARN: Voice-Sidecar-Setup fehlgeschlagen — Voice-Tab bleibt offline."
|
||||
|
||||
# Hermes-Memory-Provider-Plugin (auto-lernen/Recall via Mem0-Sidecar) nach ~/.hermes/plugins/
|
||||
# spiegeln. Context-only (kein Tool-Loop). Aktivierung in ~/.hermes/config.yaml:
|
||||
# memory.memory_enabled: true + memory.provider: mc2-memory (einmalig, box-lokal).
|
||||
if [ -d "$HOME/.hermes" ]; then
|
||||
mkdir -p "$HOME/.hermes/plugins/mc2-memory"
|
||||
cp "$SRC/hermes/plugins/mc2-memory/__init__.py" "$SRC/hermes/plugins/mc2-memory/plugin.yaml" \
|
||||
"$HOME/.hermes/plugins/mc2-memory/"
|
||||
fi
|
||||
|
||||
# systemd-USER-Units installieren/aktualisieren
|
||||
mkdir -p "$HOME/.config/systemd/user"
|
||||
cp "$SRC/deploy/mission-control-2.service" "$HOME/.config/systemd/user/mission-control-2.service"
|
||||
# MC2-Diät (Hermes Desktop, 07/2026): das Hermes-Terminal-ttyd (:7681) und das standalone
|
||||
# hermes-webui (nesquena, Karteileiche seit dem Built-in-UI-Umbau) sind entfallen — die
|
||||
# Agent-Oberfläche ist die Desktop-App bzw. /hermes-ui/. Einmalige, idempotente Stilllegung:
|
||||
systemctl --user disable --now hermes-terminal 2>/dev/null || true
|
||||
systemctl --user disable --now hermes-webui 2>/dev/null || true
|
||||
rm -f "$HOME/.config/systemd/user/hermes-terminal.service" "$HOME/.config/systemd/user/hermes-webui.service"
|
||||
# Box-Konsole (ttyd -> Login-Shell auf :7682), in MC2 als Konsole-Seite eingebettet.
|
||||
cp "$SRC/deploy/box-console.service" "$HOME/.config/systemd/user/box-console.service"
|
||||
# MC2-Gateway (UMBAU v3 P1): der /v1-Datenpfad als eigener Prozess (Loopback :9010,
|
||||
# Restart=always) — Lucys Hirn + Nacht-Autonomie überleben damit jeden MC2-Neustart.
|
||||
cp "$SRC/deploy/mc2-gateway.service" "$HOME/.config/systemd/user/mc2-gateway.service"
|
||||
# MC2-Steward (UMBAU v3 P2): Wächter-Loops (Re-Warm/Sentry/Dedupe) als eigener Dienst —
|
||||
# überleben MC2-Neustarts und melden erstmals auch ein totes Steuerpult (Telegram).
|
||||
cp "$SRC/deploy/mc2-steward.service" "$HOME/.config/systemd/user/mc2-steward.service"
|
||||
# Mem0-Sidecar-Unit (nur wenn das venv existiert).
|
||||
[ -x "$HOME/.mem0/venv/bin/python" ] && cp "$SRC/deploy/mem0-service.service" "$HOME/.config/systemd/user/mem0-service.service"
|
||||
# Voice-Sidecar-Unit (nur wenn das venv existiert).
|
||||
[ -x "$HOME/.voice/venv/bin/python" ] && cp "$SRC/deploy/voice-service.service" "$HOME/.config/systemd/user/voice-service.service"
|
||||
# Tägliches Zustands-Backup (mem0 + Configs/Secrets) — Timer + oneshot-Service. Siehe docs/BACKUP.md.
|
||||
cp "$SRC/deploy/mc2-backup.service" "$HOME/.config/systemd/user/mc2-backup.service"
|
||||
cp "$SRC/deploy/mc2-backup.timer" "$HOME/.config/systemd/user/mc2-backup.timer"
|
||||
# Wöchentliches Auto-Update (Router/Engine/Hermes, Rollback+Pin+Telegram) — Autonomie E2.
|
||||
cp "$SRC/deploy/mc2-autoupdate.service" "$HOME/.config/systemd/user/mc2-autoupdate.service"
|
||||
cp "$SRC/deploy/mc2-autoupdate.timer" "$HOME/.config/systemd/user/mc2-autoupdate.timer"
|
||||
# Tägliche Selbst-Smoke-Tests (Gateway/Tools/Voice aktiv, Alarm bei Rot) — Autonomie 0d.
|
||||
cp "$SRC/deploy/mc2-selfsmoke.service" "$HOME/.config/systemd/user/mc2-selfsmoke.service"
|
||||
cp "$SRC/deploy/mc2-selfsmoke.timer" "$HOME/.config/systemd/user/mc2-selfsmoke.timer"
|
||||
# Bagatell-Annahme (Ideen-Queue, 10.07.2026): reine Doku-Vorschläge nachts ohne Klick einspielen.
|
||||
cp "$SRC/deploy/mc2-bagatell.service" "$HOME/.config/systemd/user/mc2-bagatell.service"
|
||||
cp "$SRC/deploy/mc2-bagatell.timer" "$HOME/.config/systemd/user/mc2-bagatell.timer"
|
||||
# Morgen-Digest (Nächtliche Telegram-Zusammenfassung, 26.07.2026): fasst alle Meldungen zwischen 0 und 7 Uhr zusammen.
|
||||
cp "$SRC/deploy/mc2-morgen-digest.service" "$HOME/.config/systemd/user/mc2-morgen-digest.service"
|
||||
cp "$SRC/deploy/mc2-morgen-digest.timer" "$HOME/.config/systemd/user/mc2-morgen-digest.timer"
|
||||
# Evolution-Radar-Feed (Autonomie E4): Hermes-cron speist ihn als Prompt ein.
|
||||
# Cron-Job selbst wird EINMALIG registriert (hermes cron create, siehe docs/RUNBOOK.md).
|
||||
mkdir -p "$HOME/.hermes/scripts"
|
||||
cp "$SRC/deploy/radar-feed.sh" "$HOME/.hermes/scripts/radar-feed.sh"
|
||||
# Selbstkritik-Cron (Faden 11b): Zwilling des Radars, Blick nach INNEN (Latenzen/Journal/Skills).
|
||||
cp "$SRC/deploy/selbstkritik-feed.sh" "$HOME/.hermes/scripts/selbstkritik-feed.sh"
|
||||
# Monats-Review-Wrapper (Cron 1. d. M. 09:00): Radar + Selbstkritik in EINEM Lauf.
|
||||
cp "$SRC/deploy/radar-selbstkritik-wrapper.sh" "$HOME/.hermes/scripts/radar-selbstkritik-wrapper.sh"
|
||||
# Traum-Cron (Dreaming, Phase 2): Blick auf das GELERNTE → Wissens-Vault. Bootstrappt ~/wissens-vault.
|
||||
cp "$SRC/deploy/dreaming-feed.sh" "$HOME/.hermes/scripts/dreaming-feed.sh"
|
||||
# Fremdblick-Helfer: Ein-Schuss-Zweitmeinung von einem anderen Modell (umgeht 64K-Delegations-Floor).
|
||||
cp "$SRC/deploy/fremdblick.sh" "$HOME/.hermes/scripts/fremdblick.sh"
|
||||
# Worker-Helfer (Orchestrator): Ein-Schuss-Arbeitsauftrag an ein Worker-Modell (Gegenstück zu fremdblick).
|
||||
cp "$SRC/deploy/worker.sh" "$HOME/.hermes/scripts/worker.sh"
|
||||
# Chef-Gutachter-Cron (Orchestrator-Finale): gpt-oss urteilt nachts über die autonome Arbeit.
|
||||
cp "$SRC/deploy/chef-gutachter-feed.sh" "$HOME/.hermes/scripts/chef-gutachter-feed.sh"
|
||||
# Wrapper für nächtliche Crons, um Telegram-Spam in den Morgen-Digest zu leiten.
|
||||
cp "$SRC/deploy/night-cron-wrapper.sh" "$HOME/.hermes/scripts/night-cron-wrapper.sh"
|
||||
# Release-Radar-Cron (10.07.2026): hält neue hermes-agent-Releases wöchentlich gegen die
|
||||
# Eigenbau-Landkarte im Wissens-Vault ("mehr Hermes nativ, weniger Eigenkreationen").
|
||||
cp "$SRC/deploy/hermes-release-radar-feed.sh" "$HOME/.hermes/scripts/hermes-release-radar-feed.sh"
|
||||
# Idle-Radar (S4 "Zuendung", 12.07.2026): die Box gibt sich nachts selbst Arbeit —
|
||||
# Journal-Fehlermuster + Traum-Funde -> belegte rohe Ideen (triage) im nativen Kanban.
|
||||
cp "$SRC/deploy/idle-radar-feed.sh" "$HOME/.hermes/scripts/idle-radar-feed.sh"
|
||||
# "Lucy kennt sich" (12.07.2026): Selbst-Inventur generiert den Steckbrief + bemerkt
|
||||
# eigene Aenderungen; der Karten-Gutachter stempelt jede Karte mit einer Empfehlung.
|
||||
cp "$SRC/deploy/selbst-inventur.sh" "$HOME/.hermes/scripts/selbst-inventur.sh"
|
||||
cp "$SRC/deploy/karten-gutachter.sh" "$HOME/.hermes/scripts/karten-gutachter.sh"
|
||||
# Trend-Radar (17.07.2026, Sa 05:15): Live-Puls aller Rollen + Engine-A/B + Watch-Liste +
|
||||
# HF-Kandidaten-Suche → max. 1 Prüfstand-Kandidat/Woche. Prüfstand (So 01:00, --no-agent):
|
||||
# volles Programm (Coding/Agent/Recherche/Deutsch/Vision) gegen die Amtsinhaber-Baseline;
|
||||
# Harness + Suiten liegen im Repo ($SRC/deploy/pruefstand/), nur der Runner wird kopiert.
|
||||
cp "$SRC/deploy/trend-radar-feed.sh" "$HOME/.hermes/scripts/trend-radar-feed.sh"
|
||||
cp "$SRC/deploy/pruefstand/pruefstand.sh" "$HOME/.hermes/scripts/pruefstand.sh"
|
||||
# Restore-Probe + Venv-Audit (17.07.2026, monatliche --no-agent-Crons): echte
|
||||
# Wiederherstellungs-Drills (PBS-Canary + Tarball) und CVE-/Outdated-Audit der
|
||||
# Python-Nebendienst-venvs (mem0, voice) — melden nur, ändern nichts.
|
||||
cp "$SRC/deploy/restore-probe.sh" "$HOME/.hermes/scripts/restore-probe.sh"
|
||||
cp "$SRC/deploy/venv-audit.sh" "$HOME/.hermes/scripts/venv-audit.sh"
|
||||
# mem0-Konsolidierung (monatlich am 3., 19.07.2026): legt aehnliche Fakten zusammen,
|
||||
# loest Widersprueche (neuester gewinnt) — mem0 v2 ist ADD-only und tut das nie selbst.
|
||||
cp "$SRC/deploy/mem0-konsolidierung.py" "$HOME/.hermes/scripts/mem0-konsolidierung.py"
|
||||
cp "$SRC/deploy/mem0-konsolidierung.sh" "$HOME/.hermes/scripts/mem0-konsolidierung.sh"
|
||||
# Gitea-Repo-Anlage (17.07.2026): schließt Lucys „Projekte selbst starten"-Lücke —
|
||||
# legt PRIVATE Projekt-Repos an (Token aus ~/.git-credentials, braucht write:user-Scope).
|
||||
cp "$SRC/deploy/gitea-repo-create.sh" "$HOME/.hermes/scripts/gitea-repo-create.sh"
|
||||
# Werkstatt-Kanban-Profil (Ideen-Queue, 10.07.2026): Leitplanken des Worker-Profils nachziehen
|
||||
# (Profil selbst wurde einmalig per `hermes profile create werkstatt --clone` angelegt).
|
||||
[ -d "$HOME/.hermes/profiles/werkstatt" ] && cp "$SRC/deploy/werkstatt-SOUL.md" "$HOME/.hermes/profiles/werkstatt/SOUL.md"
|
||||
# betrieb-Profil (Ideen-Queue Faden 7, 13.07.2026): Ops-/Mess-/Bench-Worker mit Box-Wissen
|
||||
# statt Lucy-default. Profil selbst wird einmalig per `hermes profile create betrieb
|
||||
# --clone-from werkstatt` angelegt (model coder→hermes, Description fürs Specifier-Routing).
|
||||
[ -d "$HOME/.hermes/profiles/betrieb" ] && cp "$SRC/deploy/betrieb-SOUL.md" "$HOME/.hermes/profiles/betrieb/SOUL.md"
|
||||
# IDE-Vorbereiter (20.07.2026): haertet ein Konzept (konzept-fliessband), legt das Repo an und
|
||||
# fuellt es NUR mit Doku — der Commander baut dann in Zed. Sonderfall „IDEE PRUEFEN": nur Konzept,
|
||||
# kein Geruest, kein Bau-Druck. Profil einmalig per `hermes profile create projektstart
|
||||
# --clone-from werkstatt` angelegt (model coder→hermes, damit heavy warm bleibt).
|
||||
[ -d "$HOME/.hermes/profiles/projektstart" ] && cp "$SRC/deploy/projektstart-SOUL.md" "$HOME/.hermes/profiles/projektstart/SOUL.md"
|
||||
chmod +x "$HOME/.hermes/scripts/fremdblick.sh" "$HOME/.hermes/scripts/dreaming-feed.sh" "$HOME/.hermes/scripts/worker.sh" "$HOME/.hermes/scripts/chef-gutachter-feed.sh" "$HOME/.hermes/scripts/hermes-release-radar-feed.sh" "$HOME/.hermes/scripts/radar-selbstkritik-wrapper.sh" "$HOME/.hermes/scripts/idle-radar-feed.sh" "$HOME/.hermes/scripts/selbst-inventur.sh" "$HOME/.hermes/scripts/karten-gutachter.sh" "$HOME/.hermes/scripts/trend-radar-feed.sh" "$HOME/.hermes/scripts/pruefstand.sh" "$HOME/.hermes/scripts/restore-probe.sh" "$HOME/.hermes/scripts/venv-audit.sh" "$HOME/.hermes/scripts/gitea-repo-create.sh" "$HOME/.hermes/scripts/mem0-konsolidierung.sh" "$HOME/.hermes/scripts/mem0-konsolidierung.py"
|
||||
# Werkstatt-Skill (Autonomie E6): Selbstwartungs-Kreislauf für Hermes.
|
||||
mkdir -p "$HOME/.hermes/skills/wartung"
|
||||
cp "$SRC/deploy/skills/wartung/SKILL.md" "$HOME/.hermes/skills/wartung/SKILL.md"
|
||||
# Orchestrator-Skill (Autonomie): Werkstatt verallgemeinert — zerlegen → an Worker delegieren → gaten.
|
||||
mkdir -p "$HOME/.hermes/skills/orchestrator"
|
||||
cp "$SRC/deploy/skills/orchestrator/SKILL.md" "$HOME/.hermes/skills/orchestrator/SKILL.md"
|
||||
# PC-Pfad-Cache-Skill (Traum-Entwurf 09.07.2026, aufbereitet): Cache lesen → bei Miss pc_shell-Tool → merken.
|
||||
mkdir -p "$HOME/.hermes/skills/pc-pfad-cache"
|
||||
cp "$SRC/deploy/skills/pc-pfad-cache/SKILL.md" "$HOME/.hermes/skills/pc-pfad-cache/SKILL.md"
|
||||
cp "$SRC/deploy/skills/pc-pfad-cache/pc_path_lookup.sh" "$HOME/.hermes/scripts/pc_path_lookup.sh"
|
||||
chmod +x "$HOME/.hermes/scripts/pc_path_lookup.sh"
|
||||
# Projekt-Start-Skill (Abloesung 13.07.2026): "erst denken, dann bauen" fuer neue Coding-Projekte
|
||||
# in Hermes Desktop — Plan-Riegel, Savepoints, AGENTS.md/SAVEPOINT.md-Ritual, Kontext-Waechter.
|
||||
# Portiert vom PC-Skill (F:\Coding Stuff\projekt-start), Zed-Verweise durch Hermes Desktop ersetzt.
|
||||
mkdir -p "$HOME/.hermes/skills/projekt-start"
|
||||
cp "$SRC/deploy/skills/projekt-start/SKILL.md" "$HOME/.hermes/skills/projekt-start/SKILL.md"
|
||||
# Konzept-Fliessband-Skill (14.07.2026): rohe Idee → wasserdichtes KONZEPT (kein Code), BEVOR
|
||||
# gebaut wird. heavy (gpt-oss-120b) waehlt den Rollen-Cast, mehrere Denk-Runden schaerfen ein
|
||||
# lebendes Dokument, ein Advocatus Diaboli (fremdblick/GLM) haertet bis wasserdicht (max 3 Runden).
|
||||
# Behebt das delegate_task=coder-Loch: Konzept-Arbeit lief bisher stumm auf dem Code-Modell.
|
||||
# Output = Konzept-Dokument (~/konzepte/<slug>-konzept.md) → Vorlage fuer projekt-start in Hermes Desktop.
|
||||
mkdir -p "$HOME/.hermes/skills/konzept-fliessband"
|
||||
cp "$SRC/deploy/skills/konzept-fliessband/SKILL.md" "$HOME/.hermes/skills/konzept-fliessband/SKILL.md"
|
||||
# ...auch in Profile spiegeln, die den Skill schon haben (projektstart = IDE-Vorbereiter
|
||||
# nutzt ihn als Motor; Profile teilen KEINE Skills, Lehre 18.07.). Nur vorhandene updaten.
|
||||
for _psd in "$HOME"/.hermes/profiles/*/skills/konzept-fliessband; do
|
||||
[ -d "$_psd" ] && cp "$SRC/deploy/skills/konzept-fliessband/SKILL.md" "$_psd/SKILL.md"
|
||||
done
|
||||
# Betrieb-Playbook (Phase 2 Drei-Welten-Plan, 19.07.2026): Diagnose-Checklisten der
|
||||
# Betrieb-Bahn (SSH/Deploy) — Gegenstueck zur No-Progress-Bremse. Auch in die Profile,
|
||||
# damit betrieb-Worker es laden koennen (Profile teilen KEINE Skills, Lehre 18.07.).
|
||||
for _sd in "$HOME/.hermes/skills" "$HOME"/.hermes/profiles/*/skills; do
|
||||
[ -d "$(dirname "$_sd")" ] || continue
|
||||
mkdir -p "$_sd/betrieb-playbook"
|
||||
cp "$SRC/deploy/skills/betrieb-playbook/SKILL.md" "$_sd/betrieb-playbook/SKILL.md"
|
||||
done
|
||||
# Agent-Hooks (Faden 5, 13.07.2026): Skripte frisch halten (reproduzierbar, ueberlebt
|
||||
# Box-Neuaufbau). box-steckbrief-inject.sh = Box-Selbstwissen in JEDEN Kanban-Worker
|
||||
# (pre_llm_call, scoped auf task_id → Lucy-Chat unberuehrt); pre-verify-gates.sh = das
|
||||
# bestehende py_compile/dist-Gate. Die REGISTRIERUNG in ~/.hermes/config.yaml (hooks:)
|
||||
# ist EINMALIG von Hand (config.yaml ist Zwei-Schreiber-sensibel, kein Deploy-Schreiber).
|
||||
mkdir -p "$HOME/.hermes/agent-hooks"
|
||||
# Nur kopieren, wenn der Inhalt sich WIRKLICH geaendert hat — ein blindes cp aendert die
|
||||
# mtime auch bei identischem Inhalt und loest sonst bei JEDEM Deploy die harmlose
|
||||
# "script modified since approval"-Warnung von `hermes hooks doctor` aus.
|
||||
for _h in box-steckbrief-inject.sh pre-verify-gates.sh tabu-pfade-guard.py no-progress-bremse.py; do
|
||||
_src="$SRC/deploy/agent-hooks/$_h"; _dst="$HOME/.hermes/agent-hooks/$_h"
|
||||
cmp -s "$_src" "$_dst" 2>/dev/null || { cp "$_src" "$_dst"; chmod +x "$_dst"; }
|
||||
done
|
||||
# Worker-Profile lesen IHRE config.yaml, nicht die Top-Level — die worker-Hooks
|
||||
# (box-steckbrief = Box-Wissen, tabu-pfade-guard = Schreibschutz) muessen dort registriert
|
||||
# sein, sonst feuern sie fuer Worker nie (Bug-Fund 13.07.). Idempotent + Backup je Profil.
|
||||
for _prof in "$HOME"/.hermes/profiles/*/config.yaml; do
|
||||
[ -f "$_prof" ] && python3 "$SRC/deploy/ensure-profile-hooks.py" "$_prof" || true
|
||||
done
|
||||
# Hermes-Runtime-Patch-Traeger (20.07.2026): MC2-eigene Fixes im Hermes-Quellcode
|
||||
# (needs_input-wartet-auf-Commander, Decompose-erst-bei-Startreife, Orphan-Guard,
|
||||
# Projekt-Kontext-Vererbung, Etappen-Auto-Kette) idempotent (re-)eintragen — so
|
||||
# ueberleben sie ein `hermes update`, das den Quellbaum sonst zuruecksetzt.
|
||||
python3 "$SRC/deploy/hermes-patches/apply.py"; _patch_rc=$?
|
||||
if [ "$_patch_rc" -eq 10 ]; then
|
||||
# Patches haben den Quellcode geaendert → Gateway muss die neue Laufzeit laden.
|
||||
systemctl --user try-restart hermes-gateway >/dev/null 2>&1 || true
|
||||
echo "[deploy] Hermes-Runtime-Patches (neu) angewandt → hermes-gateway neu gestartet."
|
||||
elif [ "$_patch_rc" -ne 0 ]; then
|
||||
echo "[deploy] ‼ WARNUNG: hermes-patches/apply.py meldete Fehler — ein Patch passt nicht mehr (Update hat den Kontext geaendert?). Fixes pruefen!"
|
||||
fi
|
||||
# Eingebaute Hermes-Web-GUI (`hermes serve`) für die Einbettung in MC2 (routers/hermes_ui.py →
|
||||
# same-origin /hermes-ui/) vorbereiten: das SPA-Bundle mit Vite-base=/hermes-ui/ (neu) bauen, sonst
|
||||
# kollidieren seine absoluten Pfade (/assets, /api) mit MC2s eigenen. Läuft auch nach jedem
|
||||
# Hermes-Update mit → hält die Einbettung frisch. Best-effort + Build-zu-Temp-dann-Swap, damit ein
|
||||
# Fehlschlag die laufende web_dist NICHT leert (emptyOutDir).
|
||||
cp "$SRC/deploy/hermes-builtin-ui.service" "$HOME/.config/systemd/user/hermes-builtin-ui.service"
|
||||
_HUI_WEB="$HOME/.hermes/hermes-agent/web"
|
||||
_HUI_DIST="$HOME/.hermes/hermes-agent/hermes_cli/web_dist"
|
||||
if [ -d "$_HUI_WEB" ] && [ -x "$HOME/.hermes/node/bin/npx" ]; then
|
||||
if ( cd "$_HUI_WEB" && PATH="$HOME/.hermes/node/bin:$PATH" npx --no-install vite build \
|
||||
--base=/hermes-ui/ --outDir /tmp/hermes-ui-build --emptyOutDir ) >/tmp/hermes-ui-build.log 2>&1; then
|
||||
# Dienst VOR dem Tausch stoppen — sonst crasht der laufende `hermes serve`, wenn er index.html
|
||||
# im Lösch-Fenster (rm→cp) liest (FileNotFoundError → Start-Limit → tot). Der reguläre restart
|
||||
# weiter unten bringt ihn sauber auf der neuen web_dist hoch.
|
||||
systemctl --user stop hermes-builtin-ui 2>/dev/null || true
|
||||
rm -rf "$_HUI_DIST" && cp -r /tmp/hermes-ui-build "$_HUI_DIST" && echo "Hermes-GUI (base=/hermes-ui/) gebaut."
|
||||
else
|
||||
echo "WARN: Hermes-GUI-Build fehlgeschlagen (siehe /tmp/hermes-ui-build.log) — bestehende web_dist bleibt."
|
||||
fi
|
||||
fi
|
||||
# 3. Systemd Units neu laden (falls sich .service Dateien geändert haben)
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable mission-control-2 >/dev/null 2>&1 || true
|
||||
systemctl --user enable mc2-gateway >/dev/null 2>&1 || true
|
||||
systemctl --user enable mc2-steward >/dev/null 2>&1 || true
|
||||
systemctl --user enable box-console >/dev/null 2>&1 || true
|
||||
systemctl --user enable mem0-service >/dev/null 2>&1 || true
|
||||
systemctl --user enable voice-service >/dev/null 2>&1 || true
|
||||
systemctl --user enable hermes-builtin-ui >/dev/null 2>&1 || true
|
||||
systemctl --user enable --now mc2-backup.timer >/dev/null 2>&1 || true
|
||||
# mc2-autoupdate.timer wird hier BEWUSST NICHT geschaltet — der Zustand wird nur von Hand
|
||||
# geändert (Lehre 09.07.: die frühere enable-Zeile hat einen User-Entscheid bei jedem Deploy
|
||||
# still rückgängig gemacht). Aktueller User-Entscheid (10.07.2026): Timer ist AN —
|
||||
# Router/Engine/Hermes dürfen So 04:30 automatisch (Fangnetz Backup→Postcheck→Rollback+Pin).
|
||||
systemctl --user enable --now mc2-selfsmoke.timer >/dev/null 2>&1 || true
|
||||
systemctl --user enable --now mc2-bagatell.timer >/dev/null 2>&1 || true
|
||||
systemctl --user enable --now mc2-morgen-digest.timer >/dev/null 2>&1 || true
|
||||
loginctl enable-linger "$USER" >/dev/null 2>&1 || true
|
||||
# Mem0-Sidecar VOR dem Backend (re)starten, damit /api/memory sofort bedient wird.
|
||||
[ -x "$HOME/.mem0/venv/bin/python" ] && systemctl --user restart mem0-service 2>/dev/null || true
|
||||
# Voice-Sidecar (re)starten (best-effort; Erststart lädt das STT-Modell vor).
|
||||
[ -x "$HOME/.voice/venv/bin/python" ] && systemctl --user restart voice-service 2>/dev/null || true
|
||||
# Eingebaute Hermes-Web-GUI (Loopback :9119) (re)starten — MC2 bettet sie unter /hermes-ui/ ein.
|
||||
# reset-failed, damit ein zuvor am Start-Limit gestorbener Dienst wieder anläuft.
|
||||
systemctl --user reset-failed hermes-builtin-ui 2>/dev/null || true
|
||||
systemctl --user restart hermes-builtin-ui 2>/dev/null || true
|
||||
# Gateway VOR dem Steuerpult (re)starten — MC2s /v1-Weiterleiter braucht ihn sofort.
|
||||
# KEIN `|| true`: ohne Gateway ist der LLM-Datenpfad tot, das MUSS den Deploy stoppen.
|
||||
systemctl --user restart mc2-gateway
|
||||
# Steward (Wächter) — unabhängig vom Steuerpult; ohne ihn wacht niemand über die Box.
|
||||
systemctl --user restart mc2-steward
|
||||
systemctl --user restart mission-control-2
|
||||
command -v ttyd >/dev/null 2>&1 && systemctl --user restart box-console 2>/dev/null || true
|
||||
|
||||
sleep 2
|
||||
echo "--- Health ---"
|
||||
# Gateway-Kaltstart-Fenster: bis ~12 s tolerieren, DANN hart scheitern (ohne Gateway
|
||||
# ist der LLM-Datenpfad tot — das muss den Karten-Runner rot machen).
|
||||
for _i in $(seq 1 10); do
|
||||
curl -sf -m 3 http://127.0.0.1:9010/gw/health >/dev/null 2>&1 && break
|
||||
[ "$_i" -eq 10 ] && { echo "FEHLER: mc2-gateway (:9010) antwortet nicht"; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
curl -sf http://127.0.0.1:9010/gw/health && echo
|
||||
curl -sf http://127.0.0.1:9001/api/health && echo
|
||||
# Steward hat keinen HTTP-Port — systemd-Zustand ist der Beweis (crasht er sofort,
|
||||
# ist er hier bereits 'failed' statt 'active').
|
||||
if systemctl --user is-active --quiet mc2-steward; then
|
||||
echo "mc2-steward aktiv (Wächter laufen)"
|
||||
else
|
||||
echo "FEHLER: mc2-steward läuft nicht (journalctl --user -u mc2-steward -n 30)"; exit 1
|
||||
fi
|
||||
# 4. Dienste neu starten
|
||||
echo "Starte Backend neu..."
|
||||
systemctl --user restart mc2-gateway.service mission-control-2.service
|
||||
|
||||
# Warm-Set (Hirn + Augen/vision + Gedächtnis/embed) nach dem Deploy nachladen — ein Deploy bzw.
|
||||
# watch-config-Reload verwirft es sonst und die erste Anfrage wäre kalt (D16d). warmup.sh ist
|
||||
# selbst-detachend (blockiert den Deploy nicht) und lädt jedes Modell über den richtigen Endpunkt.
|
||||
bash "$SRC/deploy/warmup.sh" || true
|
||||
|
||||
echo "OK — Mission Control 2.0 läuft auf :9001 (User-Dienst, sudo-frei)."
|
||||
echo "✅ Deployment erfolgreich abgeschlossen!"
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Feed für den Traum-Cron (Dreaming / Continual-Learning-Kreislauf, Phase 2 — Capstone).
|
||||
# Zwilling von radar-feed.sh (Blick nach draußen) und selbstkritik-feed.sh (Blick auf den
|
||||
# Betrieb); der Traum blickt auf das GELERNTE: Sessions, Skills, Gedächtnis, Muster.
|
||||
# Liegt in ~/.hermes/scripts/ (deploy.sh kopiert ihn); stdout wird dem Cron-Agenten als
|
||||
# Prompt eingespeist: versionierter Auftrag (deploy/dreaming-prompt.md) + Live-Schlaf-Daten.
|
||||
set -uo pipefail
|
||||
|
||||
VAULT="$HOME/wissens-vault"
|
||||
|
||||
# --- Vault-Bootstrap (idempotent): eigenes git-Repo, menschenlesbar, NICHT im Stack-Code. ---
|
||||
if [ ! -d "$VAULT/.git" ]; then
|
||||
mkdir -p "$VAULT/traeume" "$VAULT/muster" "$VAULT/skill-kandidaten"
|
||||
git -C "$VAULT" init -q 2>/dev/null || true
|
||||
# Lokale Identität nur setzen, falls global keine da ist (Commits sollen nie scheitern).
|
||||
git -C "$VAULT" config user.email >/dev/null 2>&1 || git -C "$VAULT" config user.email "lucy@box.local"
|
||||
git -C "$VAULT" config user.name >/dev/null 2>&1 || git -C "$VAULT" config user.name "Lucy (Traum)"
|
||||
if [ ! -f "$VAULT/INDEX.md" ]; then
|
||||
cat > "$VAULT/INDEX.md" <<'EOF'
|
||||
# Wissens-Vault — Navigationskarte
|
||||
|
||||
Menschenlesbare, git-versionierte Wissensschicht NEBEN dem Live-Gedächtnis (Mem0).
|
||||
Angelegt von Lucys nächtlichem Traum. Verlinkung im Obsidian-Stil `[[dateiname]]`.
|
||||
|
||||
## Träume (chronologisch)
|
||||
|
||||
## Muster (akkumuliertes Projektwissen)
|
||||
|
||||
## Skill-Kandidaten (Vorschläge, Gate = Commander „mach")
|
||||
EOF
|
||||
git -C "$VAULT" add -A >/dev/null 2>&1 || true
|
||||
git -C "$VAULT" commit -q -m "Vault angelegt" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Versionierter Auftrag ---
|
||||
cat "$HOME/mission-control-v2/deploy/dreaming-prompt.md"
|
||||
|
||||
echo
|
||||
echo "## SCHLAF-DATEN (automatisch erhoben am $(date '+%d.%m.%Y %H:%M'))"
|
||||
echo
|
||||
# Hinweis: erst voll einsammeln, DANN kürzen — sonst schließt `head` die Pipe früh,
|
||||
# der Produzent bekommt SIGPIPE, pipefail feuert und die Fallback-Zeile lügt "nicht verfügbar".
|
||||
echo "### Insights der letzten 7 Tage (Token-/Tool-/Aktivitätsmuster):"
|
||||
_ins="$(bash -lc "hermes insights --days 7" 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g')"
|
||||
echo "${_ins:-(insights nicht verfügbar)}" | head -62
|
||||
echo
|
||||
echo "### Session-Store (Umfang):"
|
||||
_sst="$(bash -lc "hermes sessions stats" 2>/dev/null)"
|
||||
echo "${_sst:-(sessions stats nicht verfügbar)}" | head -12
|
||||
echo
|
||||
echo "### Jüngste Sessions (woran zuletzt gearbeitet wurde):"
|
||||
_sls="$(bash -lc "hermes sessions list" 2>/dev/null | sed 's/\x1b\[[0-9;]*m//g')"
|
||||
echo "${_sls:-(sessions list nicht verfügbar)}" | head -25
|
||||
echo
|
||||
echo "### Gelernt bisher (journey — Skills + Gedächtnis-Knoten, kompakt):"
|
||||
bash -lc "hermes journey --json" 2>/dev/null \
|
||||
| jq -c '{stats: .stats, skills: ([.nodes[]? | select(.kind=="skill" or .type=="skill") | .label // .name] | .[0:25]), cluster_labels: ([.clusters[]?.label] | .[0:15])}' \
|
||||
2>/dev/null || echo "(journey nicht verfügbar)"
|
||||
echo
|
||||
echo "### Curator / Skill-Hygiene (was brachliegt, was aktiv ist):"
|
||||
bash -lc "hermes curator status" 2>/dev/null | head -26 || echo "(curator status nicht verfügbar)"
|
||||
echo
|
||||
echo "### Bisheriger Vault-INDEX (nur zur Ansicht — damit du nichts doppelt anlegst + verlinken"
|
||||
echo "### kannst). Die echte Datei liegt unter ~/wissens-vault/INDEX.md; zwischen den Markern"
|
||||
echo "### steht ihr AKTUELLER Inhalt. Kopiere die Marker NICHT in die Datei zurück."
|
||||
echo "<<<INDEX-INHALT-ANFANG>>>"
|
||||
cat "$VAULT/INDEX.md" 2>/dev/null | head -120 || echo "(noch kein INDEX — beim ersten Traum anlegen)"
|
||||
echo "<<<INDEX-INHALT-ENDE>>>"
|
||||
echo
|
||||
echo "### Letzte 3 Traum-Notizen (nur Briefing — NICHT in den INDEX kopieren):"
|
||||
ls -1t "$VAULT/traeume/"*.md 2>/dev/null | head -3 | while read -r f; do
|
||||
echo " $(basename "$f"): $(head -1 "$f" 2>/dev/null)"
|
||||
done
|
||||
@@ -1,106 +0,0 @@
|
||||
# Der Traum (nächtlich) — Continual-Learning-Kreislauf
|
||||
|
||||
Es ist tiefe Nacht, niemand spricht mit dir, die Box ist im Leerlauf. Du bist Lucy und
|
||||
tust jetzt das, wozu Menschen den Schlaf brauchen: Du gehst den vergangenen Tag/die
|
||||
vergangene Woche noch einmal durch, ziehst Muster heraus, legst Gelerntes ordentlich ab
|
||||
und überlegst, wie du beim nächsten Mal besser wirst — **ohne neu trainiert zu werden.**
|
||||
|
||||
Das ist der Höhepunkt der Autonomie-Vision: „Die Box wird besser, während der Commander
|
||||
schläft." Dieser Lauf ist der Zwilling von Evolution-Radar (Blick nach draußen) und
|
||||
Selbstkritik (Blick auf den Betrieb) — der **Traum** ist der Blick auf das **Gelernte**:
|
||||
Sessions, Skills, Gedächtnis, wiederkehrende Muster.
|
||||
|
||||
## Dein Auftrag (streng in dieser Reihenfolge — der Commit und die Nachricht kommen ZULETZT)
|
||||
|
||||
1. **Reflektieren.** Lies die SCHLAF-DATEN unten gründlich: die Insights (Token-/Tool-/
|
||||
Aktivitätsmuster der letzten 7 Tage), die jüngsten Sessions, den Skill-/Gedächtnis-Stand
|
||||
(journey), den Curator-Stand. Frag dich:
|
||||
- Was ging diese Woche wiederholt gut? Was ging wiederholt schief?
|
||||
- Welche Tools laufen heiß, welche Skills liegen brach (activity=0)?
|
||||
- Gibt es ein **wiederkehrendes Muster** über mehrere Sessions hinweg — dieselbe
|
||||
Fehlerart, derselbe Handgriff, dieselbe Frage des Commanders? Ein Muster braucht
|
||||
**mehrfache Belege** (nicht ein Einzelfall), sonst ist es Rauschen.
|
||||
|
||||
2. **Notizen ENTWERFEN** (schreiben, aber NOCH NICHT committen) im Wissens-Vault
|
||||
(`~/wissens-vault/`, ein git-versioniertes, menschenlesbares Markdown-Verzeichnis — die
|
||||
dauerhafte, navigierbare Wissensschicht NEBEN dem Live-Gedächtnis Mem0):
|
||||
- Lege **eine** neue Traum-Notiz an: `~/wissens-vault/traeume/<YYYY-MM-DD>-traum.md`
|
||||
(Datum von heute). Kurz und dicht: 2–5 belegte Beobachtungen, je mit Zahl/Session/
|
||||
Log-Bezug. Keine Romane, keine Allgemeinplätze.
|
||||
- Wenn ein Muster **mehrfach** belegt ist: lege `~/wissens-vault/muster/<kurz-slug>.md` an
|
||||
(oder ergänze die vorhandene um einen neuen Beleg + heutiges Datum). Muster-Notizen sind
|
||||
das akkumulierte Projektwissen.
|
||||
- Skill-Kandidat (nur wenn sich klar einer aufdrängt): wenn aus einem ERFOLGREICHEN,
|
||||
wiederholten Handgriff ein wiederverwendbarer Skill würde (Voyager-Prinzip), beschreibe
|
||||
ihn als Entwurf unter `~/wissens-vault/skill-kandidaten/<slug>.md`. **ABER ZUERST prüfen:**
|
||||
```
|
||||
ls ~/wissens-vault/skill-kandidaten/*.md ~/wissens-vault/skill-kandidaten/beauftragt/*.md 2>/dev/null
|
||||
ls ~/.hermes/skills/ 2>/dev/null
|
||||
```
|
||||
Wenn ein thematisch passender Kandidat schon existiert → **NICHTS anlegen**, stattdessen
|
||||
im Bericht erwähnen: „Kandidat `[[slug]]` besteht bereits, seit <Datum>, kein neuer
|
||||
Vorschlag nötig."
|
||||
**WICHTIG:** Prüfe auch, ob es bereits einen fertig gebauten Skill gibt.
|
||||
Wenn ja: kein Kandidat nötig, der Skill existiert bereits.
|
||||
**Entwirf ihn gleich
|
||||
im Hausstandard deiner Skills** (wie `/learn` es täte), damit die Werkstatt ein fertiges
|
||||
Gerüst übernimmt statt Freitext zu übersetzen: ein `SKILL.md`-Block mit Frontmatter
|
||||
(`name` = slug, `description` ≤ 60 Zeichen, `version: 1.0.0`), dann die Abschnitte
|
||||
**Wann nutzen** (Auslöser), **Schritte** (mit den ECHTEN Hermes-Tools/Befehlen aus den
|
||||
belegten Sessions — keine erfundenen Kommandos) und **Belege** (Sessions/Zahlen, aus
|
||||
denen der Handgriff stammt). **Du legst den Skill NICHT selbst an** (kein skill_manage) —
|
||||
das ist ein Vorschlag für die Werkstatt, die der Commander im Auftragsbuch per
|
||||
„Beauftragen" auslöst. Höchstens EIN Kandidat pro Traum.
|
||||
- Verlinke zwischen Notizen mit `[[dateiname-ohne-endung]]` (Obsidian-Stil), großzügig.
|
||||
|
||||
3. **Fremdblick — PFLICHT-GATE** (Härtung „andere Brille": ein Agent, der seine eigenen Träume
|
||||
benotet, stimmt sich selbst zu). Hol dir eine Zweitmeinung von einem ANDEREN Modell. **Nutze
|
||||
dafür das bereitgestellte Skript** — NICHT `delegate_task` (der Kritiker heavy hat nur 32K
|
||||
Kontext und ist als Subagent-Ziel disqualifiziert; das Skript umgeht das mit einer einzelnen
|
||||
Completion). Konkret: Fasse deine 2–5 Beobachtungen je mit ihrem Zahlen-/Session-Beleg + den
|
||||
(falls vorhandenen) Skill-Kandidaten KOMPAKT zusammen (wenige Zeilen je Punkt, KEINE
|
||||
Rohdaten) und pipe sie in das Skript:
|
||||
|
||||
```
|
||||
printf '%s' "<deine kompakten Beobachtungen + Belege>" | ~/.hermes/scripts/fremdblick.sh
|
||||
```
|
||||
|
||||
Das Skript fragt gpt-oss (heavy, andere Modell-Familie als dein Qwen-Hirn) und gibt pro Punkt
|
||||
ein Urteil TRAEGT / TRAEGT-NICHT / UNSICHER zurück. **Lies die Ausgabe und richte dich danach.**
|
||||
**⛔ HARTE REGEL:** Du fährst mit Schritt 4 erst fort, wenn die Fremdblick-Ausgabe vorliegt.
|
||||
Schreibe KEINE Nachricht und committe NICHTS vorher. Steht in der Ausgabe „FREMDBLICK
|
||||
FEHLGESCHLAGEN", einmal wiederholen; klappt es dann immer noch nicht, kennzeichne deine Funde
|
||||
in der Nachricht ehrlich als **„ungeprüft (Fremdblick fiel aus)"**.
|
||||
|
||||
4. **Bereinigen + INDEX pflegen.** Streiche aus deinen Notizen alles, was der Fremdblick nicht
|
||||
getragen hat. Trage dann **jede** verbliebene neue/geänderte Notiz in `~/wissens-vault/INDEX.md`
|
||||
ein (eine Zeile je Notiz unter der passenden Überschrift: `- [[dateiname]] — Ein-Satz-Haken`).
|
||||
**Wichtig zum INDEX:** Bearbeite NUR die Markdown-Inhaltszeilen. Die Zeilen der SCHLAF-DATEN
|
||||
unten (alles ab „## SCHLAF-DATEN", inklusive Abschnitte wie „### Bisheriger Vault-INDEX" und
|
||||
„### Letzte 3 Traum-Notizen") sind BRIEFING für dich, NICHT Teil des INDEX — kopiere sie NIE
|
||||
in die INDEX-Datei.
|
||||
|
||||
5. **Commit — die LETZTE Vault-Aktion, sie fasst alles zusammen:**
|
||||
`git -C ~/wissens-vault add -A && git -C ~/wissens-vault commit -m "Traum <YYYY-MM-DD>"`.
|
||||
(Harmloser Commit in einem EIGENEN Repo, nicht im Stack-Code — erlaubt und reversibel.)
|
||||
|
||||
6. **Dem Commander berichten — deine ALLERLETZTE Handlung** (nichts danach). EINE kompakte
|
||||
Nachricht in DEINER Stimme (Lucy: Anrede „Commander", Fazit zuerst, Alltagssprache, Technik
|
||||
knapp dahinter). Sag: was du heute Nacht bemerkt hast (1–3 Funde, je mit Beleg + wie der
|
||||
Fremdblick sie bewertet hat), wohin du es im Vault gelegt hast (`[[link]]`), und — falls
|
||||
vorhanden — welchen Skill-Kandidaten du zur Prüfung vorschlägst. Wenn ehrlich NICHTS
|
||||
Nennenswertes war: genau das sagen, kurz und zufrieden — **keine Funde erfinden.**
|
||||
|
||||
## Leitplanken (nicht verhandelbar — selbstverbessernde Agenten driften sonst)
|
||||
|
||||
- **Du änderst am laufenden Betrieb NICHTS.** Der einzige Ort, an den du schreibst, ist der
|
||||
`~/wissens-vault/` (Notizen + Commit). Alles andere ist nur VORSCHLAG an den Commander;
|
||||
umgesetzt wird erst auf sein „mach" (dann übernimmt die Werkstatt mit Gates und Rollback).
|
||||
- **Kein Security-Anfassen.** Approvals, Tokens, Firewall, Config nur BENENNEN, nie ändern.
|
||||
- **Fremder Quellcode (Hermes, llama.cpp …) ist tabu** — Auto-Update-Kanal, nicht dein Revier.
|
||||
- **Nichts, was das Warm-Set oder Lucys Sprech-Latenz gefährdet.** Der Fremdblick (Schritt 3)
|
||||
nutzt ein LEICHTES Modell (GLM, lädt klein daneben) — **lade NIE gpt-oss/heavy** (60 GB,
|
||||
kollidiert mit dem VL-30B-Warm-Set → Health-Check-Tod). Keine schweren Jobs, kein Neustart.
|
||||
- **Sparsam mit Tools.** Die Schlaf-Daten unten reichen für die Reflexion; höchstens ein paar
|
||||
gezielte Nachschau-Aufrufe (eine Session exportieren, eine Log-Zeile im Kontext lesen),
|
||||
dann schreiben + berichten. Kein Tool-Feuerwerk.
|
||||
@@ -1,69 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fremdblick — Ein-Schuss-Zweitmeinung von einem ANDEREN Modell (Default seit 07.07.:
|
||||
# GLM-4.7-Flash, 30B-A3B Text — klar stärkerer Kritiker als die 9B-Vision-Schwester 4.6V,
|
||||
# gleicher Fremd-Vendor wie zuvor → echte „andere Brille" bleibt). Eine EINZELNE
|
||||
# Completion — kein agentischer Subagent — umgeht Hermes' 64K-Delegations-Floor
|
||||
# (MINIMUM_CONTEXT_LENGTH) UND das 60-GB-Ladeproblem von gpt-oss (das mit dem Warm-Set kollidiert
|
||||
# und „exited prematurely" wirft). GLM lädt klein neben dem Warm-Set und antwortet zuverlässig.
|
||||
#
|
||||
# Nutzung: echo "<Beobachtungen/Behauptungen je mit Beleg>" | fremdblick.sh
|
||||
# printf '%s' "$text" | FREMDBLICK_MODEL=Qwen3-Coder-Next FREMDBLICK_SYS="$raster" fremdblick.sh
|
||||
#
|
||||
# Env-Overrides:
|
||||
# FREMDBLICK_MODE 'prose' (Default, Dreaming-Prosa-Raster) oder 'code' (Werkstatt-Code-Review).
|
||||
# 'code' setzt Raster + Default-Modell (Qwen3-Coder-Next) + größeres Budget.
|
||||
# FREMDBLICK_MODEL Modell-ID (echte llama-swap-ID, kein Alias). Übersteuert den Mode-Default.
|
||||
# FREMDBLICK_SYS eigenes System-Raster (übersteuert das Mode-Raster).
|
||||
# FREMDBLICK_MAXTOK max_tokens (übersteuert den Mode-Default).
|
||||
set -uo pipefail
|
||||
|
||||
# Achtung: llama-swap listet unter /v1/models die ECHTEN Modell-IDs, nicht die Gateway-Aliase
|
||||
# (kein "heavy"/"scout" hier).
|
||||
ENDPOINT="${FREMDBLICK_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
|
||||
MODE="${FREMDBLICK_MODE:-prose}"
|
||||
SUBJECT="$(cat)"
|
||||
|
||||
if [ -z "${SUBJECT// /}" ]; then
|
||||
echo "=== FREMDBLICK: nichts zu prüfen (leere Eingabe) ==="
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Prosa-Raster (Dreaming): urteilt über Beobachtungen/Behauptungen. Default-Kritiker = GLM (Fremd-Vendor).
|
||||
PROSE_SYS='Du bist ein KRITISCHER Zweitgutachter und ausdrücklich ein ANDERES Modell als der Autor. Prüfe jede vorgelegte Beobachtung/Behauptung streng: Trägt der genannte Beleg (Zahl / Session / Log-Zeile) die Behauptung wirklich, oder ist es ein Einzelfall bzw. ein Bauchgefühl mit Zahlen-Deko? Verwechselt der Autor Korrelation mit Ursache? Fehlt entscheidender Kontext? Falls ein Skill-Kandidat dabei ist: ist er wirklich WIEDERVERWENDBAR oder Einmal-Kram? Antworte kompakt auf Deutsch: pro Punkt ein Urteil TRAEGT / TRAEGT-NICHT / UNSICHER mit genau einem Satz Begruendung. Sei knapp und unbestechlich; nicke nichts aus Hoeflichkeit durch. Erfinde keine neuen Belege.'
|
||||
# Code-Raster (Werkstatt): reproduziert den Fehlerfall aus der Bug-Beschreibung, statt dem Autor zu glauben.
|
||||
CODE_SYS='Du bist ein kritischer Code-Reviewer und ein ANDERES Modell als der Autor. Vertraue seiner Begruendung NICHT. Dir liegen ein Wartungs-AUFTRAG (Bug-Beschreibung) und ein git-Diff vor. Trenne klar: (A) KERNFRAGE: Behebt der Diff den im Auftrag KONKRET beschriebenen Fehlerfall? Leite die Eingabe SELBST aus der Bug-Beschreibung ab und belege einen Uebergang: Eingabe X -> VOR Patch: Y (Bug sichtbar) -> NACH Patch: Z. (B) RANDFAELLE: nenne offene Risiken/Randfaelle als Hinweis. URTEIL-Regel: ABGELEHNT NUR, wenn (A) nicht belegt behoben ist (Kern-Bug bleibt, ODER der Patch trifft die Auftrags-Absicht nicht, ODER er bricht den Normalfall). Ist der Kern-Fall belegt behoben und es bleiben nur Randfaelle: FREIGABE-MIT-VORBEHALT (Randfaelle auflisten). Ist alles sauber: FREIGABE. Ein blosses sieht-plausibel-aus OHNE reproduzierten Kern-Uebergang = ABGELEHNT. Beginne die Antwort mit der Zeile "URTEIL: <ABGELEHNT|FREIGABE-MIT-VORBEHALT|FREIGABE>". Antworte knapp auf Deutsch.'
|
||||
|
||||
if [ "$MODE" = "code" ]; then
|
||||
MODEL="${FREMDBLICK_MODEL:-Qwen3-Coder-Next}"
|
||||
MAXTOK="${FREMDBLICK_MAXTOK:-1600}"
|
||||
SYS="${FREMDBLICK_SYS:-$CODE_SYS}"
|
||||
else
|
||||
MODEL="${FREMDBLICK_MODEL:-GLM-4.7-Flash}"
|
||||
# 4000 (war 2200): GLM-4.7-Flash ist ein Reasoning-Modell und verbrennt real ~2000 Tokens
|
||||
# im reasoning_content, BEVOR das Urteil in content landet (verifiziert 14.07.). Bei 2200 lief
|
||||
# es Gefahr, mitten im Denken abzuschneiden → leeres content → "ABGESCHNITTEN/FEHLGESCHLAGEN"
|
||||
# (traf die Nacht-Kritiker mit Default-Budget). Wer explizit knapper braucht, setzt FREMDBLICK_MAXTOK.
|
||||
MAXTOK="${FREMDBLICK_MAXTOK:-4000}"
|
||||
SYS="${FREMDBLICK_SYS:-$PROSE_SYS}"
|
||||
fi
|
||||
|
||||
# max_tokens großzügig: Reasoning-Modelle (GLM) verbrauchen Tokens im reasoning_content, bevor sie
|
||||
# die eigentliche Antwort in content schreiben — zu knapp = leeres content (finish=length).
|
||||
RESP="$(jq -n --arg m "$MODEL" --arg s "$SYS" --arg u "$SUBJECT" --argjson t "$MAXTOK" \
|
||||
'{model:$m, temperature:0.2, max_tokens:$t, messages:[{role:"system",content:$s},{role:"user",content:$u}]}' \
|
||||
| curl -s --max-time 240 "$ENDPOINT" -H 'Content-Type: application/json' -d @- 2>/dev/null)"
|
||||
|
||||
OUT="$(printf '%s' "$RESP" | jq -r '.choices[0].message.content // empty' 2>/dev/null)"
|
||||
FIN="$(printf '%s' "$RESP" | jq -r '.choices[0].finish_reason // empty' 2>/dev/null)"
|
||||
|
||||
if [ -n "$OUT" ]; then
|
||||
echo "=== FREMDBLICK (Zweitmeinung von Modell: $MODEL) ==="
|
||||
echo "$OUT"
|
||||
elif [ "$FIN" = "length" ]; then
|
||||
echo "=== FREMDBLICK ABGESCHNITTEN ==="
|
||||
echo "(Das Kritiker-Modell hat sein Token-Budget im Nachdenken verbraucht, bevor ein Urteil kam."
|
||||
echo " Kürze deine Vorlage auf die Kern-Behauptungen und versuche es erneut; sonst UNGEPRUEFT.)"
|
||||
else
|
||||
echo "=== FREMDBLICK FEHLGESCHLAGEN ==="
|
||||
echo "(Kein Urteil erhalten — Endpoint/Modell nicht erreichbar. Behandle die Funde als UNGEPRUEFT.)"
|
||||
fi
|
||||
@@ -1,21 +0,0 @@
|
||||
[Unit]
|
||||
Description=Hermes Built-in Web UI (`hermes serve` — Agent-Dashboard/Chat, Loopback only)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
# Serviert die EINGEBAUTE Hermes-Web-GUI (die reiche Agent-Oberflaeche mit Threads/Tool-Calls, die
|
||||
# auch die Electron-Desktop-App umhuellt) headless.
|
||||
# SICHERHEIT: Bindet NUR an Loopback (127.0.0.1:9119) und laeuft hinter dem MC2-Reverse-Proxy
|
||||
# (routers/hermes_ui.py → same-origin /hermes-ui/ auf dem offenen Port 9001) — KEINE eigene
|
||||
# Firewall-Freigabe, KEIN Login (LAN-Trust wie Konsole/Terminal). --skip-build nutzt das von
|
||||
# deploy.sh mit Vite-base=/hermes-ui/ vorgebaute web_dist, damit alle SPA-Pfade unter /hermes-ui/
|
||||
# liegen und der Proxy sie sauber durchreichen kann.
|
||||
Type=simple
|
||||
Environment=HERMES_HOME=%h/.hermes
|
||||
ExecStart=%h/.hermes/hermes-agent/venv/bin/hermes dashboard --no-open --skip-build --host 127.0.0.1 --port 9119
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,259 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Hermes-Runtime-Patch-Traeger — haelt MC2-eigene Fixes im Hermes-Quellcode
|
||||
(``~/.hermes/hermes-agent``), damit sie ein ``hermes update`` ueberleben.
|
||||
|
||||
Warum: Der Hermes-Agent wird aus git aktualisiert; ein Direkt-Edit der Laufzeit
|
||||
wuerde beim naechsten Update still ueberschrieben. Dieses Skript wird von
|
||||
``deploy/deploy.sh`` UND ``deploy/hermes-postcheck.sh`` aufgerufen und traegt die
|
||||
Patches (re-)ein.
|
||||
|
||||
Mechanik: JEDER Patch ist eine idempotente String-Ersetzung (robust gegen
|
||||
Zeilenverschiebungen durch Updates, anders als ``git apply``):
|
||||
* ``marker`` schon im Quelltext -> Patch bereits drin, ueberspringen.
|
||||
* ``old`` genau 1x vorhanden -> ersetzen, dann ``py_compile`` pruefen.
|
||||
* ``old`` 0x oder >1x -> Kontext hat sich durch ein Update
|
||||
geaendert -> LAUT scheitern (Exit 1), damit
|
||||
der Postcheck rot wird und niemand denkt,
|
||||
der Fix sei noch aktiv.
|
||||
|
||||
Bei Fehlschlag: Exit 1 (der Deploy/Postcheck behandelt das als Fehler).
|
||||
"""
|
||||
import os
|
||||
import py_compile
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HA = Path(os.environ.get("HERMES_AGENT_DIR", str(Path.home() / ".hermes" / "hermes-agent")))
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
# --- Zusatzmodule, die in die Hermes-Laufzeit kopiert werden (update-fest) ---
|
||||
# (source relativ zu diesem Skript, dest relativ zum Hermes-Agent-Dir)
|
||||
COPIES = [
|
||||
("mc2_kanban_reaper.py", "hermes_cli/mc2_kanban_reaper.py"),
|
||||
]
|
||||
|
||||
# --- Patch-Definitionen (chronologisch, Fan-out-/Muenz-Schleifen-Fixes 20.07.2026) ---
|
||||
PATCHES = [
|
||||
{
|
||||
"name": "0001-needs-input-wait",
|
||||
"file": "hermes_cli/kanban_db.py",
|
||||
"marker": 'kind not in ("needs_input", "capability")',
|
||||
"old": " if recurrences >= BLOCK_RECURRENCE_LIMIT:\n",
|
||||
"new": (
|
||||
" # MC2-Patch (20.07.2026): needs_input/capability sind genuin\n"
|
||||
" # human-only — sie muessen in ``blocked`` auf den Commander warten\n"
|
||||
" # (Antwort via Auftragsbuch), NICHT nach triage eskalieren. Sonst\n"
|
||||
" # zerlegt der Auto-Decomposer sie neu = Duplikat-Karten-Schleife\n"
|
||||
' # ("Muenz-Schleife", Fan-out-Vorfall 20.07.). Der Loop-Breaker bleibt\n'
|
||||
" # fuer transient/untyped Blocks aktiv.\n"
|
||||
' if recurrences >= BLOCK_RECURRENCE_LIMIT and kind not in ("needs_input", "capability"):\n'
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "0002-decompose-at-readiness",
|
||||
"file": "hermes_cli/kanban_decompose.py",
|
||||
"marker": "Startreife-Gate",
|
||||
"old": (
|
||||
" cfg = _load_config()\n"
|
||||
" orchestrator = _resolve_orchestrator_profile(cfg)\n"
|
||||
),
|
||||
"new": (
|
||||
" # MC2-Patch (20.07.2026): Startreife-Gate. Eine triage-Karte mit noch\n"
|
||||
" # OFFENEN Vorgaengern NICHT zerlegen — sonst raet der Decomposer die\n"
|
||||
" # Schnittstellen der Vor-Etappe ins Blaue, bevor die gebaut hat\n"
|
||||
" # (Fan-out-Vorfall 20.07.). Billiger Parent-Check VOR dem teuren\n"
|
||||
" # LLM-Aufruf; erst zerlegen, wenn alle Eltern done/archived.\n"
|
||||
" with kb.connect_closing() as _pconn:\n"
|
||||
" _parents = [r[0] for r in _pconn.execute(\n"
|
||||
' "SELECT parent_id FROM task_links WHERE child_id = ?", (task_id,))]\n'
|
||||
" if _parents:\n"
|
||||
' _ph = ",".join("?" * len(_parents))\n'
|
||||
" _open = _pconn.execute(\n"
|
||||
' "SELECT COUNT(*) FROM tasks WHERE id IN (%s) "\n'
|
||||
" \"AND status NOT IN ('done', 'archived')\" % _ph, _parents\n"
|
||||
" ).fetchone()[0]\n"
|
||||
" if _open:\n"
|
||||
" return DecomposeOutcome(\n"
|
||||
" task_id, False,\n"
|
||||
' f"defer: {_open} unfinished parent(s) — decompose when start-ready",\n'
|
||||
" )\n"
|
||||
" cfg = _load_config()\n"
|
||||
" orchestrator = _resolve_orchestrator_profile(cfg)\n"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "0003-orphan-guard",
|
||||
"file": "gateway/kanban_watchers.py",
|
||||
"marker": "mc2_kanban_reaper",
|
||||
"old": (
|
||||
" pids = await asyncio.to_thread(_kb.reap_worker_zombies)\n"
|
||||
" if pids:\n"
|
||||
" logger.info(\n"
|
||||
' "kanban dispatcher: reaped %d zombie worker(s), pids=%s",\n'
|
||||
" len(pids),\n"
|
||||
" pids,\n"
|
||||
" )\n"
|
||||
),
|
||||
"new": (
|
||||
" pids = await asyncio.to_thread(_kb.reap_worker_zombies)\n"
|
||||
" if pids:\n"
|
||||
" logger.info(\n"
|
||||
' "kanban dispatcher: reaped %d zombie worker(s), pids=%s",\n'
|
||||
" len(pids),\n"
|
||||
" pids,\n"
|
||||
" )\n"
|
||||
" # MC2-Patch (20.07.2026): Orphan-Guard. Beende lebende\n"
|
||||
" # Worker, deren Task nicht mehr 'running' ist (extern\n"
|
||||
" # blockiert/reset) — sonst unterlaufen sie max_in_progress\n"
|
||||
" # und verbrennen Rechenzeit.\n"
|
||||
" try:\n"
|
||||
" from hermes_cli import mc2_kanban_reaper as _mc2reaper\n"
|
||||
" _orph = await asyncio.to_thread(\n"
|
||||
" _mc2reaper.reap_orphaned_workers, _kb.connect_closing\n"
|
||||
" )\n"
|
||||
" if _orph:\n"
|
||||
" logger.info(\n"
|
||||
' "kanban dispatcher: reaped %d orphaned worker(s): %s",\n'
|
||||
" len(_orph), _orph,\n"
|
||||
" )\n"
|
||||
" except Exception:\n"
|
||||
' logger.exception("kanban dispatcher: orphan reaper failed")\n'
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "0004a-context-inherit-compute",
|
||||
"file": "hermes_cli/kanban_decompose.py",
|
||||
"marker": '_parent_ctx = ""',
|
||||
"old": (
|
||||
" children: list[dict] = []\n"
|
||||
" for idx, entry in enumerate(raw_tasks):\n"
|
||||
),
|
||||
"new": (
|
||||
" children: list[dict] = []\n"
|
||||
" # MC2-Patch (20.07.2026): Projekt-/Repo-Kontext vererben. Der Parent-Body\n"
|
||||
' # traegt oft harte Randbedingungen (Ziel-Repo, Tabus, "eigenes Projekt"),\n'
|
||||
" # die bei der Zerlegung sonst verloren gehen (Worker klonte das falsche\n"
|
||||
" # Repo, Fan-out-Vorfall 20.07.). Jeder Kind-Body bekommt den voran.\n"
|
||||
' _parent_ctx = ""\n'
|
||||
' if getattr(task, "body", None) and task.body.strip():\n'
|
||||
" _parent_ctx = (\n"
|
||||
' "## PROJEKT-KONTEXT (von der uebergeordneten Aufgabe — gilt fuer dich)\\n"\n'
|
||||
" + task.body.strip()[:1500]\n"
|
||||
' + "\\n\\n## DEIN TEILAUFTRAG\\n"\n'
|
||||
" )\n"
|
||||
" for idx, entry in enumerate(raw_tasks):\n"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "0004b-context-inherit-apply",
|
||||
"file": "hermes_cli/kanban_decompose.py",
|
||||
"marker": "_parent_ctx + body",
|
||||
"old": (
|
||||
' body = entry.get("body")\n'
|
||||
" if not isinstance(body, str):\n"
|
||||
' body = ""\n'
|
||||
),
|
||||
"new": (
|
||||
' body = entry.get("body")\n'
|
||||
" if not isinstance(body, str):\n"
|
||||
' body = ""\n'
|
||||
" body = _parent_ctx + body # MC2 (20.07.): Projekt-Kontext vererben\n"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "0005-create-chain-guard",
|
||||
"file": "tools/kanban_tools.py",
|
||||
"marker": "_MC2_RECENT_ROOTS",
|
||||
"old": (
|
||||
" new_task = kb.get_task(conn, new_tid)\n"
|
||||
" subscribed = _maybe_auto_subscribe(conn, new_tid)\n"
|
||||
),
|
||||
"new": (
|
||||
" # MC2-Patch (20.07.2026): Etappen-Auto-Kette. Legt dieselbe\n"
|
||||
" # Session mehrere ROOT-triage-Karten (ohne parents) im selben\n"
|
||||
" # 120s-Fenster an, verkette sie — so greift das Startreife-Gate\n"
|
||||
" # (Karte 2 wartet auf Karte 1) statt alle sofort zu zerlegen\n"
|
||||
" # (Fan-out 20.07.). Unter max_in_progress:1 ist eine Fehl-\n"
|
||||
" # Verkettung harmlos (laeuft eh seriell).\n"
|
||||
" try:\n"
|
||||
" if triage and not parents and session_id:\n"
|
||||
" import time as _mc2t\n"
|
||||
' _roots = globals().setdefault("_MC2_RECENT_ROOTS", {})\n'
|
||||
" _prev = _roots.get(session_id)\n"
|
||||
" _now = _mc2t.monotonic()\n"
|
||||
" if _prev and (_now - _prev[1]) <= 120 and _prev[0] != new_tid:\n"
|
||||
" try:\n"
|
||||
" kb.link_tasks(conn, _prev[0], new_tid)\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" _roots[session_id] = (new_tid, _now)\n"
|
||||
" if len(_roots) > 64:\n"
|
||||
" _roots.clear()\n"
|
||||
" except Exception:\n"
|
||||
" pass\n"
|
||||
" new_task = kb.get_task(conn, new_tid)\n"
|
||||
" subscribed = _maybe_auto_subscribe(conn, new_tid)\n"
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not HA.is_dir():
|
||||
print(f"hermes-patches: Hermes-Agent-Dir fehlt ({HA}) — nichts zu tun.")
|
||||
return 0
|
||||
applied, skipped, failed = [], [], []
|
||||
|
||||
# --- Zusatzmodule kopieren (idempotent: nur wenn Inhalt abweicht) ---
|
||||
for src_rel, dst_rel in COPIES:
|
||||
src, dst = HERE / src_rel, HA / dst_rel
|
||||
if not src.is_file():
|
||||
failed.append((src_rel, f"Quelle fehlt: {src}"))
|
||||
continue
|
||||
try:
|
||||
same = dst.is_file() and dst.read_bytes() == src.read_bytes()
|
||||
if same:
|
||||
skipped.append(f"copy:{dst_rel}")
|
||||
continue
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
if dst.suffix == ".py":
|
||||
py_compile.compile(str(dst), doraise=True)
|
||||
applied.append(f"copy:{dst_rel}")
|
||||
except Exception as exc:
|
||||
failed.append((src_rel, f"Kopie/Compile fehlgeschlagen: {exc}"))
|
||||
for p in PATCHES:
|
||||
f = HA / p["file"]
|
||||
if not f.is_file():
|
||||
failed.append((p["name"], f"Datei fehlt: {f}"))
|
||||
continue
|
||||
src = f.read_text(encoding="utf-8")
|
||||
if p["marker"] in src:
|
||||
skipped.append(p["name"])
|
||||
continue
|
||||
n = src.count(p["old"])
|
||||
if n != 1:
|
||||
failed.append((p["name"], (f"erwartete 1 Vorkommen von old, fand {n} "
|
||||
"(Update hat den Kontext geaendert?)")))
|
||||
continue
|
||||
f.write_text(src.replace(p["old"], p["new"]), encoding="utf-8")
|
||||
try:
|
||||
py_compile.compile(str(f), doraise=True)
|
||||
except Exception as exc:
|
||||
failed.append((p["name"], f"py_compile fehlgeschlagen: {exc}"))
|
||||
continue
|
||||
applied.append(p["name"])
|
||||
|
||||
print(f"hermes-patches: applied={applied} skipped={skipped} failed={[n for n, _ in failed]}")
|
||||
for name, why in failed:
|
||||
print(f" FEHLER {name}: {why}", file=sys.stderr)
|
||||
if failed:
|
||||
return 1 # Fehler → Deploy/Postcheck rot
|
||||
if applied:
|
||||
return 10 # etwas geaendert → Aufrufer sollte hermes-gateway neu starten
|
||||
return 0 # nichts zu tun (alles schon drin)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,84 +0,0 @@
|
||||
"""MC2-Zusatzmodul (20.07.2026) — Orphan-Reaper für Kanban-Worker.
|
||||
|
||||
Wird vom Patch-Träger (``deploy/hermes-patches/apply.py``) nach
|
||||
``~/.hermes/hermes-agent/hermes_cli/`` kopiert und vom Gateway-Dispatcher
|
||||
(``gateway/kanban_watchers.py``, per Patch 0003) jeden Tick aufgerufen.
|
||||
|
||||
Problem: Wird eine ``running``-Karte von außen blockiert/zurückgesetzt (z. B.
|
||||
über das Auftragsbuch oder die No-Progress-Bremse), setzt ``block_task``
|
||||
``worker_pid = NULL`` — der Worker-PROZESS läuft aber weiter. Die DB zählt ihn
|
||||
nicht mehr, also unterläuft der Orphan ``kanban.max_in_progress`` (Doppel-Worker
|
||||
möglich) und verbrennt Rechenzeit. Dieser Reaper beendet lebende Worker, deren
|
||||
Task nicht mehr ``running`` ist.
|
||||
|
||||
Linux-only (/proc-Scan). Best-effort: jeder Fehler → leere Liste, nie werfen.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import sys
|
||||
|
||||
_TASK_RE = re.compile(rb"work kanban task (t_[0-9a-fA-F]+)")
|
||||
|
||||
|
||||
def reap_orphaned_workers(connect_closing) -> list[tuple[int, str]]:
|
||||
"""Beende lebende Kanban-Worker, deren Task nicht mehr ``running`` ist.
|
||||
|
||||
``connect_closing`` ist die gleichnamige Kontextmanager-Factory aus
|
||||
``kanban_db`` (durchgereicht, um Import-Zyklen zu vermeiden). Gibt die Liste
|
||||
der beendeten ``(pid, task_id)`` zurück.
|
||||
"""
|
||||
if sys.platform != "linux":
|
||||
return []
|
||||
candidates: dict[int, str] = {}
|
||||
try:
|
||||
entries = os.listdir("/proc")
|
||||
except OSError:
|
||||
return []
|
||||
for entry in entries:
|
||||
if not entry.isdigit():
|
||||
continue
|
||||
try:
|
||||
with open(f"/proc/{entry}/cmdline", "rb") as fh:
|
||||
cmd = fh.read().replace(b"\x00", b" ")
|
||||
except OSError:
|
||||
continue
|
||||
m = _TASK_RE.search(cmd)
|
||||
if m:
|
||||
candidates[int(entry)] = m.group(1).decode()
|
||||
if not candidates:
|
||||
return []
|
||||
|
||||
task_ids = list(set(candidates.values()))
|
||||
placeholders = ",".join("?" * len(task_ids))
|
||||
running: set[str] = set()
|
||||
try:
|
||||
with connect_closing() as conn:
|
||||
running = {
|
||||
row[0]
|
||||
for row in conn.execute(
|
||||
f"SELECT id FROM tasks WHERE id IN ({placeholders}) "
|
||||
"AND status = 'running'",
|
||||
task_ids,
|
||||
)
|
||||
}
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
killed: list[tuple[int, str]] = []
|
||||
for pid, task_id in candidates.items():
|
||||
if task_id in running:
|
||||
continue
|
||||
# Prozessgruppe zuerst (Worker starten start_new_session=True → eigene
|
||||
# Gruppe; killpg räumt selbst gestartete Kinder wie uvicorn gleich mit).
|
||||
try:
|
||||
os.killpg(os.getpgid(pid), signal.SIGTERM)
|
||||
except OSError:
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except OSError:
|
||||
continue
|
||||
killed.append((pid, task_id))
|
||||
return killed
|
||||
@@ -1,223 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hermes-Release-Radar (woechentlich, Mo 05:15) — "mehr Hermes nativ, weniger Eigenkreationen":
|
||||
# haelt neue hermes-agent-Releases gegen die Eigenbau-Landkarte im Wissens-Vault und meldet
|
||||
# NUR echte Treffer auf Abloese-Kriterien/Wiedervorlage-Bedingungen. Muster wie
|
||||
# chef-gutachter-feed.sh: liegt in ~/.hermes/scripts/ (deploy.sh kopiert ihn), stdout wird
|
||||
# dem Cron-Agenten als Prompt eingespeist. Der ABGLEICH laeuft als Ein-Schuss-Completion
|
||||
# direkt gegen llama-swap (:8080, fremdblick-Architektur): Analyst gpt-oss-120b (um 05:15
|
||||
# vom Chef-Gutachter 04:30 oft noch geladen), Vertretung GLM-4.7-Flash. Danach gatet
|
||||
# fremdblick.sh (Prosa-Raster, Fremd-Vendor) jede Treffer-Behauptung gegen die
|
||||
# Original-Changelogs.
|
||||
# Grundsaetze (stehen auch in der Landkarte): nur getaggte Releases (kein main-Churn bei
|
||||
# ~660 PRs/Woche) · Behauptung nur mit woertlichem Changelog-Zitat · Treffer => Probe-
|
||||
# Vorschlag, NIE Direkt-Adoption · der Radar informiert, er updatet nicht (Entscheid 04.07.).
|
||||
# State: ~/.hermes/state/release-radar-last.txt (zuletzt gemeldeter Tag). Verlust harmlos —
|
||||
# ohne State dient die installierte Version als Basis.
|
||||
set -uo pipefail
|
||||
|
||||
MC="$HOME/mission-control-v2"
|
||||
LANDKARTE="$HOME/wissens-vault/eigenbau-landkarte.md"
|
||||
STATE_DIR="$HOME/.hermes/state"
|
||||
STATE="$STATE_DIR/release-radar-last.txt"
|
||||
API="${RADAR_RELEASES_URL:-https://api.github.com/repos/NousResearch/hermes-agent/releases?per_page=10}"
|
||||
ENDPOINT="${RADAR_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
|
||||
ANALYST="${RADAR_MODEL:-gpt-oss-120b}"
|
||||
VERTRETUNG="${RADAR_FALLBACK:-GLM-4.7-Flash}"
|
||||
FREMDBLICK="$HOME/.hermes/scripts/fremdblick.sh"
|
||||
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
# Briefkasten-Spiegel (still, wie die Morgenlage): Cockpit/Chronik lesen source=release-radar.
|
||||
briefkasten() { # $1=Betreff $2=Text
|
||||
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(python3 - "$1" "$2" <<'PY'
|
||||
import json, sys
|
||||
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
|
||||
"source": "release-radar", "priority": "silent"}))
|
||||
PY
|
||||
)" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# ---------- Versionierter Auftrag fuer den Boten-Agenten ----------
|
||||
cat "$MC/deploy/release-radar-prompt.md" 2>/dev/null || cat <<'EOF'
|
||||
(Prompt-Datei fehlt noch — Kurzform:) Du bist der Bote des woechentlichen Release-Radars.
|
||||
Gib den Befund unten in Lucys Stimme wieder: bei "KEIN NEUES RELEASE" oder "TREFFER: 0"
|
||||
genau EIN kurzer Satz; bei Treffern maximal 5 Saetze. Nichts installieren oder aendern.
|
||||
EOF
|
||||
echo
|
||||
|
||||
INSTALLED_LINE="$(bash -lc 'hermes --version' 2>/dev/null | head -1)"
|
||||
INSTALLED_TAG="$(printf '%s' "$INSTALLED_LINE" | grep -oE '\([0-9][0-9.]*\)' | head -1 | tr -d '()')"
|
||||
[ -n "$INSTALLED_TAG" ] && INSTALLED_TAG="v$INSTALLED_TAG"
|
||||
LAST_SEEN="$(cat "$STATE" 2>/dev/null | tr -d '[:space:]' || true)"
|
||||
BASELINE="${LAST_SEEN:-$INSTALLED_TAG}"
|
||||
|
||||
RELEASES_JSON="$(curl -sf --max-time 30 "$API" 2>/dev/null || true)"
|
||||
if [ -z "$RELEASES_JSON" ]; then
|
||||
echo "## RADAR-BEFUND: AUSGEFALLEN"
|
||||
echo "GitHub-Releases-API nicht erreichbar. Dem Commander EINEN ehrlichen Satz melden; naechste Woche neuer Versuch."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Neue getaggte Releases seit BASELINE (Drafts/Prereleases raus, max. 6, Bodies gekuerzt).
|
||||
# Baseline unbekannt (aelter als die letzten 10 oder Erstlauf ohne Version) -> juengste 6.
|
||||
# JSON via Temp-Datei, NICHT via Pipe: das Heredoc belegt stdin schon fuer den Python-Code
|
||||
# selbst (Pipe+Heredoc gleichzeitig -> json.load(stdin) liest ins Leere; Probelauf 10.07.).
|
||||
TMPJSON="$(mktemp /tmp/release-radar.XXXXXX.json)"
|
||||
printf '%s' "$RELEASES_JSON" > "$TMPJSON"
|
||||
PARSED="$(python3 - "$BASELINE" "$TMPJSON" <<'PY'
|
||||
import json, sys
|
||||
baseline = sys.argv[1]
|
||||
try:
|
||||
data = json.load(open(sys.argv[2]))
|
||||
# Rate-Limit/Fehler antwortet als Dict statt Liste -> wie "keine Daten" behandeln.
|
||||
rels = [r for r in data if not r.get("draft") and not r.get("prerelease")] if isinstance(data, list) else []
|
||||
except Exception:
|
||||
rels = []
|
||||
rels.sort(key=lambda r: r.get("published_at") or "", reverse=True)
|
||||
if not rels:
|
||||
# API hat geantwortet, aber unbrauchbar (Rate-Limit-Dict, kaputtes JSON, leere Liste):
|
||||
# ehrlich als "blind" markieren statt faelschlich "kein neues Release" zu melden.
|
||||
print(baseline)
|
||||
print(-1)
|
||||
raise SystemExit
|
||||
base = next((r.get("published_at") for r in rels if r.get("tag_name") == baseline), None)
|
||||
new = rels[:6] if base is None else [r for r in rels if (r.get("published_at") or "") > base][:6]
|
||||
print((new or rels)[0].get("tag_name") or baseline)
|
||||
print(len(new))
|
||||
for r in new:
|
||||
body = (r.get("body") or "").strip()
|
||||
if len(body) > 3500:
|
||||
body = body[:3500] + "\n[... gekuerzt ...]"
|
||||
print(f"\n===== RELEASE {r.get('tag_name')} — {(r.get('name') or '').strip()} ({(r.get('published_at') or '')[:10]}) =====")
|
||||
print(body)
|
||||
PY
|
||||
)"
|
||||
rm -f "$TMPJSON"
|
||||
NEWEST="$(printf '%s\n' "$PARSED" | sed -n 1p)"
|
||||
COUNT="$(printf '%s\n' "$PARSED" | sed -n 2p)"
|
||||
NOTES="$(printf '%s\n' "$PARSED" | tail -n +3)"
|
||||
|
||||
if [ "${COUNT:-0}" = "-1" ]; then
|
||||
echo "## RADAR-BEFUND: AUSGEFALLEN"
|
||||
echo "GitHub-API hat geantwortet, aber unbrauchbar (Rate-Limit oder kaputtes JSON). Dem Commander EINEN ehrlichen Satz melden; State bleibt, naechste Woche neuer Versuch."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "## RADAR-BEFUND (erhoben am $(date '+%d.%m.%Y %H:%M'))"
|
||||
echo "Box installiert: ${INSTALLED_LINE:-unbekannt}"
|
||||
echo "Zuletzt gemeldet: ${BASELINE:-unbekannt} · neuestes Release: $NEWEST · neue Releases: ${COUNT:-0}"
|
||||
echo "(Updates bleiben manuell/bestaetigt — der Radar informiert nur.)"
|
||||
echo
|
||||
|
||||
if [ "${COUNT:-0}" = "0" ]; then
|
||||
echo "KEIN NEUES RELEASE seit $BASELINE."
|
||||
[ -n "$NEWEST" ] && printf '%s\n' "$NEWEST" > "$STATE"
|
||||
briefkasten "Release-Radar" "Kein neues hermes-agent-Release seit $BASELINE (Box: ${INSTALLED_TAG:-?})."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -s "$LANDKARTE" ]; then
|
||||
echo "### ABGLEICH NICHT MOEGLICH"
|
||||
echo "$COUNT neue Releases, aber die Eigenbau-Landkarte fehlt ($LANDKARTE)."
|
||||
echo "Dem Commander melden: Landkarte im Wissens-Vault wiederherstellen. State bleibt unveraendert (naechste Woche neuer Versuch)."
|
||||
briefkasten "Release-Radar" "$COUNT neue hermes-agent-Releases, aber die Eigenbau-Landkarte fehlt — Abgleich uebersprungen."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
RASTER='Du bist der RELEASE-RADAR einer lokalen KI-Box. Du bekommst (1) die EIGENBAU-LANDKARTE (selbstgebaute Bausteine mit Abloese-Kriterien und Wiedervorlage-Bedingungen) und (2) CHANGELOGS neuer hermes-agent-Releases. Pruefe AUSSCHLIESSLICH: Trifft eine Changelog-Zeile ein Abloese-Kriterium oder eine Wiedervorlage-Bedingung der Landkarte? REGELN: Jede Treffer-Behauptung MUSS die Changelog-Zeile WOERTLICH zitieren. Marketing-Prosa ohne konkretes Feature ist KEIN Treffer. Vage Aehnlichkeit ist KEIN Treffer. Eintraege unter "Bewusst NICHT adoptiert" nur melden, wenn exakt die Wiedervorlage-Bedingung erfuellt ist. Erfinde nichts. Antworte auf DEUTSCH, exakt in dieser Struktur: erste Zeile "TREFFER: <Anzahl>". Danach je Treffer ein Block: "EIGENBAU: <Name aus der Landkarte>" / "RELEASE: <Tag>" / "ZITAT: <woertliche Changelog-Zeile>" / "WARUM: <ein Satz>" / "PROBE: <ein Satz, wie man es risikofrei testet>". Bei TREFFER: 0 stattdessen EIN Satz, was die Releases fuer uns Unwichtiges bringen.'
|
||||
|
||||
EVID="EIGENBAU-LANDKARTE:
|
||||
$(cat "$LANDKARTE")
|
||||
|
||||
CHANGELOGS DER NEUEN RELEASES:
|
||||
$NOTES"
|
||||
|
||||
# Ein-Schuss-Analyst: $1=Modell $2=curl-Timeout $3=max_tokens (Muster chef-gutachter-feed.sh;
|
||||
# max_tokens grosszuegig, weil Reasoning-Modelle Budget im reasoning_content verbrauchen).
|
||||
judge() {
|
||||
local req
|
||||
req="$(jq -n --arg m "$1" --arg sys "$RASTER" --arg usr "$EVID" --argjson mt "$3" \
|
||||
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
|
||||
max_tokens:$mt, temperature:0.2}')"
|
||||
curl -s -m "$2" "$ENDPOINT" -H 'Content-Type: application/json' --data-binary "$req" \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
msg = d["choices"][0]["message"]
|
||||
print((msg.get("content") or msg.get("reasoning_content") or "").strip())
|
||||
except Exception:
|
||||
pass'
|
||||
}
|
||||
|
||||
RICHTER="$ANALYST"
|
||||
ANALYSE="$(judge "$ANALYST" 420 2600)"
|
||||
if [ -z "${ANALYSE// /}" ]; then
|
||||
RICHTER="$VERTRETUNG (Vertretung — $ANALYST hat nicht geantwortet)"
|
||||
ANALYSE="$(judge "$VERTRETUNG" 240 1800)"
|
||||
fi
|
||||
|
||||
if [ -z "${ANALYSE// /}" ]; then
|
||||
echo "### ABGLEICH AUSGEFALLEN"
|
||||
echo "Beide Analysten ($ANALYST, $VERTRETUNG) haben nicht geantwortet. State bleibt unveraendert — naechste Woche neuer Versuch. Dem Commander EINEN ehrlichen Satz melden."
|
||||
briefkasten "Release-Radar" "$COUNT neue hermes-agent-Releases, aber der Abgleich fiel aus (kein Analyst erreichbar)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "### Neue Releases:"
|
||||
printf '%s\n' "$NOTES" | grep '^===== RELEASE' | sed 's/^===== //; s/ =====$//'
|
||||
echo
|
||||
echo "### ABGLEICH gegen die Eigenbau-Landkarte (Analyst: $RICHTER)"
|
||||
echo "$ANALYSE"
|
||||
echo
|
||||
|
||||
# Fremdblick-Gate nur bei Treffern (Zitate MIT Quellmaterial gegenpruefbar machen).
|
||||
TREFFER="$(printf '%s' "$ANALYSE" | grep -oE 'TREFFER: *[0-9]+' | head -1 | grep -oE '[0-9]+' || true)"
|
||||
GEGEN=""
|
||||
if [ "${TREFFER:-1}" != "0" ] && [ -x "$FREMDBLICK" ]; then
|
||||
# Grosses Budget: GLM (Reasoning-Modell) denkt erst ueber das ganze Quellmaterial nach,
|
||||
# bevor das Urteil in content landet — 2200 (fremdblick-Default) war im Probelauf 10.07.
|
||||
# zu knapp (ABGESCHNITTEN, kein Urteil).
|
||||
GEGEN="$(printf '%s\n\nQUELLMATERIAL (Original-Changelogs zum Gegenpruefen der Zitate):\n%s' "$ANALYSE" "$NOTES" \
|
||||
| FREMDBLICK_MAXTOK="${RADAR_FREMDBLICK_MAXTOK:-4500}" "$FREMDBLICK")"
|
||||
echo "$GEGEN"
|
||||
echo
|
||||
fi
|
||||
|
||||
# Treffer fuettern die Ideen-Queue ("Lucy kennt sich", 12.07.2026): aus einem echten
|
||||
# Radar-Treffer wird EINE rohe Idee (triage) — Specifier + Commander entscheiden, ob eine
|
||||
# Probe gebaut wird. Idempotent je Release-Tag; der Radar selbst bleibt reiner Melder.
|
||||
if [ -n "${TREFFER:-}" ] && [ "$TREFFER" != "0" ] && [ -x "$HOME/.local/bin/hermes" ]; then
|
||||
IDEE_ID="$("$HOME/.local/bin/hermes" kanban create \
|
||||
"Hermes ${NEWEST}: Release-Radar-Treffer pruefen (Probe bauen?)" \
|
||||
--body "Automatisch vom Release-Radar am $(date '+%d.%m.%Y'). ABGLEICH (Analyst: ${RICHTER}):
|
||||
${ANALYSE}
|
||||
|
||||
FREMDBLICK:
|
||||
${GEGEN:-(keiner)}
|
||||
|
||||
Rohe Idee: nur pruefen bzw. eine risikofreie Probe vorschlagen — Updates bleiben manuell (Entscheid 04.07.)." \
|
||||
--triage --created-by release-radar --idempotency-key "release-radar-${NEWEST}" --json 2>/dev/null \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
t = sys.stdin.read(); i = t.find("{")
|
||||
try:
|
||||
print(json.loads(t[i:]).get("id", "") if i >= 0 else "")
|
||||
except Exception:
|
||||
print("")
|
||||
' 2>/dev/null || true)"
|
||||
if [ -n "${IDEE_ID// /}" ]; then
|
||||
echo "### IN DIE IDEEN-QUEUE GELEGT: ${IDEE_ID} — Probe-Pruefung fuer ${NEWEST} (der Specifier uebernimmt)."
|
||||
echo
|
||||
fi
|
||||
fi
|
||||
|
||||
printf '%s\n' "$NEWEST" > "$STATE"
|
||||
briefkasten "Release-Radar" "Neue hermes-agent-Releases bis $NEWEST (Box: ${INSTALLED_TAG:-?}).
|
||||
|
||||
ABGLEICH (Analyst: $RICHTER):
|
||||
$ANALYSE
|
||||
|
||||
${GEGEN:-(Kein Fremdblick noetig: TREFFER 0)}"
|
||||
@@ -1,298 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Idle-Radar (naechtlich 03:45 — S4 "Zuendung", 12.07.2026): die Box gibt sich selbst Arbeit.
|
||||
# Destilliert aus JOURNAL-Fehlermustern (24 h) und der juengsten TRAUM-Notiz 0-2 KLEINE,
|
||||
# belegte Wartungs-Kandidaten und legt sie als ROHE IDEEN (triage) ins native Hermes-Kanban.
|
||||
# Danach laeuft der bewiesene S2-Kreislauf von allein: Specifier arbeitet aus -> Werkstatt
|
||||
# baut einen Vorschlags-Branch -> Karte im Auftragsbuch -> der KLICK des Commanders bleibt
|
||||
# das einzige Gate. Muster wie hermes-release-radar-feed.sh: liegt in ~/.hermes/scripts/
|
||||
# (deploy.sh kopiert ihn), stdout wird dem Cron-Agenten als Prompt eingespeist; der
|
||||
# eigentliche ABGLEICH laeuft als Ein-Schuss-Completion gegen llama-swap (:8080).
|
||||
# Grundsaetze:
|
||||
# - Kandidat NUR mit woertlichem Beleg-Zitat; das Skript prueft das Zitat MECHANISCH
|
||||
# gegen das Material (reproduziert-oder-abgelehnt, Verdikt 04.07.) — haerter als
|
||||
# ein zweites LLM-Urteil und gratis.
|
||||
# - Max. 2 neue Ideen je Nacht + STAU-BREMSE: liegt schon genug Arbeit (volle Queue
|
||||
# oder volles Auftragsbuch), legt der Radar NICHTS nach — die Box soll Arbeit
|
||||
# abarbeiten, nicht anhaeufen.
|
||||
# - Dedup dreifach: State-Datei (schon gemeldet) + Queue/Karten-Liste im Analysten-
|
||||
# Kontext + --idempotency-key beim Anlegen.
|
||||
# - Der Radar LEGT NUR IDEEN AN — er baut nichts, merged nichts, konfiguriert nichts.
|
||||
# State: ~/.hermes/state/idle-radar-gemeldet.txt (ein KEY je bereits gemeldetem Kandidaten).
|
||||
set -uo pipefail
|
||||
|
||||
MC="$HOME/mission-control-v2"
|
||||
VAULT="$HOME/wissens-vault"
|
||||
STATE_DIR="$HOME/.hermes/state"
|
||||
STATE="$STATE_DIR/idle-radar-gemeldet.txt"
|
||||
ENDPOINT="${IDLE_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
|
||||
ANALYST="${IDLE_MODEL:-gpt-oss-120b}"
|
||||
VERTRETUNG="${IDLE_FALLBACK:-GLM-4.7-Flash}"
|
||||
HERMES="$HOME/.local/bin/hermes"
|
||||
MAX_NEU=2 # harte Obergrenze neuer Ideen je Nacht (wie Bagatell-Pfad)
|
||||
STAU_QUEUE=4 # ab so vielen offenen Queue-Aufgaben: Bremse
|
||||
STAU_KARTEN=3 # ab so vielen offenen Auftragsbuch-Karten: Bremse
|
||||
|
||||
mkdir -p "$STATE_DIR"; touch "$STATE"
|
||||
|
||||
# Briefkasten-Spiegel (still, wie Morgenlage/Release-Radar): Chronik liest source=idle-radar.
|
||||
briefkasten() { # $1=Betreff $2=Text
|
||||
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(python3 - "$1" "$2" <<'PY'
|
||||
import json, sys
|
||||
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
|
||||
"source": "idle-radar", "priority": "silent"}))
|
||||
PY
|
||||
)" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# ---------- Versionierter Auftrag fuer den Boten-Agenten ----------
|
||||
cat "$MC/deploy/idle-radar-prompt.md" 2>/dev/null || cat <<'EOF'
|
||||
(Prompt-Datei fehlt noch — Kurzform:) Du bist der Bote des naechtlichen Idle-Radars.
|
||||
Berichte den Befund unten in Lucys Stimme in hoechstens 3 Saetzen: wie viele neue
|
||||
Queue-Ideen angelegt wurden (Titel nennen) oder warum keine. Nichts selbst umsetzen,
|
||||
keine Kanban-Aktionen — das Skript hat alles Noetige bereits getan.
|
||||
EOF
|
||||
echo
|
||||
|
||||
# ---------- Stau-Bremse (vor dem teuren Analysten) ----------
|
||||
OFFEN_QUEUE="$("$HERMES" kanban list --json 2>/dev/null | python3 -c '
|
||||
import json, sys
|
||||
t = sys.stdin.read(); i = t.find("[")
|
||||
try:
|
||||
d = json.loads(t[i:]) if i >= 0 else []
|
||||
except Exception:
|
||||
d = []
|
||||
print(sum(1 for x in d if isinstance(x, dict) and x.get("status") not in ("done", "archived")))
|
||||
' 2>/dev/null || echo 0)"
|
||||
OFFEN_KARTEN="$(curl -sf -m 10 http://127.0.0.1:9001/api/auftragsbuch 2>/dev/null | python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
print(int(json.load(sys.stdin).get("open_count", 0)))
|
||||
except Exception:
|
||||
print(0)
|
||||
' 2>/dev/null || echo 0)"
|
||||
|
||||
if [ "${OFFEN_QUEUE:-0}" -ge "$STAU_QUEUE" ] || [ "${OFFEN_KARTEN:-0}" -ge "$STAU_KARTEN" ]; then
|
||||
echo "## IDLE-RADAR-BEFUND: STAU-BREMSE (erhoben am $(date '+%d.%m.%Y %H:%M'))"
|
||||
echo "Offene Queue-Aufgaben: ${OFFEN_QUEUE} (Bremse ab ${STAU_QUEUE}) · offene Karten: ${OFFEN_KARTEN} (Bremse ab ${STAU_KARTEN})."
|
||||
echo "Es liegt genug Arbeit — heute Nacht keine neuen Ideen. Dem Commander EINEN kurzen Satz melden."
|
||||
briefkasten "Idle-Radar" "Stau-Bremse: ${OFFEN_QUEUE} offene Queue-Aufgaben, ${OFFEN_KARTEN} offene Karten — keine neuen Ideen angelegt."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------- Material sammeln (erst sammeln, dann kuerzen — SIGPIPE-Falle) ----------
|
||||
TRAUM_DATEI="$(ls -1t "$VAULT/traeume/"*.md 2>/dev/null | head -1 || true)"
|
||||
EVID="$(
|
||||
echo "### JOURNAL-FEHLERMUSTER (user-Dienste, letzte 24 h; Anzahl · Muster, lange Zahlen/IDs normalisiert):"
|
||||
journalctl --user --since '24 hours ago' -p warning --no-pager 2>/dev/null \
|
||||
| sed -E 's/^[A-Z][a-z]{2} [0-9 ]{2} [0-9:]{8} [^ ]+ //; s/\[[0-9]+\]//; s/[0-9]{3,}/N/g; s/t_[0-9a-f]+/t_X/g' \
|
||||
| sort | uniq -c | sort -rn | head -18
|
||||
echo
|
||||
echo "### JUENGSTE TRAUM-NOTIZ ($(basename "${TRAUM_DATEI:-keine}") — Funde des naechtlichen Lern-Crons):"
|
||||
if [ -n "$TRAUM_DATEI" ]; then head -70 "$TRAUM_DATEI"; else echo "(keine Traum-Notiz gefunden)"; fi
|
||||
echo
|
||||
echo "### OFFENE SKILL-KANDIDATEN im Vault (nur Titelzeilen):"
|
||||
ls -1 "$VAULT/skill-kandidaten/"*.md 2>/dev/null | head -8 | while read -r f; do
|
||||
echo "- $(basename "$f"): $(head -1 "$f" 2>/dev/null)"
|
||||
done
|
||||
echo
|
||||
echo "### SELBST-STECKBRIEF (auto-generiert von selbst-inventur.sh — das EXISTIERT schon; NICHTS davon neu vorschlagen):"
|
||||
head -80 "$HOME/.hermes/state/selbst-steckbrief.md" 2>/dev/null || echo "(noch keine Selbst-Inventur gelaufen)"
|
||||
echo
|
||||
echo "### SEIT GESTERN AN MIR GEAENDERT (Selbst-Inventur-Diff — gute Kandidaten-Quelle, wenn eine kleine Massnahme erkennbar ist):"
|
||||
head -40 "$HOME/.hermes/state/selbst-steckbrief.diff" 2>/dev/null || echo "(keine Aenderung erkannt)"
|
||||
echo
|
||||
echo "### VOM COMMANDER ABGELEHNT (mit Grund — sinngemaess NIE wieder vorschlagen):"
|
||||
tail -12 "${MC2_ABLEHNUNGEN:-/srv/models/mc2-ablehnungen.jsonl}" 2>/dev/null | python3 -c '
|
||||
import json, sys
|
||||
n = 0
|
||||
for line in sys.stdin:
|
||||
try:
|
||||
d = json.loads(line)
|
||||
except Exception:
|
||||
continue
|
||||
n += 1
|
||||
print("- [{}] {} — Grund: {}".format(d.get("branch", "?"), (d.get("subject") or "")[:80],
|
||||
d.get("grund") or "(keiner angegeben)"))
|
||||
if not n:
|
||||
print("(keine)")
|
||||
' 2>/dev/null || echo "(keine)"
|
||||
echo
|
||||
echo "### SCHON IN DER IDEEN-QUEUE (inkl. archiviert = vom Commander verworfen; NICHT erneut vorschlagen):"
|
||||
"$HERMES" kanban list --archived --json 2>/dev/null | python3 -c '
|
||||
import json, sys
|
||||
t = sys.stdin.read(); i = t.find("[")
|
||||
try:
|
||||
d = json.loads(t[i:]) if i >= 0 else []
|
||||
except Exception:
|
||||
d = []
|
||||
for x in d[:40]:
|
||||
if isinstance(x, dict):
|
||||
print("- [{}] {}".format(x.get("status", "?"), (x.get("title") or "")[:110]))
|
||||
' 2>/dev/null || echo "(Queue nicht lesbar)"
|
||||
echo
|
||||
echo "### OFFENE KARTEN IM AUFTRAGSBUCH (warten auf Klick; NICHT erneut vorschlagen):"
|
||||
curl -sf -m 10 http://127.0.0.1:9001/api/auftragsbuch 2>/dev/null | python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
items = json.load(sys.stdin).get("items") or []
|
||||
except Exception:
|
||||
items = []
|
||||
if not items:
|
||||
print("(keine)")
|
||||
for it in items[:15]:
|
||||
print("- {}:{}".format(it.get("repo", "mc2"), it.get("branch", "?")))
|
||||
' 2>/dev/null || echo "(Auftragsbuch nicht lesbar)"
|
||||
echo
|
||||
echo "### VOM IDLE-RADAR BEREITS GEMELDET (KEYs; NICHT erneut vorschlagen):"
|
||||
tail -40 "$STATE" 2>/dev/null || true
|
||||
)"
|
||||
|
||||
RASTER='Du bist der IDLE-RADAR einer lokalen KI-Box (Hermes-Agent, MC2, Lucy). Du bekommst naechtliches Material: Journal-Fehlermuster, die juengste Traum-Notiz, offene Skill-Kandidaten sowie Listen dessen, was schon in Arbeit ist. Deine Aufgabe: destilliere daraus 0 bis 2 KLEINE, konkrete Wartungs-Kandidaten, die die Box-Werkstatt selbst umsetzen kann (Patch in 1-2 Dateien, Skript- oder Doku-Aenderung im MC2- oder Lucy-Repo). REGELN: Jeder Kandidat MUSS eine Zeile aus dem Material WOERTLICH zitieren (BELEG) — das Zitat wird mechanisch gegengeprueft, erfundene Zitate fliegen raus. Nichts vorschlagen, was sinngemaess schon in der IDEEN-QUEUE, den OFFENEN KARTEN oder der BEREITS-GEMELDET-Liste steht. Der SELBST-STECKBRIEF sagt dir, was die Box schon HAT und KANN — schlage NIE etwas vor, das dort schon steht (der Fehler vom 12.07.: ein Gateway-Neustart wurde vorgeschlagen, der laengst konfiguriert war). Was der Commander mit Grund ABGELEHNT hat, ist entschieden — nicht neu verpacken. Aenderungen aus dem Inventur-Diff sind gute Kandidaten-Quellen, aber nur mit konkreter kleiner Massnahme. TABU (niemals vorschlagen): Security (approvals/Tokens/ufw/sudoers/ssh), ~/.hermes/config.yaml, systemd-Units, Hermes-Quellcode, Updates/Upgrades, alles was den PC oder einen Menschen braucht. Ein wiederkehrendes Fehlermuster ist NUR dann ein Kandidat, wenn eine konkrete kleine Ursachen-Behebung erkennbar ist — "beobachten", "untersuchen" oder "Monitoring bauen" ist KEIN Auftrag. Qualitaet vor Quote: KANDIDATEN: 0 ist der Normalfall und voellig ok. Antworte auf DEUTSCH, exakt in dieser Struktur: erste Zeile "KANDIDATEN: <0|1|2>". Danach je Kandidat ein Block aus genau vier Zeilen: "TITEL: <max 90 Zeichen, Imperativ, deutsch>" / "KEY: <kurzer-kebab-slug>" / "BELEG: <woertliches Zitat aus dem Material, EINE Zeile>" / "WARUM: <ein Satz>". Bei 0 stattdessen EIN Satz, warum nichts ansteht.'
|
||||
|
||||
# Ein-Schuss-Analyst: $1=Modell $2=curl-Timeout $3=max_tokens (Muster chef-gutachter-feed.sh).
|
||||
judge() {
|
||||
local req
|
||||
req="$(jq -n --arg m "$1" --arg sys "$RASTER" --arg usr "$EVID" --argjson mt "$3" \
|
||||
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
|
||||
max_tokens:$mt, temperature:0.2}')"
|
||||
curl -s -m "$2" "$ENDPOINT" -H 'Content-Type: application/json' --data-binary "$req" \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
msg = d["choices"][0]["message"]
|
||||
print((msg.get("content") or msg.get("reasoning_content") or "").strip())
|
||||
except Exception:
|
||||
pass'
|
||||
}
|
||||
|
||||
RICHTER="$ANALYST"
|
||||
ANALYSE="$(judge "$ANALYST" 420 2600)"
|
||||
if [ -z "${ANALYSE// /}" ]; then
|
||||
RICHTER="$VERTRETUNG (Vertretung — $ANALYST hat nicht geantwortet)"
|
||||
ANALYSE="$(judge "$VERTRETUNG" 240 1800)"
|
||||
fi
|
||||
|
||||
echo "## IDLE-RADAR-BEFUND (erhoben am $(date '+%d.%m.%Y %H:%M'))"
|
||||
echo "Offene Queue-Aufgaben vorher: ${OFFEN_QUEUE} · offene Karten: ${OFFEN_KARTEN} · Deckel: ${MAX_NEU}/Nacht"
|
||||
echo
|
||||
|
||||
if [ -z "${ANALYSE// /}" ]; then
|
||||
echo "### ANALYSE AUSGEFALLEN"
|
||||
echo "Beide Analysten ($ANALYST, $VERTRETUNG) haben nicht geantwortet — dem Commander EINEN ehrlichen Satz melden; morgen neuer Versuch."
|
||||
briefkasten "Idle-Radar" "Analyse ausgefallen (kein Analyst erreichbar) — keine neuen Ideen."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "### ANALYSE (Analyst: $RICHTER)"
|
||||
echo "$ANALYSE"
|
||||
echo
|
||||
|
||||
# ---------- Ehrlichkeits-Gate + Dedup + Anlegen (deterministisch im Skript) ----------
|
||||
# Temp-Dateien statt Pipes: Heredoc + Pipe gleichzeitig frisst stdin (Release-Radar-Lehre 10.07.).
|
||||
TMPD="$(mktemp -d /tmp/idle-radar.XXXXXX)"
|
||||
printf '%s' "$ANALYSE" > "$TMPD/analyse.txt"
|
||||
printf '%s' "$EVID" > "$TMPD/material.txt"
|
||||
PLAN="$(python3 - "$TMPD/analyse.txt" "$TMPD/material.txt" "$STATE" "$MAX_NEU" <<'PY'
|
||||
import re, sys, unicodedata
|
||||
|
||||
ana = open(sys.argv[1], encoding="utf-8", errors="replace").read()
|
||||
evid = open(sys.argv[2], encoding="utf-8", errors="replace").read()
|
||||
try:
|
||||
state = {l.strip() for l in open(sys.argv[3], encoding="utf-8", errors="replace") if l.strip()}
|
||||
except Exception:
|
||||
state = set()
|
||||
max_neu = int(sys.argv[4])
|
||||
|
||||
def norm(s):
|
||||
s = unicodedata.normalize("NFKC", s)
|
||||
s = "".join(ch for ch in s if ch.isalnum() or ch.isspace())
|
||||
return " ".join(s.lower().split())
|
||||
|
||||
nev = norm(evid)
|
||||
|
||||
# Zeilenweise einsammeln (tolerant gegen Markdown-Deko und Leerzeilen zwischen Feldern).
|
||||
cands, cur = [], {}
|
||||
for line in ana.splitlines():
|
||||
line = line.strip().lstrip("*->#• ").strip()
|
||||
m = re.match(r"(TITEL|KEY|BELEG|WARUM)\s*:\s*(.+)$", line)
|
||||
if not m:
|
||||
continue
|
||||
k, v = m.group(1), m.group(2).strip()
|
||||
if k == "TITEL" and cur:
|
||||
cands.append(cur); cur = {}
|
||||
cur[k] = v
|
||||
if cur:
|
||||
cands.append(cur)
|
||||
|
||||
neu = 0
|
||||
SEP = "\x1f"
|
||||
for c in cands:
|
||||
titel = (c.get("TITEL") or "").strip()[:120]
|
||||
warum = (c.get("WARUM") or "").strip()[:300]
|
||||
beleg = (c.get("BELEG") or "").strip().strip('"„“»«>`').strip()
|
||||
key = re.sub(r"[^a-z0-9-]+", "-", (c.get("KEY") or "").strip().lower())[:48].strip("-")
|
||||
if not (titel and key and beleg):
|
||||
continue
|
||||
if key in state or ("idle-radar-" + key) in state:
|
||||
print("SKIP" + SEP + key + SEP + "schon frueher gemeldet (State)")
|
||||
continue
|
||||
if not norm(beleg) or norm(beleg) not in nev:
|
||||
print("SKIP" + SEP + key + SEP + "BELEG nicht woertlich im Material — verworfen (Ehrlichkeits-Gate)")
|
||||
continue
|
||||
if neu >= max_neu:
|
||||
print("SKIP" + SEP + key + SEP + "Deckel erreicht")
|
||||
continue
|
||||
neu += 1
|
||||
print("NEU" + SEP + key + SEP + titel + SEP + beleg + SEP + warum)
|
||||
PY
|
||||
)"
|
||||
|
||||
ANGELEGT=""; VERWORFEN=""
|
||||
while IFS=$'\x1f' read -r art key titel beleg warum; do
|
||||
[ -n "${art:-}" ] || continue
|
||||
if [ "$art" = "SKIP" ]; then
|
||||
VERWORFEN="${VERWORFEN}- ${key}: ${titel}"$'\n' # bei SKIP traegt das 3. Feld den Grund
|
||||
continue
|
||||
fi
|
||||
[ "$art" = "NEU" ] || continue
|
||||
BODY="Automatisch vom Idle-Radar erhoben am $(date '+%d.%m.%Y') (Quelle: Journal-Muster/Traum-Notiz der Nacht).
|
||||
|
||||
BELEG (woertlich aus dem Material): ${beleg}
|
||||
|
||||
WARUM: ${warum}
|
||||
|
||||
Rohe Idee — bitte pruefen, klein schneiden und nur wenn tragfaehig promoten; sonst archivieren."
|
||||
TASK_ID="$("$HERMES" kanban create "$titel" --body "$BODY" --triage \
|
||||
--created-by idle-radar --idempotency-key "idle-radar-${key}" --json 2>/dev/null \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
t = sys.stdin.read(); i = t.find("{")
|
||||
try:
|
||||
print(json.loads(t[i:]).get("id", "") if i >= 0 else "")
|
||||
except Exception:
|
||||
print("")
|
||||
' 2>/dev/null || true)"
|
||||
if [ -n "${TASK_ID// /}" ]; then
|
||||
echo "$key" >> "$STATE"
|
||||
ANGELEGT="${ANGELEGT}- ${titel} (${TASK_ID}, Key ${key})"$'\n'
|
||||
else
|
||||
VERWORFEN="${VERWORFEN}- ${key}: kanban create fehlgeschlagen"$'\n'
|
||||
fi
|
||||
done <<< "$PLAN"
|
||||
rm -rf "$TMPD"
|
||||
|
||||
echo "### ANGELEGT (rohe Ideen, triage — der Specifier uebernimmt):"
|
||||
echo "${ANGELEGT:-(keine)}"
|
||||
echo "### NICHT ANGELEGT (Gate/Dedup/Deckel):"
|
||||
echo "${VERWORFEN:-(keine)}"
|
||||
|
||||
N_NEU="$(printf '%s' "$ANGELEGT" | grep -c '^-' || true)"
|
||||
if [ "${N_NEU:-0}" -gt 0 ]; then
|
||||
briefkasten "Idle-Radar" "Idle-Radar hat ${N_NEU} neue Idee(n) in die Queue gelegt:
|
||||
${ANGELEGT}Der Specifier arbeitet sie aus; die Karten-Annahme bleibt beim Commander."
|
||||
else
|
||||
briefkasten "Idle-Radar" "Idle-Radar: keine neuen Ideen (Analyst: ${RICHTER})."
|
||||
fi
|
||||
@@ -1,26 +0,0 @@
|
||||
# Idle-Radar — Auftrag für den Boten-Agenten
|
||||
|
||||
Du bist der Bote des nächtlichen Idle-Radars. Unten steht ein automatisch erhobener
|
||||
BEFUND: aus Journal-Fehlermustern und der jüngsten Traum-Notiz destilliert ein
|
||||
Analysten-Modell kleine Wartungs-Kandidaten; das Skript hat die tragfähigen davon
|
||||
BEREITS als rohe Ideen (triage) in die Ideen-Queue gelegt — den Rest erledigt der
|
||||
bewiesene Kreislauf (Specifier → Werkstatt → Karte im Auftragsbuch → Klick des
|
||||
Commanders).
|
||||
|
||||
Deine Aufgabe — NUR berichten, in Lucys Stimme (Anrede „Commander", Alltagssprache,
|
||||
Fazit zuerst), in höchstens 3 Sätzen:
|
||||
|
||||
1. Bei „ANGELEGT: (keine)" oder „KANDIDATEN: 0": GENAU EIN kurzer Satz
|
||||
(z. B. „Idle-Radar: nichts Belegbares heute Nacht — Queue bleibt wie sie ist.").
|
||||
2. Bei neuen Ideen: nenne Anzahl und die Titel; sage dazu, dass die Box sie jetzt
|
||||
selbst ausarbeitet und die Annahme wie immer per Karten-Klick beim Commander liegt.
|
||||
3. Bei „STAU-BREMSE" oder „AUSGEFALLEN": ein ehrlicher Satz, warum der Radar heute
|
||||
Nacht nichts angelegt hat.
|
||||
|
||||
Harte Regeln:
|
||||
- NICHTS selbst umsetzen: keine Kanban-Aktionen, nichts bauen, nichts konfigurieren —
|
||||
das Anlegen hat das Skript schon deterministisch erledigt.
|
||||
- Erfinde keine Kandidaten über den Befund hinaus; im Ehrlichkeits-Gate verworfene
|
||||
Vorschläge (BELEG nicht im Material) nicht als Erfolg verkaufen.
|
||||
- Die stille Briefkasten-Karte hat das Skript bereits geschrieben — du lieferst nur
|
||||
die kurze Telegram-Meldung.
|
||||
@@ -1,192 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Karten-Gutachter (naechtlich 04:00 — "Lucy kennt sich", 12.07.2026): stempelt jede neue
|
||||
# Auftragsbuch-Karte mit einer Empfehlung (ANNEHMEN/ABLEHNEN/UNKLAR + EIN ehrlicher Satz),
|
||||
# damit der Commander nie blind klickt. Ein-Schuss-Richter gegen llama-swap (:8080,
|
||||
# fremdblick-Architektur, Muster chef-gutachter-feed.sh) — bekommt Karte (Commit-Text,
|
||||
# Dateien, Diff) UND den Selbst-Steckbrief (was schon existiert), prueft also genau die
|
||||
# Falle vom 12.07. (redundanter Gateway-Restart-Vorschlag). Der Stempel ist eine MEINUNG,
|
||||
# kein Gate: die Karte bleibt klickbar, die Entscheidung bleibt beim Commander.
|
||||
# Laeuft als Cron mit --no-agent: stdout leer = still; gestempelte Karten = kurze Zeilen.
|
||||
# Ablage: /srv/models/mc2-karten-gutachten.json (Backend mischt es in die Karten-API).
|
||||
set -uo pipefail
|
||||
|
||||
API="${MC_API:-http://127.0.0.1:9001}"
|
||||
OUT="${MC2_KARTEN_GUTACHTEN:-/srv/models/mc2-karten-gutachten.json}"
|
||||
ENDPOINT="${GUTACHTER_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}"
|
||||
CHEF="${GUTACHTER_MODEL:-gpt-oss-120b}"
|
||||
VERTRETUNG="${GUTACHTER_FALLBACK:-GLM-4.7-Flash}"
|
||||
STECK="$HOME/.hermes/state/selbst-steckbrief.md"
|
||||
MAX_JE_LAUF=3 # Nacht-GPU-Budget: Rest kommt morgen dran
|
||||
|
||||
briefkasten() { # $1=Betreff $2=Text
|
||||
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(python3 - "$1" "$2" <<'PY'
|
||||
import json, sys
|
||||
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
|
||||
"source": "karten-gutachter", "priority": "silent"}))
|
||||
PY
|
||||
)" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
TMPD="$(mktemp -d /tmp/karten-gutachter.XXXXXX)"
|
||||
trap 'rm -rf "$TMPD"' EXIT
|
||||
|
||||
if ! curl -sf -m 20 "$API/api/auftragsbuch" -o "$TMPD/items.json" 2>/dev/null; then
|
||||
echo "Karten-Gutachter: Auftragsbuch nicht erreichbar — kein Lauf."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Offene, noch ungestempelte Karten ermitteln + verwaiste Stempel aufraeumen (Karte weg
|
||||
# oder neuer Stand = Stempel weg). Schluessel: "<repo>:<branch>@<ts>" haengt am Commit-
|
||||
# Zeitstempel — ein nachgeschobener Commit macht den alten Stempel automatisch ungueltig.
|
||||
python3 - "$TMPD/items.json" "$OUT" > "$TMPD/plan.txt" <<'PY'
|
||||
import json, sys, os
|
||||
|
||||
items = (json.load(open(sys.argv[1], encoding="utf-8")) or {}).get("items") or []
|
||||
out_path = sys.argv[2]
|
||||
try:
|
||||
alt = json.load(open(out_path, encoding="utf-8"))
|
||||
if not isinstance(alt, dict):
|
||||
alt = {}
|
||||
except Exception:
|
||||
alt = {}
|
||||
|
||||
offen = {}
|
||||
for it in items:
|
||||
state = (it.get("status") or {}).get("state")
|
||||
if state in ("eingespielt", "laeuft", "rollback"):
|
||||
continue # entschieden oder gerade in Arbeit — kein Stempel noetig
|
||||
key = "{}:{}".format(it.get("repo", "mc2"), it.get("branch", "?"))
|
||||
offen[key] = it
|
||||
|
||||
# Verwaiste/veraltete Stempel raus (atomar zurueckschreiben).
|
||||
neu = {}
|
||||
for key, g in alt.items():
|
||||
it = offen.get(key)
|
||||
if it and g.get("commit_ts") == it.get("ts"):
|
||||
neu[key] = g
|
||||
tmp = out_path + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(neu, f, ensure_ascii=False)
|
||||
os.replace(tmp, out_path)
|
||||
|
||||
for key, it in offen.items():
|
||||
if key in neu:
|
||||
continue
|
||||
repo, branch = key.split(":", 1)
|
||||
print("\x1f".join([repo, branch, str(it.get("ts") or 0),
|
||||
(it.get("subject") or "")[:200],
|
||||
(it.get("shortstat") or "")[:200]]))
|
||||
PY
|
||||
|
||||
RASTER='Du bist der KARTEN-GUTACHTER einer lokalen KI-Box. Du bekommst EINE Vorschlags-Karte aus dem Auftragsbuch (Commit-Text, Datei-Liste, Diff) und den SELBST-STECKBRIEF der Box (was schon existiert und laeuft). Der Commander ist kein Entwickler — dein Urteil ist seine einzige fachliche Zweitmeinung vor dem Klick. Pruefe NUR anhand des Materials, erfinde nichts: (1) REDUNDANZ — existiert das laut Steckbrief oder Diff schon? Ein Vorschlag, der Bestehendes nur nachbaut oder verschlechtert (z. B. funktionierende Einstellungen ersetzt), ist abzulehnen. (2) RISIKO — beruehrt der Diff Security (Tokens/approvals/ufw/sudoers), loescht er Daten oder aendert er Verhalten, das der Commander nicht bestellt hat? (3) NUTZEN — loest er ein echtes, benanntes Problem? Antworte auf DEUTSCH in exakt zwei Zeilen: "URTEIL: ANNEHMEN" oder "URTEIL: ABLEHNEN" oder "URTEIL: UNKLAR", danach "SATZ: <ein ehrlicher Satz Begruendung in Alltagssprache>". Kein weiterer Text.'
|
||||
|
||||
judge() { # $1=Modell $2=Timeout $3=max_tokens (liest $EVID)
|
||||
local req
|
||||
req="$(jq -n --arg m "$1" --arg sys "$RASTER" --arg usr "$EVID" --argjson mt "$3" \
|
||||
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
|
||||
max_tokens:$mt, temperature:0.2}')"
|
||||
curl -s -m "$2" "$ENDPOINT" -H 'Content-Type: application/json' --data-binary "$req" \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
msg = d["choices"][0]["message"]
|
||||
print((msg.get("content") or msg.get("reasoning_content") or "").strip())
|
||||
except Exception:
|
||||
pass'
|
||||
}
|
||||
|
||||
N=0
|
||||
while IFS=$'\x1f' read -r repo branch cts subject stat; do
|
||||
[ -n "${repo:-}" ] || continue
|
||||
[ "$N" -ge "$MAX_JE_LAUF" ] && { echo "Karten-Gutachter: Tages-Deckel ($MAX_JE_LAUF) erreicht — Rest morgen."; break; }
|
||||
DIFF="$(curl -sfG -m 30 "$API/api/auftragsbuch/diff" \
|
||||
--data-urlencode "branch=$branch" --data-urlencode "repo=$repo" 2>/dev/null \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
print((json.load(sys.stdin).get("diff") or "")[:20000])
|
||||
except Exception:
|
||||
pass' || true)"
|
||||
# Leerer Diff = deterministisch UNKLAR, OHNE Richter (Erstlauf 12.07.: eine Karte mit
|
||||
# 0 Dateien bekam trotzdem ANNEHMEN — ein LLM kann einen leeren Vorschlag nicht pruefen).
|
||||
if [ -z "${DIFF//[[:space:]]/}" ]; then
|
||||
python3 - "$OUT" "$repo" "$branch" "$cts" <<'PY'
|
||||
import json, os, sys, time
|
||||
out_path = sys.argv[1]
|
||||
try:
|
||||
data = json.load(open(out_path, encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
except Exception:
|
||||
data = {}
|
||||
data["{}:{}".format(sys.argv[2], sys.argv[3])] = {
|
||||
"empfehlung": "UNKLAR",
|
||||
"satz": "Der Vorschlag hat einen LEEREN Diff (0 geaenderte Dateien gegen main) — da gibt es nichts zu pruefen; technisch verdaechtig, im Zweifel ablehnen.",
|
||||
"richter": "mechanische Regel (leerer Diff)",
|
||||
"commit_ts": int(sys.argv[4]) if sys.argv[4].isdigit() else None,
|
||||
"ts": int(time.time())}
|
||||
tmp = out_path + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False)
|
||||
os.replace(tmp, out_path)
|
||||
PY
|
||||
N=$((N + 1))
|
||||
echo "Karte ${branch}: Empfehlung UNKLAR — leerer Diff (mechanische Regel, kein Richter noetig)."
|
||||
briefkasten "Karten-Gutachter" "Karte '${branch}' (${repo}): Empfehlung UNKLAR — der Diff ist leer (0 Dateien), technisch verdaechtig."
|
||||
continue
|
||||
fi
|
||||
EVID="KARTE ${repo}:${branch}
|
||||
COMMIT: ${subject}
|
||||
UMFANG: ${stat}
|
||||
|
||||
DIFF (ggf. gekuerzt):
|
||||
${DIFF:-(Diff nicht lesbar)}
|
||||
|
||||
SELBST-STECKBRIEF DER BOX (was schon existiert):
|
||||
$(head -90 "$STECK" 2>/dev/null || echo '(noch keine Selbst-Inventur gelaufen)')"
|
||||
|
||||
RICHTER="$CHEF"
|
||||
URTEIL_RAW="$(judge "$CHEF" 420 1600)"
|
||||
if [ -z "${URTEIL_RAW// /}" ]; then
|
||||
RICHTER="$VERTRETUNG (Vertretung)"
|
||||
URTEIL_RAW="$(judge "$VERTRETUNG" 240 1200)"
|
||||
fi
|
||||
[ -z "${URTEIL_RAW// /}" ] && { echo "Karte ${branch}: Gutachter ausgefallen (kein Richter erreichbar)."; continue; }
|
||||
|
||||
printf '%s' "$URTEIL_RAW" > "$TMPD/urteil.txt"
|
||||
STAMP="$(python3 - "$TMPD/urteil.txt" "$OUT" "$repo" "$branch" "$cts" "$RICHTER" <<'PY'
|
||||
import json, os, re, sys, time
|
||||
|
||||
raw = open(sys.argv[1], encoding="utf-8", errors="replace").read()
|
||||
m_u = re.search(r"URTEIL:\s*(ANNEHMEN|ABLEHNEN|UNKLAR)", raw, re.I)
|
||||
m_s = re.search(r"SATZ:\s*(.+)", raw)
|
||||
urteil = (m_u.group(1).upper() if m_u else "UNKLAR")
|
||||
satz = (m_s.group(1).strip() if m_s else "Analyse unklar — Urteil nicht lesbar.")[:300]
|
||||
out_path = sys.argv[2]
|
||||
try:
|
||||
data = json.load(open(out_path, encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
except Exception:
|
||||
data = {}
|
||||
key = "{}:{}".format(sys.argv[3], sys.argv[4])
|
||||
data[key] = {"empfehlung": urteil, "satz": satz, "richter": sys.argv[6],
|
||||
"commit_ts": int(sys.argv[5]) if sys.argv[5].isdigit() else None,
|
||||
"ts": int(time.time())}
|
||||
tmp = out_path + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False)
|
||||
os.replace(tmp, out_path)
|
||||
print(urteil + "\x1f" + satz)
|
||||
PY
|
||||
)"
|
||||
URTEIL="${STAMP%%$'\x1f'*}"; SATZ="${STAMP#*$'\x1f'}"
|
||||
N=$((N + 1))
|
||||
echo "Karte ${branch}: Empfehlung ${URTEIL} — ${SATZ}"
|
||||
briefkasten "Karten-Gutachter" "Karte '${branch}' (${repo}): Empfehlung ${URTEIL} — ${SATZ} (Richter: ${RICHTER})"
|
||||
done < "$TMPD/plan.txt"
|
||||
|
||||
# stdout bleibt LEER, wenn nichts zu stempeln war (--no-agent => stille Nacht).
|
||||
@@ -1,9 +0,0 @@
|
||||
[Unit]
|
||||
Description=MC2 Bagatell-Annahme (reine Doku-Vorschläge nachts ohne Klick, mit Fangnetz)
|
||||
Documentation=file:%h/mission-control-v2/docs/AUFTRAGSBUCH.md
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
# Als /tmp-Kopie laufen lassen: die Annahme deployt (git reset --hard) und würde sonst das
|
||||
# eigene Skript unter dem laufenden bash austauschen (bekannte Selbst-Reset-Falle).
|
||||
ExecStart=/bin/bash -c 'cp "%h/mission-control-v2/deploy/bagatell-annahme.sh" /tmp/mc2-bagatell-run.sh && exec /bin/bash /tmp/mc2-bagatell-run.sh'
|
||||
@@ -1,9 +0,0 @@
|
||||
[Unit]
|
||||
Description=Täglich 04:10 — Bagatell-Annahme (nach der Nacht-Sicherung 03:30, vor der Morgenlage 04:30)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 04:10:00
|
||||
Persistent=false
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -1,7 +0,0 @@
|
||||
[Unit]
|
||||
Description=MC2 Selbst-Smoke-Tests (Gateway/Tools/Voice aktiv durchspielen, Alarm bei Rot)
|
||||
Documentation=file:%h/mission-control-v2/docs/AUTONOMIE_PLAN.md
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/bash %h/mission-control-v2/deploy/self-smoke.sh
|
||||
@@ -1,10 +0,0 @@
|
||||
[Unit]
|
||||
Description=Täglicher MC2-Selbst-Smoke-Test (07:15, unabhängig von Updates)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 07:15:00
|
||||
Persistent=true
|
||||
RandomizedDelaySec=300
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -1,27 +0,0 @@
|
||||
# systemd-USER-Unit für den MC2-Steward — die Wächter-Loops als eigener Prozess (UMBAU v3 P2).
|
||||
# Re-Warm-Wächter, Health-Sentry und Mem0-Dedupe überleben damit jeden Steuerpult-Neustart,
|
||||
# und der Sentry kann erstmals auch das Steuerpult SELBST + den mc2-gateway überwachen
|
||||
# (MC_SENTRY_WATCH_MC2=1). Meldungen gehen per HTTP in den MC2-Briefkasten (MC_ANNOUNCE_HTTP);
|
||||
# ist MC2 down, greift weiterhin der Telegram-Direktweg (notify.sh).
|
||||
# Sudo-frei wie alle MC2-Units: ~/.config/systemd/user/ + systemctl --user.
|
||||
|
||||
[Unit]
|
||||
Description=MC2 Steward (Wächter: Re-Warm, Health-Sentry, Mem0-Dedupe)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=%h/mission-control-v2/backend
|
||||
ExecStart=%h/mission-control-v2/backend/.venv/bin/python steward.py
|
||||
Environment=PYTHONPATH=%h/mission-control-v2
|
||||
Environment=MC_LLAMA_SWAP_URL=http://127.0.0.1:8080
|
||||
Environment=MC_CONFIG_PATH=/etc/llama-swap/config.yaml
|
||||
Environment=MC_MODELS_DIR=/srv/models
|
||||
Environment=MC_ANNOUNCE_HTTP=http://127.0.0.1:9001
|
||||
Environment=MC_SENTRY_WATCH_MC2=1
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,147 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""mem0-Konsolidierung (monatlich, 19.07.2026 — Nachtrag Phase 2 Drei-Welten-Plan).
|
||||
|
||||
mem0 OSS v2 ist ADD-only: Es legt Fakten an, loest aber Widersprueche nie auf und
|
||||
legt Aehnliches nie zusammen — ohne Pflege waechst das Gedaechtnis unbegrenzt und
|
||||
der Recall verrauscht. Der Steward-Dedupe faengt nur exakte Dubletten. Dieser Lauf
|
||||
macht die fehlende Konsolidierung:
|
||||
|
||||
1. Cluster aehnlicher Fakten holen — ueber den bestehenden /graph-Endpunkt des
|
||||
Sidecars (Kosinus der gespeicherten Embeddings, kein Re-Embedding, kein
|
||||
Chroma-Zugriff noetig).
|
||||
2. Je Cluster entscheidet das lokale Hirn (Alias `fast`, Thinking aus wie im
|
||||
Sidecar): zusammenlegen / Widerspruch aufloesen (neuester Zeitstempel
|
||||
gewinnt) / NICHTS tun.
|
||||
3. Anwenden ueber die normalen Sidecar-Endpunkte (PUT/DELETE /memory/<id>).
|
||||
|
||||
Sicherheitsleitplanken (hart im Code, nicht im Prompt):
|
||||
- Pro Cluster ueberlebt IMMER mindestens ein Fakt.
|
||||
- Nur ids aus dem Cluster, max. 1 Update pro Cluster, Inhalt <= 600 Zeichen.
|
||||
- LLM-Fehler / kaputtes JSON => Cluster bleibt unangetastet.
|
||||
- Kappung pro Lauf (MAX_CLUSTERS), damit ein Lauf nie das halbe Gedaechtnis
|
||||
umpfluegt — der naechste Monat macht weiter.
|
||||
|
||||
Aufruf: mem0-konsolidierung.py [--apply] (Default: Dry-Run, druckt nur Plan)
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
MEM0 = "http://127.0.0.1:8765"
|
||||
LLM = "http://127.0.0.1:8080/v1/chat/completions"
|
||||
LLM_MODEL = "fast"
|
||||
MIN_SCORE = 0.82
|
||||
MAX_CLUSTERS = 15
|
||||
MAX_FACTS_PER_CLUSTER = 6
|
||||
APPLY = "--apply" in sys.argv
|
||||
|
||||
|
||||
def _http(method: str, url: str, body: dict | None = None, timeout: float = 120.0):
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
req = urllib.request.Request(url, data=data, method=method,
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return json.loads(r.read().decode() or "{}")
|
||||
|
||||
|
||||
def clusters_from_graph() -> tuple[dict, list]:
|
||||
g = _http("GET", f"{MEM0}/graph?min_score={MIN_SCORE}&top_k=5", timeout=180)
|
||||
nodes = {n["id"]: n for n in g.get("nodes", [])}
|
||||
parent: dict = {}
|
||||
|
||||
def find(x):
|
||||
parent.setdefault(x, x)
|
||||
while parent[x] != x:
|
||||
parent[x] = parent[parent[x]]
|
||||
x = parent[x]
|
||||
return x
|
||||
|
||||
for e in g.get("edges", []):
|
||||
a, b = find(e["source"]), find(e["target"])
|
||||
if a != b:
|
||||
parent[a] = b
|
||||
groups: dict = {}
|
||||
for nid in parent:
|
||||
groups.setdefault(find(nid), []).append(nid)
|
||||
clusters = [sorted(v) for v in groups.values() if len(v) >= 2]
|
||||
clusters.sort(key=len, reverse=True)
|
||||
return nodes, clusters[:MAX_CLUSTERS]
|
||||
|
||||
|
||||
def judge(facts: list[dict]) -> list[dict]:
|
||||
"""LLM entscheidet pro Cluster. Fehler => [] (nichts tun)."""
|
||||
listing = "\n".join(
|
||||
f"- id={f['id']} | kategorie={f.get('category')} | stand={f.get('updated_at', '')[:10]} | {f.get('content', '')}"
|
||||
for f in facts)
|
||||
system = (
|
||||
"Du konsolidierst ein Langzeitgedaechtnis. Unten steht EINE Gruppe semantisch "
|
||||
"aehnlicher Fakten. Entscheide konservativ: "
|
||||
"(a) Sind es Facetten DESSELBEN Fakts: fasse sie in EINEM praezisen deutschen Satz "
|
||||
"zusammen -> genau ein update auf die beste id, die anderen delete. "
|
||||
"(b) Widersprechen sie sich: der Fakt mit dem NEUESTEN Stand gewinnt -> veraltete delete. "
|
||||
"(c) Sind es VERSCHIEDENE Fakten (nur thematisch verwandt): TUE NICHTS. "
|
||||
"Im Zweifel IMMER (c). Antworte NUR als JSON: "
|
||||
'{"actions":[{"op":"update","id":"...","content":"..."},{"op":"delete","id":"..."}]} '
|
||||
'oder {"actions":[]}.'
|
||||
)
|
||||
try:
|
||||
resp = _http("POST", LLM, {
|
||||
"model": LLM_MODEL,
|
||||
"messages": [{"role": "system", "content": system},
|
||||
{"role": "user", "content": "Fakten-Gruppe:\n" + listing}],
|
||||
"temperature": 0.1, "max_tokens": 800,
|
||||
"response_format": {"type": "json_object"},
|
||||
"chat_template_kwargs": {"enable_thinking": False},
|
||||
})
|
||||
raw = resp["choices"][0]["message"]["content"] or "{}"
|
||||
actions = json.loads(raw).get("actions", [])
|
||||
except Exception as exc:
|
||||
print(f" (LLM-Fehler, Gruppe unangetastet: {exc})")
|
||||
return []
|
||||
# Harte Validierung — Leitplanken siehe Docstring.
|
||||
ids = {f["id"] for f in facts}
|
||||
updates = [a for a in actions if a.get("op") == "update" and a.get("id") in ids
|
||||
and isinstance(a.get("content"), str) and 0 < len(a["content"]) <= 600]
|
||||
deletes = [a for a in actions if a.get("op") == "delete" and a.get("id") in ids]
|
||||
if len(updates) > 1:
|
||||
return []
|
||||
del_ids = {a["id"] for a in deletes} - {a["id"] for a in updates}
|
||||
if len(del_ids) >= len(ids):
|
||||
return []
|
||||
return updates + [{"op": "delete", "id": i} for i in sorted(del_ids)]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
t0 = time.time()
|
||||
nodes, clusters = clusters_from_graph()
|
||||
total = len(nodes)
|
||||
print(f"mem0-Konsolidierung {'(APPLY)' if APPLY else '(DRY-RUN)'} — "
|
||||
f"{total} Fakten, {len(clusters)} Gruppen >= {MIN_SCORE}")
|
||||
n_upd = n_del = 0
|
||||
for ci, cl in enumerate(clusters, 1):
|
||||
facts = [nodes[i] for i in cl][:MAX_FACTS_PER_CLUSTER]
|
||||
actions = judge(facts)
|
||||
if not actions:
|
||||
continue
|
||||
print(f"Gruppe {ci} ({len(facts)} Fakten):")
|
||||
for f in facts:
|
||||
print(f" [{f['id'][:8]}] {f.get('content', '')[:90]}")
|
||||
for a in actions:
|
||||
if a["op"] == "update":
|
||||
print(f" -> UPDATE {a['id'][:8]}: {a['content'][:90]}")
|
||||
n_upd += 1
|
||||
if APPLY:
|
||||
_http("PUT", f"{MEM0}/memory/{a['id']}", {"content": a["content"]})
|
||||
else:
|
||||
print(f" -> DELETE {a['id'][:8]}")
|
||||
n_del += 1
|
||||
if APPLY:
|
||||
_http("DELETE", f"{MEM0}/memory/{a['id']}")
|
||||
print(f"Ergebnis: {n_upd} zusammengefasst, {n_del} geloescht, "
|
||||
f"{total - (n_del if APPLY else 0)} Fakten verbleiben. "
|
||||
f"({int(time.time() - t0)} s)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Monatlicher mem0-Konsolidierungslauf (Cron-Wrapper, 3. des Monats 06:40 —
|
||||
# nach Restore-Probe am 1. und Venv-Audit am 2.). no_agent-Job: stdout geht
|
||||
# als Bericht direkt an Telegram. Logik: deploy/mem0-konsolidierung.py (--apply).
|
||||
set -u
|
||||
exec python3 "$HOME/.hermes/scripts/mem0-konsolidierung.py" --apply 2>&1
|
||||
@@ -1,26 +0,0 @@
|
||||
[Unit]
|
||||
Description=MC2 Mem0 Sidecar — auto-lernendes, semantisches Gedächtnis (mem0 + chroma)
|
||||
Documentation=https://github.com/mem0ai/mem0
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
# Mem0 + chromadb laufen nur unter Python 3.12 (~/.mem0/venv) — das MC2-Backend (3.14)
|
||||
# kann sie nicht importieren. Darum dieser schlanke Sidecar; MC2 spricht ihn per HTTP an.
|
||||
# Bind 127.0.0.1: nur lokal erreichbar (MC2 proxyt nach außen).
|
||||
Type=simple
|
||||
WorkingDirectory=%h/mission-control-v2/mem0_service
|
||||
Environment=MEM0_PORT=8765
|
||||
Environment=MEM0_CHROMA_PATH=/srv/models/mem0/chroma
|
||||
Environment=MEM0_HISTORY_DB=/srv/models/mem0/history.db
|
||||
Environment=MEM0_EMBED_URL=http://127.0.0.1:8080/v1
|
||||
Environment=MEM0_LLM_URL=http://127.0.0.1:8080/v1
|
||||
Environment=MEM0_EMBED_MODEL=embed
|
||||
Environment=MEM0_LLM_MODEL=fast
|
||||
Environment=MEM0_EMBED_DIMS=1024
|
||||
Environment=TOKENIZERS_PARALLELISM=false
|
||||
ExecStart=%h/.mem0/venv/bin/python -m uvicorn app:app --host 127.0.0.1 --port 8765
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Erwartet den Job-Namen und das Skript als ein String oder zwei Argumente
|
||||
if [ $# -eq 1 ]; then
|
||||
read -r JOB_NAME SCRIPT_NAME <<< "$1"
|
||||
else
|
||||
JOB_NAME="$1"
|
||||
SCRIPT_NAME="$2"
|
||||
fi
|
||||
|
||||
if [ -z "$SCRIPT_NAME" ]; then
|
||||
SCRIPT_NAME="$JOB_NAME"
|
||||
fi
|
||||
|
||||
# Pfad zusammenbauen
|
||||
SCRIPT_PATH="$HOME/mission-control-v2/deploy/$SCRIPT_NAME"
|
||||
if [ ! -f "$SCRIPT_PATH" ]; then
|
||||
# Versuche pruefstand/
|
||||
SCRIPT_PATH="$HOME/mission-control-v2/deploy/pruefstand/$SCRIPT_NAME"
|
||||
fi
|
||||
|
||||
bash "$SCRIPT_PATH" | bash "$HOME/mission-control-v2/deploy/notify.sh" -s "$JOB_NAME"
|
||||
@@ -1,140 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# opencode-lauf.sh — EIN begrenzter Agentenlauf auf der Box, mit denselben Regeln wie in Zed.
|
||||
#
|
||||
# Das ist die Nacht-Seite von „ein Regelwerk, zwei Ausloeser": tagsueber tippst du in Zed,
|
||||
# nachts ruft ein Hermes-Cron dieses Skript. Beide Wege benutzen
|
||||
# * dieselbe OpenCode-Version,
|
||||
# * dieselbe Mannschaft (~/.config/opencode/opencode.json: coder/hermes/kritiker),
|
||||
# * dasselbe Plugin (~/.config/opencode/plugin/mc2-governor.ts: Zaun + Pruef-Tor),
|
||||
# * denselben Token-Waechter (:8100).
|
||||
#
|
||||
# Unterschied zu deploy/worker.sh: worker.sh ist EIN zustandsloser Completion-Aufruf
|
||||
# (Hermes schreibt die Dateien selbst). Hier laeuft ein ECHTER Agent mit Datei-Haenden,
|
||||
# Subagenten und Pruef-Tor — fuer ganze Karten statt fuer Schnipsel.
|
||||
#
|
||||
# Nutzung:
|
||||
# opencode-lauf.sh <repo-pfad> "<auftrag>"
|
||||
# opencode-lauf.sh ~/projekte/foo "Baue X. Halte dich an AGENTS.md."
|
||||
#
|
||||
# Env:
|
||||
# LAUF_TIMEOUT Sekunden Hoechstdauer (Default 3600)
|
||||
# LAUF_LAUT 1 = Lucy spricht mit (Default 0 = still, Nachtbetrieb)
|
||||
# LAUF_AGENT OpenCode-Agent (Default build)
|
||||
set -uo pipefail
|
||||
|
||||
REPO="${1:-}"
|
||||
AUFTRAG="${2:-}"
|
||||
TIMEOUT="${LAUF_TIMEOUT:-3600}"
|
||||
AGENT="${LAUF_AGENT:-build}"
|
||||
OC="$HOME/.opencode/bin/opencode"
|
||||
ANNOUNCE="${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}"
|
||||
|
||||
melde () { # melde <betreff> <text> [prioritaet]
|
||||
curl -sf -m 5 -X POST "$ANNOUNCE" -H 'Content-Type: application/json' \
|
||||
--data "$(python3 -c 'import json,sys; print(json.dumps({"subject":sys.argv[1],"text":sys.argv[2],"source":"loop","priority":sys.argv[3]}))' \
|
||||
"$1" "$2" "${3:-silent}")" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
if [ -z "$REPO" ] || [ -z "$AUFTRAG" ]; then
|
||||
echo "Nutzung: $0 <repo-pfad> \"<auftrag>\"" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ ! -d "$REPO" ]; then
|
||||
echo "FEHLER: '$REPO' ist kein Verzeichnis." >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ ! -x "$OC" ]; then
|
||||
echo "FEHLER: OpenCode nicht gefunden ($OC)." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Der Governor MUSS stehen — ohne ihn liefe der Lauf ohne Sitzungs-Bremse.
|
||||
if ! curl -sf -m 5 -o /dev/null "http://127.0.0.1:8100/governor/status"; then
|
||||
echo "FEHLER: Governor (:8100) antwortet nicht — Lauf abgebrochen (keine Sitzungs-Bremse)." >&2
|
||||
melde "[Lauf]" "Ich habe einen Nachtlauf abgebrochen: der Token-Waechter antwortet nicht." "normal"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# Nachts still: Meldungen landen im Briefkasten, Lucy spricht sie aber nicht aus.
|
||||
# Das Plugin liest diese Variable; die Morgen-Zusammenfassung kommt vom Daily-Briefing.
|
||||
if [ "${LAUF_LAUT:-0}" = "1" ]; then export MC2_LOOP_SILENT=0; else export MC2_LOOP_SILENT=1; fi
|
||||
|
||||
LOGDIR="$HOME/.hermes/logs"; mkdir -p "$LOGDIR"
|
||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||
LOG="$LOGDIR/opencode-lauf-$STAMP.log"
|
||||
MAXRUNDEN="${LAUF_MAX_RUNDEN:-3}"
|
||||
|
||||
cd "$REPO" || exit 2
|
||||
NAME="$(basename "$REPO")"
|
||||
melde "[Lauf]" "Ich fange an zu bauen: $NAME." "silent"
|
||||
echo "=== Lauf $STAMP · Repo $REPO · Agent $AGENT · Timeout ${TIMEOUT}s ===" | tee "$LOG"
|
||||
|
||||
# Verify-Befehl des Projekts lesen (gleiche Datei und gleiche Regeln wie im Plugin).
|
||||
verify_cmd () {
|
||||
[ -r "$REPO/VERIFY" ] || return 1
|
||||
grep -vE '^\s*(#|$)' "$REPO/VERIFY" | paste -sd' && ' -
|
||||
}
|
||||
|
||||
START=$(date +%s)
|
||||
CODE=0
|
||||
RUNDE=0
|
||||
AUFGABE="$AUFTRAG"
|
||||
|
||||
# ── Bau-Schleife ────────────────────────────────────────────────────────────
|
||||
# Warum hier UND im Plugin? Das Plugin haengt am Ereignis `session.idle` — in Zed
|
||||
# laeuft der Prozess weiter und alles ist gut. Bei `opencode run` beendet sich der
|
||||
# Prozess aber, bevor das Pruef-Tor fertig ist (gemessen 25.07.). Fuer unbeaufsichtigte
|
||||
# Laeufe muss die Schleife deshalb HIER liegen, wo sie den Prozess ueberlebt.
|
||||
while :; do
|
||||
RUNDE=$((RUNDE + 1))
|
||||
echo "--- Runde $RUNDE/$MAXRUNDEN ---" | tee -a "$LOG"
|
||||
timeout "$TIMEOUT" "$OC" run --agent "$AGENT" "$AUFGABE" >>"$LOG" 2>&1
|
||||
CODE=$?
|
||||
[ "$CODE" -eq 124 ] && { echo "ZEITUEBERSCHREITUNG" | tee -a "$LOG"; break; }
|
||||
|
||||
VCMD="$(verify_cmd)" || { echo "Kein VERIFY — Pruef-Tor aus, Lauf endet." | tee -a "$LOG"; break; }
|
||||
|
||||
echo "--- Pruef-Tor: $VCMD ---" | tee -a "$LOG"
|
||||
VOUT="$(cd "$REPO" && eval "$VCMD" 2>&1)"; VCODE=$?
|
||||
printf '%s\n' "$VOUT" | tail -20 >> "$LOG"
|
||||
|
||||
if [ "$VCODE" -eq 0 ]; then
|
||||
echo "PRUEF-TOR GRUEN" | tee -a "$LOG"
|
||||
melde "[Pruefung]" "$NAME: Tests gruen nach $RUNDE Runde(n)." "normal"
|
||||
CODE=0
|
||||
break
|
||||
fi
|
||||
|
||||
if [ "$RUNDE" -ge "$MAXRUNDEN" ]; then
|
||||
echo "PRUEF-TOR ROT — Reparaturrunden aufgebraucht." | tee -a "$LOG"
|
||||
melde "[Pruefung]" "$NAME: Tests bleiben rot nach $RUNDE Runden. Hier komme ich allein nicht weiter." "normal"
|
||||
CODE=1
|
||||
break
|
||||
fi
|
||||
|
||||
echo "PRUEF-TOR ROT — Runde $((RUNDE + 1)) folgt." | tee -a "$LOG"
|
||||
melde "[Pruefung]" "$NAME: Tests rot, ich repariere selbst weiter (Runde $((RUNDE + 1))/$MAXRUNDEN)." "silent"
|
||||
AUFGABE="[MC2-PRUEFTOR] Der Verify-Befehl des Projekts ist fehlgeschlagen.
|
||||
|
||||
Befehl: $VCMD
|
||||
|
||||
Ausgabe (Ende):
|
||||
$(printf '%s' "$VOUT" | tail -c 3000)
|
||||
|
||||
Behebe die URSACHE — nicht das Symptom. Schalte keinen Test ab und aendere keine Tests.
|
||||
Urspruenglicher Auftrag war: $AUFTRAG"
|
||||
done
|
||||
|
||||
DAUER=$(( $(date +%s) - START ))
|
||||
# Nachweis statt Behauptung: was hat der Lauf im Arbeitsbaum tatsaechlich veraendert?
|
||||
GEAENDERT="$(git -C "$REPO" status --porcelain 2>/dev/null | wc -l | tr -d ' ')"
|
||||
|
||||
case "$CODE" in
|
||||
0) ERG="fertig, Pruefung bestanden" ;;
|
||||
124) ERG="ZEITUEBERSCHREITUNG nach ${TIMEOUT}s" ;;
|
||||
*) ERG="Pruefung NICHT bestanden (Exitcode $CODE)" ;;
|
||||
esac
|
||||
|
||||
echo "=== Ergebnis: $ERG · ${DAUER}s · $RUNDE Runde(n) · $GEAENDERT geaenderte Dateien · Log $LOG ===" | tee -a "$LOG"
|
||||
melde "[Lauf]" "$NAME: $ERG nach $((DAUER/60)) Minuten, $RUNDE Runde(n), $GEAENDERT Dateien angefasst." "silent"
|
||||
exit "$CODE"
|
||||
@@ -1,82 +0,0 @@
|
||||
# Persönliche Regeln (gelten für ALLE Projekte)
|
||||
|
||||
## So arbeitest du mit mir
|
||||
- Antworte auf **Deutsch**, Fazit zuerst, Technik in Alltagssprache. Ich bin **kein Entwickler**.
|
||||
- **Plan vor Code** bei allem Größeren: erst Vorgehen zeigen, auf mein **„los"** warten.
|
||||
- **Kleine Schritte**, eine Sache zur Zeit. Nichts Ungefragtes ausführen; **frag**, bevor du löschst
|
||||
oder überschreibst.
|
||||
- **Beweisen statt behaupten** — am Ende live testen, nicht „sollte gehen".
|
||||
- Nimm **Standard-Wege**, keine exotische Technik, außer ich will es ausdrücklich anders.
|
||||
- **Geheimnisse** (Schlüssel/Passwörter/Tokens) nie in den Code — in `.env` (+ `.gitignore`); warnen,
|
||||
bevor so etwas in einen Commit wandert.
|
||||
- **Nicht festfahren:** klemmt dieselbe Sache nach 2 Versuchen, anhalten, sichern, erklären und fragen.
|
||||
- **Bau-Prinzipien sind DEIN Job, nicht meine Frage:** KISS, YAGNI, Zuständigkeiten trennen, DRY,
|
||||
sprechende Namen, schlank starten. Frag mich nie danach — frag mich nur nach Zielen in Alltagssprache.
|
||||
- **Faulheits-Leiter vor jeder neuen Zeile/Abhängigkeit:** (1) muss das überhaupt sein? (2) gibt's das
|
||||
schon (wiederverwenden)? (3) reicht Bordmittel/Standard-Bibliothek? (4) erst dann minimal selbst bauen.
|
||||
Faul bei Lösungen, nie beim Verstehen; Sicherheitsnetze (Prüfung/Fehlerbehandlung/keine Geheimnisse)
|
||||
bleiben. „Prüf auf Über-Engineering" = Diff auf unnötige Abhängigkeiten/Abstraktion/Verdachts-Features durchsehen.
|
||||
|
||||
## Deine Mannschaft — delegiere, statt alles selbst zu tun
|
||||
- **`explore`** (Subagent): Code durchsuchen, Dateien finden, Fragen zum Bestand beantworten. Läuft auf
|
||||
dem ohnehin warmen Hirn und kostet praktisch nichts. **Nutze ihn**, statt dich selbst durch den Baum
|
||||
zu wühlen — dein eigener Kontext bleibt dadurch sauber und du hältst länger durch.
|
||||
- **`review`** (Subagent): kritisches Gegenlesen nach jeder Etappe. Läuft bewusst auf einer **fremden
|
||||
Modellfamilie**, damit er andere blinde Flecken hat als du. Nimm seine Befunde ernst.
|
||||
- **`plan`**: nur lesen, keine Änderungen.
|
||||
- Es gibt **keinen Modellwechsel mehr**. Früher galt „zum Bauen oben auf Coder umschalten" — Planen und
|
||||
Bauen laufen inzwischen auf demselben Modell. Bitte mich nie, etwas umzustellen.
|
||||
|
||||
## Das Prüf-Tor (`VERIFY`)
|
||||
- Im Projekt liegt eine Datei `VERIFY` mit **einer Zeile: wie man dieses Projekt testet**. Sobald du
|
||||
„fertig" meldest, wird sie automatisch ausgeführt.
|
||||
- **Rot heißt nicht fertig.** Der Fehler kommt zu dir zurück, du behebst die **Ursache** — nicht das
|
||||
Symptom. Einen Test abschwächen, überspringen oder umschreiben, damit er grün wird, gilt als Betrug.
|
||||
- Fehlt die Datei, ist das Prüf-Tor aus. Dann **frag mich**, was der richtige Testbefehl wäre, statt
|
||||
ungeprüft weiterzubauen.
|
||||
|
||||
## Kontext-Wächter
|
||||
- Ein Token-Wächter zählt jede Sitzung mit und schneidet sie bei ~45.000 Tokens. Du musst nichts messen
|
||||
— die Ansage kommt von allein.
|
||||
- Kommt sie: **`SAVEPOINT.md` ehrlich fertigschreiben und anhalten.** Ehrlich heißt: nur was **wirklich
|
||||
im Code steht**. Nichts aus Absicht, Plan oder git-Nachrichten ableiten. Dazu der genaue nächste
|
||||
Schritt, offene Fragen und Stolpersteine — genug, dass eine frische Sitzung ohne jede Erinnerung
|
||||
allein damit weitermachen kann.
|
||||
- Merkst du vorher schon, dass du dich wiederholst oder abdriftest: sag es proaktiv und schlag einen
|
||||
frischen Chat vor.
|
||||
|
||||
## Wenn niemand da ist (unbeaufsichtigte Läufe)
|
||||
Nachts startet ein Cron dieselbe Umgebung ohne Menschen davor. Erkennst du das — der Auftrag kam als
|
||||
ein Block, auf Rückfragen antwortet niemand: **warte nicht auf ein „los"**. Halte dich an `KONZEPT`/
|
||||
`SAVEPOINT.md`, bau in kleinen Etappen und lass das Prüf-Tor entscheiden. Was du ohne Antwort nicht
|
||||
sauber entscheiden kannst, schreibst du als offene Frage in `SAVEPOINT.md`, statt zu raten.
|
||||
|
||||
## Was du nie tust
|
||||
`git push`, Historie umschreiben, rekursiv löschen, `sudo`, Pakete veröffentlichen. Das ist gezäunt und
|
||||
wird geblockt — nicht weil du es falsch machst, sondern weil Veröffentlichen meine Entscheidung ist.
|
||||
Brauchst du so etwas wirklich: sag es mir, ich mache es selbst.
|
||||
|
||||
## Bestehendes Projekt fortsetzen?
|
||||
Ordner nicht leer / neue Sitzung: zuerst **`SAVEPOINT.md` + `AGENTS.md` lesen**, mir in 2 Sätzen sagen
|
||||
„hier stehen wir / nächster Schritt", dann weitermachen — nicht neu planen.
|
||||
|
||||
## Neues Projekt? → Projekt-Start-Ablauf
|
||||
Wenn ein **neues** Projekt beginnt (leerer/neuer Ordner, „bau mir…", „neues Projekt…", „ich hätte
|
||||
gern…"): folge diesem Ablauf. **Keine Datei, kein Code vor meiner Freigabe.**
|
||||
1. **Verstehen:** erst herausfinden, *was für ein* Projekt (Web-App / kleines Skript /
|
||||
Box-Lucy-Erweiterung / Experiment / Datensache), dann **3–6 typ-passende Fragen** + „woran ist
|
||||
es fertig?".
|
||||
2. **Plan vorlegen** (Ziel · Vorgehen · Technik + warum · Fertig-Kriterien · Risiken) → **STOPP**,
|
||||
auf mein „los" warten.
|
||||
3. **Erst dann Gerüst:** `git init` + die Projektdateien (`AGENTS.md`, `SAVEPOINT.md`, `.aiexclude`,
|
||||
`VERIFY`) + `README`, dann **Sicherungspunkt „start"** (erster Commit).
|
||||
4. In **kleinen Schritten** bauen, an jedem Meilenstein + vor riskanten Änderungen einen
|
||||
**Sicherungspunkt** (Commit mit klarem Namen) setzen, am Ende **live testen**.
|
||||
|
||||
## Bei großem Meilenstein UND Session-Ende („Feierabend"/„sichern")
|
||||
Immer fortschreiben: **`SAVEPOINT.md`** (Stand · Fertiges · nächste Schritte · offene Fragen — die
|
||||
Übergabe an die nächste Sitzung), **`AGENTS.md`** (falls Konventionen sich änderten), **`.aiexclude`**
|
||||
(neue Geheimnisse/große Dateien), **`VERIFY`** (falls sich der Testbefehl geändert hat), dann
|
||||
Sicherungspunkt setzen.
|
||||
|
||||
Volle Fassung des Projekt-Start-Ablaufs: `F:\Coding Stuff\projekt-start\SKILL.md`
|
||||
@@ -1,75 +0,0 @@
|
||||
# OpenCode-Seite: ein Regelwerk, zwei Auslöser
|
||||
|
||||
Tagsüber tippst du in **Zed** (PC), nachts ruft ein **Hermes-Cron** dasselbe (Box).
|
||||
Beide Wege benutzen dieselbe OpenCode-Version, dieselbe Mannschaft, dasselbe Plugin
|
||||
und denselben Token-Wächter. Der einzige Unterschied ist die Adresse des Governors.
|
||||
|
||||
## Was wohin gehört
|
||||
|
||||
| Datei hier | Ziel auf dem PC | Ziel auf der Box |
|
||||
|---|---|---|
|
||||
| `opencode.pc.json` | `~/.config/opencode/opencode.json` | — |
|
||||
| `opencode.box.json` | — | `~/.config/opencode/opencode.json` |
|
||||
| `AGENTS.global.md` | `~/.config/opencode/AGENTS.md` | `~/.config/opencode/AGENTS.md` |
|
||||
| `plugin/mc2-governor.ts` | `~/.config/opencode/plugin/` | `~/.config/opencode/plugin/` |
|
||||
| `VERIFY.template` | — | wird von `gitea-repo-create.sh` in **jedes neue Repo** als `VERIFY` gesät |
|
||||
|
||||
### Warum die Regeln hier liegen und nicht in Zed
|
||||
|
||||
Die persönlichen Arbeitsregeln standen bis 25.07.2026 in `AppData\Roaming\Zed\AGENTS.md` — und
|
||||
haben dort **nie den Coding-Agenten erreicht**. Zeds eigene Doku ist eindeutig: `AGENTS.md` und
|
||||
Skills gelten **nur für Zeds nativen Agenten**, nicht für ACP-Agenten wie OpenCode. Da hier über
|
||||
ACP gearbeitet wird, liefen die Regeln jahrelang ins Leere.
|
||||
|
||||
OpenCode liest stattdessen (in dieser Reihenfolge): `AGENTS.md` im Projekt (aufwärts gesucht) und
|
||||
`~/.config/opencode/AGENTS.md` global. Beide gelten zusammen — Projektregeln ergänzen die globalen.
|
||||
|
||||
Beim Umzug bewusst geändert:
|
||||
- **Zeds Commit-Vorlage entfernt.** Sie war in dieselbe Datei gerutscht und enthielt „Only return
|
||||
the commit message in your response" — in einer dauerhaft aktiven Regeldatei ein Fehlerherd.
|
||||
- **Modellwähler-Absatz gestrichen.** „Zum Bauen oben auf Coder umschalten" stimmt seit der
|
||||
Mannschafts-Umstellung nicht mehr: `plan` und `build` laufen auf demselben Modell.
|
||||
- **Kontext-Wächter umgeschrieben** auf den Governor, der den Füllstand wirklich kennt.
|
||||
- **Neu:** Prüf-Tor (`VERIFY`), die Subagenten-Mannschaft und ein Abschnitt für **unbeaufsichtigte
|
||||
Läufe** — nachts sagt niemand „los", also darf der Agent dort nicht auf Freigabe warten.
|
||||
|
||||
Der Governor selbst liegt in `../governor/` (Proxy + systemd-Unit), der unbeaufsichtigte
|
||||
Läufer in `../opencode-lauf.sh`.
|
||||
|
||||
## Die Mannschaft
|
||||
|
||||
| Rolle | Modell | Gemessen (25.07.2026) | Warum |
|
||||
|---|---|---|---|
|
||||
| `plan` + `build` | `coder` — Qwen3-Coder-Next | **51,5 t/s** | Hält den Faden, verteilt Zuarbeit. MoE mit 3B aktiv → schnell auf Strix Halo. |
|
||||
| `explore` | `hermes` — Qwen3.6-35B | **69,6 t/s** | Ohnehin dauerwarm → kostet **null** zusätzlichen Speicher. Sucht, liest, meldet kurz zurück. |
|
||||
| `review` | `kritiker` — Devstral-Small-2 | **15,0 t/s** | Bewusst eine **fremde Modellfamilie** (Mistral statt Qwen) → andere blinde Flecken. Dicht = langsam beim Schreiben, aber ein Kritiker liest viel und schreibt wenig. |
|
||||
|
||||
`heavy` (gpt-oss-120b, 63 GB) ist **nicht** mehr in der Tagesrolle: es würde beim Laden
|
||||
das ganze warme Set verdrängen. Es bleibt der Nacht-Gutachter (4:30-Cron).
|
||||
|
||||
## Nach einer Änderung
|
||||
|
||||
Die Dateien hier sind **Vorlagen**, keine Live-Konfiguration. Nach einer Änderung
|
||||
verteilen:
|
||||
|
||||
```bash
|
||||
# Box
|
||||
scp deploy/opencode/opencode.box.json hitonabi@192.168.178.151:~/.config/opencode/opencode.json
|
||||
scp deploy/opencode/plugin/*.ts hitonabi@192.168.178.151:~/.config/opencode/plugin/
|
||||
# PC (aus dem Repo heraus)
|
||||
cp deploy/opencode/opencode.pc.json ~/.config/opencode/opencode.json
|
||||
cp deploy/opencode/plugin/*.ts ~/.config/opencode/plugin/
|
||||
```
|
||||
|
||||
**Zed muss danach neu gestartet werden** — OpenCode liest seine Konfiguration nur beim Start.
|
||||
|
||||
## Fallen, die Zeit gekostet haben
|
||||
|
||||
- **Kein `_comment`-Schlüssel in `opencode.json`.** OpenCode validiert streng und
|
||||
verweigert den Start mit „Unrecognized key". Kommentare gehören in dieses README.
|
||||
- **Das Plugin läuft auf Windows UND Linux.** Deshalb `node:fs` statt `cat` und Buns
|
||||
`${{ raw: cmd }}` statt `bash -lc` — beides fehlt auf Windows bzw. verschluckt den Befehl.
|
||||
- **Bei `opencode run` beendet sich der Prozess, bevor `session.idle` fertig ist**
|
||||
(gemessen 25.07.). Für unbeaufsichtigte Läufe liegt die Prüf-Schleife deshalb
|
||||
zusätzlich in `opencode-lauf.sh`, wo sie den Prozess überlebt. In Zed greift das Plugin.
|
||||
- **Plugin-Verzeichnis:** `plugin/` und `plugins/` werden beide erkannt; wir nutzen `plugin/`.
|
||||
@@ -1,21 +0,0 @@
|
||||
# VERIFY — wie man dieses Projekt prueft.
|
||||
#
|
||||
# Diese Datei ist das Pruef-Tor. Das MC2-Governor-Plugin fuehrt sie aus, sobald der
|
||||
# Agent "fertig" sagt. Gruen -> Etappe gilt als fertig. Rot -> der Fehler geht
|
||||
# automatisch als naechster Auftrag an den Agenten zurueck, bis zu 3 Runden.
|
||||
#
|
||||
# Regeln:
|
||||
# * Eine Zeile = ein Befehl. Alle Zeilen werden mit && verkettet.
|
||||
# * Zeilen mit # sind Kommentare.
|
||||
# * KEINE Datei VERIFY im Projekt = Pruef-Tor aus (nichts passiert).
|
||||
# * Der Befehl muss ohne Rueckfragen durchlaufen und mit 0 enden, wenn alles gut ist.
|
||||
#
|
||||
# Beispiele (unzutreffende Zeilen loeschen):
|
||||
#
|
||||
# Python: ruff check . && pytest -q
|
||||
# Node/TS: npm run lint && npm test
|
||||
# Frontend: npm run build
|
||||
# Nur Syntax: python -m compileall -q .
|
||||
# Nichts da: git diff --stat (laeuft immer gruen — Platzhalter)
|
||||
|
||||
git diff --stat
|
||||
@@ -1,88 +0,0 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"aibox": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "AI-Box ueber Governor (lokal)",
|
||||
"options": {
|
||||
"baseURL": "http://127.0.0.1:8100/v1",
|
||||
"apiKey": "local"
|
||||
},
|
||||
"models": {
|
||||
"coder": {
|
||||
"name": "coder — Bauen + Planen (Qwen3-Coder-Next, 51,5 t/s)",
|
||||
"limit": {
|
||||
"context": 131072,
|
||||
"output": 16384
|
||||
}
|
||||
},
|
||||
"hermes": {
|
||||
"name": "hermes — Suchen (Qwen3.6-35B, immer warm, 69,6 t/s)",
|
||||
"limit": {
|
||||
"context": 65536,
|
||||
"output": 8192
|
||||
}
|
||||
},
|
||||
"kritiker": {
|
||||
"name": "kritiker — Gegenlesen (Devstral-2, Mistral, 16k gedeckelt)",
|
||||
"limit": {
|
||||
"context": 16384,
|
||||
"output": 4096
|
||||
}
|
||||
},
|
||||
"heavy": {
|
||||
"name": "heavy — Nacht-Gutachter (gpt-oss-120b, verdraengt das warme Set!)",
|
||||
"limit": {
|
||||
"context": 32768,
|
||||
"output": 8192
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model": "aibox/coder",
|
||||
"small_model": "aibox/hermes",
|
||||
"agent": {
|
||||
"plan": {
|
||||
"model": "aibox/coder",
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "deny"
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"model": "aibox/coder",
|
||||
"permission": {
|
||||
"edit": "allow",
|
||||
"webfetch": "allow",
|
||||
"bash": {
|
||||
"*": "allow",
|
||||
"ssh *": "deny",
|
||||
"scp *": "deny",
|
||||
"sftp *": "deny",
|
||||
"ssh arcane@192.168.178.162 *": "allow",
|
||||
"ssh -o StrictHostKeyChecking=no arcane@192.168.178.162 *": "allow",
|
||||
"scp *arcane@192.168.178.162*": "allow"
|
||||
}
|
||||
}
|
||||
},
|
||||
"explore": {
|
||||
"mode": "subagent",
|
||||
"description": "Codebase schnell durchsuchen, Dateien finden, Fragen zum Code beantworten — nur lesen, laeuft auf dem immer warmen Hirn",
|
||||
"model": "aibox/hermes",
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "deny"
|
||||
}
|
||||
},
|
||||
"review": {
|
||||
"mode": "subagent",
|
||||
"description": "Kritischer Code-Review nach jeder Etappe (Pflicht laut AGENTS.md): sucht erfundene APIs/CLI-Flags, stille Abweichungen vom KONZEPT, fehlende Tests, toten Code — meldet Befunde, aendert nichts. Laeuft bewusst auf einer FREMDEN Modellfamilie (Mistral/Devstral statt Qwen), damit er andere blinde Flecken hat als der Coder.",
|
||||
"model": "aibox/kritiker",
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "deny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"aibox": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "AI-Box ueber Governor (192.168.178.151:8100)",
|
||||
"options": {
|
||||
"baseURL": "http://192.168.178.151:8100/v1",
|
||||
"apiKey": "local"
|
||||
},
|
||||
"models": {
|
||||
"heavy": {
|
||||
"name": "heavy — Planer (gpt-oss-120b, 32k)",
|
||||
"limit": {
|
||||
"context": 32768,
|
||||
"output": 8192
|
||||
}
|
||||
},
|
||||
"coder": {
|
||||
"name": "coder — Bauen (Qwen3-Coder-Next, 131k)",
|
||||
"limit": {
|
||||
"context": 131072,
|
||||
"output": 16384
|
||||
}
|
||||
},
|
||||
"hermes": {
|
||||
"name": "hermes — Erkunden (Qwen3.6, immer warm, 69,6 t/s)",
|
||||
"limit": {
|
||||
"context": 65536,
|
||||
"output": 8192
|
||||
}
|
||||
},
|
||||
"kritiker": {
|
||||
"name": "kritiker — Gegenlesen (Devstral-2, Mistral, 16k gedeckelt)",
|
||||
"limit": {
|
||||
"context": 16384,
|
||||
"output": 4096
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model": "aibox/coder",
|
||||
"small_model": "aibox/hermes",
|
||||
"agent": {
|
||||
"plan": {
|
||||
"model": "aibox/coder",
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "deny"
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"model": "aibox/coder",
|
||||
"permission": {
|
||||
"edit": "allow",
|
||||
"webfetch": "allow",
|
||||
"bash": {
|
||||
"*": "allow",
|
||||
"ssh *": "deny",
|
||||
"scp *": "deny",
|
||||
"sftp *": "deny",
|
||||
"ssh arcane@192.168.178.162 *": "allow",
|
||||
"ssh -o StrictHostKeyChecking=no arcane@192.168.178.162 *": "allow",
|
||||
"scp *arcane@192.168.178.162*": "allow"
|
||||
}
|
||||
}
|
||||
},
|
||||
"explore": {
|
||||
"mode": "subagent",
|
||||
"description": "Codebase schnell durchsuchen, Dateien finden, Fragen zum Code beantworten — nur lesen, läuft auf dem immer warmen Hirn",
|
||||
"model": "aibox/hermes",
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "deny"
|
||||
}
|
||||
},
|
||||
"review": {
|
||||
"mode": "subagent",
|
||||
"description": "Kritischer Code-Review nach jeder Etappe (Pflicht laut AGENTS.md): sucht erfundene APIs/CLI-Flags, stille Abweichungen vom KONZEPT, fehlende Tests, toten Code — meldet Befunde, ändert nichts. Läuft bewusst auf einer FREMDEN Modellfamilie (Mistral/Devstral statt Qwen), damit er andere blinde Flecken hat als der Coder.",
|
||||
"model": "aibox/kritiker",
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "deny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
/**
|
||||
* MC2-Governor — der "Fahrlehrer" im OpenCode-Agenten.
|
||||
*
|
||||
* Der Governor-Proxy (:8100) ist die Tankuhr: er sieht nur Tokens und zieht die
|
||||
* Notbremse. Dieses Plugin sitzt IM Agenten und sieht alles andere — jeden
|
||||
* Werkzeuggriff, jede Datei, jedes Sitzungsende. Es macht vier Dinge:
|
||||
*
|
||||
* 1. WERKZEUG-ZAUN (tool.execute.before)
|
||||
* Blockt Handgriffe, die ein Agent nie unbeaufsichtigt tun darf: push,
|
||||
* Historie umschreiben, rekursiv loeschen, sudo, Fremd-Hosts. Genau dieser
|
||||
* Zustandsautomat-Zaun hob lokale Modelle in Messungen von 2/10 auf 10/10 —
|
||||
* nicht weil sie schlauer werden, sondern weil sie nicht mehr entgleisen.
|
||||
*
|
||||
* 2. PRUEF-TOR + SCHLEIFE (session.idle)
|
||||
* Sagt der Agent "fertig", laeuft der Verify-Befehl des Projekts (Datei
|
||||
* `VERIFY` im Repo-Wurzelverzeichnis). GRUEN -> Meldung. ROT -> der Fehler
|
||||
* geht als naechster Auftrag automatisch zurueck an den Agenten, bis zu
|
||||
* MC2_LOOP_MAX_ROUNDS mal. Das ist die "Ralph-Schleife", nur mit Bremse.
|
||||
*
|
||||
* 3. SAVEPOINT STATT ZUSAMMENFASSEN (session.compacted)
|
||||
* Beim Komprimieren fallen still die Regeln aus dem Kontext (Paper
|
||||
* "Governance Decay"). Wir schieben stattdessen den Auftrag nach, SAVEPOINT.md
|
||||
* zu schreiben — Wissen lebt in Datei + git, nicht im schrumpfenden Chat.
|
||||
*
|
||||
* 4. STIMME (MC2 /api/voice/announce)
|
||||
* Jedes Ereignis geht mit eigenem Absender `loop` in MC2s Melde-Briefkasten.
|
||||
* Lucy pollt ihn ohnehin und spricht ihn — ohne eine Zeile Lucy-Code.
|
||||
*
|
||||
* Schalter (Umgebungsvariablen):
|
||||
* MC2_BOX_URL MC2-Basis (Default http://192.168.178.151:9001)
|
||||
* MC2_LOOP_AUTOFIX Selbstreparatur (1 = an, Default an)
|
||||
* MC2_LOOP_MAX_ROUNDS max. Reparaturrunden (Default 3)
|
||||
* MC2_LOOP_SILENT 1 = Lucy schweigt (Nachtlauf; Meldungen kommen trotzdem an)
|
||||
* MC2_LOOP_ANNOUNCE 0 = gar keine Meldungen
|
||||
* MC2_FENCE_OFF 1 = Werkzeug-Zaun aus (nur fuer Notfaelle)
|
||||
*
|
||||
* Liegt global unter ~/.config/opencode/plugin/ und wirkt damit in JEDEM Projekt —
|
||||
* am Tag in Zed, nachts im Cron. Ein Regelwerk, zwei Ausloeser.
|
||||
*/
|
||||
|
||||
const BOX_URL = process.env.MC2_BOX_URL || "http://192.168.178.151:9001"
|
||||
const AUTOFIX = process.env.MC2_LOOP_AUTOFIX !== "0"
|
||||
const MAX_ROUNDS = parseInt(process.env.MC2_LOOP_MAX_ROUNDS || "3", 10)
|
||||
const SILENT = process.env.MC2_LOOP_SILENT === "1"
|
||||
const ANNOUNCE_ON = process.env.MC2_LOOP_ANNOUNCE !== "0"
|
||||
const FENCE_OFF = process.env.MC2_FENCE_OFF === "1"
|
||||
|
||||
/**
|
||||
* Verbotene Shell-Handgriffe. Bewusst als Muster auf der ROHEN Kommandozeile —
|
||||
* ein Agent, der `git push` in ein `bash -c` verpackt, wird trotzdem erwischt.
|
||||
* Kein Anspruch auf Sandbox-Sicherheit: das ist ein Leitplanken-Zaun gegen
|
||||
* Entgleisen, keine Abwehr gegen einen boesartigen Akteur.
|
||||
*/
|
||||
const FENCE: Array<{ rx: RegExp; why: string }> = [
|
||||
{ rx: /\bgit\s+push\b/, why: "git push — Veroeffentlichen ist Sache des Menschen (oder der CI-Ampel)." },
|
||||
{ rx: /\bgit\s+reset\s+--hard\b/, why: "git reset --hard — verwirft Arbeit unwiederbringlich." },
|
||||
{ rx: /\bgit\s+clean\s+-[a-z]*f/, why: "git clean -f — loescht ungetrackte Dateien unwiederbringlich." },
|
||||
{ rx: /\bgit\s+(rebase|filter-branch|reflog\s+expire)\b/, why: "Historie umschreiben ist tabu." },
|
||||
{ rx: /\brm\s+-[a-zA-Z]*r[a-zA-Z]*f?\s+\/(?:\s|$)/, why: "rm -rf / — nein." },
|
||||
{ rx: /\brm\s+-[a-zA-Z]*[rf]/, why: "rekursives/erzwungenes Loeschen — bitte gezielt loeschen statt pauschal." },
|
||||
{ rx: /\bsudo\b/, why: "sudo — Rechteausweitung gehoert nicht in einen Agentenlauf." },
|
||||
{ rx: /\b(shutdown|reboot|mkfs|dd\s+if=)/, why: "System-/Datentraeger-Eingriff." },
|
||||
{ rx: /\b(curl|wget)\b[^|]*\|\s*(ba)?sh\b/, why: "Aus dem Netz laden und direkt ausfuehren — klassischer Fussschuss." },
|
||||
{ rx: /\bssh\s+(?!arcane@192\.168\.178\.162|-o\s+StrictHostKeyChecking=no\s+arcane@)/, why: "ssh nur zur freigegebenen Arcane-VM." },
|
||||
{ rx: /\bnpm\s+publish\b|\btwine\s+upload\b/, why: "Veroeffentlichen von Paketen ist Sache des Menschen." },
|
||||
]
|
||||
|
||||
/** Zaehler je Sitzung: wie viele Selbstreparatur-Runden liefen schon? */
|
||||
const rounds = new Map<string, number>()
|
||||
/** Doppel-Feuern verhindern: session.idle kann mehrfach kommen. */
|
||||
const busy = new Set<string>()
|
||||
|
||||
async function announce(subject: string, text: string, priority: "normal" | "silent" = "normal") {
|
||||
if (!ANNOUNCE_ON) return
|
||||
try {
|
||||
await fetch(`${BOX_URL}/api/voice/announce`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
subject,
|
||||
text,
|
||||
source: "loop",
|
||||
priority: SILENT ? "silent" : priority,
|
||||
}),
|
||||
signal: AbortSignal.timeout(4000),
|
||||
})
|
||||
} catch {
|
||||
/* best effort — eine stumme Lucy darf den Bau nie aufhalten */
|
||||
}
|
||||
}
|
||||
|
||||
export const MC2Governor = async ({ client, $, directory, worktree }: any) => {
|
||||
const root: string = worktree || directory || process.cwd()
|
||||
|
||||
/**
|
||||
* Verify-Befehl des Projekts lesen. Fehlt die Datei, ist das Pruef-Tor AUS.
|
||||
* Bewusst ueber fs statt `cat`: das Plugin laeuft am Tag auf Windows (Zed) und
|
||||
* nachts auf der Box — `cat` gibt es auf Windows nicht zuverlaessig.
|
||||
*/
|
||||
async function readVerify(): Promise<string | null> {
|
||||
try {
|
||||
const { readFile } = await import("node:fs/promises")
|
||||
const { join } = await import("node:path")
|
||||
const raw = await readFile(join(root, "VERIFY"), "utf8")
|
||||
const cmd = raw
|
||||
.split("\n")
|
||||
.map((l: string) => l.trim())
|
||||
.filter((l: string) => l && !l.startsWith("#"))
|
||||
.join(" && ")
|
||||
return cmd || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify ausfuehren. Rueckgabe: {ok, output} — Ausgabe auf das Wesentliche gekuerzt.
|
||||
* `{ raw: cmd }` schiebt den Befehl UNESCAPED in Buns Shell; ein normales
|
||||
* `${cmd}` wuerde die ganze Zeile als EIN Argument uebergeben und nie laufen.
|
||||
* Buns Shell ist plattformunabhaengig — kein `bash -lc`, das auf Windows fehlt.
|
||||
*/
|
||||
async function runVerify(cmd: string): Promise<{ ok: boolean; out: string }> {
|
||||
try {
|
||||
const res = await $`${{ raw: cmd }}`.cwd(root).nothrow().quiet()
|
||||
const out = `${res.stdout?.toString() ?? ""}${res.stderr?.toString() ?? ""}`
|
||||
return { ok: res.exitCode === 0, out: out.slice(-4000) }
|
||||
} catch (e: any) {
|
||||
return { ok: false, out: String(e?.message ?? e).slice(-4000) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Dem laufenden Agenten einen neuen Auftrag schicken (Selbstreparatur-Schleife). */
|
||||
async function sendPrompt(sessionID: string, text: string): Promise<boolean> {
|
||||
try {
|
||||
await client.session.prompt({
|
||||
path: { id: sessionID },
|
||||
body: { parts: [{ type: "text", text }] },
|
||||
})
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// ── 1. Werkzeug-Zaun ───────────────────────────────────────────────────
|
||||
"tool.execute.before": async (input: any, output: any) => {
|
||||
if (FENCE_OFF) return
|
||||
if (input?.tool !== "bash") return
|
||||
const cmd: string = output?.args?.command ?? ""
|
||||
if (!cmd) return
|
||||
for (const rule of FENCE) {
|
||||
if (rule.rx.test(cmd)) {
|
||||
await announce(
|
||||
"[Zaun]",
|
||||
`Ich habe einen Befehl geblockt: ${rule.why}`,
|
||||
"silent",
|
||||
)
|
||||
// Werfen = OpenCode bricht genau diesen Werkzeugaufruf ab und gibt dem
|
||||
// Modell den Grund zurueck. Der Agent arbeitet weiter, nur anders.
|
||||
throw new Error(
|
||||
`[MC2-ZAUN] Blockiert: ${rule.why}\n` +
|
||||
`Befehl war: ${cmd}\n` +
|
||||
`Waehle einen anderen Weg. Wenn das wirklich noetig ist, sag es dem Menschen — ` +
|
||||
`er macht es selbst.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ── 2.-4. Ereignisse ───────────────────────────────────────────────────
|
||||
event: async ({ event }: any) => {
|
||||
const type: string = event?.type ?? ""
|
||||
const props: any = event?.properties ?? event ?? {}
|
||||
const sessionID: string = props.sessionID || props.sessionId || props.id || ""
|
||||
|
||||
// ── Savepoint statt Zusammenfassen ──────────────────────────────────
|
||||
if (type === "session.compacted" || type === "experimental.session.compacting") {
|
||||
await announce(
|
||||
"[Sitzung]",
|
||||
"Die Sitzung wurde komprimiert — ich lasse den Stand in SAVEPOINT.md sichern.",
|
||||
"silent",
|
||||
)
|
||||
if (sessionID) {
|
||||
await sendPrompt(
|
||||
sessionID,
|
||||
"[MC2-GOVERNOR] Der Kontext wurde gerade komprimiert — dabei gehen still " +
|
||||
"Regeln und Details verloren. Aktualisiere JETZT SAVEPOINT.md: was wirklich " +
|
||||
"erledigt ist (nur was im Code steht), der genaue naechste Schritt, offene " +
|
||||
"Fragen, Stolpersteine. Committe die Datei. Danach arbeite normal weiter.",
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ── Pruef-Tor + Selbstreparatur ─────────────────────────────────────
|
||||
if (type !== "session.idle" || !sessionID) return
|
||||
if (busy.has(sessionID)) return
|
||||
|
||||
const cmd = await readVerify()
|
||||
if (!cmd) return // Kein VERIFY im Projekt -> Pruef-Tor bewusst aus.
|
||||
|
||||
busy.add(sessionID)
|
||||
try {
|
||||
const { ok, out } = await runVerify(cmd)
|
||||
const round = rounds.get(sessionID) ?? 0
|
||||
|
||||
if (ok) {
|
||||
rounds.delete(sessionID)
|
||||
await announce("[Pruefung]", "Etappe fertig und die Tests sind gruen.", "normal")
|
||||
return
|
||||
}
|
||||
|
||||
if (!AUTOFIX || round >= MAX_ROUNDS) {
|
||||
rounds.delete(sessionID)
|
||||
await announce(
|
||||
"[Pruefung]",
|
||||
`Die Tests sind rot und ich habe ${round} Reparaturversuche verbraucht. ` +
|
||||
`Hier komme ich allein nicht weiter, Commander.`,
|
||||
"normal",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
rounds.set(sessionID, round + 1)
|
||||
await announce(
|
||||
"[Pruefung]",
|
||||
`Tests rot — ich repariere selbst weiter, Runde ${round + 1} von ${MAX_ROUNDS}.`,
|
||||
"silent",
|
||||
)
|
||||
await sendPrompt(
|
||||
sessionID,
|
||||
`[MC2-PRUEFTOR] Deine Etappe gilt noch NICHT als fertig: der Verify-Befehl des ` +
|
||||
`Projekts ist fehlgeschlagen.\n\n` +
|
||||
`Befehl: ${cmd}\n\n` +
|
||||
`Ausgabe (Ende):\n\`\`\`\n${out}\n\`\`\`\n\n` +
|
||||
`Behebe die Ursache — nicht das Symptom, und schalte keinen Test ab. ` +
|
||||
`Wenn du fertig bist, melde dich normal; ich pruefe dann erneut. ` +
|
||||
`(Reparaturrunde ${round + 1} von ${MAX_ROUNDS}.)`,
|
||||
)
|
||||
} finally {
|
||||
busy.delete(sessionID)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default MC2Governor
|
||||
@@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys, re, shutil, time
|
||||
|
||||
if len(sys.argv) < 5:
|
||||
print("Nutzung: auto_adopt.py <config_path> <rolle> <new_hf_id> <new_gguf_path> [new_mmproj_path]")
|
||||
sys.exit(1)
|
||||
|
||||
config_path = sys.argv[1]
|
||||
rolle = sys.argv[2]
|
||||
new_hf_id = sys.argv[3]
|
||||
new_gguf_path = sys.argv[4]
|
||||
new_mmproj_path = sys.argv[5] if len(sys.argv) > 5 else ""
|
||||
|
||||
shutil.copy(config_path, config_path + f".bak.{time.strftime('%Y%m%d%H%M%S')}")
|
||||
content = open(config_path, "r", encoding="utf-8").read()
|
||||
|
||||
blocks = re.split(r'\n ([a-zA-Z0-9_.-]+):\n', "\n" + content)
|
||||
new_content = blocks[0].lstrip("\n")
|
||||
adopted = False
|
||||
|
||||
for i in range(1, len(blocks), 2):
|
||||
m_name = blocks[i]
|
||||
m_content = blocks[i+1]
|
||||
|
||||
if re.search(r'aliases:\s*\n(?:\s+- [^\n]+\n)*\s+- ' + re.escape(rolle) + r'\b', m_content):
|
||||
m_content = re.sub(r'(-m\s+)[^\s]+', r'\g<1>' + new_gguf_path, m_content)
|
||||
if new_mmproj_path:
|
||||
if "--mmproj" in m_content:
|
||||
m_content = re.sub(r'(--mmproj\s+)[^\s]+', r'\g<1>' + new_mmproj_path, m_content)
|
||||
else:
|
||||
m_content = re.sub(r'(-m\s+[^\s]+)', r'\g<1> --mmproj ' + new_mmproj_path, m_content)
|
||||
else:
|
||||
m_content = re.sub(r'\s*--mmproj\s+[^\s]+', '', m_content)
|
||||
|
||||
adopted = True
|
||||
print(f"Auto-Adoption: {rolle} -> {new_hf_id} (ersetzt in {m_name})")
|
||||
|
||||
new_content += f" {m_name}:\n{m_content}"
|
||||
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write(new_content)
|
||||
|
||||
if not adopted:
|
||||
print(f"Warnung: Rolle {rolle} in der config nicht gefunden. Auto-Adoption übersprungen.")
|
||||
sys.exit(1)
|
||||
@@ -1,581 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# Prüfstand-Harness (17.07.2026): fährt ECHTE Prüfungen gegen ein OpenAI-kompatibles
|
||||
# Endpoint (llama-swap :8080 für Amtsinhaber, Bench-Port :5899 für Kandidaten) und
|
||||
# bewertet MECHANISCH — bestanden/durchgefallen statt Bauchgefühl.
|
||||
#
|
||||
# Suiten (deploy/pruefstand/suites/):
|
||||
# coding Aufgabe → Modell schreibt Code → echte Unit-Tests laufen im Sandkasten
|
||||
# agent Mehrschritt-Aufgabe mit Werkzeugen (Dateien lesen/schreiben/löschen im
|
||||
# Sandkasten, Tool-Calling-API) → bewertet am ENDZUSTAND + Tool-Fehlerquote
|
||||
# recherche Fragen gegen mitgelieferte Dokumente → Pflicht-Stichworte + wörtliches
|
||||
# Zitat, das mechanisch gegen die Dokumente geprüft wird (Ehrlichkeits-Gate)
|
||||
# deutsch Lucy-Alltags-Turns → Richter-Modell (Zwei-Kritiker-Muster) vergibt Noten
|
||||
# vision generiertes Testbild beschreiben (nur für Vision-Rollen)
|
||||
# speed kurze + lange Probe, pp/tg aus den llama.cpp-Timings
|
||||
#
|
||||
# Nutzung:
|
||||
# python3 harness.py --endpoint http://127.0.0.1:8080/v1 --model hermes \
|
||||
# --suites coding,agent --out ergebnis.json [--judge-model gpt-oss-120b] \
|
||||
# [--deadline-min 90] [--vision-image pfad.png]
|
||||
#
|
||||
# Lehren eingebaut: Reasoning-Modelle brauchen großzügiges max_tokens (Verdikt 14.07.:
|
||||
# knappes Budget = leerer content, nie "kaputt"); Timings stehen im "timings"-Feld der
|
||||
# llama.cpp-Antwort; jeder API-Fehler = Aufgabe durchgefallen mit Grund, nie Abbruch.
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unicodedata
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
HIER = os.path.dirname(os.path.abspath(__file__))
|
||||
SUITES_DIR = os.path.join(HIER, "suites")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- HTTP-Helfer
|
||||
def api_chat(endpoint, model, messages, max_tokens=1800, tools=None, timeout=420,
|
||||
temperature=0.1):
|
||||
"""Ein Chat-Call. Gibt (message-dict, timings-dict, fehler-string) zurück."""
|
||||
body = {"model": model, "messages": messages, "max_tokens": max_tokens,
|
||||
"temperature": temperature}
|
||||
if tools:
|
||||
body["tools"] = tools
|
||||
body["tool_choice"] = "auto"
|
||||
req = urllib.request.Request(
|
||||
endpoint.rstrip("/") + "/chat/completions",
|
||||
data=json.dumps(body).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
d = json.loads(r.read().decode("utf-8", "replace"))
|
||||
msg = d["choices"][0]["message"]
|
||||
return msg, d.get("timings") or {}, ""
|
||||
except Exception as e: # Timeout, HTTP-Fehler, kaputtes JSON — alles ehrlich melden
|
||||
return None, {}, f"{type(e).__name__}: {e}"
|
||||
|
||||
|
||||
def inhalt_von(msg):
|
||||
"""content zuerst, reasoning_content als Notnagel (Reasoning-Modelle).
|
||||
<think>-Blöcke werden entfernt: rohe :8080-Aufrufe an Denk-Modelle (Qwen3.6)
|
||||
liefern den Grübel-Text im content mit — bewertet wird NUR die Antwort
|
||||
(Baseline-Befund 17.07.: Richter benotete sonst Denk-Monologe)."""
|
||||
if not msg:
|
||||
return ""
|
||||
text = (msg.get("content") or msg.get("reasoning_content") or "")
|
||||
text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
|
||||
# abgeschnittenes Denken ohne Schluss-Tag: ehrlich als leer werten
|
||||
text = re.sub(r"<think>.*$", "", text, flags=re.DOTALL)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def norm(s):
|
||||
"""Normalisierung fürs Zitat-Gate (identisch zur idle-radar-Mechanik)."""
|
||||
s = unicodedata.normalize("NFKC", s or "")
|
||||
s = "".join(ch for ch in s if ch.isalnum() or ch.isspace())
|
||||
return " ".join(s.lower().split())
|
||||
|
||||
|
||||
def code_block(text, sprache="python"):
|
||||
"""Letzten passenden Code-Block extrahieren; ohne Zaun: ganzen Text nehmen."""
|
||||
bloecke = re.findall(rf"```(?:{re.escape(sprache)})?\s*\n(.*?)```",
|
||||
text or "", re.DOTALL | re.IGNORECASE)
|
||||
if bloecke:
|
||||
return bloecke[-1]
|
||||
bloecke = re.findall(r"```\s*\n(.*?)```", text or "", re.DOTALL)
|
||||
return bloecke[-1] if bloecke else (text or "")
|
||||
|
||||
|
||||
def lade_task(pfad):
|
||||
with open(pfad, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Suite: coding
|
||||
def suite_coding(cfg, ergebnisse):
|
||||
basis = os.path.join(SUITES_DIR, "coding", "aufgaben")
|
||||
for tid in sorted(os.listdir(basis)):
|
||||
tdir = os.path.join(basis, tid)
|
||||
if not os.path.isfile(os.path.join(tdir, "task.json")):
|
||||
continue
|
||||
if cfg.abgelaufen():
|
||||
ergebnisse.append({"suite": "coding", "id": tid, "pass": False,
|
||||
"grund": "Zeitbudget erschöpft — übersprungen"})
|
||||
continue
|
||||
task = lade_task(os.path.join(tdir, "task.json"))
|
||||
t0 = time.time()
|
||||
prompt = task["prompt"]
|
||||
for name, inhalt in (task.get("dateien") or {}).items():
|
||||
prompt += f"\n\n--- Datei `{name}` (Ist-Stand) ---\n```python\n{inhalt}\n```"
|
||||
prompt += ("\n\nAntworte mit GENAU EINEM ```python-Code-Block, der den "
|
||||
f"vollständigen Inhalt der Datei `{task['antwort_datei']}` enthält. "
|
||||
"Keine Erklärungen außerhalb des Blocks.")
|
||||
msg, tim, err = api_chat(cfg.endpoint, cfg.model,
|
||||
[{"role": "user", "content": prompt}],
|
||||
max_tokens=task.get("max_tokens", 2600))
|
||||
eintrag = {"suite": "coding", "id": tid, "dauer_s": round(time.time() - t0, 1),
|
||||
"tg_tps": tim.get("predicted_per_second"),
|
||||
"pp_tps": tim.get("prompt_per_second")}
|
||||
if err:
|
||||
eintrag.update({"pass": False, "grund": f"API-Fehler: {err}"})
|
||||
ergebnisse.append(eintrag)
|
||||
continue
|
||||
code = code_block(inhalt_von(msg))
|
||||
sbx = tempfile.mkdtemp(prefix="pruefstand-coding-")
|
||||
try:
|
||||
for name, inhalt in (task.get("dateien") or {}).items():
|
||||
with open(os.path.join(sbx, name), "w", encoding="utf-8") as f:
|
||||
f.write(inhalt)
|
||||
with open(os.path.join(sbx, task["antwort_datei"]), "w",
|
||||
encoding="utf-8") as f:
|
||||
f.write(code)
|
||||
shutil.copy(os.path.join(tdir, "verify.py"), os.path.join(sbx, "verify.py"))
|
||||
p = subprocess.run([sys.executable, "verify.py"], cwd=sbx,
|
||||
capture_output=True, text=True,
|
||||
timeout=task.get("timeout_s", 120))
|
||||
eintrag["pass"] = (p.returncode == 0)
|
||||
if p.returncode != 0:
|
||||
eintrag["grund"] = (p.stdout + p.stderr).strip()[-400:]
|
||||
except subprocess.TimeoutExpired:
|
||||
eintrag.update({"pass": False, "grund": "verify-Timeout (Endlosschleife?)"})
|
||||
except Exception as e:
|
||||
eintrag.update({"pass": False, "grund": f"Sandkasten-Fehler: {e}"})
|
||||
finally:
|
||||
shutil.rmtree(sbx, ignore_errors=True)
|
||||
ergebnisse.append(eintrag)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Suite: agent
|
||||
WERKZEUGE = [
|
||||
{"type": "function", "function": {
|
||||
"name": "liste_dateien",
|
||||
"description": "Listet alle Dateien im Arbeitsverzeichnis (rekursiv).",
|
||||
"parameters": {"type": "object", "properties": {}}}},
|
||||
{"type": "function", "function": {
|
||||
"name": "lies_datei",
|
||||
"description": "Liest eine Datei aus dem Arbeitsverzeichnis.",
|
||||
"parameters": {"type": "object", "properties": {
|
||||
"pfad": {"type": "string"}}, "required": ["pfad"]}}},
|
||||
{"type": "function", "function": {
|
||||
"name": "schreibe_datei",
|
||||
"description": "Schreibt/überschreibt eine Datei im Arbeitsverzeichnis.",
|
||||
"parameters": {"type": "object", "properties": {
|
||||
"pfad": {"type": "string"}, "inhalt": {"type": "string"}},
|
||||
"required": ["pfad", "inhalt"]}}},
|
||||
{"type": "function", "function": {
|
||||
"name": "loesche_datei",
|
||||
"description": "Löscht eine Datei im Arbeitsverzeichnis.",
|
||||
"parameters": {"type": "object", "properties": {
|
||||
"pfad": {"type": "string"}}, "required": ["pfad"]}}},
|
||||
{"type": "function", "function": {
|
||||
"name": "fertig",
|
||||
"description": "Aufgabe abgeschlossen. Kurze Abschluss-Meldung übergeben.",
|
||||
"parameters": {"type": "object", "properties": {
|
||||
"meldung": {"type": "string"}}, "required": ["meldung"]}}},
|
||||
]
|
||||
|
||||
|
||||
def sicherer_pfad(sbx, pfad):
|
||||
"""Pfad-Ausbruch verhindern — Sandkasten bleibt Sandkasten."""
|
||||
ziel = os.path.realpath(os.path.join(sbx, pfad or ""))
|
||||
if not ziel.startswith(os.path.realpath(sbx) + os.sep):
|
||||
raise ValueError(f"Pfad verlässt den Sandkasten: {pfad}")
|
||||
return ziel
|
||||
|
||||
|
||||
def werkzeug_ausfuehren(sbx, name, args):
|
||||
if name == "liste_dateien":
|
||||
alle = []
|
||||
for wurzel, _, dateien in os.walk(sbx):
|
||||
for d in dateien:
|
||||
alle.append(os.path.relpath(os.path.join(wurzel, d), sbx))
|
||||
return "\n".join(sorted(alle)) or "(leer)"
|
||||
if name == "lies_datei":
|
||||
with open(sicherer_pfad(sbx, args["pfad"]), encoding="utf-8",
|
||||
errors="replace") as f:
|
||||
return f.read()[:20000]
|
||||
if name == "schreibe_datei":
|
||||
ziel = sicherer_pfad(sbx, args["pfad"])
|
||||
os.makedirs(os.path.dirname(ziel), exist_ok=True)
|
||||
with open(ziel, "w", encoding="utf-8") as f:
|
||||
f.write(args.get("inhalt", ""))
|
||||
return f"geschrieben: {args['pfad']}"
|
||||
if name == "loesche_datei":
|
||||
os.remove(sicherer_pfad(sbx, args["pfad"]))
|
||||
return f"gelöscht: {args['pfad']}"
|
||||
raise ValueError(f"unbekanntes Werkzeug: {name}")
|
||||
|
||||
|
||||
def suite_agent(cfg, ergebnisse):
|
||||
basis = os.path.join(SUITES_DIR, "agent", "aufgaben")
|
||||
for tid in sorted(os.listdir(basis)):
|
||||
tdir = os.path.join(basis, tid)
|
||||
if not os.path.isfile(os.path.join(tdir, "task.json")):
|
||||
continue
|
||||
if cfg.abgelaufen():
|
||||
ergebnisse.append({"suite": "agent", "id": tid, "pass": False,
|
||||
"grund": "Zeitbudget erschöpft — übersprungen"})
|
||||
continue
|
||||
task = lade_task(os.path.join(tdir, "task.json"))
|
||||
t0 = time.time()
|
||||
sbx = tempfile.mkdtemp(prefix="pruefstand-agent-")
|
||||
tool_fehler = 0
|
||||
schritte = 0
|
||||
try:
|
||||
for name, inhalt in (task.get("sandkasten") or {}).items():
|
||||
ziel = os.path.join(sbx, name)
|
||||
os.makedirs(os.path.dirname(ziel), exist_ok=True)
|
||||
with open(ziel, "w", encoding="utf-8") as f:
|
||||
f.write(inhalt)
|
||||
messages = [
|
||||
{"role": "system", "content":
|
||||
"Du bist ein Arbeits-Agent mit Datei-Werkzeugen in einem "
|
||||
"Sandkasten-Verzeichnis. Erledige die Aufgabe Schritt für Schritt "
|
||||
"mit den Werkzeugen und rufe am Ende `fertig` auf."},
|
||||
{"role": "user", "content": task["prompt"]},
|
||||
]
|
||||
fertig_gemeldet = False
|
||||
for _ in range(task.get("max_schritte", 8)):
|
||||
schritte += 1
|
||||
msg, _tim, err = api_chat(cfg.endpoint, cfg.model, messages,
|
||||
max_tokens=1800, tools=WERKZEUGE)
|
||||
if err:
|
||||
break
|
||||
calls = msg.get("tool_calls") or []
|
||||
if not calls:
|
||||
break # Agent redet statt zu handeln — Endzustand zählt trotzdem
|
||||
messages.append({"role": "assistant",
|
||||
"content": msg.get("content") or "",
|
||||
"tool_calls": calls})
|
||||
for call in calls:
|
||||
fn = (call.get("function") or {}).get("name") or "?"
|
||||
try:
|
||||
args = json.loads((call.get("function") or {})
|
||||
.get("arguments") or "{}")
|
||||
except Exception:
|
||||
args, fn_ok = {}, False
|
||||
tool_fehler += 1
|
||||
antwort = "FEHLER: Argumente waren kein gültiges JSON."
|
||||
else:
|
||||
fn_ok = True
|
||||
if fn_ok:
|
||||
if fn == "fertig":
|
||||
fertig_gemeldet = True
|
||||
antwort = "ok"
|
||||
else:
|
||||
try:
|
||||
antwort = werkzeug_ausfuehren(sbx, fn, args)
|
||||
except Exception as e:
|
||||
tool_fehler += 1
|
||||
antwort = f"FEHLER: {e}"
|
||||
messages.append({"role": "tool",
|
||||
"tool_call_id": call.get("id") or "0",
|
||||
"content": antwort})
|
||||
if fertig_gemeldet:
|
||||
break
|
||||
# -------- Endzustand prüfen (die einzige Wahrheit)
|
||||
fehlschlaege = []
|
||||
for check in task.get("checks", []):
|
||||
if "nicht_vorhanden" in check:
|
||||
if os.path.exists(os.path.join(sbx, check["nicht_vorhanden"])):
|
||||
fehlschlaege.append(
|
||||
f"{check['nicht_vorhanden']} existiert noch")
|
||||
continue
|
||||
pfad = os.path.join(sbx, check["datei"])
|
||||
if not os.path.isfile(pfad):
|
||||
fehlschlaege.append(f"{check['datei']} fehlt")
|
||||
continue
|
||||
with open(pfad, encoding="utf-8", errors="replace") as f:
|
||||
text = f.read()
|
||||
if check.get("regex") and not re.search(check["regex"], text,
|
||||
re.IGNORECASE | re.MULTILINE):
|
||||
fehlschlaege.append(
|
||||
f"{check['datei']}: Muster fehlt ({check['regex']})")
|
||||
ergebnisse.append({
|
||||
"suite": "agent", "id": tid, "pass": not fehlschlaege,
|
||||
"grund": "; ".join(fehlschlaege)[:400] if fehlschlaege else "",
|
||||
"schritte": schritte, "tool_fehler": tool_fehler,
|
||||
"fertig_gemeldet": fertig_gemeldet,
|
||||
"dauer_s": round(time.time() - t0, 1)})
|
||||
except Exception as e:
|
||||
ergebnisse.append({"suite": "agent", "id": tid, "pass": False,
|
||||
"grund": f"Harness-Fehler: {e}",
|
||||
"dauer_s": round(time.time() - t0, 1)})
|
||||
finally:
|
||||
shutil.rmtree(sbx, ignore_errors=True)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Suite: recherche
|
||||
def suite_recherche(cfg, ergebnisse):
|
||||
basis = os.path.join(SUITES_DIR, "recherche")
|
||||
dok_dir = os.path.join(basis, "dokumente")
|
||||
dokumente = {}
|
||||
for d in sorted(os.listdir(dok_dir)):
|
||||
with open(os.path.join(dok_dir, d), encoding="utf-8") as f:
|
||||
dokumente[d] = f.read()
|
||||
alle_norm = norm("\n".join(dokumente.values()))
|
||||
material = "\n\n".join(f"===== DOKUMENT {n} =====\n{t}"
|
||||
for n, t in dokumente.items())
|
||||
for tid in sorted(os.listdir(os.path.join(basis, "aufgaben"))):
|
||||
tpfad = os.path.join(basis, "aufgaben", tid)
|
||||
if not tpfad.endswith(".json"):
|
||||
continue
|
||||
if cfg.abgelaufen():
|
||||
ergebnisse.append({"suite": "recherche", "id": tid, "pass": False,
|
||||
"grund": "Zeitbudget erschöpft — übersprungen"})
|
||||
continue
|
||||
task = lade_task(tpfad)
|
||||
t0 = time.time()
|
||||
prompt = (f"{material}\n\nFRAGE: {task['frage']}\n\n"
|
||||
"Beantworte die Frage NUR aus den Dokumenten oben, auf Deutsch, "
|
||||
"in höchstens 4 Sätzen. Letzte Zeile deiner Antwort: "
|
||||
'ZITAT: "<eine wörtlich aus einem Dokument übernommene Zeile, '
|
||||
'die deine Antwort belegt>"')
|
||||
msg, tim, err = api_chat(cfg.endpoint, cfg.model,
|
||||
[{"role": "user", "content": prompt}],
|
||||
max_tokens=2800)
|
||||
eintrag = {"suite": "recherche", "id": task.get("id", tid),
|
||||
"dauer_s": round(time.time() - t0, 1),
|
||||
"tg_tps": tim.get("predicted_per_second"),
|
||||
"pp_tps": tim.get("prompt_per_second")}
|
||||
if err:
|
||||
eintrag.update({"pass": False, "grund": f"API-Fehler: {err}"})
|
||||
ergebnisse.append(eintrag)
|
||||
continue
|
||||
antwort = inhalt_von(msg)
|
||||
fehlschlaege = []
|
||||
for gruppe in task.get("muss_gruppen", []):
|
||||
if not any(k.lower() in antwort.lower() for k in gruppe):
|
||||
fehlschlaege.append(f"Pflicht-Stichwort fehlt: {'/'.join(gruppe)}")
|
||||
m = re.search(r'ZITAT:\s*[„"]?(.+?)["“”]?\s*$', antwort,
|
||||
re.MULTILINE | re.DOTALL)
|
||||
zitat = (m.group(1).strip() if m else "")
|
||||
if task.get("zitat_pflicht", True):
|
||||
if not zitat:
|
||||
fehlschlaege.append("kein ZITAT geliefert")
|
||||
elif norm(zitat) not in alle_norm:
|
||||
fehlschlaege.append("ZITAT nicht wörtlich in den Dokumenten "
|
||||
"(Ehrlichkeits-Gate)")
|
||||
eintrag["pass"] = not fehlschlaege
|
||||
if fehlschlaege:
|
||||
eintrag["grund"] = "; ".join(fehlschlaege)[:400]
|
||||
eintrag["antwort_auszug"] = antwort[:300]
|
||||
ergebnisse.append(eintrag)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Suite: deutsch
|
||||
RICHTER_RASTER = (
|
||||
"Du bist ein strenger Deutsch-Richter für eine Sprach-Assistentin (Lucy): "
|
||||
"locker, natürlich, Du-Form, kurze gesprochene Sätze, kein Beamten- oder "
|
||||
"Übersetzungsdeutsch, keine Anglizismen-Ketten, fachlich korrekt. Bewerte die "
|
||||
"ANTWORT unten auf einer Skala 1–10 (10 = klingt wie eine deutsche "
|
||||
"Muttersprachlerin im Alltag). Antworte EXAKT in diesem Format:\n"
|
||||
"NOTE: <Zahl 1-10>\nBEGRUENDUNG: <ein Satz>")
|
||||
|
||||
|
||||
def suite_deutsch(cfg, ergebnisse):
|
||||
aufgaben = lade_task(os.path.join(SUITES_DIR, "deutsch", "aufgaben.json"))
|
||||
for task in aufgaben:
|
||||
if cfg.abgelaufen():
|
||||
ergebnisse.append({"suite": "deutsch", "id": task["id"], "pass": False,
|
||||
"grund": "Zeitbudget erschöpft — übersprungen"})
|
||||
continue
|
||||
t0 = time.time()
|
||||
msg, tim, err = api_chat(cfg.endpoint, cfg.model,
|
||||
[{"role": "user", "content": task["prompt"]}],
|
||||
max_tokens=2400)
|
||||
eintrag = {"suite": "deutsch", "id": task["id"],
|
||||
"dauer_s": round(time.time() - t0, 1),
|
||||
"tg_tps": tim.get("predicted_per_second"),
|
||||
"pp_tps": tim.get("prompt_per_second")}
|
||||
if err:
|
||||
eintrag.update({"pass": False, "grund": f"API-Fehler: {err}"})
|
||||
ergebnisse.append(eintrag)
|
||||
continue
|
||||
antwort = inhalt_von(msg)
|
||||
jmsg, _jt, jerr = api_chat(cfg.judge_endpoint, cfg.judge_model, [
|
||||
{"role": "system", "content": RICHTER_RASTER},
|
||||
{"role": "user", "content":
|
||||
f"AUFGABE AN DIE ASSISTENTIN: {task['prompt']}\n\n"
|
||||
f"ANTWORT:\n{antwort}"}],
|
||||
max_tokens=2200, temperature=0.2)
|
||||
note = None
|
||||
if not jerr:
|
||||
m = re.search(r"NOTE:\s*(\d{1,2})", inhalt_von(jmsg))
|
||||
if m:
|
||||
note = max(1, min(10, int(m.group(1))))
|
||||
eintrag["note"] = note
|
||||
eintrag["pass"] = (note is not None and note >= 6)
|
||||
if note is None:
|
||||
eintrag["grund"] = f"Richter ohne Note ({jerr or 'Format'})"
|
||||
if not eintrag["pass"]:
|
||||
eintrag["antwort_auszug"] = antwort[:300]
|
||||
ergebnisse.append(eintrag)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Suite: vision
|
||||
def suite_vision(cfg, ergebnisse):
|
||||
task = lade_task(os.path.join(SUITES_DIR, "vision", "task.json"))
|
||||
bild = cfg.vision_image or os.path.join(SUITES_DIR, "vision", "testbild.png")
|
||||
if not os.path.isfile(bild):
|
||||
# Testbild deterministisch erzeugen (reines Python, kein PIL nötig)
|
||||
subprocess.run([sys.executable,
|
||||
os.path.join(SUITES_DIR, "vision", "gen_testbild.py"), bild],
|
||||
check=False, timeout=30)
|
||||
if not os.path.isfile(bild):
|
||||
ergebnisse.append({"suite": "vision", "id": "testbild", "pass": False,
|
||||
"grund": "Testbild fehlt und konnte nicht erzeugt werden"})
|
||||
return
|
||||
with open(bild, "rb") as f:
|
||||
b64 = base64.b64encode(f.read()).decode("ascii")
|
||||
t0 = time.time()
|
||||
msg, tim, err = api_chat(cfg.endpoint, cfg.model, [
|
||||
{"role": "user", "content": [
|
||||
{"type": "text", "text": task["prompt"]},
|
||||
{"type": "image_url",
|
||||
"image_url": {"url": f"data:image/png;base64,{b64}"}}]}],
|
||||
max_tokens=900, timeout=600)
|
||||
eintrag = {"suite": "vision", "id": "testbild",
|
||||
"dauer_s": round(time.time() - t0, 1),
|
||||
"tg_tps": tim.get("predicted_per_second")}
|
||||
if err:
|
||||
eintrag.update({"pass": False, "grund": f"API-Fehler: {err}"})
|
||||
ergebnisse.append(eintrag)
|
||||
return
|
||||
antwort = inhalt_von(msg)
|
||||
fehlschlaege = [f"nicht erkannt: {'/'.join(g)}"
|
||||
for g in task.get("muss_gruppen", [])
|
||||
if not any(k.lower() in antwort.lower() for k in g)]
|
||||
eintrag["pass"] = not fehlschlaege
|
||||
if fehlschlaege:
|
||||
eintrag["grund"] = "; ".join(fehlschlaege)[:300]
|
||||
eintrag["antwort_auszug"] = antwort[:200]
|
||||
ergebnisse.append(eintrag)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Suite: speed
|
||||
LANG_BAUSTEIN = ("Die Box protokolliert jeden Dienststart mit Zeitstempel, "
|
||||
"Dienstname und Ergebnis, damit die Morgenlage Abweichungen "
|
||||
"gegen die Vorwoche erkennen und einordnen kann. ")
|
||||
|
||||
|
||||
def suite_speed(cfg, ergebnisse):
|
||||
# Kurz-Probe: misst tg (Alltags-Turn) — 2 Läufe, erster wärmt den Cache an.
|
||||
for lauf in ("warm", "kurz"):
|
||||
t0 = time.time()
|
||||
_msg, tim, err = api_chat(cfg.endpoint, cfg.model, [
|
||||
{"role": "user", "content":
|
||||
"Erkläre in drei kurzen Sätzen, was ein Mixture-of-Experts-Modell "
|
||||
"ist."}], max_tokens=200, temperature=0)
|
||||
if lauf == "warm":
|
||||
continue
|
||||
ergebnisse.append({"suite": "speed", "id": "kurz", "pass": not err,
|
||||
"grund": err[:200] if err else "",
|
||||
"dauer_s": round(time.time() - t0, 1),
|
||||
"tg_tps": tim.get("predicted_per_second"),
|
||||
"pp_tps": tim.get("prompt_per_second")})
|
||||
# Lang-Probe: ~12k-Token-Prompt → misst pp bei echtem Kontext (ROCm-Watch-Metrik).
|
||||
if cfg.abgelaufen():
|
||||
ergebnisse.append({"suite": "speed", "id": "lang", "pass": False,
|
||||
"grund": "Zeitbudget erschöpft — übersprungen"})
|
||||
return
|
||||
lang = LANG_BAUSTEIN * 550
|
||||
t0 = time.time()
|
||||
_msg, tim, err = api_chat(cfg.endpoint, cfg.model, [
|
||||
{"role": "user", "content":
|
||||
lang + "\n\nWie oft steht sinngemäß derselbe Satz oben? Antworte in "
|
||||
"einem Satz."}], max_tokens=80, temperature=0, timeout=900)
|
||||
ergebnisse.append({"suite": "speed", "id": "lang", "pass": not err,
|
||||
"grund": err[:200] if err else "",
|
||||
"dauer_s": round(time.time() - t0, 1),
|
||||
"tg_tps": tim.get("predicted_per_second"),
|
||||
"pp_tps": tim.get("prompt_per_second"),
|
||||
"prompt_tokens": tim.get("prompt_n")})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- Hauptlauf
|
||||
SUITE_FUNKS = {"coding": suite_coding, "agent": suite_agent,
|
||||
"recherche": suite_recherche, "deutsch": suite_deutsch,
|
||||
"vision": suite_vision, "speed": suite_speed}
|
||||
|
||||
|
||||
class Konfig:
|
||||
def __init__(self, args):
|
||||
self.endpoint = args.endpoint
|
||||
self.model = args.model
|
||||
self.judge_endpoint = args.judge_endpoint or args.endpoint
|
||||
self.judge_model = args.judge_model
|
||||
self.vision_image = args.vision_image
|
||||
self.deadline = (time.time() + args.deadline_min * 60
|
||||
if args.deadline_min else None)
|
||||
|
||||
def abgelaufen(self):
|
||||
return self.deadline is not None and time.time() > self.deadline
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Prüfstand-Harness")
|
||||
ap.add_argument("--endpoint", required=True)
|
||||
ap.add_argument("--model", required=True)
|
||||
ap.add_argument("--suites", required=True,
|
||||
help="kommagetrennt: coding,agent,recherche,deutsch,vision,speed")
|
||||
ap.add_argument("--out", required=True)
|
||||
ap.add_argument("--judge-endpoint", default="")
|
||||
ap.add_argument("--judge-model", default="gpt-oss-120b")
|
||||
ap.add_argument("--vision-image", default="")
|
||||
ap.add_argument("--deadline-min", type=float, default=0)
|
||||
args = ap.parse_args()
|
||||
|
||||
cfg = Konfig(args)
|
||||
ergebnisse = []
|
||||
for name in [s.strip() for s in args.suites.split(",") if s.strip()]:
|
||||
if name not in SUITE_FUNKS:
|
||||
ergebnisse.append({"suite": name, "id": "-", "pass": False,
|
||||
"grund": "unbekannte Suite"})
|
||||
continue
|
||||
try:
|
||||
SUITE_FUNKS[name](cfg, ergebnisse)
|
||||
except Exception as e: # kaputte Suite darf den Gesamtlauf nicht töten
|
||||
ergebnisse.append({"suite": name, "id": "-", "pass": False,
|
||||
"grund": f"Suite-Absturz: {type(e).__name__}: {e}"})
|
||||
|
||||
# -------- Zusammenfassung je Suite (bestanden/gesamt + Tempo-Mittel)
|
||||
zsm = {}
|
||||
for e in ergebnisse:
|
||||
s = zsm.setdefault(e["suite"], {"bestanden": 0, "gesamt": 0,
|
||||
"tg": [], "pp": [], "noten": []})
|
||||
s["gesamt"] += 1
|
||||
s["bestanden"] += 1 if e.get("pass") else 0
|
||||
if e.get("tg_tps"):
|
||||
s["tg"].append(e["tg_tps"])
|
||||
if e.get("pp_tps"):
|
||||
s["pp"].append(e["pp_tps"])
|
||||
if e.get("note") is not None:
|
||||
s["noten"].append(e["note"])
|
||||
for s in zsm.values():
|
||||
s["tg_mittel"] = round(sum(s["tg"]) / len(s["tg"]), 1) if s["tg"] else None
|
||||
s["pp_mittel"] = round(sum(s["pp"]) / len(s["pp"]), 1) if s["pp"] else None
|
||||
s["note_mittel"] = (round(sum(s["noten"]) / len(s["noten"]), 1)
|
||||
if s["noten"] else None)
|
||||
del s["tg"], s["pp"], s["noten"]
|
||||
|
||||
with open(args.out, "w", encoding="utf-8") as f:
|
||||
json.dump({"modell": args.model, "endpoint": args.endpoint,
|
||||
"erhoben": time.strftime("%Y-%m-%d %H:%M"),
|
||||
"zusammenfassung": zsm, "einzeln": ergebnisse},
|
||||
f, ensure_ascii=False, indent=1)
|
||||
# Kurzfassung auf stdout (für Runner-Logs)
|
||||
for name, s in zsm.items():
|
||||
extra = f" · Note {s['note_mittel']}" if s.get("note_mittel") else ""
|
||||
tempo = (f" · tg {s['tg_mittel']} t/s" if s.get("tg_mittel") else "")
|
||||
print(f"{name}: {s['bestanden']}/{s['gesamt']}{extra}{tempo}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,468 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prüfstand-Runner (Täglich 02:00, ehemals Wöchentlich): Baut die Umgebung für den
|
||||
# Harness (Modell-Sandkasten, GPU-RAM-Check) und zieht die Fäden.
|
||||
# Läuft als Cron (Täglich 02:00, nach Radar/Checkout), nimmt Kandidaten aus der Queue
|
||||
# und vergleicht sie gegen die BASELINE der Amtsinhaber.
|
||||
#
|
||||
# Modi:
|
||||
# (ohne Argument) Cron-Modus: Baseline fehlt → Baseline messen; sonst ältesten
|
||||
# Kandidaten aus der Queue prüfen; nichts zu tun → still.
|
||||
# --baseline Amtsinhaber über llama-swap (:8080) neu vermessen.
|
||||
# --kandidat <spec> einen Kandidaten-Spec (JSON) direkt prüfen.
|
||||
#
|
||||
# Leitplanken (User-Entscheid 17.07.): max 1 Kandidat je Lauf · Download-Deckel 35 GB
|
||||
# (größere Kandidaten → Karte, Download erst nach Klick) · eigener Cache mit
|
||||
# Auto-Aufräumen · Live-Betrieb bleibt unangetastet (eigener Server auf :5899) ·
|
||||
# der Prüfstand EMPFIEHLT NUR — Rollen-Wechsel entscheidet der Commander per Karte.
|
||||
# RAM-Wache vor dem Serverstart; Heredoc+Pipe-Falle vermieden (Temp-Dateien, Lehre 10.07.).
|
||||
set -uo pipefail
|
||||
|
||||
MC="$HOME/mission-control-v2"
|
||||
PSD="$MC/deploy/pruefstand"
|
||||
VAULT="$HOME/wissens-vault"
|
||||
STATE="$HOME/.hermes/state/pruefstand"
|
||||
CACHE="/srv/models/pruefstand-cache"
|
||||
HERMES="$HOME/.local/bin/hermes"
|
||||
BENCH_BIN="${BENCH_BIN:-/opt/llamacpp-vulkan/llama-server}"
|
||||
BENCH_PORT="${BENCH_PORT:-5899}"
|
||||
LIVE="http://127.0.0.1:8080/v1"
|
||||
JUDGE_MODEL="${PRUEFSTAND_JUDGE:-gpt-oss-120b}"
|
||||
DECKEL_GB="${PRUEFSTAND_DECKEL_GB:-100}"
|
||||
DEADLINE_MIN="${PRUEFSTAND_DEADLINE_MIN:-100}"
|
||||
|
||||
mkdir -p "$STATE/queue" "$STATE/done" "$STATE/ergebnisse"
|
||||
|
||||
# Suiten je Rolle (Amtsinhaber-Alias = Rollenname in llama-swap)
|
||||
suiten_fuer() {
|
||||
case "$1" in
|
||||
hermes) echo "speed,agent,recherche,deutsch" ;;
|
||||
coder) echo "speed,coding,agent" ;;
|
||||
heavy) echo "speed,coding,recherche,deutsch" ;;
|
||||
vision) echo "speed,vision" ;;
|
||||
scout) echo "speed,agent,vision" ;;
|
||||
*) echo "speed,recherche,deutsch" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
briefkasten() { # $1=Betreff $2=Text (stiller Chronik-Spiegel, source=pruefstand)
|
||||
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(python3 - "$1" "$2" <<'PY'
|
||||
import json, sys
|
||||
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
|
||||
"source": "pruefstand", "priority": "silent"}))
|
||||
PY
|
||||
)" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
engine_build() { # installierte llama.cpp-Build-Nummer (leer, wenn nicht ermittelbar)
|
||||
LD_LIBRARY_PATH="$(dirname "$BENCH_BIN")" "$BENCH_BIN" --version 2>&1 \
|
||||
| grep -o 'version: [0-9]*' | grep -o '[0-9]*' || true
|
||||
}
|
||||
|
||||
aggregiere_baseline() { # $1=Engine-Build — baut baseline.json aus den Rollen-Dateien
|
||||
python3 - "$STATE" "${1:-}" <<'PY'
|
||||
import glob, json, os, sys, time
|
||||
state, build = sys.argv[1], sys.argv[2]
|
||||
basis = {}
|
||||
for f in glob.glob(os.path.join(state, "ergebnisse", "baseline-*.json")):
|
||||
rolle = os.path.basename(f)[len("baseline-"):-len(".json")]
|
||||
try:
|
||||
basis[rolle] = json.load(open(f)).get("zusammenfassung", {})
|
||||
except Exception:
|
||||
pass
|
||||
json.dump({"erhoben": time.strftime("%Y-%m-%d %H:%M"), "engine": build,
|
||||
"rollen": basis},
|
||||
open(os.path.join(state, "baseline.json"), "w"), ensure_ascii=False, indent=1)
|
||||
print(f"\nBasislinie gespeichert ({len(basis)} Rollen, Engine b{build or '?'}).")
|
||||
PY
|
||||
}
|
||||
|
||||
# Tempo-Teil der Baseline neu messen (Lücke 2, 17.07.): Engine-Updates (So 04:30)
|
||||
# verschieben das Tempo der Amtsinhaber — die Qualitäts-Zahlen bleiben gültig, aber
|
||||
# Tempo-Vergleiche gegen eine alte Engine wären Geister-Zahlen.
|
||||
speed_refresh() { # $1=neue Engine-Build-Nummer
|
||||
local BUILD="$1"
|
||||
echo "## PRÜFSTAND: Tempo-Baseline-Refresh (Engine jetzt b${BUILD}) am $(date '+%d.%m.%Y %H:%M')"
|
||||
local rolle TMP
|
||||
for rolle in hermes coder heavy vision scout; do
|
||||
TMP="$STATE/ergebnisse/refresh-${rolle}.json"
|
||||
echo "### $rolle"
|
||||
python3 "$PSD/harness.py" --endpoint "$LIVE" --model "$rolle" --suites speed \
|
||||
--out "$TMP" --judge-endpoint "$LIVE" --judge-model "$JUDGE_MODEL" \
|
||||
--deadline-min 10 2>&1 | tail -2
|
||||
python3 - "$STATE/ergebnisse/baseline-${rolle}.json" "$TMP" <<'PY'
|
||||
import json, sys
|
||||
try:
|
||||
basis = json.load(open(sys.argv[1]))
|
||||
neu = json.load(open(sys.argv[2]))
|
||||
except Exception:
|
||||
raise SystemExit
|
||||
if "speed" in (neu.get("zusammenfassung") or {}):
|
||||
basis.setdefault("zusammenfassung", {})["speed"] = neu["zusammenfassung"]["speed"]
|
||||
basis["einzeln"] = ([e for e in basis.get("einzeln", []) if e.get("suite") != "speed"]
|
||||
+ [e for e in neu.get("einzeln", []) if e.get("suite") == "speed"])
|
||||
json.dump(basis, open(sys.argv[1], "w"), ensure_ascii=False, indent=1)
|
||||
PY
|
||||
rm -f "$TMP"
|
||||
done
|
||||
aggregiere_baseline "$BUILD"
|
||||
briefkasten "Prüfstand" "Tempo-Baseline nach Engine-Wechsel neu gemessen (jetzt b${BUILD})."
|
||||
}
|
||||
|
||||
zusammenfassung_lesen() { # $1=ergebnis.json → kompakte Zeilen "suite: x/y · Note · tg"
|
||||
python3 - "$1" <<'PY'
|
||||
import json, sys
|
||||
try:
|
||||
z = json.load(open(sys.argv[1])).get("zusammenfassung", {})
|
||||
except Exception:
|
||||
print("(Ergebnis nicht lesbar)"); raise SystemExit
|
||||
for name, s in z.items():
|
||||
teile = [f"{s['bestanden']}/{s['gesamt']}"]
|
||||
if s.get("note_mittel"): teile.append(f"Note {s['note_mittel']}")
|
||||
if s.get("tg_mittel"): teile.append(f"tg {s['tg_mittel']} t/s")
|
||||
if s.get("pp_mittel"): teile.append(f"pp {s['pp_mittel']} t/s")
|
||||
print(f" {name}: " + " · ".join(teile))
|
||||
PY
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- Baseline
|
||||
baseline_lauf() {
|
||||
echo "## PRÜFSTAND: Basislinie der Amtsinhaber (erhoben am $(date '+%d.%m.%Y %H:%M'))"
|
||||
echo "(Echte Prüfungen: Coding mit Unit-Tests, Agenten-Aufgaben, Recherche mit Zitat-Gate, Deutsch-Richter, Tempo.)"
|
||||
local rollen="hermes coder heavy vision scout"
|
||||
for rolle in $rollen; do
|
||||
local out="$STATE/ergebnisse/baseline-${rolle}.json"
|
||||
echo
|
||||
echo "### Amtsinhaber \`$rolle\`"
|
||||
python3 "$PSD/harness.py" --endpoint "$LIVE" --model "$rolle" \
|
||||
--suites "$(suiten_fuer "$rolle")" --out "$out" \
|
||||
--judge-endpoint "$LIVE" --judge-model "$JUDGE_MODEL" \
|
||||
--deadline-min 30 2>&1 | tail -8
|
||||
done
|
||||
# embed: nur Funktions- und Latenz-Probe (Suiten ergeben hier keinen Sinn)
|
||||
echo
|
||||
echo "### Amtsinhaber \`embed\`"
|
||||
python3 - "$LIVE" <<'PY'
|
||||
import json, sys, time, urllib.request
|
||||
t0 = time.time()
|
||||
req = urllib.request.Request(sys.argv[1].rstrip("/") + "/embeddings",
|
||||
data=json.dumps({"model": "embed", "input": "Prüfstand-Probe"}).encode(),
|
||||
headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
d = json.load(r)
|
||||
dim = len(d["data"][0]["embedding"])
|
||||
print(f" ok · Dimension {dim} · {round((time.time()-t0)*1000)} ms")
|
||||
except Exception as e:
|
||||
print(f" FEHLER: {e}")
|
||||
PY
|
||||
# Sammel-Baseline schreiben (eine Datei, die Kandidaten-Läufe vergleichen)
|
||||
aggregiere_baseline "$(engine_build)"
|
||||
briefkasten "Prüfstand" "Basislinie der Amtsinhaber erhoben — künftige Kandidaten werden dagegen verglichen."
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- Kandidat
|
||||
kandidat_lauf() { # $1=spec.json
|
||||
local SPEC="$1"
|
||||
local KEY ROLLE HF_ID WARUM
|
||||
KEY="$(jq -r '.key' "$SPEC")"
|
||||
ROLLE="$(jq -r '.rolle' "$SPEC")"
|
||||
HF_ID="$(jq -r '.hf_id' "$SPEC")"
|
||||
WARUM="$(jq -r '.warum // ""' "$SPEC")"
|
||||
echo "## PRÜFSTAND: Kandidat \`$HF_ID\` für Rolle \`$ROLLE\` (am $(date '+%d.%m.%Y %H:%M'))"
|
||||
[ -n "$WARUM" ] && echo "Anlass: $WARUM"
|
||||
|
||||
if [ -f "$STATE/done/${KEY}.json" ]; then
|
||||
echo "Schon geprüft (State done/${KEY}) — nichts zu tun."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ---- GGUF im HF-Repo finden (bevorzugt ~Q4, ein Stück, Deckel prüfen)
|
||||
local TREE_JSON="$STATE/tmp-tree-${KEY}.json"
|
||||
curl -sf --max-time 60 \
|
||||
"https://huggingface.co/api/models/${HF_ID}/tree/main?recursive=true" \
|
||||
-o "$TREE_JSON" || { echo "HF-API nicht erreichbar — Abbruch, Spec bleibt in der Queue."; return 1; }
|
||||
local AUSWAHL
|
||||
AUSWAHL="$(python3 - "$TREE_JSON" "$DECKEL_GB" <<'PY'
|
||||
import json, re, sys
|
||||
baum = json.load(open(sys.argv[1]))
|
||||
deckel = float(sys.argv[2]) * 2**30
|
||||
dateien = [(e.get("path",""), (e.get("lfs") or {}).get("size") or e.get("size") or 0)
|
||||
for e in baum if isinstance(e, dict)]
|
||||
ggufs = [(p, g) for p, g in dateien if p.lower().endswith(".gguf")]
|
||||
mmproj = next((p for p, g in ggufs if "mmproj" in p.lower()), "")
|
||||
haupt = [(p, g) for p, g in ggufs if "mmproj" not in p.lower()]
|
||||
# Split-GGUFs (…-00001-of-…) klammern wir in v1 aus — sauberer Einzeldatei-Download
|
||||
haupt = [(p, g) for p, g in haupt if not re.search(r"-\d{5}-of-\d{5}", p)]
|
||||
prio = ["q4_k_m", "ud-q4_k_m", "q4_k_s", "iq4", "q5_k_m", "q4", "q8_0"]
|
||||
def rang(p):
|
||||
pl = p.lower()
|
||||
for i, q in enumerate(prio):
|
||||
if q in pl:
|
||||
return i
|
||||
return len(prio)
|
||||
haupt.sort(key=lambda t: (rang(t[0]), t[1]))
|
||||
if not haupt:
|
||||
print("KEINE"); raise SystemExit
|
||||
pfad, groesse = haupt[0]
|
||||
status = "OK" if groesse and groesse <= deckel else "ZU_GROSS"
|
||||
print(status); print(pfad); print(groesse or 0); print(mmproj)
|
||||
PY
|
||||
)"
|
||||
rm -f "$TREE_JSON"
|
||||
local STATUS PFAD GROESSE MMPROJ
|
||||
STATUS="$(printf '%s\n' "$AUSWAHL" | sed -n 1p)"
|
||||
PFAD="$(printf '%s\n' "$AUSWAHL" | sed -n 2p)"
|
||||
GROESSE="$(printf '%s\n' "$AUSWAHL" | sed -n 3p)"
|
||||
MMPROJ="$(printf '%s\n' "$AUSWAHL" | sed -n 4p)"
|
||||
local GB=$(( ${GROESSE:-0} / 1073741824 ))
|
||||
|
||||
if [ "$STATUS" = "KEINE" ]; then
|
||||
echo "Kein brauchbares Einzel-GGUF im Repo (nur Splits oder gar keins) — Karte statt Download."
|
||||
"$HERMES" kanban create "Prüfstand: ${HF_ID} braucht Handarbeit (kein Einzel-GGUF)" \
|
||||
--body "Kandidat für Rolle ${ROLLE}. Im HF-Repo liegt kein Einzeldatei-GGUF ≤ ${DECKEL_GB} GB (vermutlich Split-Dateien). Bei Interesse manuell laden und 'bash ~/mission-control-v2/deploy/pruefstand/pruefstand.sh --kandidat <spec>' fahren. Anlass: ${WARUM}" \
|
||||
--triage --created-by pruefstand --idempotency-key "pruefstand-${KEY}" >/dev/null 2>&1 || true
|
||||
mv "$SPEC" "$STATE/done/${KEY}.spec.json"
|
||||
echo "{\"status\":\"kein_gguf\"}" > "$STATE/done/${KEY}.json"
|
||||
return 0
|
||||
fi
|
||||
if [ "$STATUS" = "ZU_GROSS" ]; then
|
||||
echo "GGUF ${PFAD} ist ${GB} GB > Deckel ${DECKEL_GB} GB — Download wartet auf deinen Klick (Karte liegt bereit)."
|
||||
"$HERMES" kanban create "Prüfstand: ${HF_ID} (${GB} GB) — Download freigeben?" \
|
||||
--body "Kandidat für Rolle ${ROLLE}, Datei ${PFAD} (${GB} GB) liegt ÜBER dem Autonomie-Deckel von ${DECKEL_GB} GB (Entscheid 17.07.). Nach Freigabe: Spec wieder in ~/.hermes/state/pruefstand/queue/ legen und PRUEFSTAND_DECKEL_GB erhöhen oder manuell laden. Anlass: ${WARUM}" \
|
||||
--triage --created-by pruefstand --idempotency-key "pruefstand-${KEY}" >/dev/null 2>&1 || true
|
||||
mv "$SPEC" "$STATE/done/${KEY}.spec.json"
|
||||
echo "{\"status\":\"zu_gross\",\"gb\":${GB}}" > "$STATE/done/${KEY}.json"
|
||||
briefkasten "Prüfstand" "Kandidat ${HF_ID} (${GB} GB) wartet auf Download-Freigabe (Deckel ${DECKEL_GB} GB)."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ---- Platz- und RAM-Wache
|
||||
local FREI_GB
|
||||
FREI_GB="$(df --output=avail -BG /srv/models | tail -1 | tr -dc '0-9')"
|
||||
if [ "${FREI_GB:-0}" -lt $(( GB + 20 )) ]; then
|
||||
echo "Zu wenig Platz auf /srv/models (${FREI_GB} GB frei, gebraucht ~$((GB+20))) — Lauf verschoben, Spec bleibt in der Queue."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ---- Download in den Cache (mit Resume, Auto-Aufräumen am Ende)
|
||||
mkdir -p "$CACHE/$KEY"
|
||||
local ZIEL="$CACHE/$KEY/$(basename "$PFAD")"
|
||||
echo "Lade ${PFAD} (${GB} GB) …"
|
||||
curl -fL --retry 3 -C - --max-time 7200 -o "$ZIEL" \
|
||||
"https://huggingface.co/${HF_ID}/resolve/main/${PFAD}" \
|
||||
|| { echo "Download fehlgeschlagen — Spec bleibt in der Queue."; return 1; }
|
||||
local MM_ZIEL=""
|
||||
if [ -n "$MMPROJ" ] && { [ "$ROLLE" = "vision" ] || [ "$ROLLE" = "scout" ]; }; then
|
||||
MM_ZIEL="$CACHE/$KEY/$(basename "$MMPROJ")"
|
||||
curl -fL --retry 3 -C - --max-time 3600 -o "$MM_ZIEL" \
|
||||
"https://huggingface.co/${HF_ID}/resolve/main/${MMPROJ}" || MM_ZIEL=""
|
||||
fi
|
||||
|
||||
local MEM_FREI_GB
|
||||
MEM_FREI_GB="$(awk '/MemAvailable/{printf "%d", $2/1048576}' /proc/meminfo)"
|
||||
if [ "${MEM_FREI_GB:-0}" -lt $(( GB + 8 )) ]; then
|
||||
echo "Zu wenig freier RAM (${MEM_FREI_GB} GB) für ein ${GB}-GB-Modell neben dem Warm-Set — Lauf verschoben."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# ---- Kandidaten-Server auf dem Bench-Port (Live-Betrieb unangetastet)
|
||||
echo "Starte Kandidaten-Server (:${BENCH_PORT}) …"
|
||||
local MMFLAG=""
|
||||
[ -n "$MM_ZIEL" ] && MMFLAG="--mmproj $MM_ZIEL"
|
||||
$BENCH_BIN -m "$ZIEL" --host 127.0.0.1 --port "$BENCH_PORT" \
|
||||
-c 32768 -ngl 999 -fa on --no-mmap --jinja $MMFLAG \
|
||||
>/tmp/pruefstand-server.log 2>&1 &
|
||||
local SPID=$!
|
||||
trap 'kill $SPID 2>/dev/null; wait $SPID 2>/dev/null' RETURN
|
||||
local BEREIT=0
|
||||
for i in $(seq 1 240); do
|
||||
curl -s --max-time 2 "http://127.0.0.1:$BENCH_PORT/health" | grep -q ok && { BEREIT=1; break; }
|
||||
sleep 2
|
||||
kill -0 $SPID 2>/dev/null || break
|
||||
done
|
||||
if [ "$BEREIT" != "1" ]; then
|
||||
echo "LADE-CRASH — Kandidat startet nicht (Architektur zu neu für die Engine?):"
|
||||
tail -3 /tmp/pruefstand-server.log
|
||||
echo "{\"status\":\"lade_crash\"}" > "$STATE/done/${KEY}.json"
|
||||
mv "$SPEC" "$STATE/done/${KEY}.spec.json"
|
||||
rm -rf "${CACHE:?}/$KEY"
|
||||
briefkasten "Prüfstand" "Kandidat ${HF_ID} startet auf unserer Engine nicht (Lade-Crash) — aussortiert."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ---- Das volle Programm
|
||||
local OUT="$STATE/ergebnisse/kandidat-${KEY}.json"
|
||||
echo "Fahre Suiten: $(suiten_fuer "$ROLLE") (Zeitbudget ${DEADLINE_MIN} min)"
|
||||
python3 "$PSD/harness.py" --endpoint "http://127.0.0.1:$BENCH_PORT/v1" \
|
||||
--model "kandidat" --suites "$(suiten_fuer "$ROLLE")" --out "$OUT" \
|
||||
--judge-endpoint "$LIVE" --judge-model "$JUDGE_MODEL" \
|
||||
--deadline-min "$DEADLINE_MIN" 2>&1 | tail -8
|
||||
kill $SPID 2>/dev/null; wait $SPID 2>/dev/null; trap - RETURN
|
||||
|
||||
# ---- MTP-Fairness (Lücke 1, 17.07.): das Live-Hirn läuft MIT draft-mtp (+26 %) —
|
||||
# ein Kandidat mit eingebautem MTP-Kopf würde ohne Zusatzmessung systematisch
|
||||
# unterschätzt. Erkennung: Dateiname ODER nextn-Tensoren im GGUF-Kopf.
|
||||
if printf '%s' "$PFAD" | grep -qi 'mtp' || head -c 8000000 "$ZIEL" 2>/dev/null | grep -aq 'nextn'; then
|
||||
echo "MTP-Kopf erkannt — Zusatzmessung mit draft-mtp (Fairness gegen das Live-Hirn) …"
|
||||
$BENCH_BIN -m "$ZIEL" --host 127.0.0.1 --port "$BENCH_PORT" \
|
||||
-c 32768 -ngl 999 -fa on --no-mmap --jinja $MMFLAG \
|
||||
--spec-type draft-mtp --spec-draft-n-max 3 \
|
||||
>/tmp/pruefstand-server-mtp.log 2>&1 &
|
||||
local MPID=$!
|
||||
trap 'kill $MPID 2>/dev/null; wait $MPID 2>/dev/null' RETURN
|
||||
local MBEREIT=0
|
||||
for i in $(seq 1 120); do
|
||||
curl -s --max-time 2 "http://127.0.0.1:$BENCH_PORT/health" | grep -q ok && { MBEREIT=1; break; }
|
||||
sleep 2
|
||||
kill -0 $MPID 2>/dev/null || break
|
||||
done
|
||||
if [ "$MBEREIT" = "1" ]; then
|
||||
local MOUT="$STATE/ergebnisse/kandidat-${KEY}-mtp.json"
|
||||
python3 "$PSD/harness.py" --endpoint "http://127.0.0.1:$BENCH_PORT/v1" \
|
||||
--model "kandidat" --suites speed --out "$MOUT" \
|
||||
--judge-endpoint "$LIVE" --judge-model "$JUDGE_MODEL" --deadline-min 10 2>&1 | tail -2
|
||||
python3 - "$OUT" "$MOUT" <<'PY'
|
||||
import json, sys
|
||||
haupt = json.load(open(sys.argv[1]))
|
||||
try:
|
||||
s = (json.load(open(sys.argv[2])).get("zusammenfassung") or {}).get("speed")
|
||||
except Exception:
|
||||
s = None
|
||||
if s:
|
||||
haupt.setdefault("zusammenfassung", {})["speed_mtp"] = s
|
||||
json.dump(haupt, open(sys.argv[1], "w"), ensure_ascii=False, indent=1)
|
||||
PY
|
||||
else
|
||||
echo "Kandidat startet mit draft-mtp NICHT — Zusatzmessung entfällt (im Steckbrief zählt der Basis-Wert)."
|
||||
fi
|
||||
kill $MPID 2>/dev/null; wait $MPID 2>/dev/null; trap - RETURN
|
||||
fi
|
||||
|
||||
# ---- Steckbrief: Kandidat vs. Amtsinhaber
|
||||
local BERICHT="$STATE/ergebnisse/kandidat-${KEY}.md"
|
||||
local SIEGER_FLAG="$STATE/tmp-sieger-${KEY}"
|
||||
rm -f "$SIEGER_FLAG"
|
||||
python3 - "$OUT" "$STATE/baseline.json" "$ROLLE" "$HF_ID" "$WARUM" "$BERICHT" "$SIEGER_FLAG" <<'PY'
|
||||
import json, sys, time
|
||||
kand = json.load(open(sys.argv[1]))
|
||||
try:
|
||||
basis = json.load(open(sys.argv[2]))["rollen"].get(sys.argv[3], {})
|
||||
except Exception:
|
||||
basis = {}
|
||||
rolle, hf_id, warum, ziel = sys.argv[3], sys.argv[4], sys.argv[5], sys.argv[6]
|
||||
kz = kand.get("zusammenfassung", {})
|
||||
zeilen = [f"# Prüfstand-Steckbrief: {hf_id}",
|
||||
f"Rolle: **{rolle}** · erhoben {time.strftime('%d.%m.%Y %H:%M')}",
|
||||
f"Anlass: {warum}" if warum else "", "",
|
||||
"| Suite | Kandidat | Amtsinhaber |", "|---|---|---|"]
|
||||
punkte_k = punkte_b = faelle = 0
|
||||
for name in sorted(set(kz) | set(basis)):
|
||||
def fmt(s):
|
||||
if not s: return "—"
|
||||
t = f"{s['bestanden']}/{s['gesamt']}"
|
||||
if s.get("note_mittel"): t += f" · Note {s['note_mittel']}"
|
||||
if s.get("tg_mittel"): t += f" · tg {s['tg_mittel']}"
|
||||
if s.get("pp_mittel"): t += f" · pp {s['pp_mittel']}"
|
||||
return t
|
||||
zeilen.append(f"| {name} | {fmt(kz.get(name))} | {fmt(basis.get(name))} |")
|
||||
if name in kz and name in basis and not name.startswith("speed"):
|
||||
faelle += 1
|
||||
punkte_k += kz[name]["bestanden"] / max(1, kz[name]["gesamt"])
|
||||
punkte_b += basis[name]["bestanden"] / max(1, basis[name]["gesamt"])
|
||||
# Tempo: best-of speed/speed_mtp — die Baseline ist live MIT MTP gemessen (Fairness)
|
||||
tg_k = max(((kz.get(s) or {}).get("tg_mittel") or 0) for s in ("speed", "speed_mtp"))
|
||||
tg_b = (basis.get("speed") or {}).get("tg_mittel") or 0
|
||||
zeilen.append("")
|
||||
if faelle:
|
||||
qk, qb = punkte_k / faelle, punkte_b / faelle
|
||||
tempo = (f"Tempo {round(100*tg_k/tg_b-100):+d} % vs. Amtsinhaber" if tg_k and tg_b
|
||||
else "Tempo-Vergleich unvollständig")
|
||||
if qk >= qb and tg_k >= 0.8 * (tg_b or tg_k):
|
||||
urteil = "KANDIDAT SIEHT STARK AUS — Auto-Adoption greift (Etappe E5)."
|
||||
open(sys.argv[7], "w").write("1")
|
||||
elif qk >= qb:
|
||||
urteil = "Qualität hält mit, aber deutlich langsamer — vermutlich kein Aufsteiger."
|
||||
else:
|
||||
urteil = "Qualität unter Amtsinhaber — kein Aufsteiger."
|
||||
zeilen.append(f"**Einordnung:** Bestehensquote {qk:.0%} vs. {qb:.0%} · {tempo}. {urteil}")
|
||||
zeilen.append("")
|
||||
zeilen.append("_Auto-Adoption (Etappe E5): Der Prüfstand tauscht Sieger-Modelle nun selbstständig aus._")
|
||||
text = "\n".join(z for z in zeilen if z is not None)
|
||||
open(ziel, "w", encoding="utf-8").write(text + "\n")
|
||||
print(text)
|
||||
PY
|
||||
|
||||
# ---- Vault-Bericht + Karte + stille Chronik, dann Cache aufräumen
|
||||
mkdir -p "$VAULT/pruefstand"
|
||||
cp "$BERICHT" "$VAULT/pruefstand/${KEY}.md" 2>/dev/null || true
|
||||
( cd "$VAULT" 2>/dev/null && git add "pruefstand/${KEY}.md" >/dev/null 2>&1 \
|
||||
&& git commit -q -m "pruefstand: Steckbrief ${KEY}" >/dev/null 2>&1 ) || true
|
||||
"$HERMES" kanban create "Prüfstand-Ergebnis: ${HF_ID} für Rolle ${ROLLE}" \
|
||||
--body "$(cat "$BERICHT")" \
|
||||
--triage --created-by pruefstand --idempotency-key "pruefstand-${KEY}" >/dev/null 2>&1 || true
|
||||
briefkasten "Prüfstand" "Kandidat ${HF_ID} (Rolle ${ROLLE}) geprüft — Steckbrief im Auftrags-Kanban und Vault (pruefstand/${KEY}.md)."
|
||||
mv "$SPEC" "$STATE/done/${KEY}.spec.json" 2>/dev/null || true
|
||||
cp "$OUT" "$STATE/done/${KEY}.json" 2>/dev/null || echo "{\"status\":\"geprueft\"}" > "$STATE/done/${KEY}.json"
|
||||
|
||||
# ---- Auto-Adoption (Etappe E5)
|
||||
if [ -f "$SIEGER_FLAG" ]; then
|
||||
echo "Kandidat hat gewonnen — starte Auto-Adoption für Rolle $ROLLE..."
|
||||
local FINAL_DIR="/srv/models/${HF_ID//\//-}-GGUF"
|
||||
sudo -n mkdir -p "$FINAL_DIR"
|
||||
sudo -n mv "$ZIEL" "$FINAL_DIR/"
|
||||
local FINAL_GGUF="$FINAL_DIR/$(basename "$PFAD")"
|
||||
local FINAL_MMPROJ=""
|
||||
if [ -n "$MM_ZIEL" ]; then
|
||||
sudo -n mv "$MM_ZIEL" "$FINAL_DIR/"
|
||||
FINAL_MMPROJ="$FINAL_DIR/$(basename "$MMPROJ")"
|
||||
fi
|
||||
sudo -n chown -R hitonabi:hitonabi "$FINAL_DIR" 2>/dev/null || true
|
||||
|
||||
# Config updaten
|
||||
local CONFIG="$MC/deploy/llama-swap.config.yaml"
|
||||
python3 "$PSD/auto_adopt.py" "$CONFIG" "$ROLLE" "$HF_ID" "$FINAL_GGUF" "$FINAL_MMPROJ"
|
||||
|
||||
# Reload llama-swap (via worker oder systemctl)
|
||||
if systemctl --user is-active --quiet llama-swap; then
|
||||
systemctl --user restart llama-swap
|
||||
briefkasten "Prüfstand Auto-Adoption" "Rolle '$ROLLE' wurde erfolgreich durch '$HF_ID' ersetzt (Neustart erledigt)."
|
||||
else
|
||||
briefkasten "Prüfstand Auto-Adoption" "Rolle '$ROLLE' wurde durch '$HF_ID' ersetzt (llama-swap war inaktiv)."
|
||||
fi
|
||||
rm -f "$SIEGER_FLAG"
|
||||
fi
|
||||
|
||||
rm -rf "${CACHE:?}/$KEY"
|
||||
echo
|
||||
echo "(Cache aufgeräumt; Roh-Ergebnisse: $STATE/ergebnisse/kandidat-${KEY}.*)"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- Einstieg
|
||||
case "${1:-}" in
|
||||
--baseline)
|
||||
baseline_lauf
|
||||
;;
|
||||
--kandidat)
|
||||
[ -n "${2:-}" ] || { echo "Nutzung: pruefstand.sh --kandidat <spec.json>"; exit 1; }
|
||||
kandidat_lauf "$2"
|
||||
;;
|
||||
*)
|
||||
if [ ! -f "$STATE/baseline.json" ]; then
|
||||
baseline_lauf
|
||||
exit 0
|
||||
fi
|
||||
# Engine gewechselt seit der Baseline? → Tempo-Teil neu messen (Geister-Zahlen-Schutz)
|
||||
BUILD="$(engine_build)"
|
||||
BASE_BUILD="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("engine") or "")' "$STATE/baseline.json" 2>/dev/null || true)"
|
||||
if [ -n "$BUILD" ] && [ "$BUILD" != "$BASE_BUILD" ]; then
|
||||
speed_refresh "$BUILD"
|
||||
fi
|
||||
NAECHSTER="$(ls -1tr "$STATE/queue/"*.json 2>/dev/null | head -1 || true)"
|
||||
if [ -n "$NAECHSTER" ]; then
|
||||
kandidat_lauf "$NAECHSTER"
|
||||
fi
|
||||
# keine Queue + keine Engine-Drift → LEERER stdout → --no-agent-Cron bleibt still
|
||||
;;
|
||||
esac
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"prompt": "Im Arbeitsverzeichnis liegen Notiz-Dateien im Ordner `notizen/`. Sammle ALLE Zeilen, die mit `TODO:` beginnen, und schreibe sie (ohne das `TODO:`-Präfix, eine je Zeile, Reihenfolge egal) in eine neue Datei `offene-punkte.txt` im Wurzelverzeichnis. Rufe danach `fertig` auf.",
|
||||
"max_schritte": 8,
|
||||
"sandkasten": {
|
||||
"notizen/montag.txt": "Besprechung war ok.\nTODO: Backup-Log prüfen\nSonst nichts.\n",
|
||||
"notizen/dienstag.txt": "TODO: Firmware-Update Router\nWetter schlecht.\n",
|
||||
"notizen/mittwoch.txt": "Nichts offen heute.\n"
|
||||
},
|
||||
"checks": [
|
||||
{"datei": "offene-punkte.txt", "regex": "Backup-Log prüfen"},
|
||||
{"datei": "offene-punkte.txt", "regex": "Firmware-Update Router"}
|
||||
]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"prompt": "Die Datei `dienst/config.ini` ist kaputt. In `anleitung.txt` steht, welche Werte gelten sollen. Repariere die config.ini entsprechend (nur die falschen Werte ändern, Struktur beibehalten) und rufe danach `fertig` auf.",
|
||||
"max_schritte": 8,
|
||||
"sandkasten": {
|
||||
"anleitung.txt": "Sollwerte für dienst/config.ini:\n- port muss 9001 sein (Zahl, nicht Text!)\n- loglevel muss info sein\n- Der Eintrag host bleibt unverändert.\n",
|
||||
"dienst/config.ini": "[server]\nhost = 127.0.0.1\nport = abc\nloglevel = debug\n"
|
||||
},
|
||||
"checks": [
|
||||
{"datei": "dienst/config.ini", "regex": "^port\\s*=\\s*9001\\s*$"},
|
||||
{"datei": "dienst/config.ini", "regex": "^loglevel\\s*=\\s*info\\s*$"},
|
||||
{"datei": "dienst/config.ini", "regex": "^host\\s*=\\s*127\\.0\\.0\\.1\\s*$"}
|
||||
]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"prompt": "Lies die Messwerte in `messwerte.csv` (Spalten: zeit;temperatur_c). Erstelle einen Bericht `bericht.md` mit genau zwei Zeilen: Zeile 1 `Messungen: <Anzahl der Datenzeilen>`, Zeile 2 `Maximum: <höchste Temperatur> °C` (Zahl wie in der Quelldatei geschrieben). Rufe danach `fertig` auf.",
|
||||
"max_schritte": 8,
|
||||
"sandkasten": {
|
||||
"messwerte.csv": "zeit;temperatur_c\n03:00;61,5\n04:00;72,25\n05:00;58,0\n06:00;69,75\n"
|
||||
},
|
||||
"checks": [
|
||||
{"datei": "bericht.md", "regex": "Messungen:\\s*4"},
|
||||
{"datei": "bericht.md", "regex": "Maximum:\\s*72,25\\s*°?C"}
|
||||
]
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"prompt": "In `regeln.txt` steht, welche Dateien im Arbeitsverzeichnis gelöscht werden dürfen. Lösche GENAU die Dateien, die laut Regeln weg dürfen — nichts anderes. Rufe danach `fertig` auf.",
|
||||
"max_schritte": 10,
|
||||
"sandkasten": {
|
||||
"regeln.txt": "Aufräum-Regeln:\n1. Alle Dateien mit Endung .tmp dürfen gelöscht werden.\n2. Dateien, deren Name mit wichtig beginnt, dürfen NIE gelöscht werden.\n3. Alles andere bleibt liegen.\n",
|
||||
"cache-alt.tmp": "wegwerf-inhalt\n",
|
||||
"sitzung.tmp": "wegwerf-inhalt\n",
|
||||
"wichtig-vertrag.txt": "bleibt!\n",
|
||||
"notiz.md": "bleibt auch.\n"
|
||||
},
|
||||
"checks": [
|
||||
{"nicht_vorhanden": "cache-alt.tmp"},
|
||||
{"nicht_vorhanden": "sitzung.tmp"},
|
||||
{"datei": "wichtig-vertrag.txt", "regex": "bleibt!"},
|
||||
{"datei": "notiz.md", "regex": "bleibt auch"}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"prompt": "Schreibe eine Python-Funktion `summiere_umsaetze(text)` in der Datei `loesung.py`. Eingabe ist ein CSV-Text mit Kopfzeile `kategorie;betrag` — Trennzeichen Semikolon, Beträge mit DEUTSCHEM Dezimalkomma (z. B. `12,50`). Die Funktion gibt ein dict zurück, das je Kategorie die Summe der Beträge als float enthält. Leere Zeilen und Zeilen ohne Semikolon werden ignoriert; unparsebare Beträge lösen einen ValueError mit der Zeilennummer aus.",
|
||||
"antwort_datei": "loesung.py",
|
||||
"timeout_s": 60
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from loesung import summiere_umsaetze
|
||||
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def test_summen(self):
|
||||
text = ("kategorie;betrag\n"
|
||||
"essen;12,50\n"
|
||||
"essen;7,50\n"
|
||||
"\n"
|
||||
"strom;30,00\n")
|
||||
self.assertEqual(summiere_umsaetze(text),
|
||||
{"essen": 20.0, "strom": 30.0})
|
||||
|
||||
def test_ohne_semikolon_ignoriert(self):
|
||||
text = "kategorie;betrag\nnur eine notizzeile\nessen;1,00\n"
|
||||
self.assertEqual(summiere_umsaetze(text), {"essen": 1.0})
|
||||
|
||||
def test_kaputter_betrag(self):
|
||||
with self.assertRaises(ValueError):
|
||||
summiere_umsaetze("kategorie;betrag\nessen;zwoelf\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
r = unittest.main(exit=False).result
|
||||
sys.exit(0 if r.wasSuccessful() else 1)
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"prompt": "In der Datei `zeitfenster.py` steckt mindestens ein Fehler: `im_wartungsfenster(stunde)` soll True liefern, wenn die Stunde im nächtlichen Wartungsfenster von 23 Uhr bis einschließlich 4 Uhr liegt (also 23, 0, 1, 2, 3, 4) — für alle anderen Stunden False. Ungültige Stunden (<0 oder >23) sollen einen ValueError auslösen. Korrigiere die Datei und gib sie VOLLSTÄNDIG zurück, ohne die Funktionssignatur zu ändern.",
|
||||
"antwort_datei": "zeitfenster.py",
|
||||
"dateien": {
|
||||
"zeitfenster.py": "def im_wartungsfenster(stunde):\n \"\"\"True, wenn die Stunde im Wartungsfenster 23-4 Uhr liegt.\"\"\"\n if stunde < 0 or stunde > 24:\n raise ValueError(\"ungueltige Stunde\")\n return 23 <= stunde or stunde < 4\n"
|
||||
},
|
||||
"timeout_s": 60
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from zeitfenster import im_wartungsfenster
|
||||
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def test_im_fenster(self):
|
||||
for h in (23, 0, 1, 2, 3, 4):
|
||||
self.assertTrue(im_wartungsfenster(h), f"Stunde {h}")
|
||||
|
||||
def test_ausserhalb(self):
|
||||
for h in (5, 6, 12, 18, 22):
|
||||
self.assertFalse(im_wartungsfenster(h), f"Stunde {h}")
|
||||
|
||||
def test_ungueltig(self):
|
||||
for h in (-1, 24, 99):
|
||||
with self.assertRaises(ValueError):
|
||||
im_wartungsfenster(h)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
r = unittest.main(exit=False).result
|
||||
sys.exit(0 if r.wasSuccessful() else 1)
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"prompt": "Schreibe in `loesung.py` eine Funktion `fehler_zeilen(log_text)`. Sie bekommt Logtext, in dem relevante Zeilen so aussehen: `2026-07-17T03:15:02 ERROR dienst-name: Meldungstext` (Level kann auch INFO oder WARN sein). Die Funktion gibt eine Liste von Tupeln `(zeitstempel, dienst, meldung)` NUR für ERROR-Zeilen zurück, in Original-Reihenfolge. Zeilen in anderem Format werden still ignoriert. Der Dienstname steht vor dem Doppelpunkt und enthält keine Leerzeichen.",
|
||||
"antwort_datei": "loesung.py",
|
||||
"timeout_s": 60
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from loesung import fehler_zeilen
|
||||
|
||||
LOG = """2026-07-17T03:15:02 ERROR llama-swap: Modell nicht gefunden
|
||||
2026-07-17T03:15:03 INFO mc2: Start ok
|
||||
kaputte zeile ohne format
|
||||
2026-07-17T03:16:10 WARN voice: Latenz hoch
|
||||
2026-07-17T03:17:00 ERROR mem0-service: Timeout nach 30s
|
||||
"""
|
||||
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def test_nur_error(self):
|
||||
erg = fehler_zeilen(LOG)
|
||||
self.assertEqual(len(erg), 2)
|
||||
self.assertEqual(erg[0][0], "2026-07-17T03:15:02")
|
||||
self.assertEqual(erg[0][1], "llama-swap")
|
||||
self.assertEqual(erg[0][2], "Modell nicht gefunden")
|
||||
self.assertEqual(erg[1][1], "mem0-service")
|
||||
|
||||
def test_leer(self):
|
||||
self.assertEqual(fehler_zeilen("nur INFO hier\n"), [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
r = unittest.main(exit=False).result
|
||||
sys.exit(0 if r.wasSuccessful() else 1)
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"prompt": "Schreibe in `loesung.py` eine Klasse `Lager`. Konstruktor ohne Argumente. Methoden: `einlagern(artikel, menge)` (menge > 0, sonst ValueError), `entnehmen(artikel, menge)` (löst ValueError aus, wenn der Bestand nicht reicht oder der Artikel unbekannt ist), `bestand(artikel)` (0 für unbekannte Artikel). Bestände sind ganze Zahlen je Artikelname.",
|
||||
"antwort_datei": "loesung.py",
|
||||
"timeout_s": 60
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from loesung import Lager
|
||||
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def test_ablauf(self):
|
||||
l = Lager()
|
||||
l.einlagern("kabel", 10)
|
||||
l.entnehmen("kabel", 4)
|
||||
self.assertEqual(l.bestand("kabel"), 6)
|
||||
self.assertEqual(l.bestand("unbekannt"), 0)
|
||||
|
||||
def test_unterbestand(self):
|
||||
l = Lager()
|
||||
l.einlagern("kabel", 2)
|
||||
with self.assertRaises(ValueError):
|
||||
l.entnehmen("kabel", 3)
|
||||
with self.assertRaises(ValueError):
|
||||
l.entnehmen("gibtsnicht", 1)
|
||||
|
||||
def test_menge_positiv(self):
|
||||
l = Lager()
|
||||
with self.assertRaises(ValueError):
|
||||
l.einlagern("kabel", 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
r = unittest.main(exit=False).result
|
||||
sys.exit(0 if r.wasSuccessful() else 1)
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"prompt": "Schreibe in `loesung.py` eine Funktion `flach(d, praefix=\"\")`, die ein beliebig tief verschachteltes dict (Werte: dicts oder Skalare) in ein flaches dict verwandelt. Schlüsselpfade werden mit Punkt verbunden, z. B. macht `{\"a\": {\"b\": 1}, \"c\": 2}` daraus `{\"a.b\": 1, \"c\": 2}`. Leere dicts verschwinden ersatzlos. Nicht-dict-Eingaben lösen einen TypeError aus.",
|
||||
"antwort_datei": "loesung.py",
|
||||
"timeout_s": 60
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from loesung import flach
|
||||
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def test_verschachtelt(self):
|
||||
self.assertEqual(flach({"a": {"b": 1, "c": {"d": 2}}, "e": 3}),
|
||||
{"a.b": 1, "a.c.d": 2, "e": 3})
|
||||
|
||||
def test_leere_dicts(self):
|
||||
self.assertEqual(flach({"a": {}, "b": 1}), {"b": 1})
|
||||
|
||||
def test_typfehler(self):
|
||||
with self.assertRaises(TypeError):
|
||||
flach([1, 2])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
r = unittest.main(exit=False).result
|
||||
sys.exit(0 if r.wasSuccessful() else 1)
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"prompt": "Schreibe in `loesung.py` eine Funktion `slug(text)`: wandelt deutschen Text in einen URL-Slug. Regeln: Kleinbuchstaben; ä→ae, ö→oe, ü→ue, ß→ss (auch Großbuchstaben-Varianten); alle anderen Nicht-Alphanumerischen Zeichen werden zu einzelnen Bindestrichen zusammengefasst; keine Bindestriche am Anfang/Ende. Beispiel: `Größte \"Änderung\" 2026!` → `groesste-aenderung-2026`.",
|
||||
"antwort_datei": "loesung.py",
|
||||
"timeout_s": 60
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from loesung import slug
|
||||
|
||||
|
||||
class Tests(unittest.TestCase):
|
||||
def test_beispiel(self):
|
||||
self.assertEqual(slug('Größte "Änderung" 2026!'),
|
||||
"groesste-aenderung-2026")
|
||||
|
||||
def test_umlaute(self):
|
||||
self.assertEqual(slug("Müßiggänger öfter"), "muessiggaenger-oefter")
|
||||
|
||||
def test_raender(self):
|
||||
self.assertEqual(slug(" --Hallo Welt-- "), "hallo-welt")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
r = unittest.main(exit=False).result
|
||||
sys.exit(0 if r.wasSuccessful() else 1)
|
||||
@@ -1,19 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "alltag-erklaerung",
|
||||
"prompt": "Du bist eine deutsche Sprach-Assistentin. Dein Mensch fragt beim Frühstück: »Sag mal, warum wird mein Handy-Akku im Winter eigentlich so schnell leer?« Antworte gesprochen, locker, in 2–3 kurzen Sätzen, Du-Form."
|
||||
},
|
||||
{
|
||||
"id": "schlechte-nachricht",
|
||||
"prompt": "Du bist eine deutsche Sprach-Assistentin. Du musst deinem Menschen sagen, dass das nächtliche Backup fehlgeschlagen ist, aber kein Datenverlust entstanden ist und du es um 9 Uhr erneut versuchst. Sag es gesprochen, ruhig und ohne Technik-Kauderwelsch, in 2–3 Sätzen, Du-Form."
|
||||
},
|
||||
{
|
||||
"id": "terminplanung",
|
||||
"prompt": "Du bist eine deutsche Sprach-Assistentin. Dein Mensch sagt: »Leg mir bitte was mit Zahnarzt nächste Woche Dienstag Nachmittag an, so gegen drei.« Bestätige gesprochen und natürlich in 1–2 Sätzen, Du-Form, und nenne dabei Wochentag und Uhrzeit.",
|
||||
"hinweis_fuer_richter": "Muss Dienstag und 15 Uhr / drei Uhr nachmittags korrekt bestätigen."
|
||||
},
|
||||
{
|
||||
"id": "smalltalk-wetter",
|
||||
"prompt": "Du bist eine deutsche Sprach-Assistentin. Dein Mensch kommt durchnässt rein und sagt nur: »Boah, sag nichts.« Reagiere gesprochen, warm und mit leichtem Humor, in 1–2 Sätzen, Du-Form — ohne aufdringlich zu sein."
|
||||
}
|
||||
]
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"id": "zeitserver-ausfall",
|
||||
"frage": "Was passiert, wenn der Zeitserver der Werkhalle länger ausfällt, und wie heißt er?",
|
||||
"muss_gruppen": [["chronos-w"], ["Haltemodus"], ["90"]],
|
||||
"zitat_pflicht": true
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"id": "band-schluessel",
|
||||
"frage": "Warum kann ein Dieb mit den Sicherungsbändern aus Nordhof nichts anfangen?",
|
||||
"muss_gruppen": [["AES-256", "AES"], ["Schlüsseltresor", "Verwaltung"], ["verschlüsselt"]],
|
||||
"zitat_pflicht": true
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"id": "pv-anteil",
|
||||
"frage": "Wie viel Strom lieferte die Photovoltaik-Anlage im zweiten Quartal und welchen Anteil am Verbrauch deckte das?",
|
||||
"muss_gruppen": [["96"], ["Viertel", "25", "24", "23"]],
|
||||
"zitat_pflicht": true
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"id": "quer-wartungszugriff",
|
||||
"frage": "Ein externer Techniker will nachts um 01:45 Uhr auf eine Maschinensteuerung zugreifen. Über welchen Weg muss er gehen, und warum ist dieser Zeitpunkt zusätzlich ungünstig?",
|
||||
"muss_gruppen": [["falke"], ["Zwei-Faktor", "Zwei Faktor", "2-Faktor", "zweifaktor"], ["01:30", "Vollsicherung", "Sicherung"]],
|
||||
"zitat_pflicht": true
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
# Backup-Konzept Weststadt-Werkhalle (Auszug)
|
||||
|
||||
Gesichert wird dreistufig. Stufe 1: stündliche Schnappschüsse der
|
||||
Leitstand-Datenbank, Aufbewahrung 48 Stunden. Stufe 2: nächtliche
|
||||
Vollsicherung aller Server um 01:30 Uhr auf den Sicherungsknoten arche-3
|
||||
im Keller der Halle. Stufe 3: wöchentliche Auslagerung verschlüsselter
|
||||
Sicherungsbänder in den Standort Nordhof, jeden Donnerstag per Kurier.
|
||||
|
||||
Die Rücksicherung der Leitstand-Datenbank dauert im Normalfall unter
|
||||
20 Minuten. Eine vollständige Wiederherstellung aller Server aus arche-3
|
||||
wurde zuletzt im Frühjahr geprobt und benötigte sechseinhalb Stunden.
|
||||
|
||||
Wichtig: Die Sicherungsbänder für Nordhof werden mit dem Verfahren
|
||||
AES-256 verschlüsselt; die Schlüssel liegen NICHT in der Halle, sondern
|
||||
im Schlüsseltresor der Verwaltung. Ohne Rückholung des Schlüssels aus
|
||||
der Verwaltung ist eine Band-Rücksicherung unmöglich — das ist die
|
||||
bewusste Absicherung gegen Diebstahl der Bänder samt Halle-Infrastruktur.
|
||||
@@ -1,16 +0,0 @@
|
||||
# Energie-Quartalsbericht Weststadt-Werkhalle (Auszug)
|
||||
|
||||
Der Stromverbrauch der Halle lag im zweiten Quartal bei 412 Megawattstunden
|
||||
und damit acht Prozent unter dem Vorjahresquartal. Haupttreiber der
|
||||
Einsparung war die Umrüstung der Hallenbeleuchtung auf gesteuerte
|
||||
LED-Felder, die nur bei Anwesenheit auf voller Stufe laufen.
|
||||
|
||||
Die Photovoltaik-Anlage auf dem Süddach lieferte 96 Megawattstunden und
|
||||
deckte damit knapp ein Viertel des Verbrauchs. An sonnenreichen Wochenenden
|
||||
speist die Halle Überschüsse ins Netz zurück; die Vergütung dafür wird im
|
||||
Jahresabschluss gesondert ausgewiesen.
|
||||
|
||||
Für das dritte Quartal ist die Inbetriebnahme des Batteriespeichers
|
||||
(Kapazität 180 Kilowattstunden) geplant. Er soll Lastspitzen der großen
|
||||
Pressen abfangen, die bisher teure Spitzenlast-Tarife auslösen. Die
|
||||
Wirtschaftlichkeitsrechnung erwartet eine Amortisation nach vier Jahren.
|
||||
@@ -1,17 +0,0 @@
|
||||
# Netzwerk-Handbuch Weststadt-Werkhalle (Auszug)
|
||||
|
||||
Die Werkhalle Weststadt betreibt zwei getrennte Netze: das Betriebsnetz
|
||||
(10.40.0.0/16) für Maschinensteuerungen und das Büronetz (192.168.20.0/24)
|
||||
für Arbeitsplätze. Ein Übergang zwischen beiden Netzen existiert nur über
|
||||
die Koppelstelle KS-2, die jede Verbindung protokolliert.
|
||||
|
||||
Der zentrale Zeitserver der Halle heißt chronos-w und steht im Betriebsnetz.
|
||||
Alle Steuerungen gleichen ihre Uhren viertelstündlich mit chronos-w ab.
|
||||
Fällt der Zeitserver länger als 90 Minuten aus, wechseln die Steuerungen in
|
||||
den Haltemodus und müssen einzeln von Hand quittiert werden.
|
||||
|
||||
Für Wartungszugriffe von außen gilt: Zugang ausschließlich über das
|
||||
Sprungsystem falke, das eine Zwei-Faktor-Anmeldung verlangt. Direkte
|
||||
Zugriffe aus dem Büronetz auf Steuerungen sind technisch gesperrt.
|
||||
Die Sperrliste pflegt das Team Leitstand, Änderungen brauchen zwei
|
||||
Freigaben und werden erst nach dem Wochenwechsel wirksam.
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# Erzeugt das Prüfstand-Testbild deterministisch OHNE Zusatz-Pakete (reines
|
||||
# zlib/struct-PNG): weißer Grund 256x256, rotes Rechteck links oben,
|
||||
# blaues Rechteck rechts unten. Die Vision-Suite prüft genau diese Aussagen.
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
B, H = 256, 256
|
||||
|
||||
|
||||
def pixel(x, y):
|
||||
if 24 <= x < 104 and 24 <= y < 104:
|
||||
return (220, 30, 30) # rotes Rechteck links oben
|
||||
if 152 <= x < 232 and 152 <= y < 232:
|
||||
return (30, 60, 220) # blaues Rechteck rechts unten
|
||||
return (255, 255, 255)
|
||||
|
||||
|
||||
def chunk(typ, daten):
|
||||
c = typ + daten
|
||||
return struct.pack(">I", len(daten)) + c + struct.pack(">I", zlib.crc32(c))
|
||||
|
||||
|
||||
roh = b""
|
||||
for y in range(H):
|
||||
roh += b"\x00" + b"".join(bytes(pixel(x, y)) for x in range(B))
|
||||
|
||||
png = (b"\x89PNG\r\n\x1a\n"
|
||||
+ chunk(b"IHDR", struct.pack(">IIBBBBB", B, H, 8, 2, 0, 0, 0))
|
||||
+ chunk(b"IDAT", zlib.compress(roh, 9))
|
||||
+ chunk(b"IEND", b""))
|
||||
|
||||
ziel = sys.argv[1] if len(sys.argv) > 1 else "testbild.png"
|
||||
with open(ziel, "wb") as f:
|
||||
f.write(png)
|
||||
print(f"Testbild geschrieben: {ziel}")
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"prompt": "Beschreibe kurz auf Deutsch, was auf diesem Bild zu sehen ist: welche Formen, welche Farben, und wo sie ungefähr liegen.",
|
||||
"muss_gruppen": [["rot"], ["blau"], ["links oben", "oben links", "oberen link", "obere link", "links-oben"], ["rechts unten", "unten rechts", "unteren recht", "untere recht", "rechts-unten"]]
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Feed für den Evolution-Radar-Cron (Autonomie E4). Liegt in ~/.hermes/scripts/ (deploy.sh
|
||||
# kopiert ihn); sein stdout wird dem Cron-Agenten als Prompt eingespeist:
|
||||
# versionierter Auftrag (deploy/radar-prompt.md im MC2-Repo) + Live-Inventar der Box.
|
||||
set -uo pipefail
|
||||
|
||||
cat "$HOME/mission-control-v2/deploy/radar-prompt.md"
|
||||
|
||||
echo
|
||||
echo "## LIVE-INVENTAR (automatisch erhoben am $(date '+%d.%m.%Y'))"
|
||||
echo
|
||||
echo "### Update-Status (MC2 /api/maintenance/updates):"
|
||||
curl -sf --max-time 30 http://127.0.0.1:9001/api/maintenance/updates \
|
||||
| jq -c '{os_pakete: .os, engine_update: .engine, router_update: .swap, komponenten: .components}' \
|
||||
2>/dev/null || echo "(MC2-API nicht erreichbar)"
|
||||
echo
|
||||
echo "### Installierte Modelle (llama-swap):"
|
||||
curl -sf --max-time 20 http://127.0.0.1:8080/v1/models | jq -r '.data[].id' 2>/dev/null \
|
||||
|| echo "(llama-swap nicht erreichbar)"
|
||||
echo
|
||||
# llama-server braucht LD_LIBRARY_PATH auf das Vulkan-Verzeichnis für --version.
|
||||
echo "### Versionen: llama.cpp $( LD_LIBRARY_PATH=/opt/llamacpp-vulkan /usr/local/bin/llama-server --version 2>&1 | grep -o 'version: [0-9]*' || true ) · llama-swap $( /usr/local/bin/llama-swap --version 2>/dev/null | head -1 || true ) · hermes-agent $( bash -lc 'hermes --version' 2>/dev/null | head -1 || echo '?' )"
|
||||
echo
|
||||
echo "### Pin-Register (gepinnte Ebenen NICHT zum Update vorschlagen, aber Sprünge melden):"
|
||||
cat /srv/models/mc2-pins.json 2>/dev/null || echo "{}"
|
||||
@@ -1,64 +0,0 @@
|
||||
# Evolution-Radar (monatlicher Auftrag)
|
||||
|
||||
Du bist das Evolution-Radar der AI-Box (AMD Strix Halo, 128 GB, 100 % lokal, Vulkan/RADV).
|
||||
Dein Job: EINMAL im Monat prüfen, ob sich bei unseren Kern-Komponenten oder in deren
|
||||
Kategorien ein echter Sprung ergeben hat — und das als kurzen deutschen Report melden.
|
||||
Unten ist das Live-Inventar der Box eingespeist (Update-Status, installierte Modelle, Pins).
|
||||
|
||||
## Schritt 1 (PFLICHT, vor jeder Recherche): Inventar lesen und respektieren
|
||||
|
||||
Lehre aus Report #1 (02.07.): vier Fehler, eine Ursache — Inventar ignoriert. Deshalb hart:
|
||||
- **Empfiehl NIE ein Update auf eine Version, die laut Live-Inventar schon installiert ist.**
|
||||
(Report #1 empfahl „Hermes v0.18" — die Box WAR auf v0.18.0. `komponenten` im Inventar
|
||||
zeigt das echte git-behind; behind=0 heißt AKTUELL, egal was Release-Notes suggerieren.)
|
||||
- **Bei Komponenten, die wir aktiv nutzen, erst die EIGENE Version/Nutzung feststellen,
|
||||
dann Neuigkeiten bewerten.** Aktiv im Einsatz (Stand Repo):
|
||||
Parakeet via onnx-asr = UNSER STT (25 EU-Sprachen inkl. Deutsch, ~0,45 s — nie als
|
||||
„kein Deutsch-Fokus" abtun) · Silero VAD **v5** via vad-web in Lucy (v6 existiert) ·
|
||||
pocket-tts german_24l (CPU, auf dem PC) · Hirn Qwen3.6-35B (MTP-Draft, immer warm).
|
||||
- **Die Modell-Liste im Inventar enthält auch schon geladene KANDIDATEN** (z. B. gpt-oss-120b,
|
||||
Qwen3-VL-30B) — die liegen bereit und sind gebencht; nicht als „Neuentdeckung" verkaufen.
|
||||
- Jede Versions-Aussage im Report muss gegen das Inventar geprüft sein.
|
||||
|
||||
## Arbeitsweise: Fan-out mit Subagents (WICHTIG)
|
||||
|
||||
Nutze `delegate_task` mit einem `tasks`-Array (role: leaf), um die Recherche in ISOLIERTE
|
||||
Subagents aufzuteilen — pro Task eine Komponenten-Gruppe. Jeder Subagent recherchiert per
|
||||
Web-Suche, FILTERT Marketing-Geschwätz aus und liefert dir nur eine Substanz-Zusammenfassung
|
||||
(max. 5 Zeilen pro Komponente). Du konsolidierst am Ende. Erwähne im Endbericht nur, was
|
||||
Substanz hat — „nichts Nennenswertes" ist ein gültiges und gutes Ergebnis je Gruppe.
|
||||
|
||||
Scheitern Subagents wiederholt (z. B. Kontext-Fehler), recherchiere die betroffenen Gruppen
|
||||
selbst sequenziell — der Report muss IMMER zustande kommen.
|
||||
|
||||
Task-Aufteilung (5 Subagents):
|
||||
1. **Engine/Router:** llama.cpp (Vulkan, gfx1151/Strix Halo relevant!), llama-swap (mostlygeek)
|
||||
2. **Agent/Runtime:** hermes-agent (NousResearch), Electron (Major-Sprünge), three-vrm/@pixiv
|
||||
3. **Voice:** pocket-tts (kyutai), onnx-asr/Parakeet, Silero-VAD, smart-turn — nur Deutsch-taugliches
|
||||
4. **Modell-Kategorien** für 128-GB-Strix-Halo: bessere lokale Coder-Modelle (vs. Qwen3-Coder-Next),
|
||||
Chat/Reasoning-MoE (vs. Qwen3.6-35B/Qwen3.5-122B), Vision (vs. Qwen3-VL), deutsche TTS/STT.
|
||||
Nur GGUF-/llama.cpp-lauffähig, Substanz = Benchmarks/Community-Erfahrung, kein Ankündigungs-Hype.
|
||||
5. **IDE-/Agent-Tools** (Zed, Kilo Code, Claude Code): besonders Tod-/Nachfolger-Meldungen —
|
||||
„Projekt eingestellt/archiviert/Fork übernimmt" ist GENAU die Meldung, die wir fangen müssen
|
||||
(Lehre: Roo Code galt als Empfehlung und war längst eingestellt). Auch: neues dominantes Tool?
|
||||
|
||||
## Bereits ENTSCHIEDEN — nicht wieder vorschlagen (Verdikte der Box)
|
||||
|
||||
- Vulkan/RADV statt ROCm (gemessen schneller auf gfx1151) · llama.cpp+llama-swap gesetzt
|
||||
- Hirn = Qwen3.6-35B-A3B mit MTP-Spec-Draft, immer warm · Mem0 als Memory · Electron-App für Lucy
|
||||
- Pocket-TTS für Lucys Stimme (deutsch, CPU) · KEIN Kyutai-Streaming-STT (kann kein Deutsch)
|
||||
- VERWORFEN: LobeChat/WebUI-Ersatz, Unmute-Vollstack, AnythingLLM, gemma als Hirn
|
||||
- Latenz ist heilig: nichts vorschlagen, was Lucys ~2-s-Sprech-Latenz gefährdet
|
||||
|
||||
## Report-Format (Endantwort = geht direkt als Telegram-Nachricht raus)
|
||||
|
||||
- **Schreibe die Endnachricht in Lucys Stimme an den Commander** (du BIST Lucy, siehe SOUL.md —
|
||||
das Radar ist nur dein Auftrag): erst 1–2 Sätze unmissverständlich, was die Funde für ihn
|
||||
bedeuten und ob er etwas tun muss — danach die knappen Fund-Zeilen.
|
||||
- Deutsch, maximal ~20 Zeilen, Du-Form, kein Markdown-Overkill, nicht technisch —
|
||||
Technik in Alltagssprache, Details nur auf Nachfrage
|
||||
- Struktur: „🛰️ Evolution-Radar <Monat>" → je Fund 1–3 Zeilen: WAS, WARUM relevant für UNS,
|
||||
Einstufung **[lohnt vermutlich]** / **[beobachten]** / ggf. Quelle kurz
|
||||
- Wenn ein Fund ein GEPINNTES Level betrifft (siehe Pins im Inventar): explizit erwähnen
|
||||
- Nichts gefunden? Dann genau das in 2 Zeilen sagen. Keine Pflicht-Funde erfinden.
|
||||
- KEINE Konfig-Änderungen vornehmen, KEINE Downloads starten — nur melden.
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Wrapper: Evolution-Radar + Selbstkritik zusammen
|
||||
# Wird vom Hermes-Cron "Monats-Review" (1. des Monats, 09:00) als --script referenziert.
|
||||
# Ins Repo geholt 10.07.2026 (Übergabe S1) — lag vorher NUR auf der Box.
|
||||
set -uo pipefail
|
||||
echo "=============================="
|
||||
echo "## EVOLUTION-RADAR"
|
||||
echo "=============================="
|
||||
"$HOME/.hermes/scripts/radar-feed.sh"
|
||||
echo
|
||||
echo "=============================="
|
||||
echo "## SELBSTKRITIK"
|
||||
echo "=============================="
|
||||
"$HOME/.hermes/scripts/selbstkritik-feed.sh"
|
||||
@@ -1,25 +0,0 @@
|
||||
# Release-Radar — Auftrag für den Boten-Agenten
|
||||
|
||||
Du bist der Bote des wöchentlichen Hermes-Release-Radars. Unten stehen ein RADAR-BEFUND
|
||||
(automatisch erhoben), bei neuen Releases ein ABGLEICH eines Analysten-Modells gegen die
|
||||
Eigenbau-Landkarte (`~/wissens-vault/eigenbau-landkarte.md`) und ein FREMDBLICK
|
||||
(Zweitmeinung eines anderen Modells, prüft die Changelog-Zitate).
|
||||
|
||||
Deine Aufgabe — NUR berichten, in Lucys Stimme (Anrede „Commander", Alltagssprache,
|
||||
Fazit zuerst):
|
||||
|
||||
1. Bei „KEIN NEUES RELEASE" oder „TREFFER: 0": antworte mit GENAU EINEM kurzen Satz
|
||||
(z. B. „Radar: hermes vX ist da, nichts für unsere Landkarte dabei.").
|
||||
2. Bei Treffern: maximal 5 Sätze — welcher Eigenbau betroffen ist, welches native Feature
|
||||
das Release bringt, der Kern des Zitats, die empfohlene Probe, und was der Fremdblick
|
||||
dazu sagt (TRAEGT / TRAEGT-NICHT ehrlich nennen, nichts glattbügeln).
|
||||
3. Bei „AUSGEFALLEN" oder „NICHT MOEGLICH": ein ehrlicher Satz, dass der Radar diese
|
||||
Woche blind war und warum.
|
||||
|
||||
Harte Regeln:
|
||||
- NICHTS installieren, updaten, mergen oder konfigurieren — der Radar informiert nur.
|
||||
Updates bleiben eine manuelle Entscheidung des Commanders (Entscheid 04.07.).
|
||||
- Erfinde keine Treffer über den ABGLEICH hinaus; vom Fremdblick abgelehnte Behauptungen
|
||||
(TRAEGT-NICHT) als abgelehnt benennen, nicht verschweigen.
|
||||
- Die stille Briefkasten-Karte hat das Skript bereits geschrieben — du lieferst nur die
|
||||
kurze Telegram-Meldung.
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Feed für den Selbstkritik-Cron (Faden 11b — Zwilling des Evolution-Radars, Blick nach INNEN).
|
||||
# Liegt in ~/.hermes/scripts/ (deploy.sh kopiert ihn); stdout wird dem Cron-Agenten als
|
||||
# Prompt eingespeist: versionierter Auftrag (deploy/selbstkritik-prompt.md) + Live-Betriebsdaten.
|
||||
set -uo pipefail
|
||||
|
||||
cat "$HOME/mission-control-v2/deploy/selbstkritik-prompt.md"
|
||||
|
||||
echo
|
||||
echo "## LIVE-DATEN (automatisch erhoben am $(date '+%d.%m.%Y %H:%M'))"
|
||||
echo
|
||||
echo "### Voice-Pipeline-Latenzen (rollend, ms — :9001/api/voice/metrics):"
|
||||
curl -sf --max-time 15 http://127.0.0.1:9001/api/voice/metrics | jq -c . 2>/dev/null \
|
||||
|| echo "(nicht erreichbar)"
|
||||
echo
|
||||
echo "### Token-Verbrauch / Cloud-Ersparnis:"
|
||||
curl -sf --max-time 15 http://127.0.0.1:9001/api/system/token-stats | jq -c . 2>/dev/null \
|
||||
|| echo "(nicht erreichbar)"
|
||||
echo
|
||||
echo "### Prompt-Budget (Hinweis: prompt-size ist Allowlist-blind — die Voice-Lane hat real"
|
||||
echo "### deutlich weniger Tools; als Trend-Wächter für den GESAMT-Bestand trotzdem nützlich):"
|
||||
bash -lc "hermes prompt-size" 2>/dev/null | grep -E "total|skills index|Tool schemas" || echo " (Messung fehlgeschlagen)"
|
||||
echo
|
||||
echo "### Gateway-Journal: häufigste Warnungen/Fehler der letzten 30 Tage (Anzahl · Muster):"
|
||||
journalctl --user -u hermes-gateway -p warning --since "30 days ago" --no-pager 2>/dev/null \
|
||||
| grep -oE "(WARNING|ERROR)[^:]*: .{0,90}" | sed "s/[0-9]\{3,\}/N/g" | sort | uniq -c | sort -rn | head -12 \
|
||||
|| echo "(keine oder Journal nicht lesbar)"
|
||||
echo
|
||||
echo "### Tool-Loop-Guardrail-Treffer (letzte 30 Tage):"
|
||||
n=$(journalctl --user -u hermes-gateway --since "30 days ago" --no-pager 2>/dev/null \
|
||||
| grep -ci "loop guard\|hard_stop\|tool loop" || true)
|
||||
echo "${n:-0}"
|
||||
echo
|
||||
echo "### Skill-Bestand & Curator:"
|
||||
bash -lc "hermes curator status" 2>/dev/null | head -14 || echo "(Curator-Status fehlgeschlagen)"
|
||||
echo
|
||||
echo "### Meldeweg-Ausfälle (notify.sh-Fallbacks, letzte 30 Tage):"
|
||||
n=$(grep -c "FALLBACK" "$HOME/mc2-notify.log" 2>/dev/null || true)
|
||||
echo "${n:-0}"
|
||||
@@ -1,36 +0,0 @@
|
||||
# Selbstkritik-Runde (monatlich) — der Blick nach INNEN
|
||||
|
||||
Du bist Lucy und schaust einmal im Monat kritisch auf DICH SELBST und deinen eigenen
|
||||
Betrieb: Wo warst du langsam, wo liefen Tools in Schleifen, wo häufen sich Warnungen,
|
||||
was wird nie benutzt? Das ist der Zwilling des Evolution-Radars — der schaut nach
|
||||
draußen (neue Software), du schaust hier nach innen (eigener Betrieb).
|
||||
|
||||
## Auftrag
|
||||
|
||||
1. Lies die LIVE-DATEN unten sorgfältig (Latenzen, Token-Verbrauch, Journal-Auffälligkeiten,
|
||||
Skill-/Curator-Stand, Prompt-Größen).
|
||||
2. Finde die 1 bis 3 WICHTIGSTEN konkreten Verbesserungen. Jede Idee braucht einen BELEG
|
||||
aus den Daten (Zahl, Log-Zeile, Trend) — keine Bauchgefühle, keine Allgemeinplätze.
|
||||
3. **Fremdblick vor dem Absenden (Härtung 04.07. — „andere Brille"):** Bevor du die Vorschläge
|
||||
verschickst, lass sie von einem ANDEREN Modell gegenlesen. Delegiere dazu EINE kurze
|
||||
Kritiker-Runde (`delegate_task`, role=leaf — die läuft per `delegation.model` auf `heavy`,
|
||||
also einem anderen Modell als du selbst). Gib dem Kritiker deinen Vorschlags-Entwurf + die
|
||||
belegenden Daten-Ausschnitte und den Auftrag: **„Du bist ein anderes Modell. Zerpflücke jeden
|
||||
Vorschlag: Trägt der Beleg die Behauptung wirklich, oder ist es ein Bauchgefühl mit
|
||||
Zahl-Deko? Verwechselt er Korrelation mit Ursache? Fehlt Kontext?"** Nur Vorschläge, deren
|
||||
Beleg diesen Fremdblick überlebt, gehen raus; verworfene lässt du weg. Ist `delegation.model`
|
||||
dasselbe Modell wie deins (kein echter Fremdblick möglich) → vermerke das kurz in der Nachricht.
|
||||
4. Schicke dem Commander EINE kompakte Nachricht in DEINER Stimme (Lucy: Anrede „Commander",
|
||||
Fazit zuerst, Alltagssprache, Technik-Details knapp dahinter). Je Vorschlag: Was ist
|
||||
auffällig (Beleg) → was schlägst du vor → was bringt es. Wenn ehrlich NICHTS
|
||||
Nennenswertes auffällt: genau das sagen, kurz und zufrieden — KEINE Vorschläge erfinden.
|
||||
|
||||
## Leitplanken (nicht verhandelbar)
|
||||
|
||||
- Du ÄNDERST in dieser Runde NICHTS — du schlägst nur vor. Umgesetzt wird erst, wenn der
|
||||
Commander „mach" sagt (dann übernimmt die Werkstatt mit Gates und Rollback).
|
||||
- Der Hermes-Quellcode ist TABU (fremde Software, Auto-Update-Kanal). Verbesserungen nur
|
||||
über UNSERE Schichten: Config, SOUL.md, Skills, Gedächtnis, Modellwahl, MC2-Code.
|
||||
- Security-Themen (Approvals, Tokens, Firewall) nur BENENNEN, nie selbst anfassen.
|
||||
- Kein Tool-Feuerwerk: die Daten unten reichen; höchstens 2–3 gezielte Nachschau-Aufrufe
|
||||
(z. B. eine Journal-Zeile im Kontext lesen), dann antworten.
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Stack-Rundumschlag (monatlich am 4. um 06:40, angelegt 22.07.2026):
|
||||
# Lucys freier Rundumblick über den GANZEN Stack — ergänzt die mechanischen
|
||||
# Wächter (Sa Trend-Radar, So Prüfstand, Mo Release-Radar, Wartungsserie 1.–3.)
|
||||
# um tagesaktuelle Web-Recherche + Quervergleich mit den Verdikten.
|
||||
# Anlass/Lehrbeispiel: Googles Gemma-4-Stealth-Update vom 15./16.07.2026 kam
|
||||
# OHNE Versionssprung — kein mechanischer Wächter schlug an, erst freie
|
||||
# Recherche fand es (Session 22.07., Claude-Desktop).
|
||||
# Muster wie die anderen Feeds: dieses Skript sammelt DETERMINISTISCH den
|
||||
# IST-Stand, der Agent recherchiert + berichtet. Er EMPFIEHLT NUR — umgesetzt
|
||||
# wird per Karten-Klick durch den Commander.
|
||||
set -uo pipefail
|
||||
|
||||
MC="$HOME/mission-control-v2"
|
||||
STATE_DIR="$HOME/.hermes/state"
|
||||
|
||||
# ---------- Versionierter Auftrag für den Agenten ----------
|
||||
cat "$MC/deploy/stack-rundumschlag-prompt.md" 2>/dev/null || cat <<'EOF'
|
||||
(Prompt-Datei fehlt — Kurzform:) Du bist der monatliche Stack-Rundumschlag.
|
||||
Recherchiere tagesaktuell im Web zu allen Bausteinen unten (auch Stealth-Updates
|
||||
ohne Versionssprung!), gleiche mit den Verdikten ab und berichte in Lucys Stimme
|
||||
mit höchstens 3 konkreten Empfehlungen. Nichts selbst umsetzen.
|
||||
EOF
|
||||
echo
|
||||
echo "## RUNDUMSCHLAG-BEFUND (IST-Stand erhoben am $(date '+%d.%m.%Y %H:%M'))"
|
||||
echo
|
||||
|
||||
echo "### Rollen laut llama-swap (:8080)"
|
||||
curl -sf -m 10 http://127.0.0.1:8080/v1/models 2>/dev/null \
|
||||
| python3 -c 'import json,sys
|
||||
for m in json.load(sys.stdin).get("data", []):
|
||||
print("-", m.get("id"))' 2>/dev/null || echo "(llama-swap nicht erreichbar)"
|
||||
echo
|
||||
|
||||
echo "### Laufende Engine(s) — Version + Startzeile"
|
||||
gefunden=0
|
||||
for pid in $(pgrep -f 'llama-server' 2>/dev/null | head -3); do
|
||||
exe="$(readlink -f "/proc/$pid/exe" 2>/dev/null)" || continue
|
||||
[ -x "$exe" ] || continue
|
||||
ver="$("$exe" --version 2>&1 | head -1)"
|
||||
echo "- PID $pid — $ver"
|
||||
echo " $(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null | cut -c1-320)"
|
||||
gefunden=1
|
||||
done
|
||||
[ "$gefunden" = 1 ] || echo "(kein llama-server aktiv — Warm-Set prüfen)"
|
||||
echo
|
||||
|
||||
echo "### Puls-Historie (letzte 2 Erhebungen des Trend-Radars)"
|
||||
tail -2 "$STATE_DIR/trend-radar-puls-history.jsonl" 2>/dev/null || echo "(keine Historie)"
|
||||
echo
|
||||
|
||||
echo "### Watch-Stände (Trend-Radar-State)"
|
||||
python3 -m json.tool "$STATE_DIR/trend-radar-state.json" 2>/dev/null | head -40 || echo "(kein State)"
|
||||
echo
|
||||
|
||||
echo "### Prüfstand: Queue + letzte Verdikte"
|
||||
echo "Queue:"
|
||||
ls "$STATE_DIR/pruefstand/queue" 2>/dev/null || true
|
||||
echo "Fertig (Zusammenfassungen):"
|
||||
for f in "$STATE_DIR"/pruefstand/done/*.json; do
|
||||
case "$f" in *.spec.json) continue ;; esac
|
||||
[ -f "$f" ] || continue
|
||||
python3 - "$f" <<'PY' 2>/dev/null || true
|
||||
import json, os, sys
|
||||
d = json.load(open(sys.argv[1]))
|
||||
teile = [f"{k}:{v.get('bestanden')}/{v.get('gesamt')}"
|
||||
for k, v in d.get("zusammenfassung", {}).items()]
|
||||
print(f"- {os.path.basename(sys.argv[1])} ({d.get('erhoben', '?')}): " + " ".join(teile))
|
||||
PY
|
||||
done
|
||||
echo
|
||||
|
||||
echo "### Ressourcen"
|
||||
free -g | sed -n '2p'
|
||||
df -h /srv 2>/dev/null | tail -1
|
||||
df -h / | tail -1
|
||||
echo
|
||||
|
||||
echo "### Fehlerbild (best effort)"
|
||||
tail -5 "$HOME/.hermes/logs/monitoring/error_history.csv" 2>/dev/null || true
|
||||
journalctl --user -u hermes-gateway --since '-7 days' -p warning --no-pager 2>/dev/null | tail -5 || true
|
||||
echo
|
||||
|
||||
echo "### Nachschlagewerke für den Abgleich (per file_read bzw. Browser lesen)"
|
||||
echo "- Verdikte (NICHT neu aufrollen, außer Faktenlage messbar neu): $MC/docs/wissen/VERDIKTE.md"
|
||||
echo "- Watch-Liste (was mechanisch schon überwacht wird): $MC/deploy/trend-radar-watchlist.json"
|
||||
echo "- Community-Messwerte unserer Hardware: https://github.com/hogeheer499-commits/strix-halo-guide"
|
||||
@@ -1,36 +0,0 @@
|
||||
# Stack-Rundumschlag — Auftrag (monatlich am 4., 06:40)
|
||||
|
||||
Du bist Lucys monatlicher Rundumschlag: EIN freier Blick über den ganzen Stack.
|
||||
Die mechanischen Wächter prüfen Bedingungen (neuer Release? Issue zu? HF-Trending?) —
|
||||
DU suchst, was durch dieses Raster fällt: Stealth-Updates ohne Versionssprung,
|
||||
still ausgetauschte Gewichte, neue Verfahren (Spec-Decoding, Kernels, Quant-Formate),
|
||||
frische Community-Messwerte auf unserer Hardware (Strix Halo / Radeon 8060S, 122 GB unified).
|
||||
Lehrbeispiel: Googles Gemma-4-Update vom 15./16.07.2026 kam OHNE Versionssprung —
|
||||
kein Wächter schlug an, erst freie Recherche fand es.
|
||||
|
||||
Vorgehen:
|
||||
|
||||
1. Lies den BEFUND unten: das ist der IST-Stand (Rollen, Engine, Puls, Watch-Stände,
|
||||
Prüfstand, Ressourcen, Fehlerbild). Auffälligkeiten darin gehören in den Bericht.
|
||||
2. Recherchiere im Web (Browser-Werkzeug) TAGESAKTUELL zu jedem Baustein:
|
||||
- die Rollen-Modelle aus dem Befund (Familien: Qwen, GLM, …) — Refreshes, Nachfolger,
|
||||
still neu hochgeladene GGUFs,
|
||||
- llama.cpp (neue Builds/Features seit dem Engine-Stand im Befund),
|
||||
- ROCm und Vulkan auf Strix Halo (gfx1151),
|
||||
- Zed + OpenCode (IDE-Welt am PC), Electron (Lucy-Shell am PC), pocket-tts (Stimme).
|
||||
Suchmuster: „<Baustein> update stealth refresh performance <Monat Jahr>".
|
||||
3. Gleiche jeden Fund mit den Verdikten ab (Pfad im Befund): Entschiedenes NICHT neu
|
||||
aufrollen — AUSSER die Faktenlage hat sich messbar geändert; dann genau DAS melden,
|
||||
mit dem alten Verdikt daneben.
|
||||
4. Bericht in Lucys Stimme, kompakt (höchstens 15 Sätze):
|
||||
(a) Neues draußen, jeweils mit Datum + Quelle,
|
||||
(b) Auffälligkeiten im IST-Stand (Puls-Drift, Fehlerbild, volle Platte …),
|
||||
(c) HÖCHSTENS 3 Empfehlungen — jede mit einem Satz Beleg und dem nächsten Schritt
|
||||
(„Prüfstand-Kandidat einreihen", „Engine-A/B abwarten", „Karte anlegen",
|
||||
„Handarbeit am PC").
|
||||
5. NICHTS selbst umsetzen, nichts installieren, keine Configs anfassen — du empfiehlst,
|
||||
der Commander entscheidet per Klick. Verdient ein Fund einen Prüfstand-Test, sag es
|
||||
ausdrücklich; die Spec legt der Commander oder ein Folgeauftrag an.
|
||||
6. HARTE PRÄMISSE: Dieser Stack ist 100 % lokal (Ablösungs-Zielbild — das Cloud-Abo
|
||||
endet genau deswegen). Empfiehl NIEMALS Cloud-API-Integrationen als Modell-Ersatz;
|
||||
ein Modell, das nicht in 122 GB Unified passt, ist schlicht KEIN Kandidat.
|
||||
@@ -1,485 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Trend-Radar (wöchentlich Sa 05:15, 17.07.2026): hält das MODELL- UND ENGINE-GEFÜGE
|
||||
# der Box aktuell im Blick — für ALLE Rollen (hermes/coder/heavy/vision/scout/embed),
|
||||
# nicht nur das Hirn. Vier Schritte, alles deterministisch im Skript, der Agent ist
|
||||
# nur der Bote:
|
||||
# 1. LIVE-PULS: echte Tempo-Messung jeder Rolle über llama-swap (:8080), Vergleich
|
||||
# zur Vorwoche (State), Regressionen >10 % werden gemeldet.
|
||||
# 2. ENGINE-A/B: gibt es einen neueren llama.cpp-Vulkan-Build, wird er nach /tmp
|
||||
# geladen und auf dem Bench-Port GEGEN den installierten Build gemessen —
|
||||
# installiert wird weiterhin NICHTS (Update bleibt Button/Entscheid).
|
||||
# 3. WATCH-LISTE: mechanisch prüfbare Bedingungen (Issue zu? neuer ROCm-Release?
|
||||
# Community-Grid-Zahlen) aus deploy/trend-radar-watchlist.json.
|
||||
# 4. KANDIDATEN-SUCHE: HuggingFace-Trending (GGUF) je Rolle; ein Analyst mit
|
||||
# Zitat-Gate destilliert MAX. 1 Prüfstand-Kandidaten pro Woche → Spec in die
|
||||
# Prüfstand-Queue (So 01:00 prüft ihn mit dem vollen Programm).
|
||||
# Der Radar EMPFIEHLT NUR — Rollen-Wechsel, Engine-Updates und Adoptionen entscheidet
|
||||
# der Commander (Karten-Klick). Muster wie idle-radar/release-radar: Temp-Dateien statt
|
||||
# Pipe+Heredoc (Lehre 10.07.), Ein-Schuss-Analyst gegen :8080, stiller Briefkasten.
|
||||
set -uo pipefail
|
||||
|
||||
MC="$HOME/mission-control-v2"
|
||||
STATE_DIR="$HOME/.hermes/state"
|
||||
STATE="$STATE_DIR/trend-radar-state.json"
|
||||
HIST="$STATE_DIR/trend-radar-puls-history.jsonl" # Langzeit-Verlauf (Lücke 3, 17.07.)
|
||||
PS_QUEUE="$STATE_DIR/pruefstand/queue"
|
||||
PS_DONE="$STATE_DIR/pruefstand/done"
|
||||
WATCHLIST="$MC/deploy/trend-radar-watchlist.json"
|
||||
ENDPOINT="${RADAR_ENDPOINT:-http://127.0.0.1:8080/v1}"
|
||||
ANALYST="${RADAR_MODEL:-gpt-oss-120b}"
|
||||
VERTRETUNG="${RADAR_FALLBACK:-GLM-4.7-Flash}"
|
||||
BRAIN_GGUF="${BRAIN_GGUF:-/srv/models/Qwen3.6-35B-A3B-MTP-GGUF/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf}"
|
||||
BENCH_PORT="${BENCH_PORT:-5899}"
|
||||
VULKAN_DIR=/opt/llamacpp-vulkan
|
||||
|
||||
mkdir -p "$STATE_DIR" "$PS_QUEUE" "$PS_DONE"
|
||||
[ -f "$STATE" ] || echo '{}' > "$STATE"
|
||||
TMPD="$(mktemp -d /tmp/trend-radar.XXXXXX)"
|
||||
trap 'rm -rf "$TMPD"' EXIT
|
||||
|
||||
briefkasten() { # $1=Betreff $2=Text
|
||||
curl -sf -m 5 -X POST "${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(python3 - "$1" "$2" <<'PY'
|
||||
import json, sys
|
||||
print(json.dumps({"text": sys.argv[2], "subject": sys.argv[1],
|
||||
"source": "trend-radar", "priority": "silent"}))
|
||||
PY
|
||||
)" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# ---------- Versionierter Auftrag für den Boten-Agenten ----------
|
||||
cat "$MC/deploy/trend-radar-prompt.md" 2>/dev/null || cat <<'EOF'
|
||||
(Prompt-Datei fehlt noch — Kurzform:) Du bist der Bote des wöchentlichen Trend-Radars.
|
||||
Berichte den Befund unten in Lucys Stimme in höchstens 6 Sätzen: Puls-Auffälligkeiten,
|
||||
Engine-A/B-Ergebnis, Watch-Treffer, ob ein Prüfstand-Kandidat eingereiht wurde.
|
||||
Nichts selbst umsetzen — das Skript hat alles Nötige bereits getan.
|
||||
EOF
|
||||
echo
|
||||
echo "## TREND-RADAR-BEFUND (erhoben am $(date '+%d.%m.%Y %H:%M'))"
|
||||
echo
|
||||
|
||||
# =====================================================================
|
||||
# 1. LIVE-PULS aller Rollen (heavy zuletzt → Analyst findet es noch warm)
|
||||
# =====================================================================
|
||||
echo "### 1. LIVE-PULS (echte Messung über llama-swap, Vergleich zur Vorwoche + Langzeit)"
|
||||
python3 - "$ENDPOINT" "$STATE" "$TMPD/puls.json" "$HIST" <<'PY'
|
||||
import json, sys, time, urllib.request
|
||||
|
||||
endpoint, state_pfad, out_pfad = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
hist_pfad = sys.argv[4]
|
||||
ROLLEN = ["hermes", "coder", "vision", "scout", "heavy"] # heavy zuletzt (Analyst-Wärme)
|
||||
|
||||
def chat(model, timeout=420):
|
||||
body = {"model": model, "max_tokens": 160, "temperature": 0, "messages": [
|
||||
{"role": "user", "content":
|
||||
"Erkläre in drei kurzen Sätzen, was ein Mixture-of-Experts-Modell ist."}]}
|
||||
req = urllib.request.Request(endpoint.rstrip("/") + "/chat/completions",
|
||||
data=json.dumps(body).encode(), headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return json.load(r).get("timings") or {}
|
||||
|
||||
puls = {}
|
||||
for rolle in ROLLEN:
|
||||
try:
|
||||
chat(rolle) # Lauf 1: lädt/wärmt (on-demand-Modelle brauchen Minuten)
|
||||
t = chat(rolle) # Lauf 2: die eigentliche Messung
|
||||
puls[rolle] = {"tg": round(t.get("predicted_per_second") or 0, 1),
|
||||
"pp": round(t.get("prompt_per_second") or 0, 1)}
|
||||
except Exception as e:
|
||||
puls[rolle] = {"fehler": f"{type(e).__name__}"}
|
||||
# embed: Funktions- und Latenz-Probe
|
||||
try:
|
||||
t0 = time.time()
|
||||
req = urllib.request.Request(endpoint.rstrip("/") + "/embeddings",
|
||||
data=json.dumps({"model": "embed", "input": "Puls-Probe"}).encode(),
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
json.load(r)
|
||||
puls["embed"] = {"ms": round((time.time() - t0) * 1000)}
|
||||
except Exception as e:
|
||||
puls["embed"] = {"fehler": f"{type(e).__name__}"}
|
||||
|
||||
try:
|
||||
alt = json.load(open(state_pfad)).get("puls") or {}
|
||||
except Exception:
|
||||
alt = {}
|
||||
for rolle, w in puls.items():
|
||||
zeile = f"- {rolle}: "
|
||||
if "fehler" in w:
|
||||
zeile += f"MESSUNG FEHLGESCHLAGEN ({w['fehler']})"
|
||||
elif "ms" in w:
|
||||
zeile += f"{w['ms']} ms"
|
||||
else:
|
||||
zeile += f"tg {w['tg']} t/s · pp {w['pp']} t/s"
|
||||
a = alt.get(rolle) or {}
|
||||
if a.get("tg") and w.get("tg"):
|
||||
d = 100 * w["tg"] / a["tg"] - 100
|
||||
zeile += f" · Vorwoche {a['tg']} ({d:+.0f} %)"
|
||||
if d <= -10:
|
||||
zeile += " ⚠️ REGRESSION"
|
||||
print(zeile)
|
||||
|
||||
# Langzeit-Vergleich (~8 Wochen): fängt SCHLEICHENDE Regression, die jede Woche
|
||||
# einzeln unter der 10-%-Schwelle bleibt (Lücke 3, 17.07.).
|
||||
eintraege = []
|
||||
try:
|
||||
with open(hist_pfad, encoding="utf-8") as f:
|
||||
for l in f:
|
||||
l = l.strip()
|
||||
if l:
|
||||
try:
|
||||
eintraege.append(json.loads(l))
|
||||
except Exception:
|
||||
pass
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
grenze = time.time() - 49 * 86400
|
||||
alte = [z for z in eintraege if (z.get("ts") or 0) <= grenze]
|
||||
ref = alte[-1] if alte else None
|
||||
if ref:
|
||||
treffer = []
|
||||
for rolle, w in puls.items():
|
||||
a = (ref.get("puls") or {}).get(rolle) or {}
|
||||
if a.get("tg") and w.get("tg"):
|
||||
d = 100 * w["tg"] / a["tg"] - 100
|
||||
if d <= -10:
|
||||
treffer.append(f"- LANGZEIT {rolle}: tg {w['tg']} vs. {a['tg']} "
|
||||
f"({ref.get('datum','?')}, Engine b{ref.get('engine','?')}) "
|
||||
f"= {d:+.0f} % ⚠️ SCHLEICHENDE REGRESSION")
|
||||
if treffer:
|
||||
print(f"Langzeit-Vergleich (vs. {ref.get('datum','?')}):")
|
||||
for t in treffer:
|
||||
print(t)
|
||||
else:
|
||||
print(f"Langzeit-Vergleich vs. {ref.get('datum','?')}: unauffällig.")
|
||||
json.dump(puls, open(out_pfad, "w"))
|
||||
PY
|
||||
PULS_BLOCK="$(cat "$TMPD/puls.json" 2>/dev/null || echo '{}')"
|
||||
echo
|
||||
|
||||
# =====================================================================
|
||||
# 2. ENGINE-A/B (nur wenn ein neuerer Vulkan-Build existiert)
|
||||
# =====================================================================
|
||||
echo "### 2. ENGINE-A/B (neuer llama.cpp-Build gegen installierten, Hirn-GGUF, Bench-Port)"
|
||||
ENGINE_BLOCK="kein Vergleich gelaufen"
|
||||
INST_NUM="$(LD_LIBRARY_PATH=$VULKAN_DIR $VULKAN_DIR/llama-server --version 2>&1 \
|
||||
| grep -o 'version: [0-9]*' | grep -o '[0-9]*' || true)"
|
||||
ASSET_RX='ubuntu-vulkan-x64\.tar\.gz$'
|
||||
REL_JSON="$(curl -sf --max-time 30 'https://api.github.com/repos/ggml-org/llama.cpp/releases?per_page=15' || true)"
|
||||
NEU_TAG=""; NEU_URL=""
|
||||
if [ -n "$REL_JSON" ]; then
|
||||
NEU_URL="$(printf '%s' "$REL_JSON" | jq -r --arg rx "$ASSET_RX" \
|
||||
'[.[] | .assets[]? | select(.name|test($rx))][0].browser_download_url // empty' 2>/dev/null || true)"
|
||||
NEU_TAG="$(printf '%s' "$REL_JSON" | jq -r --arg rx "$ASSET_RX" \
|
||||
'[.[] | select(any(.assets[]?; .name|test($rx)))][0].tag_name // empty' 2>/dev/null || true)"
|
||||
fi
|
||||
NEU_NUM="$(printf '%s' "$NEU_TAG" | grep -o '[0-9]*' | head -1 || true)"
|
||||
MEM_FREI_GB="$(awk '/MemAvailable/{printf "%d", $2/1048576}' /proc/meminfo)"
|
||||
|
||||
if [ -z "$INST_NUM" ] || [ -z "$NEU_NUM" ]; then
|
||||
ENGINE_BLOCK="Versionsermittlung unvollständig (installiert: ${INST_NUM:-?}, GitHub: ${NEU_NUM:-?}) — ehrlich: kein Vergleich."
|
||||
elif [ "$NEU_NUM" -le "$INST_NUM" ]; then
|
||||
ENGINE_BLOCK="installiert b${INST_NUM} = aktuellster Build mit Vulkan-Paket (b${NEU_NUM}) — nichts zu messen."
|
||||
elif [ "${MEM_FREI_GB:-0}" -lt 30 ]; then
|
||||
ENGINE_BLOCK="neuer Build b${NEU_NUM} verfügbar, aber nur ${MEM_FREI_GB} GB RAM frei — A/B verschoben."
|
||||
elif [ ! -f "$BRAIN_GGUF" ]; then
|
||||
ENGINE_BLOCK="neuer Build b${NEU_NUM} verfügbar, aber Hirn-GGUF nicht unter ${BRAIN_GGUF} — Pfad prüfen (BRAIN_GGUF)."
|
||||
else
|
||||
mkdir -p "$TMPD/engine"
|
||||
if curl -fsSL --max-time 600 -o "$TMPD/engine/neu.tgz" "$NEU_URL" \
|
||||
&& tar xzf "$TMPD/engine/neu.tgz" -C "$TMPD/engine"; then
|
||||
NEU_DIR="$(echo "$TMPD"/engine/llama-*/ | awk '{print $1}')"
|
||||
ab_bench() { # $1=bin-dir → mittlere tg (2 Messläufe nach Warmlauf) oder "crash"
|
||||
local BD="${1%/}"
|
||||
LD_LIBRARY_PATH="$BD" "$BD/llama-server" -m "$BRAIN_GGUF" --host 127.0.0.1 \
|
||||
--port "$BENCH_PORT" -c 8192 -ngl 999 -fa on --no-mmap --jinja \
|
||||
--spec-type draft-mtp --spec-draft-n-max 3 >/tmp/trend-radar-bench.log 2>&1 &
|
||||
local PID=$!
|
||||
local OK=0
|
||||
for i in $(seq 1 120); do
|
||||
curl -s --max-time 2 "http://127.0.0.1:$BENCH_PORT/health" | grep -q ok && { OK=1; break; }
|
||||
sleep 2
|
||||
kill -0 $PID 2>/dev/null || break
|
||||
done
|
||||
if [ "$OK" != "1" ]; then echo "crash"; kill $PID 2>/dev/null; wait $PID 2>/dev/null; return; fi
|
||||
local SUMME=0 N=0
|
||||
for r in 0 1 2; do
|
||||
curl -s --max-time 300 -X POST "http://127.0.0.1:$BENCH_PORT/completion" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"prompt":"Erklaere in drei kurzen Saetzen, was ein Mixture-of-Experts-Modell ist.","n_predict":150,"temperature":0}' \
|
||||
> "$TMPD/engine/probe.json" 2>/dev/null
|
||||
[ "$r" = "0" ] && continue
|
||||
local TG
|
||||
TG="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("timings",{}).get("predicted_per_second") or 0)' "$TMPD/engine/probe.json" 2>/dev/null || echo 0)"
|
||||
SUMME="$(python3 -c "print($SUMME + $TG)")"; N=$((N+1))
|
||||
done
|
||||
kill $PID 2>/dev/null; wait $PID 2>/dev/null; sleep 2
|
||||
[ "$N" -gt 0 ] && python3 -c "print(round($SUMME/$N,1))" || echo 0
|
||||
}
|
||||
TG_ALT="$(ab_bench "$VULKAN_DIR")"
|
||||
TG_NEU="$(ab_bench "$NEU_DIR")"
|
||||
if [ "$TG_NEU" = "crash" ]; then
|
||||
ENGINE_BLOCK="Kandidat b${NEU_NUM} STARTET NICHT auf dem Hirn-GGUF (Lade-Crash) — Update wäre riskant, Finger weg bis zum nächsten Build."
|
||||
elif [ "$TG_ALT" = "crash" ] || [ "${TG_ALT%.*}" = "0" ]; then
|
||||
ENGINE_BLOCK="Referenzmessung des installierten Builds fehlgeschlagen — kein belastbares A/B."
|
||||
else
|
||||
DELTA="$(python3 -c "print(round(100*$TG_NEU/$TG_ALT-100,1))" 2>/dev/null || echo '?')"
|
||||
ENGINE_BLOCK="b${NEU_NUM} (neu) tg ${TG_NEU} t/s vs. b${INST_NUM} (installiert) ${TG_ALT} t/s → ${DELTA} %. Anwendung übernimmt das So-04:30-Auto-Update (Rollback-Fangnetz) bzw. der Cockpit-Knopf — bei klar NEGATIVEM Delta vorher pinnen erwägen."
|
||||
fi
|
||||
else
|
||||
ENGINE_BLOCK="Download/Entpacken von b${NEU_NUM} fehlgeschlagen — nächste Woche neuer Versuch."
|
||||
fi
|
||||
fi
|
||||
echo "$ENGINE_BLOCK"
|
||||
echo
|
||||
|
||||
# =====================================================================
|
||||
# 3. WATCH-LISTE (mechanische Bedingungen)
|
||||
# =====================================================================
|
||||
echo "### 3. WATCH-LISTE (mechanisch geprüft)"
|
||||
python3 - "$WATCHLIST" "$STATE" "$TMPD/watch.txt" <<'PY'
|
||||
import json, re, sys, urllib.request
|
||||
|
||||
def hole(url, timeout=30):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "mc2-trend-radar"})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return r.read().decode("utf-8", "replace")
|
||||
|
||||
try:
|
||||
eintraege = json.load(open(sys.argv[1], encoding="utf-8")).get("eintraege", [])
|
||||
except Exception as e:
|
||||
print(f"(Watch-Liste nicht lesbar: {e})")
|
||||
open(sys.argv[3], "w").write("")
|
||||
raise SystemExit
|
||||
try:
|
||||
state = json.load(open(sys.argv[2]))
|
||||
except Exception:
|
||||
state = {}
|
||||
gesehen = state.get("watch_gesehen") or {}
|
||||
zeilen = []
|
||||
for e in eintraege:
|
||||
key, typ = e.get("key", "?"), e.get("typ")
|
||||
try:
|
||||
if typ == "github_issue":
|
||||
d = json.loads(hole(f"https://api.github.com/repos/{e['repo']}/issues/{e['nummer']}"))
|
||||
status = d.get("state", "?")
|
||||
treffer = " ⚠️ BEDINGUNG ERFÜLLT!" if status == "closed" else ""
|
||||
zeilen.append(f"- {key}: Issue #{e['nummer']} ist {status}{treffer} ({e['warum']})")
|
||||
elif typ == "github_release":
|
||||
d = json.loads(hole(f"https://api.github.com/repos/{e['repo']}/releases?per_page=1"))
|
||||
tag = (d[0].get("tag_name") if isinstance(d, list) and d else "?") or "?"
|
||||
neu = " ⚠️ NEU seit letzter Woche!" if tag != gesehen.get(key) else ""
|
||||
zeilen.append(f"- {key}: neuester Release {tag}{neu} ({e['warum']})")
|
||||
gesehen[key] = tag
|
||||
elif typ == "github_release_major":
|
||||
# PC-Watch (17.07.): nur MAJOR-Sprünge melden — Electron released wöchentlich,
|
||||
# relevant ist der Chromium-Jahrgang, nicht jeder Patch.
|
||||
d = json.loads(hole(f"https://api.github.com/repos/{e['repo']}/releases?per_page=1"))
|
||||
tag = (d[0].get("tag_name") if isinstance(d, list) and d else "?") or "?"
|
||||
m = re.search(r"(\d+)", tag)
|
||||
major = m.group(1) if m else "?"
|
||||
neu = " ⚠️ NEUE MAJOR-VERSION!" if major != gesehen.get(key, major) and major != "?" else ""
|
||||
zeilen.append(f"- {key}: neuester Release {tag} (Major {major}){neu} ({e['warum']})")
|
||||
gesehen[key] = major
|
||||
elif typ == "pypi":
|
||||
d = json.loads(hole(f"https://pypi.org/pypi/{e['paket']}/json"))
|
||||
ver = ((d.get("info") or {}).get("version")) or "?"
|
||||
neu = " ⚠️ NEU seit letzter Woche!" if ver != gesehen.get(key, ver) and ver != "?" else ""
|
||||
zeilen.append(f"- {key}: PyPI-Version {ver}{neu} ({e['warum']})")
|
||||
gesehen[key] = ver
|
||||
elif typ == "url_zeilen":
|
||||
text = hole(e["url"], timeout=45)
|
||||
passend = [z.strip() for z in text.splitlines() if e.get("muster", "") in z]
|
||||
passend = passend[: int(e.get("max_zeilen", 20))]
|
||||
zeilen.append(f"- {key}: {len(passend)} Messwert-Zeilen ({e['warum']}):")
|
||||
zeilen += [f" {z[:180]}" for z in passend]
|
||||
else:
|
||||
zeilen.append(f"- {key}: unbekannter Typ {typ}")
|
||||
except Exception as ex:
|
||||
zeilen.append(f"- {key}: PRÜFUNG AUSGEFALLEN ({type(ex).__name__})")
|
||||
state["watch_gesehen"] = gesehen
|
||||
json.dump(state, open(sys.argv[2], "w"))
|
||||
ausgabe = "\n".join(zeilen) or "(Watch-Liste leer)"
|
||||
print(ausgabe)
|
||||
open(sys.argv[3], "w", encoding="utf-8").write(ausgabe)
|
||||
PY
|
||||
echo
|
||||
|
||||
# =====================================================================
|
||||
# 4. KANDIDATEN-SUCHE (HF-Trending → Analyst mit Zitat-Gate → Prüfstand-Queue)
|
||||
# =====================================================================
|
||||
echo "### 4. KANDIDATEN-SUCHE (HuggingFace-Trending, GGUF)"
|
||||
curl -sf --max-time 45 \
|
||||
'https://huggingface.co/api/models?filter=gguf&sort=trendingScore&direction=-1&limit=40' \
|
||||
-o "$TMPD/hf.json" || echo '[]' > "$TMPD/hf.json"
|
||||
python3 - "$TMPD/hf.json" "$TMPD/hf.txt" <<'PY'
|
||||
import json, sys
|
||||
try:
|
||||
modelle = json.load(open(sys.argv[1]))
|
||||
except Exception:
|
||||
modelle = []
|
||||
zeilen = []
|
||||
for m in modelle if isinstance(modelle, list) else []:
|
||||
mid = m.get("modelId") or m.get("id") or "?"
|
||||
zeilen.append(f"- {mid} · downloads {m.get('downloads', 0)} · likes {m.get('likes', 0)}"
|
||||
f" · angelegt {(m.get('createdAt') or '')[:10]}")
|
||||
ausgabe = "\n".join(zeilen[:40]) or "(HF-API nicht erreichbar oder leer)"
|
||||
print(ausgabe)
|
||||
open(sys.argv[2], "w", encoding="utf-8").write(ausgabe)
|
||||
PY
|
||||
echo
|
||||
|
||||
# Schon bekannt (Dedup): gemeldete Keys + Prüfstand-Queue + erledigte Prüfungen
|
||||
BEKANNT="$(python3 - "$STATE" "$PS_QUEUE" "$PS_DONE" <<'PY'
|
||||
import glob, json, os, sys
|
||||
try:
|
||||
state = json.load(open(sys.argv[1]))
|
||||
except Exception:
|
||||
state = {}
|
||||
zeilen = [f"- {k}" for k in (state.get("kandidaten_gemeldet") or [])]
|
||||
for d in (sys.argv[2], sys.argv[3]):
|
||||
for f in glob.glob(os.path.join(d, "*.json")):
|
||||
zeilen.append(f"- {os.path.splitext(os.path.basename(f))[0]}")
|
||||
print("\n".join(sorted(set(zeilen))) or "(noch keine)")
|
||||
PY
|
||||
)"
|
||||
|
||||
RASTER='Du bist der TREND-RADAR einer lokalen KI-Box (AMD Strix Halo, 128 GB Unified RAM, ~256 GB/s Speicherbandbreite, llama.cpp/Vulkan). Du bekommst: LIVE-PULS aller Modell-Rollen, ein Engine-A/B-Ergebnis, WATCH-LISTEN-Befunde und die HuggingFace-Trending-Liste (GGUF). ROLLEN-STECKBRIEF (Amtsinhaber und harte Anforderungen): hermes = Lucys Sprach-Hirn, Qwen3.6-35B-A3B (MoE, 3B aktiv) — Kandidaten MÜSSEN MoE mit wenigen aktiven Parametern sein, Latenz ist heilig, dichte Modelle sind NIE Hirn-Kandidaten (Bandbreiten-Physik, Verdikt 17.07.); coder = Qwen3-Coder-Next, braucht 128k+ Kontext; heavy = gpt-oss-120b (Denker, MoE); vision = Qwen3-VL-30B-A3B (braucht mmproj); scout = GLM-4.6V-Flash (klein, Vision+Tools); embed = Qwen3-Embedding-0.6B. Deine Aufgabe: destilliere AUS DEM MATERIAL (a) maximal EINEN Prüfstand-Kandidaten (nur wenn ein Trending-Modell eine Rolle PLAUSIBEL verbessern könnte und aufs RAM-Budget passt) und (b) 0-2 HINWEISE (z. B. erfüllte Watch-Bedingung, Engine-Regression, neues Beschleuniger-Verfahren). REGELN: Die hf_id des Kandidaten MUSS WOERTLICH in der HF-Liste stehen (wird mechanisch geprüft, erfundene IDs fliegen raus). NIE einen Amtsinhaber selbst oder ein Repackaging/eine Quantisierung desselben Modells vorschlagen (gleicher Modellname = KEIN Kandidat; wird mechanisch geblockt). Nichts vorschlagen, was unter SCHON BEKANNT steht. Reine Namens-Hypes ohne erkennbare Rollen-Passung sind KEIN Kandidat. KANDIDAT: 0 ist der Normalfall und völlig ok. Antworte auf DEUTSCH, exakt so: erste Zeile "KANDIDAT: <0|1>". Bei 1 folgen vier Zeilen: "HF_ID: <exakte id aus der Liste>" / "ROLLE: <hermes|coder|heavy|vision|scout|embed>" / "BELEG: <die HF-Listenzeile wörtlich>" / "WARUM: <ein Satz mit der erwarteten Verbesserung>". Danach optional: "HINWEISE:" gefolgt von maximal 2 Spiegelstrichen mit je einem Satz + wörtlichem Beleg-Zitat aus dem Material.'
|
||||
|
||||
EVID="$(cat <<EOF
|
||||
LIVE-PULS (JSON): $PULS_BLOCK
|
||||
|
||||
ENGINE-A/B: $ENGINE_BLOCK
|
||||
|
||||
WATCH-LISTE:
|
||||
$(cat "$TMPD/watch.txt" 2>/dev/null || echo '(leer)')
|
||||
|
||||
HF-TRENDING (GGUF, Top 40):
|
||||
$(cat "$TMPD/hf.txt" 2>/dev/null || echo '(leer)')
|
||||
|
||||
SCHON BEKANNT (nicht erneut vorschlagen):
|
||||
$BEKANNT
|
||||
EOF
|
||||
)"
|
||||
|
||||
judge() { # $1=Modell $2=Timeout $3=max_tokens
|
||||
local req
|
||||
req="$(jq -n --arg m "$1" --arg sys "$RASTER" --arg usr "$EVID" --argjson mt "$3" \
|
||||
'{model:$m, messages:[{role:"system",content:$sys},{role:"user",content:$usr}],
|
||||
max_tokens:$mt, temperature:0.2}')"
|
||||
curl -s -m "$2" "$ENDPOINT/chat/completions" -H 'Content-Type: application/json' \
|
||||
--data-binary "$req" | python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
msg = d["choices"][0]["message"]
|
||||
print((msg.get("content") or msg.get("reasoning_content") or "").strip())
|
||||
except Exception:
|
||||
pass'
|
||||
}
|
||||
|
||||
RICHTER="$ANALYST"
|
||||
ANALYSE="$(judge "$ANALYST" 420 2600)"
|
||||
if [ -z "${ANALYSE// /}" ]; then
|
||||
RICHTER="$VERTRETUNG (Vertretung — $ANALYST hat nicht geantwortet)"
|
||||
ANALYSE="$(judge "$VERTRETUNG" 240 1800)"
|
||||
fi
|
||||
|
||||
echo "### 5. ANALYSE (Analyst: $RICHTER)"
|
||||
if [ -z "${ANALYSE// /}" ]; then
|
||||
echo "AUSGEFALLEN — beide Analysten haben nicht geantwortet. Puls/Engine/Watch oben gelten trotzdem; dem Commander EINEN ehrlichen Satz melden."
|
||||
briefkasten "Trend-Radar" "Wochenlauf: Puls/Engine/Watch erhoben, Kandidaten-Analyse ausgefallen (kein Analyst erreichbar)."
|
||||
exit 0
|
||||
fi
|
||||
echo "$ANALYSE"
|
||||
echo
|
||||
|
||||
# ---------- Zitat-Gate + Spec in die Prüfstand-Queue (deterministisch) ----------
|
||||
printf '%s' "$ANALYSE" > "$TMPD/analyse.txt"
|
||||
printf '%s' "$EVID" > "$TMPD/material.txt"
|
||||
ERGEBNIS="$(python3 - "$TMPD/analyse.txt" "$TMPD/material.txt" "$STATE" "$PS_QUEUE" <<'PY'
|
||||
import json, os, re, sys, time, unicodedata
|
||||
|
||||
ana = open(sys.argv[1], encoding="utf-8", errors="replace").read()
|
||||
evid = open(sys.argv[2], encoding="utf-8", errors="replace").read()
|
||||
state_pfad, queue = sys.argv[3], sys.argv[4]
|
||||
|
||||
def norm(s):
|
||||
s = unicodedata.normalize("NFKC", s or "")
|
||||
s = "".join(ch for ch in s if ch.isalnum() or ch.isspace())
|
||||
return " ".join(s.lower().split())
|
||||
|
||||
def feld(name):
|
||||
m = re.search(rf"^{name}\s*:\s*(.+)$", ana, re.MULTILINE)
|
||||
return m.group(1).strip() if m else ""
|
||||
|
||||
anzahl = feld("KANDIDAT")
|
||||
if not anzahl.startswith("1"):
|
||||
print("KEIN_KANDIDAT")
|
||||
raise SystemExit
|
||||
hf_id = feld("HF_ID").strip("`").strip()
|
||||
rolle = feld("ROLLE").lower().strip()
|
||||
beleg = feld("BELEG").strip()
|
||||
warum = feld("WARUM").strip()
|
||||
if rolle not in ("hermes", "coder", "heavy", "vision", "scout", "embed"):
|
||||
print(f"VERWORFEN: unbekannte Rolle {rolle!r}")
|
||||
raise SystemExit
|
||||
# Amtsinhaber-Sperre (Testlauf-Befund 17.07.: Radar schlug das eigene Hirn-Repo vor):
|
||||
# gleicher Modellname = Repackaging, kein Kandidat.
|
||||
AMTSINHABER = ("qwen3.6-35b-a3b", "qwen3-vl-30b", "gpt-oss-120b",
|
||||
"qwen3-coder-next", "glm-4.6v-flash", "qwen3-embedding")
|
||||
if any(t in hf_id.lower() for t in AMTSINHABER):
|
||||
print(f"VERWORFEN: {hf_id} ist ein Amtsinhaber-Repackaging (mechanische Sperre)")
|
||||
raise SystemExit
|
||||
if hf_id not in evid:
|
||||
print(f"VERWORFEN: HF_ID {hf_id!r} steht nicht wörtlich im Material (Zitat-Gate)")
|
||||
raise SystemExit
|
||||
if beleg and norm(beleg) not in norm(evid):
|
||||
print("VERWORFEN: BELEG nicht wörtlich im Material (Zitat-Gate)")
|
||||
raise SystemExit
|
||||
key = re.sub(r"[^a-z0-9-]+", "-", hf_id.lower())[:60].strip("-")
|
||||
try:
|
||||
state = json.load(open(state_pfad))
|
||||
except Exception:
|
||||
state = {}
|
||||
gemeldet = state.get("kandidaten_gemeldet") or []
|
||||
if key in gemeldet:
|
||||
print(f"VERWORFEN: {key} schon früher gemeldet")
|
||||
raise SystemExit
|
||||
spec = {"key": key, "rolle": rolle, "hf_id": hf_id, "warum": warum,
|
||||
"beleg": beleg, "erstellt": time.strftime("%Y-%m-%d %H:%M"),
|
||||
"quelle": "trend-radar"}
|
||||
with open(os.path.join(queue, key + ".json"), "w", encoding="utf-8") as f:
|
||||
json.dump(spec, f, ensure_ascii=False, indent=1)
|
||||
state["kandidaten_gemeldet"] = gemeldet + [key]
|
||||
json.dump(state, open(state_pfad, "w"))
|
||||
print(f"EINGEREIHT: {hf_id} → Rolle {rolle} (Prüfstand prüft in der Nacht So 01:00)")
|
||||
PY
|
||||
)"
|
||||
echo "### 6. PRÜFSTAND-QUEUE"
|
||||
echo "$ERGEBNIS"
|
||||
|
||||
# ---------- Puls in den State (Vorwoche) + an die Langzeit-Historie anhängen ----------
|
||||
python3 - "$STATE" "$TMPD/puls.json" "$HIST" "${INST_NUM:-}" <<'PY'
|
||||
import json, sys, time
|
||||
try:
|
||||
state = json.load(open(sys.argv[1]))
|
||||
except Exception:
|
||||
state = {}
|
||||
try:
|
||||
puls = json.load(open(sys.argv[2]))
|
||||
except Exception:
|
||||
puls = None
|
||||
if puls:
|
||||
state["puls"] = puls
|
||||
with open(sys.argv[3], "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps({"ts": int(time.time()),
|
||||
"datum": time.strftime("%Y-%m-%d"),
|
||||
"engine": sys.argv[4], "puls": puls},
|
||||
ensure_ascii=False) + "\n")
|
||||
json.dump(state, open(sys.argv[1], "w"))
|
||||
PY
|
||||
|
||||
briefkasten "Trend-Radar" "Wochenlauf fertig. Engine: ${ENGINE_BLOCK}
|
||||
Queue: ${ERGEBNIS}"
|
||||
@@ -1,31 +0,0 @@
|
||||
# Trend-Radar — Auftrag für den Boten-Agenten
|
||||
|
||||
Du bist der Bote des wöchentlichen Trend-Radars. Unten steht ein automatisch
|
||||
erhobener BEFUND in sechs Abschnitten: Live-Puls aller Modell-Rollen, Engine-A/B,
|
||||
Watch-Liste, HuggingFace-Trending, Analysten-Analyse und Prüfstand-Queue. Das
|
||||
Skript hat alles Nötige BEREITS getan (gemessen, geprüft, ggf. einen Kandidaten
|
||||
für den nächtlichen Prüfstand eingereiht).
|
||||
|
||||
Deine Aufgabe — NUR berichten, in Lucys Stimme (Anrede „Commander",
|
||||
Alltagssprache, Fazit zuerst), in höchstens 6 Sätzen:
|
||||
|
||||
1. Puls: nur Auffälligkeiten nennen (⚠️-Zeilen: Regression oder ausgefallene
|
||||
Messung); wenn alles im Rahmen ist, reicht „Tempo aller Rollen stabil".
|
||||
2. Engine-A/B: Ergebnis in einem Satz (Prozent-Delta oder warum kein Vergleich
|
||||
lief). Das ist die FRÜHWARNUNG vor dem So-04:30-Auto-Update: bei Lade-Crash
|
||||
oder klar negativem Delta dem Commander empfehlen, die Engine vorher zu
|
||||
pinnen (Wartungs-Drawer / mc2-pins.json) — sonst reicht der Messwert.
|
||||
3. Watch-Liste: nur Zeilen mit „⚠️" erwähnen — eine erfüllte Bedingung ist die
|
||||
wichtigste Nachricht des Berichts.
|
||||
4. Kandidat: wenn einer eingereiht wurde, Modell + Rolle nennen und sagen, dass
|
||||
der Prüfstand ihn nachts mit dem vollen Programm prüft und das Ergebnis als
|
||||
Karte kommt; bei „KEIN_KANDIDAT" GENAU EIN kurzer Satz dazu.
|
||||
|
||||
Harte Regeln:
|
||||
- NICHTS selbst umsetzen: keine Updates, keine Rollen-Wechsel, keine Downloads,
|
||||
keine Kanban-Aktionen — alles Entscheidende liegt beim Commander bzw. hat das
|
||||
Skript schon erledigt.
|
||||
- Keine Zahlen erfinden oder runden, die nicht im Befund stehen; im Zitat-Gate
|
||||
VERWORFENE Kandidaten nicht als Erfolg verkaufen.
|
||||
- Die stille Briefkasten-Karte hat das Skript bereits geschrieben — du lieferst
|
||||
nur die kurze Telegram-Meldung.
|
||||
@@ -1,23 +0,0 @@
|
||||
[Unit]
|
||||
Description=MC2 Voice Sidecar — lokales STT (faster-whisper) + gestuftes TTS (Piper/Chatterbox)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
# Eigenes Python-3.12-venv (~/.voice/venv) — torch/chatterbox/faster-whisper passen nicht ins
|
||||
# 3.14-Backend-venv (analog mem0-service). Bind 127.0.0.1: nur lokal; MC2 proxyt nach außen.
|
||||
Type=simple
|
||||
WorkingDirectory=%h/mission-control-v2/voice_service
|
||||
Environment=VOICE_PORT=8650
|
||||
Environment=VOICE_STT_MODEL=medium
|
||||
Environment=VOICE_STT_LANG=de
|
||||
Environment=VOICE_PIPER_DIR=%h/.voice/voices
|
||||
Environment=VOICE_PIPER_DEFAULT=de_DE-thorsten-medium
|
||||
Environment=VOICE_CHATTERBOX_DEVICE=cpu
|
||||
Environment=VOICE_CHATTERBOX_LANG=de
|
||||
Environment=TOKENIZERS_PARALLELISM=false
|
||||
ExecStart=%h/.voice/venv/bin/python -m uvicorn app:app --host 127.0.0.1 --port 8650
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
Reference in New Issue
Block a user