#!/usr/bin/env bash # Auftragsbuch: einen LUCY-Vorschlags-Branch ANNEHMEN — das PC-Gegenstück zu auftrag-annehmen.sh. # Lucy (Electron) lebt auf dem Windows-PC (F:\Coding Stuff\lucy); dist liegt NICHT im Repo. # Annehmen heißt darum: Merge auf main (hier, Box) → der PC zieht main, baut dist neu und # startet Lucy neu, WENN sie lief (User-Entscheid: Lucy startet nur, wenn der Commander sie will). # Schlägt am PC irgendetwas fehl, wird der Merge auf main automatisch revertiert — die laufende # Lucy bleibt die alte (der PC-Runner stellt dist aus seinem Backup wieder her). # # Der PC wird über den PC-Executor (client/hermes-pc/executor.py, :7777, Bearer-Token aus # ~/.hermes/config.yaml) gesteuert. /shell hat 60 s Timeout → der Build läuft DETACHED # (deploy/lucy-annahme.ps1 im Lucy-Repo schreibt eine Status-Datei, wir pollen sie). # # Läuft wie der MC2-Runner als EIGENE systemd-Unit und als /tmp-Kopie (Selbst-Reset-Falle). # Status-Schlüssel in der Auftragsbuch-JSON: "lucy:". # # Nutzung: lucy-annahme.sh (z. B. wartung/turncheck-default-aus) set -uo pipefail BRANCH="${1:?Nutzung: lucy-annahme.sh }" SRC="${MC2_SRC:-$HOME/mission-control-v2}" LUCY="${MC2_LUCY_DIR:-$HOME/lucy}" STATUS="${MC2_AUFTRAG_STATUS:-/srv/models/mc2-auftragsbuch.json}" HERMES_CONF="${HERMES_CONF:-$HOME/.hermes/config.yaml}" SLUG="lucy-$(echo "$BRANCH" | tr '/' '-')" WT="/tmp/annahme-$SLUG" LOG="/tmp/annahme-$SLUG.log" # Windows-Seite (Pfade im PC-Dateisystem; einfach gehalten — EIN PC, EIN Checkout). PC_REPO='F:\Coding Stuff\lucy' PC_STATUSFILE='F:\Coding Stuff\lucy\.lucy-annahme.json' PC_RUNNER='F:\Coding Stuff\lucy\deploy\lucy-annahme.ps1' BUILD_TIMEOUT_S=1500 # 25 min Deckel für npm ci + tsc + electron-builder + Stimm-Warmup notify(){ bash "$SRC/deploy/notify.sh" -s "[Auftragsbuch]" "$1" || true; } # Status-Fortschritt für die UI (atomar via tmp+replace; die API liest die Datei nur). status(){ # $1=state $2=detail python3 - "$STATUS" "lucy:$BRANCH" "$1" "$2" <<'PY' import json, os, sys, time p, key, state, detail = sys.argv[1:5] try: d = json.load(open(p, encoding="utf-8")) except Exception: d = {} d.setdefault("branches", {})[key] = {"state": state, "detail": detail, "ts": time.time()} tmp = p + ".tmp" json.dump(d, open(tmp, "w", encoding="utf-8"), ensure_ascii=False) os.replace(tmp, p) PY } # PC-Executor: PowerShell-Kommando ausführen. stdout des PCs → stdout; rc 0 nur wenn # Transport UND Kommando grün. Token/URL kommen aus der Hermes-Config (kein zweiter Ablageort). PC_URL="$(sed -n 's/^[[:space:]]*PC_EXECUTOR_URL:[[:space:]]*//p' "$HERMES_CONF" | head -1 | tr -d "\"'" | tr -d '[:space:]')" PC_TOKEN="$(sed -n 's/^[[:space:]]*PC_EXECUTOR_TOKEN:[[:space:]]*//p' "$HERMES_CONF" | head -1 | tr -d "\"'" | tr -d '[:space:]')" pc_shell(){ # $1 = PowerShell-Kommando python3 - "$PC_URL" "$PC_TOKEN" "$1" <<'PY' import json, sys, urllib.request url, token, cmd = sys.argv[1:4] req = urllib.request.Request(url + "/shell", data=json.dumps({"command": cmd}).encode(), headers={"Authorization": "Bearer " + token, "Content-Type": "application/json"}) try: r = json.load(urllib.request.urlopen(req, timeout=58)) except Exception as e: print(f"(PC-Executor nicht erreichbar: {e})") sys.exit(2) out = (r.get("stdout") or "").strip() if out: print(out) err = (r.get("stderr") or "").strip() if err: print(err, file=sys.stderr) sys.exit(0 if r.get("returncode") == 0 else 1) PY } cleanup_wt(){ git -C "$LUCY" worktree remove --force "$WT" 2>/dev/null || true; } fail(){ status "fehlgeschlagen" "$1" notify "Lucy-Vorschlag '$BRANCH' konnte NICHT eingespielt werden: $1 — nichts wurde verändert, der Branch bleibt liegen." cleanup_wt exit 1 } # Revert nach schon gepushtem Merge (PC-Phase rot): main zurückdrehen, PC-Checkout nachziehen. revert_main(){ # $1 = Grund für die Meldung status "rollback" "PC-Phase rot — Merge wird automatisch zurückgedreht" git -C "$LUCY" fetch -q origin || true cleanup_wt if git -C "$LUCY" worktree add --detach "$WT" origin/main >/dev/null 2>&1 \ && git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \ revert -m 1 --no-edit HEAD >>"$LOG" 2>&1 \ && git -C "$WT" push origin HEAD:main >>"$LOG" 2>&1; then cleanup_wt # PC-Checkout best-effort auf den revertierten Stand nachziehen (Quelle = laufende Lucy). pc_shell "git -C '$PC_REPO' fetch origin main 2>&1; git -C '$PC_REPO' merge --ff-only FETCH_HEAD 2>&1" >>"$LOG" 2>&1 || true status "zurueckgerollt" "$1 — Merge automatisch revertiert, Lucy läuft in der alten Version weiter" notify "Lucy-Vorschlag '$BRANCH' hat den PC-Build/Neustart GERISSEN: $1. Merge automatisch zurückgedreht — Lucy läuft in der alten Version weiter. Der Branch bleibt zur Analyse liegen. Log: $LOG" exit 1 fi cleanup_wt status "kritisch" "Revert fehlgeschlagen — Lucy-Repo und PC von Hand prüfen" notify "KRITISCH: Lucy-Annahme '$BRANCH' scheiterte UND der automatische Revert schlug fehl — bitte Lucy-Repo (Gitea main) und PC prüfen. Log: $LOG" exit 2 } [ -n "$PC_URL" ] && [ -n "$PC_TOKEN" ] || fail "PC-Executor-Zugang fehlt in $HERMES_CONF (PC_EXECUTOR_URL/TOKEN)" # ── Vorprüfungen am PC (VOR dem Merge — solange ist nichts kaputtzumachen) ── status "laeuft" "Vorprüfung: ist der PC bereit?" curl -sf -m 8 "$PC_URL/health" >/dev/null 2>&1 \ || fail "PC-Executor nicht erreichbar — Lucy-Annahme braucht den eingeschalteten PC (Executor-Task läuft bei Anmeldung)" PC_BRANCH="$(pc_shell "git -C '$PC_REPO' rev-parse --abbrev-ref HEAD" 2>>"$LOG")" \ || fail "PC-Arbeitskopie nicht lesbar (git am PC): $PC_BRANCH" [ "$PC_BRANCH" = "main" ] || fail "PC-Arbeitskopie steht auf '$PC_BRANCH' statt main — bitte am PC aufräumen" PC_DIRTY="$(pc_shell "git -C '$PC_REPO' status --porcelain -uno" 2>>"$LOG")" \ || fail "PC-Arbeitskopie nicht prüfbar" [ -z "$PC_DIRTY" ] || fail "PC-Arbeitskopie hat ungespeicherte Änderungen — bitte erst am PC committen/aufräumen: $(echo "$PC_DIRTY" | head -3 | tr '\n' ' ')" WAS_RUNNING="$(pc_shell "if (Get-Process Lucy -ErrorAction SilentlyContinue) { '1' } else { '0' }" 2>>"$LOG")" || WAS_RUNNING="0" case "$WAS_RUNNING" in 1|0) ;; *) WAS_RUNNING="0" ;; esac # ── Merge auf main (Box, isolierter Worktree — wie beim MC2-Runner) ───────── status "laeuft" "Merge wird vorbereitet" git -C "$LUCY" fetch -q origin || fail "git fetch (Gitea) fehlgeschlagen" git -C "$LUCY" rev-parse --verify -q "refs/remotes/origin/$BRANCH" >/dev/null \ || fail "Branch origin/$BRANCH existiert nicht (schon gemergt/gelöscht?)" # Kaputt aufgesetzte Branches (kein gemeinsamer Vorfahre) → Merge kann NIE gelingen. git -C "$LUCY" merge-base origin/main "origin/$BRANCH" >/dev/null 2>&1 \ || fail "Branch hat keinen gemeinsamen Ursprung mit main (kaputt aufgesetzt, z. B. git init statt Klonen) — bitte ablehnen (mit Grund) und die Idee neu in die Queue geben" cleanup_wt git -C "$LUCY" worktree add --detach "$WT" origin/main >/dev/null 2>&1 || fail "Worktree konnte nicht angelegt werden" MERGE_MSG="Auftragsbuch: Lucy-Vorschlag '$BRANCH' angenommen (Ein-Klick-Gate)" if ! git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \ merge --no-ff "origin/$BRANCH" -m "$MERGE_MSG" >>"$LOG" 2>&1; then git -C "$WT" merge --abort 2>/dev/null || true # Selbstheilung: veralteten Branch mechanisch auf main rebasen (wie im MC2-Runner). status "laeuft" "Merge-Konflikt — Auto-Rebase wird versucht" WT2="/tmp/annahme-rebase-$SLUG" git -C "$LUCY" worktree remove --force "$WT2" 2>/dev/null || true REBASED="" if git -C "$LUCY" worktree add --detach "$WT2" "origin/$BRANCH" >/dev/null 2>&1 \ && git -C "$WT2" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \ rebase origin/main >>"$LOG" 2>&1; then REBASED="$(git -C "$WT2" rev-parse HEAD)" else git -C "$WT2" rebase --abort 2>/dev/null || true fi git -C "$LUCY" worktree remove --force "$WT2" 2>/dev/null || true [ -n "$REBASED" ] || fail "Merge-Konflikt mit main, Auto-Rebase scheiterte ebenfalls — echter Inhaltskonflikt; am einfachsten ablehnen und die Idee neu in die Queue geben (frischer Branch von aktuellem main)" git -C "$WT" -c user.name="Auftragsbuch" -c user.email="auftragsbuch@box.local" \ merge --no-ff "$REBASED" -m "$MERGE_MSG — auto-rebased (Branch war veraltet)" >>"$LOG" 2>&1 \ || { git -C "$WT" merge --abort 2>/dev/null || true; fail "Merge nach Auto-Rebase fehlgeschlagen (unerwartet) — Log: $LOG"; } fi status "laeuft" "Push nach main läuft" PUSHED=0 for _ in 1 2 3; do git -C "$WT" push origin HEAD:main >>"$LOG" 2>&1 && { PUSHED=1; break; } sleep 5 done [ "$PUSHED" = 1 ] || fail "Push nach main fehlgeschlagen (Gitea nicht erreichbar / Token?)" cleanup_wt # ── PC: main ziehen, Build detached starten ───────────────────────────────── status "laeuft" "PC zieht den neuen Stand" pc_shell "git -C '$PC_REPO' fetch origin main 2>&1; if (\$LASTEXITCODE -ne 0) { exit 1 }; git -C '$PC_REPO' merge --ff-only FETCH_HEAD 2>&1" >>"$LOG" 2>&1 \ || revert_main "PC konnte main nicht ziehen (git fetch/merge am PC rot)" pc_shell "if (Test-Path '$PC_RUNNER') { exit 0 } else { exit 1 }" >>"$LOG" 2>&1 \ || revert_main "PC-Runner-Skript fehlt im Lucy-Repo (deploy/lucy-annahme.ps1)" # WICHTIG: -File-Pfad MUSS eingebettete Anführungszeichen tragen — Start-Process fügt # die ArgumentList-Elemente OHNE Quoting zusammen; 'F:\Coding Stuff\…' zerbricht sonst am # Leerzeichen und die gespawnte powershell stirbt still (erster Live-Lauf, 12.07.). pc_shell "Remove-Item -Force '$PC_STATUSFILE' -ErrorAction SilentlyContinue; Start-Process powershell -WindowStyle Hidden -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','\"$PC_RUNNER\"','-WasRunning','$WAS_RUNNING'" >>"$LOG" 2>&1 \ || revert_main "PC-Build ließ sich nicht starten" # ── Status-Datei pollen, Fortschritt in die UI spiegeln ───────────────────── status "laeuft" "PC baut Lucy (dist) — das dauert ein paar Minuten" DEADLINE=$(( $(date +%s) + BUILD_TIMEOUT_S )) LAST_DETAIL="" FINAL="" while [ "$(date +%s)" -lt "$DEADLINE" ]; do sleep 10 RAW="$(pc_shell "if (Test-Path '$PC_STATUSFILE') { Get-Content -Raw '$PC_STATUSFILE' }" 2>>"$LOG")" || continue [ -n "$RAW" ] || continue # Parse OHNE f-String-Escapes: \" in f-String-Ausdruecken ist seit Python 3.12/3.14 # ein SyntaxError (alter Pre-3.12-Stil) — genau das legte den ersten Live-Lauf lahm. PARSED="$(printf '%s' "$RAW" | python3 -c ' import json, sys try: d = json.loads(sys.stdin.read()) except Exception: sys.exit(0) detail = str(d.get("detail", "")).replace("|", "/").replace("\n", " ") done = "1" if d.get("done") else "0" ok = "1" if d.get("ok") else "0" print(done + "|" + ok + "|" + detail) ')" [ -n "$PARSED" ] || continue DONE="${PARSED%%|*}"; REST="${PARSED#*|}"; OK="${REST%%|*}"; DETAIL="${REST#*|}" if [ "$DETAIL" != "$LAST_DETAIL" ] && [ -n "$DETAIL" ]; then LAST_DETAIL="$DETAIL" status "laeuft" "PC: $DETAIL" fi if [ "$DONE" = "1" ]; then FINAL="$OK"; break; fi done [ -n "$FINAL" ] || revert_main "PC-Build-Timeout nach $((BUILD_TIMEOUT_S/60)) Minuten (Status-Datei blieb offen)" [ "$FINAL" = "1" ] || revert_main "${LAST_DETAIL:-PC meldet Fehler ohne Detail}" # ── Grün: Branch aufräumen, ehrlich melden ────────────────────────────────── git -C "$LUCY" push origin --delete "$BRANCH" >>"$LOG" 2>&1 || true git -C "$LUCY" fetch -q --prune origin 2>/dev/null || true status "eingespielt" "${LAST_DETAIL:-Build grün}" if [ "$WAS_RUNNING" = "1" ]; then notify "Lucy-Vorschlag '$BRANCH' ist LIVE — dist am PC neu gebaut, Lucy neu gestartet, Stimme meldet sich. ✅" else notify "Lucy-Vorschlag '$BRANCH' ist eingespielt — dist am PC neu gebaut. Lucy war aus und bleibt aus; die neue Version startet beim nächsten Öffnen. ✅" fi exit 0