Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34a9862591 | ||
|
|
f3139d2b5d | ||
|
|
6e573d5551 | ||
|
|
2210bf5c40 | ||
|
|
dc78114102 | ||
|
|
259e1b2ca8 | ||
|
|
148a039545 | ||
|
|
25dbfb2b61 | ||
|
|
fa69edc1aa | ||
|
|
9cf04c427e | ||
|
|
f446da8ae6 | ||
|
|
c2a54bd25d | ||
|
|
56668ee43d | ||
|
|
c8e5a7162b | ||
|
|
00b64b9fdc | ||
|
|
7c23ebf382 |
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
# coder-vollkontext.sh — gibt dem Coder den ganzen Kontext (23.08.2026).
|
||||
#
|
||||
# WARUM: llama-swap startet den Coder mit `-c 131072 --parallel 2`. Das sind
|
||||
# zwei Slots à 65.536 — und nur diese 65.536 stehen einer Sitzung zur Verfuegung.
|
||||
# Der Coder hat aber genau EINEN Verbraucher: OpenChamber. Lucy benutzt `hermes`,
|
||||
# der explore-Subagent auch, `review` benutzt `heavy`. Die zweite Bahn liegt brach.
|
||||
#
|
||||
# Mit `--parallel 1` bekommt eine Sitzung die vollen 131.072 — doppelt so viel
|
||||
# Platz, halb so viel Komprimieren.
|
||||
#
|
||||
# ‼️ NICHT waehrend eines laufenden Auftrags ausfuehren: llama-swap laeuft mit
|
||||
# --watch-config und laedt beim Speichern SOFORT neu. Das schneidet eine
|
||||
# laufende Sitzung mitten im Satz ab.
|
||||
#
|
||||
# Aufruf: bash deploy/coder-vollkontext.sh (Trockenlauf, zeigt den Diff)
|
||||
# bash deploy/coder-vollkontext.sh --ja (wendet an)
|
||||
set -uo pipefail
|
||||
|
||||
CONF="${LLAMA_SWAP_CONF:-/etc/llama-swap/config.yaml}"
|
||||
BLOCK="${CODER_BLOCK:- Qwen3.8-27B:}"
|
||||
|
||||
if [ ! -r "$CONF" ]; then echo "ABBRUCH: $CONF nicht lesbar"; exit 1; fi
|
||||
|
||||
TMP="$(mktemp)"
|
||||
cp "$CONF" "$TMP"
|
||||
|
||||
python3 - "$TMP" "$BLOCK" <<'PY'
|
||||
import io, re, sys
|
||||
pfad, block = sys.argv[1], sys.argv[2]
|
||||
z = io.open(pfad, encoding="utf-8").read().split("\n")
|
||||
try:
|
||||
start = next(i for i, l in enumerate(z) if l.startswith(block))
|
||||
except StopIteration:
|
||||
print("ABBRUCH: Block '%s' nicht gefunden" % block); sys.exit(1)
|
||||
ende = next((i for i in range(start + 1, len(z)) if re.match(r"^ [A-Za-z]", z[i])), len(z))
|
||||
|
||||
# Streng auf den Coder-Block begrenzt — `fast`/`hermes` MUESSEN ihre zwei Slots
|
||||
# behalten, dort greifen Lucy und der Explorer wirklich gleichzeitig zu.
|
||||
n_par = n_ctx = 0
|
||||
for i in range(start, ende):
|
||||
if "--parallel 2" in z[i]:
|
||||
z[i] = z[i].replace("--parallel 2", "--parallel 1"); n_par += 1
|
||||
if z[i].strip() == "context: 65536":
|
||||
z[i] = z[i].replace("65536", "131072"); n_ctx += 1
|
||||
|
||||
if n_par != 1 or n_ctx != 1:
|
||||
print(f"ABBRUCH: unerwartet (parallel {n_par}x, context {n_ctx}x) — Konfiguration hat sich geaendert")
|
||||
sys.exit(1)
|
||||
io.open(pfad, "w", encoding="utf-8").write("\n".join(z))
|
||||
PY
|
||||
if [ $? -ne 0 ]; then rm -f "$TMP"; exit 1; fi
|
||||
|
||||
echo "=== Aenderung ==="
|
||||
diff "$CONF" "$TMP"
|
||||
echo
|
||||
|
||||
if [ "${1:-}" != "--ja" ]; then
|
||||
echo "TROCKENLAUF — nichts geaendert. Anwenden mit: bash $0 --ja"
|
||||
rm -f "$TMP"; exit 0
|
||||
fi
|
||||
|
||||
SICHER="${CONF}.bak-$(date +%Y%m%d-%H%M%S)"
|
||||
sudo -n cp "$CONF" "$SICHER" && echo "Sicherung: $SICHER"
|
||||
sudo -n cp "$TMP" "$CONF" && echo "angewendet — llama-swap laedt durch --watch-config selbst neu"
|
||||
rm -f "$TMP"
|
||||
|
||||
echo
|
||||
echo "Warte auf den Neustart des Coders..."
|
||||
sleep 8
|
||||
curl -s -m 15 "http://127.0.0.1:8080/v1/models" >/dev/null 2>&1 \
|
||||
&& echo "llama-swap antwortet wieder" \
|
||||
|| echo "ACHTUNG: llama-swap antwortet nicht — Sicherung liegt unter $SICHER"
|
||||
|
||||
echo
|
||||
echo "Danach in ~/.config/opencode/opencode.json auf dem PC nachziehen:"
|
||||
echo " coder: limit.context 65536 -> 131072"
|
||||
@@ -0,0 +1,199 @@
|
||||
# Die drei Jobs (KISS-Umbau, 21.08.2026)
|
||||
|
||||
Vorher: **4 Hermes-Crons + 6 systemd-Timer**, verteilt auf zwei Mechanismen.
|
||||
Deshalb fiel am 20.08. tagelang niemandem auf, dass ein Waechter fehlte —
|
||||
niemand schaut an zwei Orten nach.
|
||||
|
||||
Jetzt: **drei Jobs, ein Ort.** `hermes cron list` zeigt die gesamte Automatik.
|
||||
|
||||
| Job | Wann | Art | Skript |
|
||||
|---|---|---|---|
|
||||
| **Daily News Report** | taeglich 07:00 | Agent + Websuche | — |
|
||||
| **KI und Stack Radar** | samstags 08:00 | `--no-agent` | `stack-radar.sh` → `stack-ist.sh` |
|
||||
| **Updates am Sonntag** | sonntags 04:30 | `--no-agent` | `sonntags-update.sh` → `autoupdate.sh` |
|
||||
|
||||
Daneben laufen als stille Rohrleitung weiter: `mc2-backup` (03:33) und
|
||||
`projekte-sync` (stuendlich). Die melden sich nie, die sichern und synchronisieren nur.
|
||||
|
||||
## Der Entwurfsgrundsatz
|
||||
|
||||
> **Fakten sammelt ein Skript, Prosa schreibt das Modell.**
|
||||
|
||||
Der alte Selbsttest war am 20.08. gruen, waehrend vier Dinge kaputt waren: das
|
||||
Kritiker-Gate zeigte seit einem Tag auf ein umbenanntes Modell (404), Lucys
|
||||
`SOUL.md` war blockiert, das Dashboard stand ohne Passwort offen, die CI war rot.
|
||||
Er hatte geprueft, ob Dienste **antworten** — nicht, ob sie **stimmen**.
|
||||
|
||||
`stack-ist.sh` prueft deshalb Ergebnisse: loesen die Rollen-Aliase noch auf? Ist
|
||||
der letzte Timer-Lauf gutgegangen? Ist eine Sicherung juenger als zwei Tage?
|
||||
Liefert die oeffentliche Adresse Daten ohne Anmeldung? Beim ersten Lauf hat es
|
||||
sofort zwei echte Befunde gefunden.
|
||||
|
||||
## Kugelsicher heisst konkret
|
||||
|
||||
- **Das Modell steht nicht im kritischen Pfad.** Antwortet es nicht, gehen die
|
||||
Rohbefunde trotzdem raus. Nur die Prosa faellt weg, nie die Meldung.
|
||||
- **Es meldet immer.** „Alles gruen" ist ein Ergebnis, kein Grund zu schweigen.
|
||||
- **Kein `set -e`.** Ein einzelner fehlschlagender Test darf den Bericht nicht
|
||||
abschneiden.
|
||||
- **Ein Melderweg:** `deploy/notify.sh` erreicht Telegram **und** Lucys
|
||||
Briefkasten (aus dem sie spricht) in einem Aufruf. Die Jobs laufen mit
|
||||
`--deliver local`, damit Hermes nicht ein zweites Mal sendet.
|
||||
- **Kein Fuellmaterial.** Die Prompts verbieten ausgedachte Vorschlaege
|
||||
ausdruecklich — ein Bericht mit Fuellmaterial wird nicht gelesen, und dann
|
||||
auch der echte Befund nicht.
|
||||
|
||||
## Ausbringen
|
||||
|
||||
Die Skripte muessen unter `~/.hermes/scripts/` liegen (Vorgabe von `hermes cron --script`).
|
||||
Quelle ist dieses Verzeichnis:
|
||||
|
||||
```bash
|
||||
scp deploy/jobs/*.sh hitonabi@192.168.178.151:/home/hitonabi/.hermes/scripts/
|
||||
ssh hitonabi@192.168.178.151 'cd ~/.hermes/scripts && sed -i "s/\r$//" *.sh && chmod +x *.sh'
|
||||
```
|
||||
|
||||
‼️ Das `sed` ist Pflicht: vom Windows-PC kopierte Dateien haben CRLF, und bash
|
||||
scheitert daran mit unverstaendlichen Meldungen.
|
||||
|
||||
## Abgeschaltet am 21.08.
|
||||
|
||||
| Weg | Warum |
|
||||
|---|---|
|
||||
| Cron `morgen-digest` | geht im Daily News Report auf |
|
||||
| Cron `tech-radar` | geht im Stack Radar auf |
|
||||
| Cron `nacht-wartung` | pruefte Lebenszeichen, nicht Ergebnisse |
|
||||
| Cron `wissens-sync` | **war nie gelaufen** — kein `last_run_at` |
|
||||
| Timer `mc2-bagatell` | **15 Naechte hintereinander „0 eingespielt"** |
|
||||
| Timer `mc2-selfsmoke` | geht in `stack-ist.sh` auf |
|
||||
| Timer `pbs-backup` | doppelt zu `mc2-backup` und seit 21.08. rot (`/srv/models/mem0` gibt es nicht mehr) |
|
||||
| Timer `mc2-autoupdate` | wird jetzt vom Cron „Updates am Sonntag" gestartet |
|
||||
|
||||
Die Unit-Dateien liegen noch da, nur `disable`d — Rueckbau ist ein Befehl.
|
||||
|
||||
---
|
||||
|
||||
## Nachtrag 21.08.: Persona, Quellen-Links und Stimme
|
||||
|
||||
**Fehler, den ich gemacht hatte:** Mein erster Prompt fuer den Daily News Report
|
||||
schrieb woertlich *„keine Emojis, keine Aufzaehlungspunkte, keine
|
||||
Ueberschriften-Deko"* — und hat damit genau das wegoptimiert, was den Bericht
|
||||
vorher gut machte.
|
||||
|
||||
★★ **Die Formatierung kam nie aus dem Job-Prompt.** Die alten Prompts waren
|
||||
kurz (*„eine praegnante 3-Punkte-Zusammenfassung an den Commander"*). Emojis,
|
||||
Ton und die Anrede stehen in **`~/.hermes/SOUL.md`** — Lucys Persona:
|
||||
|
||||
> Du sprichst den Nutzer IMMER mit **Commander** an.
|
||||
> Locker, herzlich, schlagfertig, charmant, selbstbewusst.
|
||||
|
||||
**Lehre: den Job-Prompt kurz halten und die Persona arbeiten lassen.** Wer im
|
||||
Prompt Stil verbietet, ueberstimmt die Persona — und merkt es erst, wenn die
|
||||
Nachricht seelenlos ankommt.
|
||||
|
||||
### Stimme in Telegram
|
||||
|
||||
`hermes send` kann **`MEDIA:<pfad>`**. Damit geht eine fertige Audiodatei als
|
||||
Anhang nach Telegram. `deploy/jobs/news-melden.sh` macht daraus einen Aufruf:
|
||||
|
||||
```
|
||||
Agent schreibt zwei Dateien
|
||||
/tmp/news-text.md (Emojis, Links, Formatierung) -> notify.sh -> Telegram + Briefkasten
|
||||
/tmp/news-sprich.txt (3-4 Saetze, nichts Vorlesbares fehlt) -> :8650/tts -> WAV -> hermes send MEDIA:
|
||||
```
|
||||
|
||||
‼️ **Nicht Hermes' eingebautes TTS nehmen.** Das steht auf `tts.provider: edge`
|
||||
mit `en-US-AriaNeural` — englisch. Lucys echte Stimme ist MC2s `voice-service`
|
||||
auf `:8650` (Piper `de_DE-thorsten-medium`). Deshalb ruft das Skript den
|
||||
Sidecar direkt per curl.
|
||||
|
||||
‼️ **Kein ffmpeg noetig.** Telegram nimmt die WAV direkt an. Fuer eine echte
|
||||
Sprachnachricht mit Wellenform braeuchte es OGG/Opus und damit ffmpeg — bewusst
|
||||
nicht installiert, eine Abhaengigkeit weniger.
|
||||
|
||||
Der Agent ruft **einen** Befehl auf, alles danach ist deterministisch. Faellt
|
||||
die Stimme aus, geht der Text trotzdem raus.
|
||||
|
||||
### ‼️ Folgefehler der Werkzeug-Abschaltung — gefunden und behoben
|
||||
|
||||
Nach dem Abschalten von `kanban` und `delegation` standen in `SOUL.md` noch
|
||||
zwei Anweisungen, die ins Leere zeigten: *„rufst du sofort dein Werkzeug
|
||||
delegate_task auf"* und *„Ideen traegst du sofort im Kanban-Auftragsbuch ein"*.
|
||||
Genau die Klasse stiller Defekt, die am 19.08. das Kritiker-Gate zerlegt hat.
|
||||
Beide Abschnitte ersetzt (Sicherung: `~/.hermes/SOUL.md.bak-20260821`).
|
||||
|
||||
**Merke: wer einen Werkzeugsatz abschaltet, muss `SOUL.md` mitlesen.**
|
||||
|
||||
### Kugelsicher-Regeln, die dazugekommen sind
|
||||
|
||||
- `hermes cron edit <id> "text"` **speichert nichts** — der Prompt muss ueber
|
||||
**`--prompt`** kommen. Ohne Flag gibt der Befehl den Text nur aus und die
|
||||
alte Fassung bleibt stehen. Nach jeder Aenderung in `jobs.json` nachsehen.
|
||||
- Alte Job-Fassungen liegen im Zustands-Backup:
|
||||
`tar -xzf /srv/models/mc2-backups/mc2-state-*.tar.gz ./hermes/cron/jobs.json`
|
||||
- Tagesaktualitaet muss man erzwingen: Datum per `date` feststellen lassen,
|
||||
mehrere Suchen verlangen, und Meldungen ohne belegbares Datum verwerfen.
|
||||
|
||||
---
|
||||
|
||||
## Nachtrag 3 (21.08.): Lucys ECHTE Stimme, echte Sprachnachricht
|
||||
|
||||
### Der Fehler: :8650 ist nicht Lucy
|
||||
|
||||
Ich hatte `/tts` auf MC2s `voice-service` (`:8650`) ohne Angabe von Engine und
|
||||
Stimme aufgerufen. Dessen `/health` sagt:
|
||||
|
||||
```
|
||||
"engines":["elevenlabs","edge"]
|
||||
```
|
||||
|
||||
Piper und Chatterbox sind dort **gar nicht geladen** — die Vorgabe fiel auf die
|
||||
erste verfuegbare: ElevenLabs *„Artoria DE · Saber · Hermes-Stimme"*. Das ist
|
||||
**Hermes' Stimme**, nicht Lucys. Deutsch und weiblich, deshalb faellt es nicht
|
||||
sofort auf. Beide verfuegbaren Engines sind ausserdem **Cloud** — gegen die
|
||||
100-%-lokal-Praemisse.
|
||||
|
||||
### Lucys Stimme: `lucy-stimme.service` auf `:8021`
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Engine | **Kyutai pocket-tts 2.1.0** (neueste, seit 04.05. unveraendert) |
|
||||
| Modell | **`german_24l`** — die volle Fassung, nicht die destillierte |
|
||||
| Stimme | geklont aus `ref.mp3`, gecacht in `lucy_voice.safetensors` (44 MB) |
|
||||
| Laeuft | `~/.lucy-stimme/`, systemd-**user**-Dienst, `enable`d, CPU |
|
||||
| Start | ~48 s Ladezeit, danach ~5 s fuer 3,7 s Audio |
|
||||
|
||||
Mitgezogen wurde die **ganze Abstimmung**, nicht nur das Modell: `text_norm.py`
|
||||
(Symbole/Pfade/URLs → Zahlen → Akronyme), Emotions-Voreinstellungen mit
|
||||
Anlaufwoertern, Umlaut-Wortliste, Hochpass auf der Referenz, kalibrierter Pegel.
|
||||
Ohne die klingt pocket nicht wie Lucy.
|
||||
|
||||
‼️ Der Sweep im TTS-Plan (*„seriell schlaegt parallel"*) wurde auf einem **9700X**
|
||||
gemessen. Die Box ist ein Ryzen AI MAX+ 395 — das Ergebnis ist **nicht
|
||||
uebertragen**, nur uebernommen. Wer Tempo braucht, misst neu.
|
||||
|
||||
### Recherche 21.08.: pocket bleibt
|
||||
|
||||
Nichts seit Mai schlaegt es auf dieser Achse. Die Alternativen sind 5–17× groesser:
|
||||
Qwen3-TTS 0,6–1,7 Mrd. (Apache 2.0, Deutsch, 3-Sekunden-Klon), NeuTTS Air 0,5 Mrd.
|
||||
(GGUF), CosyVoice2 0,5 Mrd. — gegen pockets **100 Mio.** Fuer einen taeglichen
|
||||
Sprachnachrichten-Job auf CPU ist das der falsche Handel.
|
||||
**Ihr wart bereits auf dem neuesten Stand** — nichts zu aktualisieren.
|
||||
|
||||
### Echte Sprachnachricht braucht OGG/Opus
|
||||
|
||||
Eine WAV kommt in Telegram als **Dateianhang** an. Fuer das runde Sprachmemo mit
|
||||
Wellenform braucht es OGG/Opus — dafuer wurde **ffmpeg installiert** (8.0.1):
|
||||
|
||||
```bash
|
||||
ffmpeg -y -i ton.wav -c:a libopus -b:a 32k -ar 48000 -ac 1 ton.ogg
|
||||
hermes send --to telegram "MEDIA:ton.ogg"
|
||||
```
|
||||
|
||||
Ganze Kette (Text + Stimme + Versand) gemessen: **10 Sekunden**.
|
||||
|
||||
### Altlast am Rande
|
||||
|
||||
`F:\Coding Stuff\lucy\lucy-tts\Lucy-Startklar.bat` zeigt auf
|
||||
`mission-control-2\client\lucy-tts` — den Ordner gibt es seit der Repo-Trennung
|
||||
nicht mehr.
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
# news-melden.sh — schickt den fertigen Nachrichtenbericht als TEXT und als
|
||||
# SPRACHNACHRICHT an den Commander (21.08.2026, überarbeitet 22.08.).
|
||||
#
|
||||
# Der Agent ruft genau EINEN Befehl auf. Alles danach ist deterministisch —
|
||||
# Zustellung darf nicht davon abhaengen, ob ein Modell die Reihenfolge einhaelt.
|
||||
#
|
||||
# ‼️ WARUM DAS SKRIPT SOFORT ZURUECKKEHRT (Fund vom 22.08., erster echter Lauf):
|
||||
# Hermes' terminal-Werkzeug bricht nach 30 s ab. Die Sprachsynthese dauert
|
||||
# laenger. Der Agent sah den Timeout, hielt den Aufruf fuer gescheitert und
|
||||
# rief das Skript ein zweites Mal auf — der Text ging DOPPELT raus
|
||||
# (07:01:42 und 07:02:14).
|
||||
# Deshalb: Text senden, Stimme abgekoppelt im Hintergrund, sofort raus.
|
||||
# Dazu eine Doppelversand-Sperre, die auch kuenftige Wiederholungen abfaengt.
|
||||
#
|
||||
# Erwartet zwei Dateien, die der Agent vorher geschrieben hat:
|
||||
# $1 Textfassung (mit Emojis, Links, Formatierung — geht nach Telegram)
|
||||
# $2 Sprechfassung (kurz, ohne Links/Emojis — wird vorgelesen)
|
||||
#
|
||||
# ‼️ STIMME: :8021, nicht :8650.
|
||||
# :8021 = lucy-stimme.service — Kyutai pocket-tts, Modell `german_24l`,
|
||||
# Stimme geklont aus ref.mp3. DAS ist Lucy. Lokal, auf der CPU.
|
||||
# :8650 = MC2s voice-service. Dort sind nur `elevenlabs` und `edge` geladen —
|
||||
# beides CLOUD, und die Vorgabe ist die ElevenLabs-Stimme
|
||||
# "Artoria/Saber", also HERMES' Stimme, nicht Lucys.
|
||||
set -uo pipefail
|
||||
|
||||
NOTIFY="${NOTIFY:-$HOME/mission-control-v2/deploy/notify.sh}"
|
||||
VOICE="${VOICE_URL:-http://127.0.0.1:8021}"
|
||||
HERMES="${HERMES_BIN:-$HOME/.local/bin/hermes}"
|
||||
TON="/tmp/news-stimme.wav"
|
||||
TON_OGG="/tmp/news-stimme.ogg"
|
||||
|
||||
# ---------------------------------------------- Zweiter Durchgang: Stimme ---
|
||||
# Wird vom ersten Durchgang abgekoppelt neu gestartet. Laeuft beliebig lange,
|
||||
# ohne dass irgendein Werkzeug-Timeout hineinredet.
|
||||
if [ "${1:-}" = "--nur-stimme" ]; then
|
||||
SPRECHDATEI="${2:-/tmp/news-sprich.txt}"
|
||||
[ -s "$SPRECHDATEI" ] || exit 0
|
||||
|
||||
CODE=$(curl -s -m 600 -X POST "$VOICE/tts" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$(python3 -c 'import json,sys; print(json.dumps({"text": open(sys.argv[1], encoding="utf-8").read()[:1500]}))' "$SPRECHDATEI")" \
|
||||
-o "$TON" -w '%{http_code}' 2>/dev/null)
|
||||
|
||||
[ "$CODE" = "200" ] && [ -s "$TON" ] || exit 0
|
||||
|
||||
# Telegram zeigt eine ECHTE Sprachnachricht (runde Blase, Wellenform) nur bei
|
||||
# OGG/Opus. Eine WAV kaeme als Dateianhang an. Mono/48k ist das erwartete Format.
|
||||
if ! ffmpeg -y -loglevel error -i "$TON" -c:a libopus -b:a 32k -ar 48000 -ac 1 "$TON_OGG" 2>/dev/null; then
|
||||
TON_OGG="$TON"
|
||||
fi
|
||||
"$HERMES" send --to telegram "MEDIA:$TON_OGG" >/dev/null 2>&1
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ------------------------------------------- Erster Durchgang: Text + Start ---
|
||||
TEXTDATEI="${1:-/tmp/news-text.md}"
|
||||
SPRECHDATEI="${2:-/tmp/news-sprich.txt}"
|
||||
|
||||
if [ ! -s "$TEXTDATEI" ]; then
|
||||
bash "$NOTIFY" -s "[News]" "Der Nachrichtenbericht ist leer geblieben ($TEXTDATEI). Da lief etwas schief."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Doppelversand-Sperre. Greift, egal WARUM ein zweiter Aufruf kommt —
|
||||
# Timeout, Modell-Wiederholung, versehentlicher Handstart.
|
||||
MARKE="/tmp/.news-gesendet-$(sha256sum "$TEXTDATEI" | cut -c1-16)"
|
||||
if [ -f "$MARKE" ]; then
|
||||
ALTER=$(( $(date +%s) - $(stat -c %Y "$MARKE" 2>/dev/null || echo 0) ))
|
||||
if [ "$ALTER" -lt 7200 ]; then
|
||||
echo "gemeldet (dieser Bericht ging vor ${ALTER}s bereits raus — nicht doppelt geschickt)"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
: > "$MARKE"
|
||||
find /tmp -maxdepth 1 -name '.news-gesendet-*' -mtime +2 -delete 2>/dev/null
|
||||
|
||||
bash "$NOTIFY" -s "" "$(cat "$TEXTDATEI")"
|
||||
|
||||
# Stimme abkoppeln: eigene Sitzung, kein Elternprozess, keine offenen Deskriptoren.
|
||||
# Damit ueberlebt sie das Ende dieses Skripts und blockiert es nicht.
|
||||
if [ -s "$SPRECHDATEI" ]; then
|
||||
setsid nohup bash "$0" --nur-stimme "$SPRECHDATEI" >/dev/null 2>&1 < /dev/null &
|
||||
disown 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "gemeldet"
|
||||
exit 0
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# sonntags-update.sh — Job 2: "Updates am Sonntag" (21.08.2026).
|
||||
#
|
||||
# Duenner Aufsatz auf deploy/autoupdate.sh. Bewusst KEIN Neubau: autoupdate.sh
|
||||
# macht seit Juli genau das Richtige — Router -> Engine -> Hermes, jede Ebene
|
||||
# mit Update-Pruefung, Postcheck, und bei Rot automatischem Rueckbau plus
|
||||
# Selbst-Pinnung. Es meldet auch schon selbst ueber notify.sh, also Telegram
|
||||
# UND Lucys Stimme.
|
||||
#
|
||||
# Dieser Aufsatz existiert nur, damit der Job unter `hermes cron list` steht
|
||||
# statt in einem separaten systemd-Timer. Ein Ort fuer alle Automatik — genau
|
||||
# deshalb ist am 20.08. tagelang niemandem aufgefallen, dass ein Waechter fehlte.
|
||||
#
|
||||
# Aufruf ueber Hermes-Cron:
|
||||
# hermes cron create '30 4 * * 0' --name 'Updates am Sonntag' \
|
||||
# --no-agent --script sonntags-update.sh --deliver local
|
||||
set -uo pipefail
|
||||
|
||||
AUTOUPDATE="${AUTOUPDATE:-$HOME/mission-control-v2/deploy/autoupdate.sh}"
|
||||
NOTIFY="${NOTIFY:-$HOME/mission-control-v2/deploy/notify.sh}"
|
||||
|
||||
melde() { bash "$NOTIFY" -s "[Box-Update]" "$1" 2>/dev/null || echo "$1" >&2; }
|
||||
|
||||
if [ ! -f "$AUTOUPDATE" ]; then
|
||||
melde "Sonntags-Update ausgefallen: $AUTOUPDATE gibt es nicht. Nichts wurde aktualisiert."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# autoupdate.sh meldet waehrend des Laufs selbst (inkl. der KRITISCH-Faelle, in
|
||||
# denen ein Bericht am Ende zu spaet kaeme). Wir fangen hier nur den Fall ab,
|
||||
# dass es gar nicht erst durchlaeuft — sonst waere das Schweigen zweideutig.
|
||||
AUSGABE="$(bash "$AUTOUPDATE" 2>&1)"
|
||||
CODE=$?
|
||||
|
||||
if [ "$CODE" -ne 0 ]; then
|
||||
melde "Sonntags-Update mit Fehler beendet (exit $CODE). Letzte Zeilen:
|
||||
|
||||
$(printf '%s\n' "$AUSGABE" | tail -15)"
|
||||
fi
|
||||
|
||||
# Erfolgsfall: autoupdate.sh hat bereits gemeldet. Leere Ausgabe -> Hermes
|
||||
# schweigt, also genau eine Nachricht statt zwei.
|
||||
exit 0
|
||||
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env bash
|
||||
# stack-ist.sh — der IST-Zustand der Box, in Fakten (21.08.2026).
|
||||
#
|
||||
# Wird von Hermes-Cron "stack-radar" ueber --script eingespeist: die Ausgabe hier
|
||||
# landet im Prompt des Agenten, der daraus den Bericht schreibt.
|
||||
#
|
||||
# GRUNDSATZ: Dieses Skript prueft ERGEBNISSE, nicht Lebenszeichen.
|
||||
# Der alte Selbsttest war am 20.08. gruen, waehrend vier Dinge kaputt waren:
|
||||
# das Kritiker-Gate zeigte auf ein umbenanntes Modell (404), Lucys SOUL.md war
|
||||
# blockiert, das Dashboard stand ohne Passwort offen, die CI war rot.
|
||||
# "Dienst antwortet" haette das nie gefunden. Deshalb wird hier nachgesehen,
|
||||
# ob Dinge STIMMEN — nicht ob sie laufen.
|
||||
#
|
||||
# Nie mit set -e: ein einzelner fehlschlagender Test darf den Bericht nicht
|
||||
# abschneiden. Fehler werden gemeldet, nicht verschwiegen.
|
||||
set -uo pipefail
|
||||
|
||||
MC2="${MC2_URL:-http://127.0.0.1:9001}"
|
||||
SWAP="${SWAP_URL:-http://127.0.0.1:8080}"
|
||||
GITEA="${GITEA_URL:-http://192.168.178.153:3000}"
|
||||
OEFFENTLICH="${OEFFENTLICH_URL:-https://mc.tobisniceshomelab.ddnsfree.com}"
|
||||
|
||||
ok() { printf ' OK %s\n' "$*"; }
|
||||
warn() { printf ' ACHTUNG %s\n' "$*"; }
|
||||
bad() { printf ' ROT %s\n' "$*"; }
|
||||
|
||||
echo "IST-ZUSTAND DER BOX — $(date '+%d.%m.%Y %H:%M')"
|
||||
echo
|
||||
|
||||
# ---------------------------------------------------------------- Dienste ---
|
||||
echo "DIENSTE"
|
||||
# systemd stellt gescheiterten Units ein "●" voran — das muss weg, sonst meldet
|
||||
# der Bericht lauter Aufzaehlungspunkte als kaputte Dienste.
|
||||
# EGAL: Units, die dauerhaft scheitern und niemanden stoeren. Ein Bericht, der
|
||||
# jeden Tag denselben harmlosen Fehler zeigt, erzieht zum Wegsehen.
|
||||
EGAL="at-spi-dbus-bus.service"
|
||||
fehl=$(systemctl --user list-units --type=service --state=failed --no-legend --no-pager 2>/dev/null \
|
||||
| sed 's/^[^a-zA-Z]*//' | awk '{print $1}')
|
||||
gefunden=0
|
||||
for d in $fehl; do
|
||||
case " $EGAL " in *" $d "*) continue ;; esac
|
||||
bad "Dienst gescheitert: $d"; gefunden=1
|
||||
done
|
||||
[ "$gefunden" = "0" ] && ok "kein Dienst im Fehlerzustand"
|
||||
|
||||
for d in mc2-gateway mission-control-2 hermes-gateway voice-service; do
|
||||
systemctl --user is-active --quiet "$d.service" 2>/dev/null \
|
||||
&& ok "$d laeuft" || bad "$d laeuft NICHT"
|
||||
done
|
||||
systemctl is-active --quiet llama-swap.service 2>/dev/null \
|
||||
&& ok "llama-swap laeuft" || bad "llama-swap laeuft NICHT"
|
||||
echo
|
||||
|
||||
# ------------------------------------------------------------------ Timer ---
|
||||
# Nicht "ist der Timer aktiv", sondern "ist der letzte Lauf gutgegangen".
|
||||
echo "TIMER — letzter Lauf"
|
||||
for t in $(systemctl --user list-timers --no-legend --no-pager 2>/dev/null | awk '{print $NF}'); do
|
||||
dienst="${t%.service}.service"
|
||||
case "$dienst" in launchpadlib-*) continue ;; esac # Ubuntu-Eigenes, nicht unseres
|
||||
code=$(systemctl --user show -p ExecMainStatus --value "$dienst" 2>/dev/null)
|
||||
wann=$(systemctl --user show -p ExecMainExitTimestamp --value "$dienst" 2>/dev/null | cut -d' ' -f2,3)
|
||||
if [ -z "$wann" ]; then warn "$dienst ist eingerichtet, aber NIE gelaufen"
|
||||
elif [ "${code:-0}" = "0" ]; then ok "$dienst zuletzt $wann"
|
||||
else bad "$dienst zuletzt GESCHEITERT (exit $code) am $wann"; fi
|
||||
done
|
||||
echo
|
||||
|
||||
# --------------------------------------------------------- Modell-Aliase ---
|
||||
# Der Fund vom 19./20.08.: Skripte und Configs nennen Modelle beim Eigennamen,
|
||||
# das Modell wird umbenannt, und alles laeuft still in 404. Hier wird geprueft,
|
||||
# ob jeder Alias, den irgendwer benutzt, heute noch antwortet.
|
||||
echo "MODELL-ALIASE (Rollen muessen aufloesen)"
|
||||
liste=$(curl -s -m 10 "$MC2/v1/models" 2>/dev/null \
|
||||
| python3 -c 'import sys,json; print(" ".join(m["id"] for m in json.load(sys.stdin)["data"]))' 2>/dev/null)
|
||||
if [ -z "$liste" ]; then bad "Modell-Liste nicht abrufbar ueber $MC2/v1"
|
||||
else
|
||||
ok "verfuegbar: $liste"
|
||||
for rolle in hermes fast heavy; do
|
||||
case " $liste " in
|
||||
*" $rolle "*) ok "Rolle '$rolle' loest auf" ;;
|
||||
*) bad "Rolle '$rolle' FEHLT — Skripte, die sie nennen, laufen in 404" ;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
echo
|
||||
|
||||
# ------------------------------------------------------------- Erreichbar ---
|
||||
echo "ERREICHBARKEIT"
|
||||
for paar in "MC2:$MC2/api/health" "llama-swap:$SWAP/v1/models" "Gitea:$GITEA/api/v1/version"; do
|
||||
name="${paar%%:*}"; url="${paar#*:}"
|
||||
code=$(curl -s -m 10 -o /dev/null -w '%{http_code}' "$url" 2>/dev/null)
|
||||
[ "$code" = "200" ] && ok "$name antwortet (200)" || bad "$name antwortet mit '$code'"
|
||||
done
|
||||
echo
|
||||
|
||||
# ------------------------------------------------ Offen im Internet? --------
|
||||
# Der Fund vom 21.08.: MC2 hing ueber den Nginx Proxy Manager ungeschuetzt im
|
||||
# Netz. ufw sah nichts davon, weil der Proxy im LAN steht. Also von aussen
|
||||
# nachsehen — und zwar auf einem Endpunkt, der ECHTE Daten liefert.
|
||||
echo "OEFFENTLICHE ERREICHBARKEIT"
|
||||
code=$(curl -s -m 15 -o /tmp/.ist-oeff -w '%{http_code}' "$OEFFENTLICH/api/system/status" 2>/dev/null)
|
||||
if [ "$code" = "200" ] && head -c1 /tmp/.ist-oeff 2>/dev/null | grep -q '{'; then
|
||||
bad "MC2 liefert Systemdaten OHNE Anmeldung ins Internet ($OEFFENTLICH)"
|
||||
elif [ "$code" = "401" ] || [ "$code" = "403" ]; then
|
||||
ok "oeffentlicher Zugang verlangt Anmeldung ($code)"
|
||||
elif [ "$code" = "000" ]; then
|
||||
ok "von aussen nicht erreichbar"
|
||||
else
|
||||
warn "oeffentlicher Zugang antwortet mit '$code' — nachsehen"
|
||||
fi
|
||||
rm -f /tmp/.ist-oeff
|
||||
echo
|
||||
|
||||
# ----------------------------------------------------------------- Ports ---
|
||||
echo "OFFENE PORTS (ufw)"
|
||||
sudo -n ufw status 2>/dev/null | grep ALLOW | sed 's/^/ /' || warn "ufw-Status nicht lesbar"
|
||||
echo
|
||||
|
||||
# --------------------------------------------------------------- CI-Ampel ---
|
||||
echo "CI-AMPEL (Gitea)"
|
||||
T=$(tr -d '[:space:]' < "$HOME/.config/gitea/create-token" 2>/dev/null)
|
||||
if [ -z "$T" ]; then warn "kein Gitea-Token — CI nicht pruefbar"
|
||||
else
|
||||
curl -s -m 20 -H "Authorization: token $T" "$GITEA/api/v1/user/repos?limit=100" 2>/dev/null \
|
||||
| python3 -c '
|
||||
import sys, json
|
||||
try:
|
||||
repos = json.load(sys.stdin)
|
||||
except Exception as e:
|
||||
print(" ACHTUNG Repo-Liste nicht lesbar:", e); sys.exit(0)
|
||||
print(f" OK {len(repos)} Repos in Gitea")
|
||||
' 2>/dev/null || warn "Repo-Liste nicht lesbar"
|
||||
fi
|
||||
echo
|
||||
|
||||
# ------------------------------------------------------------- Sicherung ---
|
||||
echo "SICHERUNGEN"
|
||||
# Pfad aus backup.sh: DEST_DIR="$MODELS_DIR/mc2-backups"
|
||||
SICHER="${MC_BACKUP_DIR:-/srv/models/mc2-backups}"
|
||||
neuestes=$(find "$SICHER" -maxdepth 2 -type f -newermt '-2 days' 2>/dev/null | head -1)
|
||||
anzahl=$(find "$SICHER" -maxdepth 2 -type f 2>/dev/null | wc -l)
|
||||
if [ -n "$neuestes" ]; then
|
||||
ok "Sicherung juenger als 2 Tage vorhanden ($anzahl Staende in $SICHER)"
|
||||
elif [ "$anzahl" -gt 0 ]; then
|
||||
bad "juengste Sicherung ist AELTER als 2 Tage ($anzahl Staende in $SICHER)"
|
||||
else
|
||||
bad "gar keine Sicherung in $SICHER"
|
||||
fi
|
||||
echo
|
||||
|
||||
# ------------------------------------------------------------------ Platz ---
|
||||
echo "PLATZ UND LAST"
|
||||
# Nur eindeutige Dateisysteme: / und /srv liegen hier auf demselben Geraet,
|
||||
# doppelt gemeldet las sich das wie ein Befund.
|
||||
df -h --output=source,size,used,avail,pcent,target / /srv 2>/dev/null \
|
||||
| awk '!gesehen[$1]++ {printf " %s\n", $0}'
|
||||
echo " RAM: $(free -h | awk '/^Mem:/{print $3" von "$2" belegt"}')"
|
||||
echo
|
||||
|
||||
# ------------------------------------------------------------- Warm/Kalt ---
|
||||
# ---------------------------------------------------------------- Curator ---
|
||||
# Hierher gefaltet am 22.08.2026 aus dem Repo `curator-staleness-check`.
|
||||
# Das war ein eigener Wachhund mit eigenem systemd-Timer und eigener CI-Ampel —
|
||||
# und er ueberwachte einen Cron-Job ueber eine FEST VERDRAHTETE ID
|
||||
# (a47910e2ef05), den es laengst nicht mehr gibt. Er haette bei jedem Lauf
|
||||
# Alarm geschlagen fuer etwas, das gar nicht existiert.
|
||||
# Hier steht dieselbe Frage in vier Zeilen, ohne Repo, Timer und Ampel.
|
||||
echo "CURATOR"
|
||||
AN=$(grep -A3 '^curator:' "$HOME/.hermes/config.yaml" 2>/dev/null | grep -m1 'enabled:' | awk '{print $2}')
|
||||
if [ "${AN:-false}" != "true" ]; then
|
||||
ok "Curator ist abgeschaltet — nichts zu ueberwachen"
|
||||
else
|
||||
# %Ts liefert GANZE Sekunden. %T@ waere ein Float — bash rechnet damit nicht
|
||||
# und bricht mit "invalid arithmetic operator" ab.
|
||||
JUENGSTES=$(find "$HOME/.hermes/logs/curator" -type f -name '*.log' -printf '%Ts\n' 2>/dev/null | sort -rn | head -1)
|
||||
if [ -z "$JUENGSTES" ]; then
|
||||
bad "Curator ist AN, hat aber noch nie etwas protokolliert"
|
||||
else
|
||||
TAGE=$(( ( $(date +%s) - JUENGSTES ) / 86400 ))
|
||||
if [ "$TAGE" -le 7 ]; then ok "Curator war vor $TAGE Tagen zuletzt aktiv"
|
||||
elif [ "$TAGE" -le 21 ]; then warn "Curator seit $TAGE Tagen still — laeuft er noch?"
|
||||
else bad "Curator seit $TAGE Tagen still, obwohl eingeschaltet"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo
|
||||
|
||||
echo "MODELLE IM SPEICHER"
|
||||
curl -s -m 10 "$SWAP/running" 2>/dev/null \
|
||||
| python3 -c '
|
||||
import sys, json
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
except Exception:
|
||||
print(" ACHTUNG llama-swap-Status nicht lesbar"); sys.exit(0)
|
||||
lauf = d.get("running", [])
|
||||
if not lauf:
|
||||
print(" OK kein Modell geladen (alles kalt)")
|
||||
for m in lauf:
|
||||
print(" OK " + str(m.get("model")) + " - " + str(m.get("state")))
|
||||
'
|
||||
|
||||
echo
|
||||
echo "ENDE IST-ZUSTAND"
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env bash
|
||||
# stack-radar.sh — Job 3: "KI & Stack Radar" (21.08.2026).
|
||||
#
|
||||
# Ablauf: IST-Zustand holen (Fakten) -> Modell bewerten lassen (Prosa)
|
||||
# -> ueber notify.sh melden (Telegram + Lucys Stimme)
|
||||
#
|
||||
# KUGELSICHER heisst hier konkret:
|
||||
# * Die Fakten kommen aus stack-ist.sh, nicht aus dem Modell. Faellt das Modell
|
||||
# aus, werden die ROTEN Zeilen trotzdem gemeldet — roh statt gar nicht.
|
||||
# * Es meldet IMMER. "Alles gruen" ist ein Ergebnis, kein Grund zu schweigen.
|
||||
# * Kein set -e: ein einzelner Fehlschlag darf den Bericht nicht abschneiden.
|
||||
#
|
||||
# Aufruf ueber Hermes-Cron:
|
||||
# hermes cron create '0 8 * * 6' --name 'KI & Stack Radar' \
|
||||
# --no-agent --script stack-radar.sh --deliver local
|
||||
# (--no-agent, weil dieses Skript selbst meldet. Leere Ausgabe = Hermes schweigt.)
|
||||
set -uo pipefail
|
||||
|
||||
HIER="$(cd "$(dirname "$0")" && pwd)"
|
||||
IST_SKRIPT="${IST_SKRIPT:-$HOME/.hermes/scripts/stack-ist.sh}"
|
||||
NOTIFY="${NOTIFY:-$HOME/mission-control-v2/deploy/notify.sh}"
|
||||
GATEWAY="${GATEWAY_URL:-http://127.0.0.1:9010/v1}"
|
||||
MODELL="${RADAR_MODELL:-fast}" # bewusst NICHT heavy: das laedt 68 GB und der
|
||||
# Radar meldet danach seinen eigenen Speicherverbrauch als Befund
|
||||
|
||||
[ -x "$IST_SKRIPT" ] || IST_SKRIPT="$HIER/stack-ist.sh"
|
||||
|
||||
melde() {
|
||||
if [ -x "$NOTIFY" ] || [ -f "$NOTIFY" ]; then
|
||||
bash "$NOTIFY" -s "[Stack-Radar]" "$1"
|
||||
else
|
||||
# Letzte Rueckfallebene: wenigstens ins Log, damit nichts still verschwindet.
|
||||
echo "[stack-radar] MELDEWEG FEHLT — Bericht folgt roh:" >&2
|
||||
echo "$1" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------- 1. Fakten ---
|
||||
IST="$(bash "$IST_SKRIPT" 2>&1)"
|
||||
if [ -z "$IST" ]; then
|
||||
melde "Radar konnte den IST-Zustand nicht erheben — $IST_SKRIPT lieferte nichts. Das ist selbst ein Befund: bitte nachsehen."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ROT="$(printf '%s\n' "$IST" | grep -E '^\s+(ROT|ACHTUNG)' || true)"
|
||||
ANZ_ROT="$(printf '%s\n' "$ROT" | grep -c 'ROT' || true)"
|
||||
|
||||
# ------------------------------------------------------- 2. Bewertung ------
|
||||
# Das Modell bekommt NUR die Fakten und darf nichts dazuerfinden. Es soll
|
||||
# einordnen, nicht ermitteln.
|
||||
PROMPT="Du bist der Stack-Radar einer selbstgehosteten KI-Box. Unten steht der
|
||||
gemessene IST-Zustand. Schreibe einen kurzen Bericht auf Deutsch, per Du.
|
||||
|
||||
Regeln:
|
||||
- Erfinde NICHTS. Nur was unten steht.
|
||||
- Beginne mit einer Zeile Gesamturteil.
|
||||
- Dann: was ist ROT oder ACHTUNG, und was waere jeweils der naechste Schritt.
|
||||
- Verbesserungsvorschlaege NUR, wenn sich aus den Zahlen unten wirklich etwas
|
||||
Konkretes ergibt. Gibt es nichts, lass den Abschnitt ersatzlos weg. Schreibe
|
||||
lieber gar keinen Vorschlag als einen ausgedachten - ein Bericht mit
|
||||
Fuellmaterial wird nicht gelesen.
|
||||
- Was du an den Messwerten selbst merkwuerdig findest, gehoert unter ACHTUNG,
|
||||
nicht unter Verbesserungen.
|
||||
- Wenn alles gruen ist, sag das in einem Satz und hoere auf.
|
||||
- Keine Ueberschriften-Deko, keine Emojis, hoechstens 200 Woerter.
|
||||
|
||||
IST-ZUSTAND:
|
||||
$IST"
|
||||
|
||||
ANTWORT=""
|
||||
if NUTZLAST="$(python3 -c '
|
||||
import json, sys
|
||||
print(json.dumps({
|
||||
"model": sys.argv[1],
|
||||
"messages": [{"role": "user", "content": sys.argv[2]}],
|
||||
"max_tokens": 900,
|
||||
"temperature": 0.3,
|
||||
}))' "$MODELL" "$PROMPT" 2>/dev/null)"; then
|
||||
ANTWORT="$(curl -s -m 300 -X POST "$GATEWAY/chat/completions" \
|
||||
-H 'Content-Type: application/json' -d "$NUTZLAST" 2>/dev/null \
|
||||
| python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
print(json.load(sys.stdin)["choices"][0]["message"]["content"].strip())
|
||||
except Exception:
|
||||
pass' 2>/dev/null)"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------- 3. Melden ---
|
||||
if [ -n "$ANTWORT" ]; then
|
||||
melde "$ANTWORT"
|
||||
else
|
||||
# Modell stumm oder kaputt -> die Fakten gehen TROTZDEM raus. Genau dafuer
|
||||
# steht das Modell nicht im kritischen Pfad.
|
||||
if [ -n "$ROT" ]; then
|
||||
melde "Radar: Bewertung durch das Modell hat nicht geklappt — hier die Rohbefunde:
|
||||
|
||||
$ROT"
|
||||
else
|
||||
melde "Radar: alles gruen (Bewertung durch das Modell hat nicht geklappt, die Messung schon)."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Hermes bekommt eine LEERE Ausgabe -> keine zweite Nachricht. Gemeldet wurde
|
||||
# bereits ueber notify.sh, und das erreicht Telegram UND Lucys Stimme.
|
||||
exit 0
|
||||
@@ -0,0 +1,10 @@
|
||||
# Arbeitsanweisung (gilt fuer jeden Auftrag)
|
||||
|
||||
Arbeite Datei fuer Datei. Lies HOECHSTENS zwei Dateien, bevor du die erste aenderst.
|
||||
Kein Gesamtplan, keine Vollinventur. Eine halbfertige Aenderung ist wertvoller als
|
||||
eine vollstaendige Analyse ohne Aenderung.
|
||||
|
||||
- Nach jeder geaenderten Datei: kurz sagen, was geaendert wurde, dann zur naechsten.
|
||||
- Nicht erst das ganze Projekt kartieren. Nicht auf Vollstaendigkeit warten.
|
||||
- Wenn du unsicher bist, aendere die wahrscheinlichste Datei und pruefe danach.
|
||||
- Schreiben schlaegt Lesen.
|
||||
@@ -0,0 +1,51 @@
|
||||
# OpenCode-Konfiguration des PCs
|
||||
|
||||
**Hier liegt die Quelle. `%USERPROFILE%\.config\opencode\` ist die Kopie.**
|
||||
|
||||
Am 21.08.2026 habe ich die dortige `opencode.json` ueberschrieben, ohne sie
|
||||
vorher zu lesen — sie existierte nur an dieser einen Stelle, ohne Sicherung und
|
||||
ohne Historie. Rekonstruiert aus einer Juli-Sicherung. Damit das nicht nochmal
|
||||
passiert, liegt sie jetzt hier.
|
||||
|
||||
```powershell
|
||||
Copy-Item "deploy\opencode-config\*" "$env:USERPROFILE\.config\opencode\" -Force
|
||||
```
|
||||
|
||||
## Modellwahl (Stand 22.08.2026)
|
||||
|
||||
| Rolle | Modell | warum |
|
||||
|---|---|---|
|
||||
| **build** | `aibox/coder` — Qwen3.8-27B (dicht) | macht die eigentliche Arbeit |
|
||||
| **plan** | `aibox/heavy` — gpt-oss-120b | denkt vor, aendert nichts |
|
||||
| **explore** | `aibox/hermes` — Qwen3.6-35B-A3B | liest und sucht, immer warm |
|
||||
| `small_model` | `aibox/hermes` | Titel, Zusammenfassungen |
|
||||
|
||||
★★ **Korrektur einer falschen Empfehlung.** Bis zum 22.08. stand hier `fast`
|
||||
(Qwen3.6-35B-A3B, MoE) als Standard, mit der Begruendung, es schlage den `coder`
|
||||
„in Tempo und Qualitaet". **Das Tempo war gemessen, die Qualitaet nie** — in der
|
||||
Notiz vom 20.08. steht fuer den Coder woertlich „keine agentische Messung".
|
||||
|
||||
Die veroeffentlichten Zahlen sagen etwas anderes:
|
||||
|
||||
| | `fast` (3.6-35B-A3B) | `coder` (3.8-27B) |
|
||||
|---|---|---|
|
||||
| SWE-bench Verified | ~73,4 | 73,4 |
|
||||
| Terminal-Bench 2.1 | — | **73,0** |
|
||||
| DeepSWE 1.1 | — | **42,2** (Vorgaenger 3.6-27B: 13,3) |
|
||||
| OSWorld-Verified | — | **84,3** |
|
||||
|
||||
**Einzelschuss gleichauf, agentisch ueber viele Schritte zieht der Coder davon** —
|
||||
und genau das ist der Betrieb hier. Alle Zahlen stammen von Qwen selbst, teils
|
||||
aus eigenen Benchmark-Fassungen; sie sind ein Indiz, kein Beweis.
|
||||
|
||||
**Der Preis, gemessen:** dieselbe Leseaufgabe brauchte mit `fast` **24,3 s**,
|
||||
mit `coder` **89,3 s** — 3,7× laenger in der Wanduhr. Der Coder ist dicht und
|
||||
bandbreitenlimitiert (17 GB / 215 GB/s ≈ 12,6 t/s, Hardware-Grenze, kein
|
||||
Konfigfehler). Er hat ausserdem `ttl=5400`, faellt also nach 90 Minuten aus dem
|
||||
Speicher und muss neu laden.
|
||||
|
||||
**Was noch aussteht:** der echte Vergleich an einer echten Aufgabe.
|
||||
`F:\Coding Stuff\mc2-referenz` liegt genau dafuer bereit — 17 Dateien, davon 5
|
||||
erledigt, mit objektivem Pruefbefehl `docs/aufgaben/referenz-check.sh`.
|
||||
Erst dieser Lauf entscheidet die Frage; bis dahin ist die Modellwahl begruendet,
|
||||
aber nicht bewiesen.
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"aibox": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "AI-Box (ueber MC2 :9001)",
|
||||
"options": {
|
||||
"baseURL": "http://192.168.178.151:9001/v1",
|
||||
"apiKey": "local"
|
||||
},
|
||||
"models": {
|
||||
"fast": {
|
||||
"name": "fast - Qwen3.6-35B-A3B (MoE) - schnell, fuer Erkunden und Kleinkram",
|
||||
"tool_call": true,
|
||||
"limit": {
|
||||
"context": 65536,
|
||||
"output": 16384
|
||||
}
|
||||
},
|
||||
"heavy": {
|
||||
"name": "heavy - Planen / Review",
|
||||
"tool_call": true,
|
||||
"limit": {
|
||||
"context": 32768,
|
||||
"output": 8192
|
||||
}
|
||||
},
|
||||
"hermes": {
|
||||
"name": "hermes - Erkunden (immer warm)",
|
||||
"tool_call": true,
|
||||
"limit": {
|
||||
"context": 65536,
|
||||
"output": 8192
|
||||
}
|
||||
},
|
||||
"coder": {
|
||||
"name": "coder - Qwen3.8-27B (dicht) - Standard fuers Bauen, agentisch am staerksten",
|
||||
"tool_call": true,
|
||||
"limit": {
|
||||
"context": 131072,
|
||||
"output": 16384
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"model": "aibox/coder",
|
||||
"small_model": "aibox/hermes",
|
||||
"instructions": [
|
||||
"C:/Users/TobisPC/.config/opencode/ARBEITSANWEISUNG.md"
|
||||
],
|
||||
"autoupdate": false,
|
||||
"agent": {
|
||||
"plan": {
|
||||
"model": "aibox/heavy",
|
||||
"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",
|
||||
"model": "aibox/heavy",
|
||||
"permission": {
|
||||
"edit": "deny",
|
||||
"bash": "deny"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
# OpenCode-Plugin: MC2-Governor
|
||||
|
||||
**Hier liegt die Quelle. Der Einsatzort ist eine Kopie.**
|
||||
|
||||
```
|
||||
deploy/opencode-plugin/mc2-governor.ts <- versioniert, hier aendern
|
||||
| kopieren
|
||||
v
|
||||
%USERPROFILE%\.config\opencode\plugin\ <- wird von OpenCode automatisch geladen
|
||||
```
|
||||
|
||||
Bis zum 21.08.2026 existierte diese Datei **nur** im Konfigordner des PCs — 11 KB
|
||||
verhaltensbestimmender Code ohne Sicherung und ohne Historie. Deshalb liegt sie jetzt hier.
|
||||
|
||||
## Ausbringen
|
||||
|
||||
```powershell
|
||||
Copy-Item "deploy\opencode-plugin\mc2-governor.ts" "$env:USERPROFILE\.config\opencode\plugin\" -Force
|
||||
```
|
||||
|
||||
Wirkt sofort in der naechsten OpenCode-Sitzung; OpenChamber muss dafuer nicht neu starten,
|
||||
eine neue Sitzung reicht.
|
||||
|
||||
## Was es tut
|
||||
|
||||
1. **Werkzeug-Zaun** (`tool.execute.before`) — blockt Handgriffe, die ein Agent nie
|
||||
unbeaufsichtigt tun darf. Leitplanke gegen Entgleisen, **keine** Sandbox.
|
||||
2. **Pruef-Tor + Reparaturschleife** (`session.idle`) — sagt der Agent „fertig", laeuft der
|
||||
Verify-Befehl aus der Datei `VERIFY` im Repo-Wurzelverzeichnis. ROT geht als naechster
|
||||
Auftrag zurueck an den Agenten, bis zu `MC2_LOOP_MAX_ROUNDS` mal.
|
||||
‼️ **Schlaeft derzeit:** es gibt in keinem Projekt eine `VERIFY`-Datei. Ohne sie tut
|
||||
diese Funktion nichts — schadet aber auch nicht.
|
||||
3. **Savepoint statt Zusammenfassen** (`session.compacted`) — beim Komprimieren fallen
|
||||
still die Regeln aus dem Kontext; stattdessen wird `SAVEPOINT.md` geschrieben.
|
||||
4. **Stimme** — Ereignisse gehen an MC2 `/api/voice/announce`, Lucy spricht sie.
|
||||
|
||||
Der zugehoerige **Governor-Proxy auf `:8100`** (Token-Zaehler mit Hart-Deckel, Juli 2026)
|
||||
**existiert nicht mehr** — kein Port, kein Ordner, keine Unit. Nachgemessen am 21.08.2026.
|
||||
Dieses Plugin haengt nicht an ihm.
|
||||
|
||||
## Schalter (Umgebungsvariablen)
|
||||
|
||||
| Variable | Default | Wirkung |
|
||||
|---|---|---|
|
||||
| `MC2_BOX_URL` | `http://192.168.178.151:9001` | MC2-Basis fuer Meldungen |
|
||||
| `MC2_FENCE_OFF` | — | `1` schaltet den **ganzen** Zaun ab (alles oder nichts) |
|
||||
| `MC2_LOOP_AUTOFIX` | an | `0` schaltet die Reparaturschleife ab |
|
||||
| `MC2_LOOP_MAX_ROUNDS` | `3` | Runden der Reparaturschleife |
|
||||
| `MC2_LOOP_ANNOUNCE` | an | `0` macht Lucy stumm |
|
||||
| `MC2_LOOP_SILENT` | — | `1` = Meldungen ohne Ton |
|
||||
|
||||
## Aenderung 21.08.2026: `git push` ist frei
|
||||
|
||||
Der Zaun blockte urspruenglich **jedes** `git push` („Veroeffentlichen ist Sache des
|
||||
Menschen"). Das passt nicht mehr: die Bahn ist jetzt PC → Gitea → Box, und jede Aenderung
|
||||
soll auch im git landen. Ein Agent, der committen, aber nicht pushen darf, laesst Arbeit
|
||||
auf einem einzelnen Rechner liegen.
|
||||
|
||||
**Erzwungenes Pushen bleibt verboten** — `--force`, `--mirror`, `--delete`. Das
|
||||
ueberschreibt fremde Arbeit auf dem Server und ist genau die Klasse Unfall, gegen die der
|
||||
Zaun existiert. `--force-with-lease` ist bewusst ausgenommen.
|
||||
|
||||
Beides gemessen: `git push --dry-run origin main` laeuft durch,
|
||||
`git push --force --dry-run origin main` wird mit `[MC2-ZAUN]` abgewiesen.
|
||||
|
||||
## ‼️ Falle: das `bash`-Werkzeug ist auf Windows kein bash
|
||||
|
||||
Es laeuft in **PowerShell**. `tail`, `head`, `grep` und Freunde gibt es dort nicht, und
|
||||
Pipes verhalten sich anders. Wer dem Agenten Befehle vorgibt, muss das beruecksichtigen.
|
||||
|
||||
## ‼️ Falle: Gitea-Anmeldung ist flatterhaft
|
||||
|
||||
Der Git Credential Manager haelt fuer die DDNS-Domain ein **OAuth-Token mit einer Stunde
|
||||
Laufzeit**. Laeuft es ab, scheitert der Push des Agenten mit
|
||||
`remote: Failed to authenticate user` — ein Push aus PowerShell erneuert es, danach geht es
|
||||
wieder. Dauerhafte Loesung waere ein langlebiges Gitea-Token auf die **interne** Adresse
|
||||
`http://192.168.178.153:3000` (die DDNS-Domain ist nachts durch die Zwangstrennung ohnehin
|
||||
zeitweise tot).
|
||||
|
||||
---
|
||||
|
||||
## ‼️ ABGESCHALTET am 22.08.2026
|
||||
|
||||
**Das Plugin ist nicht mehr aktiv.** Es liegt hier als Quelle und im Konfigordner
|
||||
unter `plugin-archiv/`, aber nicht mehr in `~/.config/opencode/plugin/`.
|
||||
|
||||
**Warum:** Beim ersten echten Arbeitslauf fiel auf, dass bei jeder Komprimierung
|
||||
zwei Dinge uebereinander passierten — OpenChamber dampfte den Verlauf ein, und der
|
||||
Governor schob sofort einen zusaetzlichen Auftrag nach („schreib SAVEPOINT.md").
|
||||
Der Agent verlor dadurch jedes Mal einen Zug an Buchhaltung statt an die Aufgabe.
|
||||
Nachgewiesen: `SAVEPOINT.md` in `mc2-referenz`, geschrieben 22.08. 14:47 mitten
|
||||
im Lauf.
|
||||
|
||||
Der User hatte den Abbau schon am 21.08. angeordnet („alle Guards, alle
|
||||
Governor, Stewards — brauchen wir nicht mehr"). Ich hatte damals das Behalten
|
||||
empfohlen und seine Antwort zum `git push`-Riegel als Zustimmung zum Rest
|
||||
gewertet. **Das war eine Annahme, keine Entscheidung.**
|
||||
|
||||
## Was damit wegfaellt — bewusst
|
||||
|
||||
| war | jetzt |
|
||||
|---|---|
|
||||
| Sperrliste: `rm -rf`, `sudo`, force push, `curl\|sh`, `ssh` ausser Arcane, `npm publish` | **kein Netz mehr** — der Agent darf alles, was die Shell hergibt |
|
||||
| Pruef-Tor + Reparaturschleife (`VERIFY`) | schlief ohnehin, es gab nirgends eine `VERIFY`-Datei im PC-Baum |
|
||||
| SAVEPOINT beim Komprimieren | weg — genau der Grund fuer den Abbau |
|
||||
| Meldungen an Lucys Stimme | weg |
|
||||
|
||||
**Wiederherstellen** (wenn der fehlende Zaun sich raecht):
|
||||
|
||||
```powershell
|
||||
Copy-Item "deploy\opencode-plugin\mc2-governor.ts" "$env:USERPROFILE\.config\opencode\plugin\"
|
||||
```
|
||||
|
||||
Plugins werden beim **Start des OpenCode-Servers** geladen — Aendern wirkt erst
|
||||
nach einem Neustart von OpenChamber, nicht sofort. Deshalb liess sich das Plugin
|
||||
auch mitten in einem laufenden Referenzlauf gefahrlos entfernen.
|
||||
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* 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 }> = [
|
||||
// 21.08.2026: normales `git push` ist FREI. Der Weg ist jetzt PC -> Gitea -> Box, und
|
||||
// jede Aenderung soll auch im git landen — ein Agent, der committen, aber nicht pushen
|
||||
// darf, laesst Arbeit auf einem einzelnen Rechner liegen. Was schlecht ist, faengt
|
||||
// hinterher die CI-Ampel; dieser Zaun muss es nicht vorher verhindern.
|
||||
// Erzwungenes Pushen bleibt verboten: das ueberschreibt fremde Arbeit auf dem Server
|
||||
// und ist genau die Klasse Unfall, gegen die der Zaun ueberhaupt existiert.
|
||||
{ rx: /\bgit\s+push\b[^\n]*(--force(?!-with-lease)|\s-f\b|--mirror\b|--delete\b)/,
|
||||
why: "git push --force/--mirror/--delete — ueberschreibt fremde Arbeit auf dem Server. Normales `git push` ist erlaubt; muss wirklich erzwungen werden, sag es dem Menschen." },
|
||||
{ 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
|
||||
@@ -0,0 +1,91 @@
|
||||
<#
|
||||
push-und-sync.ps1 — der Handschlag zwischen PC und AI-Box (21.08.2026).
|
||||
|
||||
Bahn: PC (Quelle der Wahrheit) -> Gitea -> Box ~/projekte
|
||||
|
||||
Warum ueberhaupt ein Skript: `projekte-sync` laeuft auf der Box stuendlich. Wer
|
||||
gerade gepusht hat, will aber nicht bis zu 60 Minuten warten. Das hier stoesst
|
||||
denselben Dienst sofort an — kein zweiter Mechanismus, nur ein Ausloeser.
|
||||
|
||||
Warum PowerShell und nicht Git-Bash: Push zum Gitea geht vom PC nur ueber den
|
||||
Git Credential Manager, und den gibt es hier (Lehre vom 24.07.).
|
||||
|
||||
Aufruf (im Projektordner):
|
||||
.\deploy\push-und-sync.ps1
|
||||
.\deploy\push-und-sync.ps1 -NurSync # nicht pushen, nur Box nachziehen
|
||||
.\deploy\push-und-sync.ps1 -Pfad "F:\Coding Stuff\rippy"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string] $Pfad = (Get-Location).Path,
|
||||
[switch] $NurSync,
|
||||
[string] $BoxHost = "hitonabi@192.168.178.151"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Schritt($text) { Write-Host "`n== $text" -ForegroundColor Cyan }
|
||||
function Gut($text) { Write-Host " $text" -ForegroundColor Green }
|
||||
function Warn($text) { Write-Host " $text" -ForegroundColor Yellow }
|
||||
|
||||
Set-Location $Pfad
|
||||
|
||||
# --- 1. Ist das ueberhaupt ein Repo, und was steht an? ---------------------
|
||||
$null = git rev-parse --is-inside-work-tree 2>$null
|
||||
if ($LASTEXITCODE -ne 0) { throw "Kein Git-Repository: $Pfad" }
|
||||
|
||||
$zweig = (git branch --show-current).Trim()
|
||||
|
||||
# Repo-Name aus der Remote-URL, NICHT aus dem Ordnernamen: der lokale Ordner heisst
|
||||
# z. B. "mission-control-2", das Gitea-Repo aber "mission-control-v2". Der Ordnername
|
||||
# haette die Box-Abfrage still ins Leere laufen lassen.
|
||||
$remote = (git remote get-url origin).Trim()
|
||||
$repo = [System.IO.Path]::GetFileNameWithoutExtension(($remote -split '/')[-1])
|
||||
Schritt "Repo '$repo', Zweig '$zweig'"
|
||||
|
||||
$offen = git status --porcelain
|
||||
if ($offen) {
|
||||
Warn "Es liegen unversionierte/ungespeicherte Aenderungen:"
|
||||
$offen | Select-Object -First 10 | ForEach-Object { Warn " $_" }
|
||||
Warn "Diese werden NICHT mitgeschickt. Erst committen, dann erneut aufrufen."
|
||||
}
|
||||
|
||||
# --- 2. Pushen ------------------------------------------------------------
|
||||
if (-not $NurSync) {
|
||||
$ausstehend = git log --oneline "origin/$zweig..HEAD" 2>$null
|
||||
if (-not $ausstehend) {
|
||||
Gut "Nichts zu pushen — Gitea ist bereits auf Stand."
|
||||
} else {
|
||||
Schritt "Push nach Gitea ($(($ausstehend | Measure-Object).Count) Commit(s))"
|
||||
$ausstehend | ForEach-Object { Write-Host " $_" }
|
||||
git push origin $zweig
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw @"
|
||||
Push fehlgeschlagen.
|
||||
Seit 21.08. laeuft der Weg ueber SSH: ssh://gitea@192.168.178.153:2222/...
|
||||
Pruefen mit: ssh -T gitea
|
||||
Kommt dort keine Begruessung, lauscht Giteas SSH-Server nicht (Port 2222,
|
||||
START_SSH_SERVER in /etc/gitea/app.ini) oder id_general ist nicht hinterlegt.
|
||||
Der SSH-Benutzer heisst 'gitea', NICHT 'git' - Gitea laeuft unter diesem Namen.
|
||||
"@
|
||||
}
|
||||
Gut "gepusht"
|
||||
}
|
||||
}
|
||||
|
||||
# --- 3. Box nachziehen lassen --------------------------------------------
|
||||
Schritt "Box zieht nach (projekte-sync)"
|
||||
ssh $BoxHost "systemctl --user start projekte-sync.service"
|
||||
if ($LASTEXITCODE -ne 0) { throw "Sync-Dienst liess sich nicht starten." }
|
||||
|
||||
$stand = ssh $BoxHost "cd ~/projekte/$repo 2>/dev/null && git log --oneline -1 || echo 'NICHT-IN-PROJEKTE'"
|
||||
if ($stand -eq "NICHT-IN-PROJEKTE") {
|
||||
Warn "'$repo' liegt nicht unter ~/projekte."
|
||||
Warn "Bei mission-control-v2 ist das ABSICHT: das Live-Deployment liegt in ~/mission-control-v2"
|
||||
Warn "und wird bewusst nicht automatisch gezogen — es braucht seinen eigenen Deploy-Schritt."
|
||||
} else {
|
||||
Gut "Box steht jetzt auf: $stand"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
@@ -0,0 +1,418 @@
|
||||
# Umbau: OpenChamber als Coding-Bahn, Lucy bleibt SysAdmin
|
||||
|
||||
_Entschieden 21.08.2026. Rückbau der Hermes-Coding-Bahn ist **erledigt**, der Aufbau von
|
||||
OpenChamber steht aus. Diese Datei ist die Übergabe an die nächste Sitzung._
|
||||
|
||||
## Die Entscheidung
|
||||
|
||||
**Zwei getrennte Bahnen** — der Weg, der bei diesem Stack schon einmal funktioniert hat:
|
||||
|
||||
| Bahn | Werkzeug | Zweck |
|
||||
|---|---|---|
|
||||
| **Betrieb** | Hermes-Runtime (Lucy) | SysAdmin, Telegram, Crons, Gedächtnis, Skills, Stimme, PC-Steuerung |
|
||||
| **Coding** | **OpenChamber** über OpenCode | Nur programmieren. Getrennt von Lucy. |
|
||||
| **Motor** | llama-swap `:8080` / MC2-Gateway `:9010` | beide Bahnen teilen sich die Modelle |
|
||||
| **Steuerpult** | MC2 `:9001` | unverändert |
|
||||
|
||||
**Warum getrennt:** Der Versuch, beides in Hermes Desktop zu vereinen, ist an zwei Dingen
|
||||
gescheitert — die Gateway-Registry akzeptiert kein Benutzer/Passwort (nur Session-Token oder
|
||||
OAuth, beides für eine selbstgehostete Box ungeeignet), und der Sessions-Reiter fiel dadurch
|
||||
immer auf „This device" zurück. Details in der Gedächtnisnotiz vom 20./21.08.
|
||||
|
||||
## Was bereits erledigt ist (21.08.)
|
||||
|
||||
**PC:**
|
||||
- Hermes Desktop + CLI deinstalliert (`hermes uninstall --full`), Reste von Hand entfernt
|
||||
- `AppData\Local\hermes`, `AppData\Roaming\Hermes`, `~\.hermes` — alle weg
|
||||
- Startmenü- und Desktop-Verknüpfungen entfernt
|
||||
- Env-Variablen bereinigt; **`HERMES_PC_TOKEN` bleibt bewusst** (Lucys PC-Executor auf `:7777`)
|
||||
- Node v24.19.0 in `Program Files` ist unabhängig → bleibt, erfüllt OpenChambers Anforderung (≥22)
|
||||
|
||||
**Box:**
|
||||
- Bot-Profile `coder` + `debugger` entfernt (gesichert in `~/archiv-aufraeumen-20260820/`)
|
||||
- `~/.hermes/desktop-ssh/` entfernt, `desktop-auth.env` entfernt
|
||||
- ufw: Port **9119 wieder zu** — nur noch `22 · 9001 · 8080 · 7681`
|
||||
- Dashboard bleibt auf `0.0.0.0:9119` gebunden **mit Anmeldung**, damit MC2s `/hermes-ui`-Proxy
|
||||
nicht mehr passwortlos aus dem LAN erreichbar ist (das war ein offenes Scheunentor).
|
||||
Zugang: `~/.hermes/dashboard-login.txt` (0600), Benutzer `commander`.
|
||||
‼️ Zugangsdaten stehen **nur** in `config.yaml` — die frühere `EnvironmentFile` hat sie still
|
||||
überstimmt und kostete eine Fehlersuche. Eine Quelle, eine Wahrheit.
|
||||
|
||||
**Verifiziert nach dem Rückbau:** 5 Dienste aktiv · 4 MCP-Server · 4 Crons · Telegram `connected`
|
||||
· PC-Executor HTTP 200. Lucy ist vollständig funktionsfähig.
|
||||
|
||||
## Was aufzubauen ist
|
||||
|
||||
### 1. OpenCode auf der Box
|
||||
|
||||
Wurde beim Juli-Rückbau entfernt (`command -v opencode` → leer). Muss neu.
|
||||
|
||||
```bash
|
||||
# Installation (Box)
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
# Provider auf den lokalen Motor zeigen lassen: ~/.config/opencode/opencode.json
|
||||
# base_url http://127.0.0.1:9010/v1 (MC2-Gateway, kann model:auto)
|
||||
# oder http://127.0.0.1:8080/v1 (llama-swap direkt)
|
||||
# api_key local
|
||||
```
|
||||
|
||||
Als systemd-**User**-Dienst (wie alle anderen — reboot-fest, sudo-frei):
|
||||
|
||||
```
|
||||
opencode serve --hostname 0.0.0.0 --port 4096
|
||||
```
|
||||
|
||||
Danach `ufw allow from 192.168.178.0/24 to any port 4096 proto tcp`.
|
||||
|
||||
### 2. OpenChamber auf dem PC
|
||||
|
||||
Desktop-Version von den GitHub-Releases (`openchamber/openchamber`, MIT, ~9,1k ★).
|
||||
**Wichtig:** Die Desktop-Variante bringt eine eigene OpenCode-CLI mit — für den Remote-Betrieb
|
||||
muss sie ausgeschaltet werden:
|
||||
|
||||
```
|
||||
OPENCODE_HOST = http://192.168.178.151:4096 ← volle Adresse MIT Port, OHNE Pfad
|
||||
OPENCODE_SKIP_START = true
|
||||
```
|
||||
|
||||
> Fehlt der Port oder hängt ein Pfad dran, **ignoriert OpenChamber die Variable und startet
|
||||
> stillschweigend seinen eigenen Server.** Genau diese Klasse Fehler hat uns bei Hermes einen
|
||||
> halben Tag gekostet.
|
||||
|
||||
Modelle, Provider und Schlüssel liegen in **OpenCode**, nicht in OpenChamber.
|
||||
|
||||
### 3. Modellwahl — nicht wieder den dichten Coder
|
||||
|
||||
★★ **`fast` (Qwen3.6-35B-A3B) nehmen, nicht `coder` (Qwen3.8-27B).** Gemessen:
|
||||
|
||||
| | Ausgabe | Eingabe | SWE-bench Verified |
|
||||
|---|---|---|---|
|
||||
| `fast` Qwen3.6-35B-A3B (MoE) | **95,7 t/s** | 221 t/s | **73,4** |
|
||||
| `coder` Qwen3.8-27B (dicht) | 12,6 t/s | 144 t/s | keine agentische Messung |
|
||||
|
||||
Das dichte Modell ist bandbreitenlimitiert (17 GB ÷ 215 GB/s ≈ 12,6 t/s = Hardware-Limit).
|
||||
Der MoE ist **schneller und besser**.
|
||||
|
||||
### 4. Die Arbeitsanweisung mitnehmen — der wichtigste Fund überhaupt
|
||||
|
||||
Der Agent las in Lauf A eine ganze Nacht lang und schrieb **null Dateien**. Mit dieser Zeile
|
||||
im Auftrag schrieb er in 24 Minuten fünf:
|
||||
|
||||
> **„Arbeite Datei für Datei. Lies HÖCHSTENS zwei Dateien, bevor du die erste änderst.
|
||||
> Kein Gesamtplan, keine Vollinventur. Eine halbfertige Änderung ist wertvoller als eine
|
||||
> vollständige Analyse ohne Änderung."**
|
||||
|
||||
Gehört in OpenChambers System-Prompt bzw. `AGENTS.md` der Projekte.
|
||||
|
||||
## Bekannte Fallen (aus dem Juli-Audit + heute)
|
||||
|
||||
- **Bus-Faktor 1** bei OpenChamber: ~70–80 % der Commits von einer Person. Version **pinnen**,
|
||||
nicht blind auto-updaten.
|
||||
- **Windows-Installer unsigniert** → SmartScreen-Warnung ist erwartbar, kein Alarm.
|
||||
- Nie ohne `--ui-password` über localhost hinaus binden.
|
||||
- `opencode web` (nicht OpenChamber!) hat einen kaputten Projekt-Finder für `$HOME` — falls
|
||||
irgendwo ein leerer Projektbaum auftaucht, ist das der Grund.
|
||||
- Git-Push zum Gitea geht vom PC **nur über PowerShell/GCM**, nicht aus der Git-Bash.
|
||||
|
||||
## Erster Test nach dem Aufbau
|
||||
|
||||
Die Referenzaufgabe liegt bereit und ist halb erledigt — ideal zum Vergleich:
|
||||
|
||||
```
|
||||
Worktree: ~/projekte/mc2-referenz (Zweig referenz/mem0-ausbau)
|
||||
Aufgabe: docs/aufgaben/referenzaufgabe-mem0-ausbau.md
|
||||
Prüfung: bash docs/aufgaben/referenz-check.sh
|
||||
Stand: 5 von 17 Dateien erledigt (Hermes-Lauf A2, abgebrochen durch SSH-Fehler)
|
||||
```
|
||||
|
||||
Damit lässt sich direkt vergleichen: **Wie lange braucht OpenChamber + `fast` für dieselben
|
||||
17 Dateien?** Hermes + dichter Coder brauchte hochgerechnet ~80 Minuten; die Rechnung für
|
||||
`fast` sagt ~30.
|
||||
|
||||
---
|
||||
|
||||
## Stand nach dem Aufbau (21.08.2026)
|
||||
|
||||
**Beide Bahnen stehen. Der Aufbau ist erledigt, der Referenzlauf steht aus.**
|
||||
|
||||
### Box (`192.168.178.151`)
|
||||
|
||||
| Sache | Wert |
|
||||
|---|---|
|
||||
| OpenCode | **1.18.20**, `~/.opencode/bin/opencode` (eigenständiges Binary, kein Node nötig) |
|
||||
| Dienst | `opencode-server.service` (systemd **user**, `enable`d, Linger=yes → reboot-fest) |
|
||||
| Lauscht | `0.0.0.0:4096`, Arbeitsverzeichnis `~/projekte` |
|
||||
| ufw | Regel 5: `4096/tcp ALLOW IN 192.168.178.0/24` |
|
||||
| Provider | `box` → `http://127.0.0.1:9010/v1` (MC2-Gateway), `apiKey: local` |
|
||||
| Modelle | `box/fast` (Standard) · `box/heavy` · `box/hermes` (small_model) · `box/coder` (nur Vergleich) |
|
||||
| Autoupdate | `false` in `opencode.json` |
|
||||
|
||||
Konfiguration: `~/.config/opencode/opencode.json`.
|
||||
**Nur Rollen-Aliase**, keine Modell-Eigennamen — die Konsolidierung wirft Eigennamen sonst raus (Regel vom 20.08.).
|
||||
|
||||
### Arbeitsanweisung — global verdrahtet
|
||||
|
||||
Liegt als `~/.config/opencode/ARBEITSANWEISUNG.md` und ist über `instructions` in
|
||||
`opencode.json` in **jeder** Sitzung aktiv, nicht nur in Projekten mit `AGENTS.md`.
|
||||
Gegengeprüft: „Wie viele Dateien höchstens?" → `2`. `~` wird von OpenCode expandiert.
|
||||
|
||||
### PC
|
||||
|
||||
| Sache | Wert |
|
||||
|---|---|
|
||||
| OpenChamber | **v1.19.0** (18.08.), `%LOCALAPPDATA%\Programs\@openchamberelectron` |
|
||||
| Signatur | `NotSigned` — erwartet, kein Alarm |
|
||||
| `OPENCODE_HOST` | `http://192.168.178.151:4096` (User-Env, mit Port, ohne Pfad) |
|
||||
| `OPENCODE_SKIP_START` | `true` (User-Env) |
|
||||
|
||||
**Verifiziert:** OpenChamber hält 8 offene TCP-Verbindungen zur Box auf `:4096`,
|
||||
startet **keinen** eigenen OpenCode (kein lokaler Prozess, kein lokaler Port).
|
||||
Damit ist genau der Punkt genommen, an dem Hermes Desktop gescheitert ist.
|
||||
|
||||
**Auto-Update:** kein Riegel nötig. Im Bundle steht `autoUpdater.autoDownload = false` und
|
||||
`autoInstallOnAppQuit = false` — OpenChamber lädt nie von selbst, es meldet nur und wartet
|
||||
auf einen Klick. Die Version ist damit faktisch gepinnt.
|
||||
|
||||
### ‼️ Offene Sicherheitsentscheidung — bewusst so gewählt
|
||||
|
||||
Der Server läuft **ohne Anmeldung** im LAN. Beim Start meldet er selbst:
|
||||
|
||||
```
|
||||
Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.
|
||||
```
|
||||
|
||||
Praktisch heißt das: jedes Gerät im `192.168.178.0/24` kann über die API beliebige
|
||||
Befehle als `hitonabi` auf der Box ausführen — dieselbe Klasse Loch wie das passwortlose
|
||||
Dashboard auf `:9119`, das am 20.08. zugemacht wurde.
|
||||
|
||||
★★ **Ein Riegel wäre da und kostet nichts:** OpenChamber schickt HTTP-Basic-Auth mit, wenn
|
||||
`OPENCODE_SERVER_PASSWORD` gesetzt ist (Benutzer aus `OPENCODE_SERVER_USERNAME`,
|
||||
Standard `opencode`). Server-Variable in der systemd-Unit, gleiche Variable als User-Env
|
||||
auf dem PC — **kein Tunnel, keine Umstellung, der Port bleibt offen im LAN.**
|
||||
|
||||
Der User hat am 21.08. **bewusst dagegen entschieden** („so lassen"). Nicht neu aufrollen,
|
||||
aber hier notiert, damit es kein stiller Defekt bleibt.
|
||||
|
||||
### Nächster Schritt: der Referenzlauf
|
||||
|
||||
Alles steht bereit, nichts wurde am Prüfstand angefasst:
|
||||
|
||||
```
|
||||
Worktree: ~/projekte/mc2-referenz (Zweig referenz/mem0-ausbau)
|
||||
Stand: 5 von 17 Dateien erledigt, referenz-check.sh sagt ROT
|
||||
Prüfung: bash docs/aufgaben/referenz-check.sh
|
||||
```
|
||||
|
||||
Die Arbeitsanweisung wirkt dort über die **globale** Instruktion — `AGENTS.md` im Worktree
|
||||
wurde absichtlich **nicht** angefasst, damit der Vergleich zu Lauf A/A2 sauber bleibt.
|
||||
Zu messen: **Wie lange braucht OpenChamber + `fast` für dieselben 17 Dateien?**
|
||||
Hermes + dichter `coder` brauchte hochgerechnet ~80 Minuten; die Rechnung für `fast` sagt ~30.
|
||||
|
||||
### ‼️ Falle: der Ordner-Knopf reicht Windows-Pfade an die Box durch
|
||||
|
||||
**Beim ersten Lauf sofort hineingetappt (21.08., ~1 h verloren).** Die Sitzung stand auf:
|
||||
|
||||
```
|
||||
/home/hitonabi/projekte/F:\Coding Stuff\mission-control-2
|
||||
```
|
||||
|
||||
OpenChamber hatte den **Windows-Pfad des PCs** an den Box-Server durchgereicht, der ihn hinten
|
||||
an sein `WorkingDirectory` klebte. Das Verzeichnis existiert auf der Box nicht → der Agent hat
|
||||
kein Arbeitsverzeichnis, es geht **keine einzige Anfrage** an den Motor raus. In der Oberfläche
|
||||
sieht das aus wie „hängt": Nachricht steht da, eine Zusammenfassung erscheint, danach nichts.
|
||||
`tokens: 0/0`.
|
||||
|
||||
**Ursache, im Bundle nachgesehen:** OpenChamber öffnet Ordner über Electrons natives
|
||||
`showOpenDialog` — den **lokalen** Windows-Dateibrowser. Einen Fern-Auswähler gibt es nicht.
|
||||
Im Fernbetrieb liefert dieser Knopf also *immer* einen unbrauchbaren Pfad.
|
||||
|
||||
**Der richtige Weg:** Seitenleiste → **„Projekt hinzufügen"**. Das ist ein eigener Dialog
|
||||
(`directoryExplorerDialog`) mit Baum **und** Eingabefeld („Enter path or select from tree…"),
|
||||
und der geht über den Server — also über die Box.
|
||||
|
||||
**Diagnose in einem Befehl** — wenn wieder „nichts passiert", zeigt das den Grund sofort:
|
||||
|
||||
```bash
|
||||
ssh hitonabi@192.168.178.151 "curl -s http://127.0.0.1:4096/session | python3 -c \"import json,sys; s=json.load(sys.stdin); s.sort(key=lambda x: x['time']['updated'], reverse=True); x=s[0]; print(x['directory'], x.get('model'), x.get('tokens'))\""
|
||||
```
|
||||
|
||||
Steht dort ein Pfad mit `F:\` oder `tokens 0/0`, ist es diese Falle.
|
||||
|
||||
**Merke:** Ein Projekt taucht in der Server-Liste erst auf, wenn dort einmal eine Sitzung lief.
|
||||
`mc2-referenz` wurde am 21.08. mit einem harmlosen Lauf angemeldet (`box/hermes`, „Antworte nur
|
||||
mit OK") — Worktree danach nachgemessen unverändert: 5 geänderte Dateien, HEAD `e04ace2`.
|
||||
|
||||
---
|
||||
|
||||
## ▶▶ Kurswechsel 21.08. (nachmittags): Quelle ist der PC, nicht die Box
|
||||
|
||||
Der Fernbetrieb oben war **falsch herum gedacht**. Der User arbeitet so — und so ist es
|
||||
jetzt gebaut:
|
||||
|
||||
```
|
||||
F:\Coding Stuff\… ← Quelle der Wahrheit, liegt IMMER lokal
|
||||
│ OpenChamber startet seine EIGENE OpenCode-CLI und arbeitet an lokalen Dateien
|
||||
│ git push (PowerShell/GCM)
|
||||
▼
|
||||
Gitea 192.168.178.153:3000
|
||||
│ projekte-sync (stuendlich, --ff-only) — oder sofort per Skript
|
||||
▼
|
||||
AI-Box ~/projekte/… ← Spiegel fuer Deploy / CI / Lucy
|
||||
▲
|
||||
└─ von der Box kommt NUR noch das Modell (Inferenz ueber HTTP)
|
||||
```
|
||||
|
||||
**Damit faellt die Windows-Pfad-Falle ersatzlos weg** — es gibt keine Fernpfade mehr.
|
||||
|
||||
### ★★ Kein Box-Umbau noetig: MC2 `:9001/v1` ist der Modellweg
|
||||
|
||||
Erst war geplant, den Gateway `:9010` ins LAN zu binden. Ueberfluessig — die Gateway-Unit
|
||||
sagt es selbst: *„LAN-Clients kommen weiter ueber MC2 `:9001/v1`, das roh hierher
|
||||
durchreicht."* Der Port ist seit jeher offen.
|
||||
|
||||
Vom PC aus gemessen: `:9001/v1/models` liefert **die Rollen-Aliase** (`fast`, `heavy`,
|
||||
`hermes`, `coder`), `fast` antwortet in **0,7 s**. Kein neuer Port, keine Neubindung,
|
||||
keine ufw-Regel — und **keine Modell-Eigennamen** in der PC-Konfiguration (die Regel vom
|
||||
20.08. bleibt gewahrt). llama-swap `:8080` waere die Alternative gewesen, liefert aber nur
|
||||
Eigennamen (`Qwen3.6-35B-A3B`) → genau der stille 404 vom 19.08.
|
||||
|
||||
### Was auf dem PC steht
|
||||
|
||||
`C:\Users\TobisPC\.config\opencode\opencode.json` — Provider `aibox` →
|
||||
`http://192.168.178.151:9001/v1`, Standard `aibox/fast`, `small_model` `aibox/hermes`.
|
||||
Die Juli-Struktur ist erhalten: Agenten `plan`(heavy) · `build`(fast) · `explore`(hermes) ·
|
||||
`review`(heavy), inklusive **IDE-Zaun** (`ssh`/`scp`/`sftp` deny, gezielte Arcane-Ausnahme;
|
||||
Catch-all `*: allow` steht ZUERST, weil OpenCode `findLast` auswertet).
|
||||
Arbeitsanweisung als `ARBEITSANWEISUNG.md` daneben, ueber `instructions` global geladen.
|
||||
|
||||
Das Plugin `plugin/mc2-governor.ts` wird automatisch mitgeladen: Werkzeug-Zaun,
|
||||
Pruef-Tor-Schleife, Savepoint statt Zusammenfassen, Meldungen an Lucys Stimme.
|
||||
‼️ Es blockt **`git push`** absichtlich — *„Veroeffentlichen ist Sache des Menschen."*
|
||||
Der Agent codet, gepusht wird von Hand (siehe Skript unten).
|
||||
|
||||
**Gebuendelte CLI:** OpenCode **1.18.18** in OpenChambers `resources\opencode-cli`.
|
||||
Im lokalen Betrieb vergibt OpenChamber dem Server automatisch ein Passwort
|
||||
(`OPENCODE_SERVER_PASSWORD`, lifecycle-owned) — `/config` antwortet von aussen mit `401`.
|
||||
Der Schutz, den der Box-Server nicht hatte, ist hier also gratis dabei.
|
||||
|
||||
**Env-Variablen `OPENCODE_HOST` und `OPENCODE_SKIP_START` sind wieder ENTFERNT.**
|
||||
Sie erzwingen den Fernbetrieb; mit ihnen startet OpenChamber keine eigene CLI.
|
||||
|
||||
### Box zurueckgebaut
|
||||
|
||||
`opencode-server.service` ist **gestoppt, deaktiviert und geloescht**, die ufw-Regel fuer
|
||||
`4096` wieder entfernt. Damit erledigt sich die offene Sicherheitsfrage von heute Mittag
|
||||
von selbst: der ungeschuetzte Dienst existiert nicht mehr. ufw steht wieder auf
|
||||
`22 · 9001 · 8080 · 7681`.
|
||||
|
||||
### `deploy/push-und-sync.ps1` — der Handschlag
|
||||
|
||||
```powershell
|
||||
.\deploy\push-und-sync.ps1 # committen -> pushen -> Box zieht sofort nach
|
||||
.\deploy\push-und-sync.ps1 -NurSync # nur nachziehen
|
||||
.\deploy\push-und-sync.ps1 -Pfad "F:\Coding Stuff\rippy"
|
||||
```
|
||||
|
||||
Stoesst `projekte-sync` sofort an, statt bis zu 60 Minuten auf den Timer zu warten —
|
||||
**kein zweiter Mechanismus, nur ein Ausloeser.** Meldet danach, auf welchem Commit die Box steht.
|
||||
|
||||
‼️ **Falle beim Bauen gefunden:** Der Repo-Name darf **nicht** aus dem Ordnernamen kommen —
|
||||
lokal heisst es `mission-control-2`, in Gitea `mission-control-v2`. Das Skript liest ihn
|
||||
aus der Remote-URL. Getestet gegen beide Faelle: MC2 (Live-Deployment, liegt als
|
||||
Verknuepfung unter `~/projekte`) und `rippy` (normales Projekt, `a14449e`).
|
||||
|
||||
### Offener Punkt: PC-Remote haengt an der DDNS-Domain
|
||||
|
||||
Der PC pusht nach `https://git.tobisniceshomelab.ddnsfree.com/…`, die Box nutzt intern
|
||||
`http://192.168.178.153:3000`. Die Domain ist nachts durch die Zwangstrennung zeitweise
|
||||
tot (Lehre vom 24.07.). Umstellen mit:
|
||||
|
||||
```bash
|
||||
git remote set-url origin http://192.168.178.153:3000/Hitonabi/mission-control-v2.git
|
||||
```
|
||||
|
||||
Noch **nicht** gemacht — aendert Git-Konfiguration in den Repos des Users.
|
||||
|
||||
---
|
||||
|
||||
## Abschluss der Coding Lane (21.08.2026, abends)
|
||||
|
||||
### Der Zugang: ein SSH-Schluessel, kein Token mehr
|
||||
|
||||
**Der Ausloeser:** Mitten in der Sitzung scheiterte ein Push, der eine Stunde
|
||||
vorher noch ging — `remote: Failed to authenticate user`. Der Git Credential
|
||||
Manager haelt fuer Gitea ein **OAuth-Token mit einer Stunde Laufzeit**. Damit ist
|
||||
HTTP fuer einen Agenten, der selbst pushen soll, strukturell ungeeignet.
|
||||
|
||||
**Was dabei herauskam:** Giteas SSH war **nie funktionsfaehig**. In der `app.ini`
|
||||
stand zwar `DISABLE_SSH = false` und `SSH_PORT = 22`, aber im Container gibt es
|
||||
**keinen `git`-Benutzer** und keine `authorized_keys` — Port 22 gehoert dem
|
||||
System-sshd. Es sah nur so aus, als koennte man ueber SSH klonen.
|
||||
|
||||
**Loesung:** Giteas **eingebauten** SSH-Server eingeschaltet (Port 22 ist belegt,
|
||||
deshalb 2222). Der verwaltet die Schluessel selbst, direkt aus der Datenbank —
|
||||
kein `git`-Benutzer, keine `authorized_keys`.
|
||||
|
||||
```ini
|
||||
# /etc/gitea/app.ini, [server] (Sicherung: app.ini.bak-20260821)
|
||||
SSH_PORT = 2222
|
||||
SSH_LISTEN_PORT = 2222
|
||||
START_SSH_SERVER = true
|
||||
SSH_LISTEN_HOST = 0.0.0.0
|
||||
```
|
||||
|
||||
‼️‼️ **Der SSH-Benutzer heisst `gitea`, NICHT `git`.** Gitea laeuft unter diesem
|
||||
Namen und weist alles andere ab. Die Fehlermeldung steht nur im Gitea-Log, nicht
|
||||
beim Client — ssh sagt bloss `Permission denied (publickey)`:
|
||||
|
||||
```
|
||||
Invalid SSH username git - must use gitea for all git operations via ssh
|
||||
```
|
||||
|
||||
**Auf dem PC** (`~/.ssh/config`) — noetig, weil hier **acht** Schluessel liegen
|
||||
und ssh sonst der Reihe nach durchprobiert, bis Gitea abbricht:
|
||||
|
||||
```
|
||||
Host 192.168.178.153 gitea gitea.heimnetz
|
||||
HostName 192.168.178.153
|
||||
Port 2222
|
||||
User gitea
|
||||
IdentityFile ~/.ssh/id_general
|
||||
IdentitiesOnly yes
|
||||
```
|
||||
|
||||
Der Schluessel `id_general` ist in Gitea hinterlegt (Nr. 3) — **derselbe, mit dem
|
||||
du auf Box, pve und Arcane kommst.** Von acht Zugangsdaten auf einen.
|
||||
|
||||
Alle sieben PC-Repos stehen jetzt auf
|
||||
`ssh://gitea@192.168.178.153:2222/Hitonabi/<repo>.git`.
|
||||
Das TTT2-Projekt bleibt unberuehrt — es haengt an einer anderen Domain und ist
|
||||
als „Finished" markiert.
|
||||
|
||||
### Gemessen, nicht angenommen
|
||||
|
||||
| Test | Ergebnis |
|
||||
|---|---|
|
||||
| `ssh -T gitea` | „Hi there, Hitonabi! You've successfully authenticated with the key named id_general" |
|
||||
| Push aus PowerShell | 8 Commits, `7c23ebf..25dbfb2` |
|
||||
| **Push aus dem Agenten** | durchgelaufen, **keine Passwortabfrage** |
|
||||
| Erzwungener Push aus dem Agenten | vom Werkzeug-Zaun abgewiesen |
|
||||
| `push-und-sync.ps1` komplett | Commit → Push → Box zieht nach |
|
||||
|
||||
### Der Sonderfall MC2
|
||||
|
||||
`projekte-sync` zieht `mission-control-v2` **absichtlich nicht** — die Box
|
||||
bedient daraus den laufenden Dienst auf `:9001`. Nach einem Push steht dort
|
||||
weiter der alte Commit, und das ist richtig so. Das Live-Deployment braucht
|
||||
seinen eigenen, bewussten Schritt. Alle anderen Projekte werden normal gezogen.
|
||||
|
||||
### Fallen fuer das naechste Mal
|
||||
|
||||
- **`sed` mit `$` und `\n` ueber PowerShell zerlegt sich.** Der erste Anlauf,
|
||||
die `app.ini` zu aendern, lief ins Leere und die Datei blieb unveraendert —
|
||||
ohne Fehlermeldung. In einzelne Ersetzungen zerlegen und **danach nachsehen**.
|
||||
- Ein Dienst-Neustart nach einer Konfig-Aenderung ist noch keine Bestaetigung:
|
||||
Gitea startete brav neu — mit der alten Datei.
|
||||
- Acht SSH-Schluessel auf dem PC sind selbst ein Befund. `IdentitiesOnly yes`
|
||||
ist Pflicht, sonst bricht die Gegenseite vorher ab.
|
||||
@@ -0,0 +1,93 @@
|
||||
# Hermes: Werkzeuge nach Bedarf (21.08.2026)
|
||||
|
||||
## Die Frage war: kann Hermes Werkzeuge je nach Aufgabe an- und abschalten?
|
||||
|
||||
**Ja — auf drei Ebenen, und die wichtigste lief schon.**
|
||||
|
||||
### 1. Automatisch zur Laufzeit — `tool_search` (war bereits an)
|
||||
|
||||
```
|
||||
tools:
|
||||
tool_search:
|
||||
enabled: true
|
||||
threshold_pct: 10
|
||||
listing: auto
|
||||
listing_max_tokens: 4000
|
||||
```
|
||||
|
||||
Im Log nachweisbar:
|
||||
|
||||
```
|
||||
tool_search activated (tier 1): 47 core/visible tools kept,
|
||||
59 deferred (~5636 tokens), listing full (budget ~4000 tokens)
|
||||
```
|
||||
|
||||
Hermes haelt einen Kern sichtbar und schiebt den Rest hinter eine Suche; der Agent
|
||||
zieht nach, was er braucht. **Hier war nichts einzurichten.**
|
||||
|
||||
Aber: das Aufschieben allein sparte nur 1.100–5.600 Tokens, waehrend `kanban` mit
|
||||
22,7 KB im sichtbaren Kern blieb. Der Hebel lag woanders.
|
||||
|
||||
### 2. Global abschalten — `agent.disabled_toolsets`
|
||||
|
||||
```bash
|
||||
hermes config set agent.disabled_toolsets "[computer_use, image_gen, video, video_gen, kanban, delegation]"
|
||||
hermes tools disable session_search
|
||||
```
|
||||
|
||||
Abgeschaltet **anhand von 9 Tagen echter Nutzung**, nicht nach Gefuehl:
|
||||
|
||||
| Werkzeugsatz | Groesse | Nutzung 13.–21.08. |
|
||||
|---|---|---|
|
||||
| `kanban` (14 Werkzeuge) | 22,7 KB | 35× lesen, **1× schreiben** |
|
||||
| `session_search` | 6,8 KB | **1×** |
|
||||
| `delegation` | 5,6 KB | Fan-out, seit OpenChamber tot |
|
||||
|
||||
‼️ `kanban` kennt `hermes tools disable` **nicht** — es haengt an `toolsets` /
|
||||
`platform_toolsets` und muss ueber `agent.disabled_toolsets` weg.
|
||||
|
||||
### 3. Pro Zugangsweg — `platform_toolsets.<platform>`
|
||||
|
||||
★★ **Der groesste Fund.** Es gibt einen eigenen Zugangsweg `cron` (17 Sitzungen im
|
||||
Log), fuer den **kein** Eintrag existierte — Cron-Jobs bekamen also die volle
|
||||
Werkzeugkiste. Von den drei Jobs braucht nur der Nachrichtenbericht ueberhaupt
|
||||
Werkzeuge, und der braucht zwei:
|
||||
|
||||
```bash
|
||||
hermes config set platform_toolsets.cron "[web, terminal]"
|
||||
```
|
||||
|
||||
`web` fuer die Suche, `terminal` um `notify.sh` aufzurufen. Mehr nicht.
|
||||
|
||||
Ausserdem moeglich, hier nicht gebraucht: `hermes -t TOOLSETS` setzt den
|
||||
Werkzeugsatz fuer einen einzelnen Aufruf.
|
||||
|
||||
## Gemessen mit `hermes prompt-size`
|
||||
|
||||
Vorlast **je Anfrage**, Systemprompt plus Werkzeug-Schemata:
|
||||
|
||||
| Zugangsweg | vorher | nachher | |
|
||||
|---|---|---|---|
|
||||
| `cli` | 99,4 KB | **56,5 KB** | −43 % |
|
||||
| `cron` | 63,1 KB | **16,4 KB** | **−74 %** |
|
||||
|
||||
Rund 11.000 Tokens weniger bei jedem interaktiven Zug, rund 12.000 bei jedem
|
||||
Cron-Lauf. Das ist Kontext, der vorher fuer Werkzeugbeschreibungen draufging,
|
||||
die nie aufgerufen wurden.
|
||||
|
||||
## Nachgeprueft, nicht angenommen
|
||||
|
||||
- Lucy interaktiv: Terminal, Dateien, Websuche, Gedaechtnis — alle vier da.
|
||||
- Daily News Report mit nur `[web, terminal]` durchgelaufen und zugestellt.
|
||||
Der Bericht war sogar besser als der Lauf davor.
|
||||
- Sicherung der Konfiguration liegt unter `~/.hermes/config.yaml.bak-kiss-20260821`.
|
||||
|
||||
## Noch drin, wenn es noch schlanker soll
|
||||
|
||||
- `api_server` traegt 44,8 KB (19 Werkzeuge) — mehr als `cli`. Dort haengt noch
|
||||
`hermes-pc-control`, das in 9 Tagen **null Mal** aufgerufen wurde und an dem
|
||||
eine geplante Aufgabe auf dem PC plus ein eigenes Token haengen.
|
||||
- MCP `hermes-web-fetch` (1 Aufruf) doppelt das eingebaute `web_extract`.
|
||||
- MCP `mission-control-voice` (1 Aufruf) doppelt das eingebaute `text_to_speech`.
|
||||
- Die alten Eintraege `kanban` / `delegation` stehen noch in `platform_toolsets`;
|
||||
`agent.disabled_toolsets` sticht sie, aber aufgeraeumt ist es nicht.
|
||||
Reference in New Issue
Block a user