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.
300 lines
9.6 KiB
Bash
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
|