Files
mission-control-v2/deploy/trend-radar-feed.sh
T
Hitonabi de21e1ac68 Pruefstand+Radar: MTP-Fairness, Tempo-Baseline-Refresh, Puls-Langzeit-Historie
Drei Luecken aus der Selbst-Review 17.07.:
1. MTP-Fairness: Kandidaten mit MTP-Kopf (Dateiname oder nextn-Tensoren im
   GGUF-Kopf) bekommen eine Zusatz-Tempomessung mit draft-mtp; Steckbrief
   rechnet best-of speed/speed_mtp - das Live-Hirn laeuft schliesslich MIT MTP.
2. Baseline-Refresh: baseline.json traegt jetzt den Engine-Build; weicht die
   installierte Engine ab (So-Auto-Update), misst der Pruefstand-Cron nur den
   Tempo-Teil aller Rollen neu (Geister-Zahlen-Schutz, Qualitaet bleibt gueltig).
3. Langzeit-Historie: Puls wird als JSONL angehaengt statt ueberschrieben;
   Radar vergleicht zusaetzlich gegen ~8 Wochen zurueck und meldet
   SCHLEICHENDE REGRESSION, die woechentlich unter der 10-%-Schwelle bleibt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 16:28:27 +02:00

470 lines
22 KiB
Bash

#!/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, 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 == "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}"