pi-system/bin/subagent-tab
Raimund Bauer fb3daab33f feat/init: PiSystem Infrastruktur-Repo mit SubConfirm
Enthält alle Pi-Orchestrator-Infrastrukturkomponenten:
- bin/Sub* Skripte (SubAgenten, SubStatus, SubWatcher, SubConfirm)
- extensions/ (arbeitsweise-guard, confirm-deletion, etc.)
- memory/ (arbeitsweise, subagent-autocheck)
- agent/AGENTS.md mit SubConfirm-Reaktionslogik
- install.sh: deterministisches, idempotentes Setup für neue Maschinen

SubConfirm (neu): Stasis-Detektor der alle 30s tmux-Sessions prüft.
Bei unverändertem Output sendet er den vollständigen Pane-Inhalt
an die Alert-Datei — der Orchestrator beurteilt selbst ob Handlung nötig.
Kein Keyword-Matching.
2026-06-02 11:53:37 +02:00

300 lines
9.6 KiB
Bash

#!/bin/bash
# subagent-tab — Subagenten als Panes (tiled) in einem xterm auf dem Arzopa-Monitor
#
# Nutzung:
# subagent-tab init — Session + xterm auf Arzopa starten
# subagent-tab spawn <name> [aufgabe] — Neuen Pane mit pi
# subagent-tab list — Alle Panes anzeigen
# subagent-tab kill <name> — Pane schliessen
# subagent-tab killall — Alles schliessen
#
# Im Pane:
# Ctrl+B Pfeiltasten — Zwischen Panes wechseln
# Ctrl+B z — Pane Vollbild (nochmal zurück)
# Ctrl+B d — Detach
set -euo pipefail
SESSION="agents"
# --- Monitor-Erkennung ---
detect_monitor() {
# Manuelle Override
if [ -n "${AGENTS_MONITOR:-}" ]; then
echo "$AGENTS_MONITOR"
return
fi
# Auto: erster nicht-primary Monitor
xrandr --query 2>/dev/null | grep " connected" | grep -v "primary" | head -1 | awk '{print $1}'
}
get_monitor_geometry() {
local MONITOR="${1:-$(detect_monitor)}"
local LINE=$(xrandr --query 2>/dev/null | grep "$MONITOR" | head -1)
# Auflösung und Position extrahieren: z.B. "1920x1080+0+1080"
local GEO=$(echo "$LINE" | grep -oE '[0-9]+x[0-9]+\+[0-9]+\+[0-9]+' | head -1)
echo "$GEO"
}
# --- Farben ---
C_RESET="\033[0m"
C_GREEN="\033[32m"
C_CYAN="\033[36m"
C_YELLOW="\033[33m"
C_RED="\033[31m"
C_DIM="\033[2m"
usage() {
cat <<EOF
subagent-tab — Subagenten als Panes auf dem Arzopa-Monitor
Nutzung:
subagent-tab init Session + xterm auf Arzopa starten
subagent-tab spawn <name> [aufgabe] Neuen Pane mit pi
subagent-tab spawn --provider P --model M <name> [aufgabe]
subagent-tab list Alle Panes anzeigen
subagent-tab kill <name> Pane schliessen
subagent-tab killall Alles schliessen
Steuerung:
Ctrl+B Pfeiltasten Zwischen Panes wechseln
Ctrl+B z Pane Vollbild (nochmal zurück)
Ctrl+B d Detach (läuft weiter)
Monitor:
Automatisch: erster nicht-primary Monitor (Arzopa)
Manuell: AGENTS_MONITOR=DP-3 subagent-tab init
EOF
}
# --- Init ---
cmd_init() {
if tmux has-session -t "$SESSION" 2>/dev/null; then
echo -e "${C_GREEN}Session '$SESSION' existiert bereits.${C_RESET}"
return 0
fi
local MONITOR=$(detect_monitor)
if [ -z "$MONITOR" ]; then
echo -e "${C_YELLOW}Kein zweiter Monitor gefunden. Starte im aktuellen Terminal.${C_RESET}"
tmux new-session -d -s "$SESSION" -x 220 -y 50
else
local GEO=$(get_monitor_geometry "$MONITOR")
local WIDTH=$(echo "$GEO" | grep -oE '^[0-9]+')
local HEIGHT=$(echo "$GEO" | grep -oE 'x[0-9]+' | tr -d 'x')
local POS_X=$(echo "$GEO" | grep -oE '\+[0-9]+' | head -1 | tr -d '+')
local POS_Y=$(echo "$GEO" | grep -oE '\+[0-9]+$' | tr -d '+')
# Geometrie für xterm berechnen (Font ~8x16 bei 1920x1080 ≈ 240x67)
local COLS=$((WIDTH / 8))
local ROWS=$((HEIGHT / 16))
# tmux Session starten
tmux new-session -d -s "$SESSION" -x "$COLS" -y "$ROWS"
# xterm auf dem Arzopa-Monitor positionieren
xterm -geometry "${COLS}x${ROWS}+${POS_X}+${POS_Y}" \
-fa 'Monospace' -fs 10 \
-title "⚡ Agents" \
-e "tmux attach -t $SESSION" &
local XTERM_PID=$!
echo -e "${C_GREEN}xterm auf $MONITOR gestartet (PID $XTERM_PID).${C_RESET}"
echo -e " Geometrie: ${COLS}x${ROWS} an Position +${POS_X}+${POS_Y}"
fi
# tmux Styling
tmux set-option -t "$SESSION" status-position top
tmux set-option -t "$SESSION" status-style "bg=#1a1b26,fg=#a9b1d6"
tmux set-option -t "$SESSION" status-left-length 30
tmux set-option -t "$SESSION" status-left " #[bold]⚡ Agents "
tmux set-option -t "$SESSION" status-right " %H:%M "
tmux set-option -t "$SESSION" mouse on
tmux set-option -t "$SESSION" set-clipboard on
# Tiled-Layout Hooks
tmux set-hook -t "$SESSION" after-split-window 'select-layout tiled'
tmux set-hook -t "$SESSION" pane-exited 'select-layout tiled'
sleep 1
echo -e "Neuer Pane: ${C_CYAN}subagent-tab spawn <name> <aufgabe>${C_RESET}"
}
# --- Spawn ---
cmd_spawn() {
local PROVIDER=""
local MODEL=""
while [ $# -gt 0 ]; do
case "$1" in
--provider) PROVIDER="$2"; shift 2 ;;
--model) MODEL="$2"; shift 2 ;;
--) shift; break ;;
-*) echo "Unbekannt: $1"; exit 1 ;;
*) break ;;
esac
done
local NAME="${1:-}"
local TASK="${2:-}"
if [ -z "$NAME" ]; then
echo "Fehler: Name erforderlich."
exit 1
fi
# Session muss existieren
cmd_init
local SLUG=$(echo "$NAME" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '-')
local WORKDIR="/tmp/subagent-${SLUG}"
mkdir -p "$WORKDIR"
# Pi-Befehl bauen
local PI_CMD="pi"
[ -n "$PROVIDER" ] && PI_CMD="$PI_CMD --provider $PROVIDER"
[ -n "$MODEL" ] && PI_CMD="$PI_CMD --model $MODEL"
# Pane erstellen
local PANE_COUNT=$(tmux list-panes -t "$SESSION:0" 2>/dev/null | wc -l)
if [ "$PANE_COUNT" -eq 1 ]; then
# Prüfen ob das erste Pane leer ist (kein Prozess)
local CONTENT=$(tmux capture-pane -t "$SESSION:0.0" -p 2>/dev/null | tail -3)
if [ -z "$(echo "$CONTENT" | tr -d '[:space:]')" ] || echo "$CONTENT" | grep -qE '^\$'; then
# Leer → Pi hier starten
tmux send-keys -t "$SESSION:0.0" "cd $WORKDIR && $PI_CMD" Enter
tmux send-keys -t "$SESSION:0.0"" ; echo '=== Pi beendet ==='; read" 2>/dev/null || true
else
# Belegt → split
tmux split-window -t "$SESSION:0" -c "$WORKDIR" "$PI_CMD; echo '=== Pi beendet ==='; read"
fi
else
# Bereits mehrere Panes → split
tmux split-window -t "$SESSION:0" -c "$WORKDIR" "$PI_CMD; echo '=== Pi beendet ==='; read"
fi
# Tiled-Layout
tmux select-layout -t "$SESSION:0" tiled
echo -e "${C_GREEN}Pane '$SLUG' erstellt.${C_RESET}"
echo -e " Arbeitsverzeichnis: ${C_DIM}$WORKDIR${C_RESET}"
# Warten bis Pi bereit
local TIMEOUT=30
local ELAPSED=0
local PATTERN="(auto)|(zai)|(openrouter)"
if [ -n "$PROVIDER" ]; then
local PSHORT=$(echo "$PROVIDER" | sed 's/[^a-zA-Z0-9]//g')
PATTERN="$PATTERN|($PSHORT)"
fi
# Neueste Pane finden
local NEW_PANE=$(tmux list-panes -t "$SESSION:0" -F '#{pane_index}' | tail -1)
echo -n " Warte auf Pi"
while [ $ELAPSED -lt $TIMEOUT ]; do
if ! tmux has-session -t "$SESSION" 2>/dev/null; then
echo -e "\n ${C_RED}Session abgestürzt.${C_RESET}"
exit 1
fi
local CAPTURE=$(tmux capture-pane -t "$SESSION:0.$NEW_PANE" -p 2>/dev/null | tail -3)
if echo "$CAPTURE" | grep -a -qE "$PATTERN" 2>/dev/null; then
break
fi
echo -n "."
sleep 1
ELAPSED=$((ELAPSED + 1))
done
echo ""
if [ $ELAPSED -ge $TIMEOUT ]; then
echo -e " ${C_YELLOW}Timeout, versuche trotzdem...${C_RESET}"
fi
# Intercom-Namen setzen
tmux send-keys -t "$SESSION:0.$NEW_PANE" "/name ${SLUG}" Enter
sleep 1
# Aufgabe senden
if [ -n "$TASK" ]; then
tmux send-keys -t "$SESSION:0.$NEW_PANE" "$TASK" Enter
echo -e " Aufgabe gesendet."
fi
echo -e " Wechseln: ${C_CYAN}Ctrl+B Pfeiltasten${C_RESET} | Vollbild: ${C_CYAN}Ctrl+B z${C_RESET}"
}
# --- List ---
cmd_list() {
if ! tmux has-session -t "$SESSION" 2>/dev/null; then
echo "Keine aktive Session."
exit 0
fi
local COUNT=$(tmux list-panes -t "$SESSION:0" 2>/dev/null | wc -l)
echo -e "${C_CYAN}Session '$SESSION' — $COUNT Panes (tiled):${C_RESET}"
echo ""
tmux list-panes -t "$SESSION:0" -F '#{pane_index}: #{pane_current_command} #{pane_active}' | while read line; do
local IDX=$(echo "$line" | cut -d: -f1 | tr -d ' ')
local CMD=$(echo "$line" | awk '{print $2}')
local ACTIVE=$(echo "$line" | awk '{print $NF}')
if [ "$ACTIVE" = "1" ]; then
echo -e " ${C_GREEN}▶ Pane $IDX: $CMD${C_RESET}"
else
echo -e " Pane $IDX: $CMD"
fi
done
}
# --- Kill ---
cmd_kill() {
local NAME="${1:-}"
if [ -z "$NAME" ]; then
echo "Fehler: Name erforderlich."
exit 1
fi
# Pane anhand des working-directory finden
local SLUG=$(echo "$NAME" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '-')
local TARGET_PANE=""
for pane in $(tmux list-panes -t "$SESSION:0" -F '#{pane_index} #{pane_current_path}'); do
local IDX=$(echo "$pane" | awk '{print $1}')
local PATH_PANE=$(echo "$pane" | awk '{print $2}')
if echo "$PATH_PANE" | grep -q "$SLUG"; then
TARGET_PANE="$IDX"
break
fi
done
if [ -n "$TARGET_PANE" ]; then
tmux kill-pane -t "$SESSION:0.$TARGET_PANE"
tmux select-layout -t "$SESSION:0" tiled
echo -e "${C_GREEN}Pane '$NAME' geschlossen.${C_RESET}"
else
echo -e "${C_RED}Pane '$NAME' nicht gefunden.${C_RESET}"
fi
}
# --- Killall ---
cmd_killall() {
if tmux has-session -t "$SESSION" 2>/dev/null; then
tmux kill-session -t "$SESSION"
echo -e "${C_GREEN}Session '$SESSION' beendet.${C_RESET}"
else
echo "Keine aktive Session."
fi
}
# --- Main ---
CMD="${1:-}"
shift || true
case "$CMD" in
init) cmd_init ;;
spawn) cmd_spawn "$@" ;;
list) cmd_list ;;
kill) cmd_kill "$@" ;;
killall) cmd_killall ;;
help|--help|-h) usage ;;
"") usage ;;
*) echo "Unbekannt: $CMD"; usage; exit 1 ;;
esac