Umbau Stufe 1-5: Governor v2 + OpenCode-Plugin + Subagenten-Mannschaft

Die Intelligenz sitzt nicht mehr NEBEN dem Coden, sondern DRIN. Alles auf der
Box gemessen, nicht angenommen.

Stufe 1 — Speicher-Haushalt (llama-swap-Config, nicht im Repo):
  Coder dauerwarm statt ttl 600 (Kaltstart 23 s -> 491 ms), Devstral als
  'kritiker' verdrahtet, Swap 11 GB -> 0. Zwei Befunde: Kontext 131k->65k spart
  bei Qwen3-Next 0 GB (Hybrid-Attention), und llama-swap haelt nur EINE Gruppe
  resident -> alles Ko-Residente in dieselbe Gruppe. `persistent: true` verhindert
  dabei das Freiraeumen vor grossen Modellen -> ausgeloester Kernel-OOM, behoben
  durch persistent:false + TTLs (Vision laedt jetzt in 10 s statt 117 s + Absturz).

Stufe 2 — Governor v2 (deploy/governor/):
  Steht jetzt IM Pfad (:8100 -> MC2 :9001) statt daneben. Zaehlt den GANZEN
  Anfragekoerper inkl. tools/tool_calls und kalibriert sich aus den echten
  usage.prompt_tokens jeder Antwort nach: Schaetzfehler 200 % -> 0,3 %.
  Soft-Einschub nur noch, wenn die Nachrichtenkette es erlaubt (kein Dazwischen-
  funken in offene tool_calls). Neuer Status-Endpunkt + systemd-Unit.
  Lucys Alltagsmodelle sind vom Schnitt ausgenommen.

Stufe 3 — OpenCode-Plugin (deploy/opencode/plugin/mc2-governor.ts):
  Werkzeug-Zaun (git push, rm -rf, sudo, curl|sh — bewiesen), Pruef-Tor auf
  session.idle mit Selbstreparatur, Savepoint statt Kompression, Meldungen an
  Lucys Briefkasten mit eigenem Absender 'loop'.

Stufe 4 — Mannschaft (opencode.json):
  plan+build -> coder (51,5 t/s) · explore -> hermes (69,6 t/s, warm, gratis) ·
  review -> Devstral (15,0 t/s, FREMDE Modellfamilie gegen blinde Flecken).
  Der 63-GB-Planer faellt aus der Tagesrolle raus.

Stufe 5 — ein Regelwerk, zwei Ausloeser:
  deploy/opencode-lauf.sh faehrt dieselbe Bau-Pruef-Schleife unbeaufsichtigt
  (die Schleife liegt hier UND im Plugin: bei `opencode run` endet der Prozess,
  bevor session.idle fertig ist — gemessen). Bricht ab, wenn der Governor fehlt.
  gitea-repo-create.sh saet jetzt VERIFY neben der CI-Ampel: jedes neue Repo
  wird mit Innen- UND Aussen-Pruefung geboren.

Oberflaeche:
  Token-Waechter-Kachel im Cockpit (Fuellstand, Marken, Ehrlichkeits-Nachweis),
  /api/governor als gleichursprüngliches Fenster, Devstral im Modellkatalog,
  Rollen-Texte auf die neue Mannschaft aktualisiert.

