#!/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 "" # 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 [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 \"\"" >&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"