#!/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" 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)" python3 - "$ENDPOINT" "$STATE" "$TMPD/puls.json" <<'PY' import json, sys, time, urllib.request endpoint, state_pfad, out_pfad = sys.argv[1], sys.argv[2], sys.argv[3] 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) 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: " / "ROLLE: " / "BELEG: " / "WARUM: ". Danach optional: "HINWEISE:" gefolgt von maximal 2 Spiegelstrichen mit je einem Satz + wörtlichem Beleg-Zitat aus dem Material.' EVID="$(cat </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 (für den Vorwochen-Vergleich) ---------- python3 - "$STATE" "$TMPD/puls.json" <<'PY' import json, sys try: state = json.load(open(sys.argv[1])) except Exception: state = {} try: state["puls"] = json.load(open(sys.argv[2])) except Exception: pass json.dump(state, open(sys.argv[1], "w")) PY briefkasten "Trend-Radar" "Wochenlauf fertig. Engine: ${ENGINE_BLOCK} Queue: ${ERGEBNIS}"