.gitattributes: deploy/**/*.py auf LF (deploy/*.py greift nur eine Ebene tief).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-25 21:39:32 +02:00
parent 775e862652
commit 7fac17ed9a
43 changed files with 1542 additions and 610 deletions
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
# opencode-lauf.sh — EIN begrenzter Agentenlauf auf der Box, mit denselben Regeln wie in Zed.
#
# Das ist die Nacht-Seite von „ein Regelwerk, zwei Ausloeser": tagsueber tippst du in Zed,
# nachts ruft ein Hermes-Cron dieses Skript. Beide Wege benutzen
# * dieselbe OpenCode-Version,
# * dieselbe Mannschaft (~/.config/opencode/opencode.json: coder/hermes/kritiker),
# * dasselbe Plugin (~/.config/opencode/plugin/mc2-governor.ts: Zaun + Pruef-Tor),
# * denselben Token-Waechter (:8100).
#
# Unterschied zu deploy/worker.sh: worker.sh ist EIN zustandsloser Completion-Aufruf
# (Hermes schreibt die Dateien selbst). Hier laeuft ein ECHTER Agent mit Datei-Haenden,
# Subagenten und Pruef-Tor — fuer ganze Karten statt fuer Schnipsel.
#
# Nutzung:
# opencode-lauf.sh <repo-pfad> "<auftrag>"
# opencode-lauf.sh ~/projekte/foo "Baue X. Halte dich an AGENTS.md."
#
# Env:
# LAUF_TIMEOUT Sekunden Hoechstdauer (Default 3600)
# LAUF_LAUT 1 = Lucy spricht mit (Default 0 = still, Nachtbetrieb)
# LAUF_AGENT OpenCode-Agent (Default build)
set -uo pipefail
REPO="${1:-}"
AUFTRAG="${2:-}"
TIMEOUT="${LAUF_TIMEOUT:-3600}"
AGENT="${LAUF_AGENT:-build}"
OC="$HOME/.opencode/bin/opencode"
ANNOUNCE="${MC_ANNOUNCE_URL:-http://127.0.0.1:9001/api/voice/announce}"
melde () { # melde <betreff> <text> [prioritaet]
curl -sf -m 5 -X POST "$ANNOUNCE" -H 'Content-Type: application/json' \
--data "$(python3 -c 'import json,sys; print(json.dumps({"subject":sys.argv[1],"text":sys.argv[2],"source":"loop","priority":sys.argv[3]}))' \
"$1" "$2" "${3:-silent}")" >/dev/null 2>&1 || true
}
if [ -z "$REPO" ] || [ -z "$AUFTRAG" ]; then
echo "Nutzung: $0 <repo-pfad> \"<auftrag>\"" >&2
exit 2
fi
if [ ! -d "$REPO" ]; then
echo "FEHLER: '$REPO' ist kein Verzeichnis." >&2
exit 2
fi
if [ ! -x "$OC" ]; then
echo "FEHLER: OpenCode nicht gefunden ($OC)." >&2
exit 2
fi
# Der Governor MUSS stehen — ohne ihn liefe der Lauf ohne Sitzungs-Bremse.
if ! curl -sf -m 5 -o /dev/null "http://127.0.0.1:8100/governor/status"; then
echo "FEHLER: Governor (:8100) antwortet nicht — Lauf abgebrochen (keine Sitzungs-Bremse)." >&2
melde "[Lauf]" "Ich habe einen Nachtlauf abgebrochen: der Token-Waechter antwortet nicht." "normal"
exit 3
fi
# Nachts still: Meldungen landen im Briefkasten, Lucy spricht sie aber nicht aus.
# Das Plugin liest diese Variable; die Morgen-Zusammenfassung kommt vom Daily-Briefing.
if [ "${LAUF_LAUT:-0}" = "1" ]; then export MC2_LOOP_SILENT=0; else export MC2_LOOP_SILENT=1; fi
LOGDIR="$HOME/.hermes/logs"; mkdir -p "$LOGDIR"
STAMP="$(date +%Y%m%d-%H%M%S)"
LOG="$LOGDIR/opencode-lauf-$STAMP.log"
MAXRUNDEN="${LAUF_MAX_RUNDEN:-3}"
cd "$REPO" || exit 2
NAME="$(basename "$REPO")"
melde "[Lauf]" "Ich fange an zu bauen: $NAME." "silent"
echo "=== Lauf $STAMP · Repo $REPO · Agent $AGENT · Timeout ${TIMEOUT}s ===" | tee "$LOG"
# Verify-Befehl des Projekts lesen (gleiche Datei und gleiche Regeln wie im Plugin).
verify_cmd () {
[ -r "$REPO/VERIFY" ] || return 1
grep -vE '^\s*(#|$)' "$REPO/VERIFY" | paste -sd' && ' -
}
START=$(date +%s)
CODE=0
RUNDE=0
AUFGABE="$AUFTRAG"
# ── Bau-Schleife ────────────────────────────────────────────────────────────
# Warum hier UND im Plugin? Das Plugin haengt am Ereignis `session.idle` — in Zed
# laeuft der Prozess weiter und alles ist gut. Bei `opencode run` beendet sich der
# Prozess aber, bevor das Pruef-Tor fertig ist (gemessen 25.07.). Fuer unbeaufsichtigte
# Laeufe muss die Schleife deshalb HIER liegen, wo sie den Prozess ueberlebt.
while :; do
RUNDE=$((RUNDE + 1))
echo "--- Runde $RUNDE/$MAXRUNDEN ---" | tee -a "$LOG"
timeout "$TIMEOUT" "$OC" run --agent "$AGENT" "$AUFGABE" >>"$LOG" 2>&1
CODE=$?
[ "$CODE" -eq 124 ] && { echo "ZEITUEBERSCHREITUNG" | tee -a "$LOG"; break; }
VCMD="$(verify_cmd)" || { echo "Kein VERIFY — Pruef-Tor aus, Lauf endet." | tee -a "$LOG"; break; }
echo "--- Pruef-Tor: $VCMD ---" | tee -a "$LOG"
VOUT="$(cd "$REPO" && eval "$VCMD" 2>&1)"; VCODE=$?
printf '%s\n' "$VOUT" | tail -20 >> "$LOG"
if [ "$VCODE" -eq 0 ]; then
echo "PRUEF-TOR GRUEN" | tee -a "$LOG"
melde "[Pruefung]" "$NAME: Tests gruen nach $RUNDE Runde(n)." "normal"
CODE=0
break
fi
if [ "$RUNDE" -ge "$MAXRUNDEN" ]; then
echo "PRUEF-TOR ROT — Reparaturrunden aufgebraucht." | tee -a "$LOG"
melde "[Pruefung]" "$NAME: Tests bleiben rot nach $RUNDE Runden. Hier komme ich allein nicht weiter." "normal"
CODE=1
break
fi
echo "PRUEF-TOR ROT — Runde $((RUNDE + 1)) folgt." | tee -a "$LOG"
melde "[Pruefung]" "$NAME: Tests rot, ich repariere selbst weiter (Runde $((RUNDE + 1))/$MAXRUNDEN)." "silent"
AUFGABE="[MC2-PRUEFTOR] Der Verify-Befehl des Projekts ist fehlgeschlagen.
Befehl: $VCMD
Ausgabe (Ende):
$(printf '%s' "$VOUT" | tail -c 3000)
Behebe die URSACHE — nicht das Symptom. Schalte keinen Test ab und aendere keine Tests.
Urspruenglicher Auftrag war: $AUFTRAG"
done
DAUER=$(( $(date +%s) - START ))
# Nachweis statt Behauptung: was hat der Lauf im Arbeitsbaum tatsaechlich veraendert?
GEAENDERT="$(git -C "$REPO" status --porcelain 2>/dev/null | wc -l | tr -d ' ')"
case "$CODE" in
0) ERG="fertig, Pruefung bestanden" ;;
124) ERG="ZEITUEBERSCHREITUNG nach ${TIMEOUT}s" ;;
*) ERG="Pruefung NICHT bestanden (Exitcode $CODE)" ;;
esac
echo "=== Ergebnis: $ERG · ${DAUER}s · $RUNDE Runde(n) · $GEAENDERT geaenderte Dateien · Log $LOG ===" | tee -a "$LOG"
melde "[Lauf]" "$NAME: $ERG nach $((DAUER/60)) Minuten, $RUNDE Runde(n), $GEAENDERT Dateien angefasst." "silent"
exit "$CODE"