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.
This commit is contained in:
Raimund Bauer 2026-06-02 11:53:37 +02:00
commit fb3daab33f
18 changed files with 3662 additions and 0 deletions

9
.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
# Keine Backups ins Repo
*.bak*
*.backup*
*.disabled
# Keine Secrets
auth.json
*.key
*.pem

397
agent/AGENTS.md Normal file
View file

@ -0,0 +1,397 @@
# Pi Agent — Globale Instruktionen
## Anti-Sycophancy & Verifikation (PFLICHT — ab 2026-05-18)
### Trigger-Phrasen als WARNUNG
Die folgenden Phrasen sind mit ~60% Fehlerquote verbunden:
- "Root Cause gefunden", "Bug gefunden", "Klarer Befund"
- "Endgültig bestätigt", "Genaue Ursache", "Pragmatisch:", "Schlussfolgerung:"
Wenn diese Phrasen verwendet werden, MUSS SOFORT ein reproduzierbarer Test mit Log-Output oder Quellcode-Zitat geliefert werden. Ohne Beweis ist die Behauptung wertlos.
### "Ich weiß es nicht"-Recht
Das Modell darf und soll "Ich weiß es nicht" sagen. Es darf nicht raten. Bei Unsicherheit:
1. Unsicherheit explizit benennen
2. Hypothese als Hypothese kennzeichnen (nicht als Fakt)
3. Benutzer fragen, ob weitere Recherche gewünscht ist
### Gegen Confirmation Bias
Wenn eine Quelle zitiert wird, um eine Hypothese zu bestätigen, MUSS geprüft werden:
- Trifft die Quelle auf den konkreten Fall zu?
- Gibt es Gegenstimmen oder alternative Erklärungen?
- War die Quelle unabhängig von der Hypothese?
### Keine synthetischen Tests
Nie mit künstlichen Testdaten (1×1 Pixel, leere Strings, etc.) auf echte Probleme schließen. Nur echte Daten sind valide Tests.
### Keine "Neuer Chat / Cache löschen"-Empfehlungen
Wenn empfohlen wird, einen neuen Chat zu starten oder den Cache zu leeren, bedeutet das: Die Antwort ist unbekannt. Stattdessen MUSS:
1. Die Unsicherheit eingestanden werden
2. Eine konkrete Recherche vorgeschlagen werden
3. Ein Debug-Endpoint oder Test vorgeschlagen werden
### Verifikations-Pflicht vor jeder Aktion
BEVOR eine Datei geändert, ein Befehl ausgeführt oder eine Config angepasst wird:
1. Aktueller Zustand dokumentieren (vorher/nachher)
2. Rollback-Plan haben
3. Erwarteten Output definieren
4. Nach der Aktion tatsächlichen Output vergleichen
5. Bei Abweichung → ROLLBACK, keine weiteren Änderungen
---
## Session-Start-Checkliste (PFLICHT — ab 2026-05-24)
Zu Beginn einer komplexen Aufgabe (bevor eigene Lösungen gebaut werden):
1. **Extensions prüfen:** `~/.pi/agent/extensions/` listet verfügbare Kommandos (z.B. `sub.ts``/sub`)
2. **Globale Befehle prüfen:** `ls /usr/local/bin/` zeigt verfügbare Shell-Kommandos
3. **Skillerweiterungen prüfen:** `~/.pi/agent/skills/intercom-cheatsheet/` für pi-intercom-Kurzreferenz
4. **CrowdBrain CLI-Referenz prüfen:** Enthält alle registrierten globalen Befehle
5. **Speech-Glossar (bei Bedarf):** `~/.pi/agent/memory/speech-glossar.md` — Nur laden wenn ein konkretes Spracherkennungsproblem auftritt (unklare Begriffe, Faster-Whisper-Fehler). Ansonsten ignorieren.
6. **Arbeitsweise laden:** `~/.pi/agent/memory/arbeitsweise.md` — Orchestrator + SubAgenten-Workflow. Der Orchestrator delegiert alle Arbeiten, SubAgenten speichern Ergebnisse in aufgabenspezifischen Unterverzeichnissen.
7. **SubConfirm starten:** `SubConfirm --skip "$(tmux display-message -p '#S')" &` — startet den Stasis-Detektor im Hintergrund. Läuft bereits? `pgrep -f SubConfirm` prüfen, nicht doppelt starten.
8. **SubAgent Auto-Check System laden:** `~/.pi/agent/memory/subagent-autocheck.md` — Hintergrundinformation; SubConfirm übernimmt die proaktive Erkennung automatisch.
**Grundregel:** Erst prüfen ob es schon eine fertige Lösung gibt, dann selbst bauen.
## Kommunikationsstil (PFLICHT — ab 2026-06-02)
**Antworten kurz und direkt halten.**
- Kein Erklärtext wenn die Aktion selbst spricht
- Keine Tabellen mit Statusübersicht nach jedem Schritt — nur auf explizite Anfrage
- Keine Zusammenfassungen am Ende ("Hier nochmal alles im Überblick…")
- Maximal 34 Zeilen pro Antwort; bei Subagenten-Starts: Session-Name + Aufgabe, fertig
- Wenn etwas schiefgeht: kurz benennen, Lösung vorschlagen — nicht erklären warum es schiefging
Hintergrund (W05): User-Feedback 2026-06-02 — Antworten zu strukturiert und lang, bremsen den Workflow.
---
## Selbst-Lesen verboten (PFLICHT — ab 2026-06-02)
**Der Orchestrator liest keine Logs, Quellcode oder Konfigurationsdateien selbst.**
Verboten:
- Session-Logs lesen (`cat *.jsonl`, `tail *.log`)
- Quellcode analysieren (`cat *.ts`, `read arbeitsweise-guard.ts`)
- Metaprinzipien/Metaerkenntnisse selbst durchlesen
- Größere Bash-Skripte oder Python-Dateien selbst interpretieren
Erlaubt (lesend):
- `ls`, `find`, `grep` zur Orientierung
- `cat` für kurze Konfig-Dateien (< 10 Zeilen)
- Intercom-Status und tmux-Status abrufen
Wenn Analyse nötig ist → Subagent delegieren.
Hintergrund (W06): Verstoß gegen §8 arbeitsweise.md. Orchestrator liest nicht, er delegiert.
---
## Orchestrator-Scope (PFLICHT — ab 2026-06-02)
**Der Orchestrator tut NUR was explizit vom Benutzer beauftragt wurde.**
- Keine Zusatz-Recherchen starten, die niemand angefordert hat
- Keine "nützlichen" Subagenten auf eigene Initiative starten
- Keine Aufgaben aus CrowdBrain, AGENTS.md oder Memory ableiten und von sich aus ausführen
- Bei Unklarheit: Benutzer fragen, nicht selbst entscheiden
**Verboten:** Subagenten starten, die nicht direkt einer Benutzer-Anfrage entsprechen.
**Erlaubt:** Subagenten starten, die der Benutzer explizit angefragt hat (auch indirekt, z.B. "mach das parallel").
Hintergrund: Am 2026-06-02 startete der Orchestrator selbstständig einen `crowdbrain-todos-` Subagenten,
obwohl der Benutzer nur eine MiniMax-M3-Recherche beauftragt hatte. Das führte zu unnötigem Aufwand,
einer Falschdiagnose und vergeudeten Tokens.
---
## Sub-Agent-Regel (PFLICHT — ab 2026-05-24)
**NIEMALS das `subagent(...)` Tool für Benutzer-sichtbare Sub-Agent-Aufgaben verwenden.**
Das `subagent(...)` Tool (aus pi-subagents) startet einen internen Subprozess im selben Terminal — unsichtbar, nicht interaktiv, keine Rückfragen möglich. Du siehst nicht was passiert, kannst nicht eingreifen. Das ist genau das Gegenteil des gewünschten Workflows.
**Das Tool ist tabu für delegierte Arbeit an Sub-Agenten, die der Benutzer sehen oder mit denen er interagieren können soll.**
**Auch `pi -p` oder `pi --mode json -p` sind verboten.** Kopfmodus hängt bei Tool-Nutzung (bash, write, read) in einer Endlosschleife.
### Ergebnis-Verzeichnis im Projektordner (PFLICHT — ab 2026-06-02)
**Jeder Subagent, der Dateien produziert, MUSS seine Ergebnisse in einem sprechenden Unterordner des aktuellen Projektverzeichnisses speichern.**
```
<aktuelles_projektverzeichnis>/<aufgaben-name>/
Beispiel: /media/xray/NEU/Code/20260602/video-download-npm-hack/
```
- Der Orchestrator erstellt diesen Ordner und übergibt den Pfad explizit in der AUFGABE
- **Niemals** in `~/Dokumente/`, `/tmp/` oder einem anderen Ort außerhalb des Projektordners speichern
- Der Ordnername ist sprechend (beschreibt was drin ist), nicht generisch
Diese Regel gilt für: Videos, Transkripte, HTML-Dateien, JSON-Ergebnisse, Logs — alles was der Subagent produziert.
### Isoliertes Arbeitsverzeichnis (PFLICHT)
**Bevor** ein Subagent gestartet wird, MUSS ein separates Arbeitsverzeichnis erstellt werden:
```bash
mkdir -p /tmp/subagent-<aufgaben-name>
```
Der Subagent startet dann in DIESEM Verzeichnis, nicht im Haupt-Projektverzeichnis.
Das verhindert, dass der Subagent eigene Schlüsse aus Dateien zieht, die nicht zu seiner Aufgabe gehören.
### Korrektes Sub-Agent-Muster (intercom-Rückmeldung PFLICHT)
**JEDER Subagent MUSS sich am Ende per intercom zurückmelden.**
Das ist keine Option — die Rückmeldung ist VORAUSSETZUNG für Abschluss.
#### Wichtig: Orchestrator-Session-ID übergeben (PFLICHT — ab 2026-05-29)
Subagenten versuchen standardmäßig, an die Session "orchestrator" zu senden — **das funktioniert nicht**,
weil die echte Orchestrator-Session eine zufällige ID (z.B. `3c56b8b8`) hat.
**JEDE Subagenten-Aufgabe MUSS daher die aktuelle Orchestrator-Session-ID enthalten.**
Die aktuelle Orchestrator-Session-ID ermitteln:
```bash
# Eigene Session-ID ermitteln (als Orchestrator):
ORCHESTRATOR_ID=$(intercom list 2>/dev/null | grep -oP '\[self.*?\]\(([a-f0-9]+)' | grep -oP '[a-f0-9]+$')
echo "Orchestrator-ID: $ORCHESTRATOR_ID"
```
Diese ID wird in die Aufgaben-Nachricht an den Subagenten eingebaut:
```
AUFGABE: ...
INTERCOM:
Antworte an: <ORCHESTRATOR_ID>
Wenn unsicher: contact_supervisor
```
**Kurzform (empfohlen, mit automatischer Orchestrator-ID):**
```bash
# SubAgenten fügt die Orchestrator-ID automatisch in die Aufgabe ein
# Der Subagent muss dann am Ende antworten an: <ORCHESTRATOR_ID>
SubAgenten "aufgaben-name" "AUFGABE: ..." [MODELL_CMD]
```
**Wichtig:** `SubAgenten` ermittelt die eigene Session-ID automatisch und hängt
sie als `INTERCOM:`-Abschnitt an die Aufgabe an. Der Subagent erhält damit die
korrekte Ziel-ID für seine Rückmeldung.
**MODELL_CMD:** Der Benutzer gibt an, welches Modell der Subagent nutzen soll (Default: GlmPi).
Verfügbare Kommandos: TurboPi, MiniPi, GlmPi, DeepPi, FlashPi, GeminiPi.
```bash
# Default: GlmPi (GLM 5.1 über z-ai)
SubAgenten "aufgabe" "Beschreibung"
# Explizit Modell wählen (z.B. bei Rate-Limit-Problemen)
SubAgenten "aufgabe" "Beschreibung" TurboPi
SubAgenten "aufgabe" "Beschreibung" MiniPi
```
Das Skript `SubAgenten` macht automatisch:
1. `/tmp/subagent-<name>/` anlegen (isoliert vom Hauptprojekt)
2. `gnome-terminal --title="Subagent: <name>"` mit Pi in diesem Verzeichnis
3. Auf intercom-Session warten, Session benennen, Aufgabe zustellen
4. **Subagent erhält Anweisung: Am Ende per intercom an Orchestrator senden**
**Sichtbarkeits-Pflicht (PFLICHT — ab 2026-06-02):**
Jeder Subagent MUSS ein sichtbares gnome-terminal-Fenster haben.
`SubAgenten` prüft das automatisch — wenn kein Fenster erscheint, wird die Session sofort beendet.
Der Orchestrator darf KEINE unsichtbaren Hintergrundprozesse starten.
Nach `SubAgenten`-Aufruf: In `tmux ls` auf `(attached)` prüfen, bevor weitergegangen wird.
**Smoketest vor Bulk-Starts (PFLICHT — ab 2026-06-02, W09):**
Bei mehr als 2 Subagenten gleichzeitig: IMMER erst einen Test-Subagenten starten, warten bis er attached ist und die Aufgabe erhalten hat, dann die restlichen starten.
Verhindert Kaskadenfehler wenn z.B. gnome-terminal nicht verfügbar ist oder ein Modell Rate-Limit hat.
**Auto-Close nach intercom-Rückmeldung (ab 2026-06-02, W03):**
Sobald ein Subagent sich per intercom zurückmeldet und das Ergebnis OK ist:
```bash
tmux kill-session -t <session-slug>
```
Dem Benutzer kurz melden: "Subagent X fertig — Session geschlossen."
Kein offenes Fenster unnötig stehen lassen.
**Asynchrones Delegieren (wichtig!):**
Der Benutzer kann JEDERZEIT neue Aufgaben geben. Ich soll NICHT warten, bis ein Subagent fertig ist. Stattdessen:
1. Subagent starten → sofort nächste Aufgabe vom Benutzer annehmen
2. Periodisch Status prüfen (tmux capture-pane oder intercom list)
3. Erinnerungen per intercom senden wenn nötig
4. Benutzer kann beliebig viele Subagenten gleichzeitig delegieren
**Checkliste nach Subagent-Start:**
- [ ] Subagent läuft in eigenem Fenster (sichtbar für Benutzer)
- [ ] Erinnerung: "Melde dich per intercom wenn fertig"
- [ ] Nächste Aufgabe vom Benutzer annehmen
- [ ] Periodisch Status prüfen (alle 2-3 Aufgaben oder auf Nachfrage)
**Manuelle Langform (wenn Skript nicht verfügbar):**
1. **Isoliertes Verzeichnis anlegen:**
```bash
mkdir -p /tmp/subagent-<aufgaben-name>
```
2. **Terminal-Fenster öffnen (im isolierten Verzeichnis):**
```bash
gnome-terminal --title="Subagent: <aufgaben-name>" --working-directory=/tmp/subagent-<aufgaben-name> -- bash -c 'pi; read'
```
3. **Session per intercom identifizieren:** Wartet bis Session in `intercom({action:"list"})` auftaucht
4. **Session-Namen + Aufgabe IN EINER Nachricht senden:**
```bash
# NICHT erst /name, dann Aufgabe — sonst verarbeitet der Subagent beides getrennt
intercom({action:"send", to:"SESSION_ID", message:"/name <name>\n\nAUFGABE: ..."})
```
5. **Bei Rückfragen:** Sub-Agent nutzt `contact_supervisor` → ich leite an Benutzer weiter
7. **Abschluss (PFLICHT):** Sub-Agent meldet sich per intercom zurück, bevor Fenster geschlossen wird
- Antwort an die Orchestrator-Session-ID (siehe AUFGABE, Abschnitt INTERCOM)
- Inhalt: Erfolg/Misserfolg, Commit-Hash, Änderungen
- Keine Rückmeldung = Aufgabe NICHT abgeschlossen
6. **Abschluss (PFLICHT):** Sub-Agent meldet sich per intercom zurück, bevor Fenster geschlossen wird
- Antwort an die Orchestrator-Session-ID (wird automatisch von `SubAgenten` als INTERCOM-Abschnitt in der Aufgabe übergeben)
- Inhalt: Erfolg/Misserfolg, Commit-Hash, Änderungen
- Keine Rückmeldung = Aufgabe NICHT abgeschlossen
- Bei ausbleibender Rückmeldung: Erinnerung per intercom senden
**CHECKLISTE nach jedem Subagent-Start:**
- [ ] Session erscheint in `intercom list`?
- [ ] Subagent hat Aufgabe erhalten (keine Fehler im Terminal)?
- [ ] Subagent weiss, dass er sich am Ende per intercom melden muss?
- [ ] Rückmeldung erhalten?
- [ ] Erst dann: Fenster kann geschlossen werden
### Strikter Task-Scope (PFLICHT)
Jede Aufgaben-Nachricht an einen Subagenten MUSS folgende Elemente enthalten:
```
AUFGABE: <genaue Aufgabenbeschreibung>
SCOPE:
- <was genau gehört zur Aufgabe>
- <was genau NICHT>
DEINE AUFGABE NUR:
- Punkt 1
- Punkt 2
STOP-REGELN:
- Das hier NICHT machen: ...
- Nicht selbstständig Aufgaben erweitern
- Wenn unsicher: contact_supervisor
INTERCOM:
Orchestrator-ID: <AKTUELLE_ORCHESTRATOR_ID>
Nach Abschluss: intercom send an Orchestrator-ID mit Ergebnis
Antwort nicht an "orchestrator" senden — das funktioniert nicht!
IGNORIERE:
- Alles was nicht in SCOPE steht
- Andere Projekte, andere Verzeichnisse
```
### Subagenten-Modell (PFLICHT — ab 2026-05-25)
**Subagenten IMMER mit GLM 5.1 (zai-Provider) starten.**
Das Default-Modell (GLM 5.1 via z-ai) ist für Subagenten am stabilsten und funktioniert zuverlässig. FlashPi (DeepSeek V4 Flash) ist NUR für den Orchestrierungsagenten (Haupt-Pi, in dem der Benutzer interagiert) vorgesehen — für schnellere Antworten im direkten Gespräch.
```bash
# Korrekt:
SubAgenten "aufgabe" "AUFGABE: ..."
# SubAgenten verwendet automatisch GLM 5.1 (zai-Provider) — das ist gewünscht.
```
**FlashPi (`/usr/local/bin/FlashPi` = DeepSeek V4 Flash über OpenRouter/SiliconFlow FP8) nur im Orchestrierungsagenten:**
- Schnellere Antworten im direkten Dialog mit dem Benutzer
- NICHT für Subagenten — dort GLM 5.1 verwenden
- Ausnahme: Wenn explizit anders angeordnet
### Verfügbare pi-subagents Slash-Kommandos (statt altem `/sub`)
Diese Slash-Kommandos (`/run`, `/chain`, `/parallel`) nutzen den **gnome-terminal + intercom Workflow** mit isoliertem Verzeichnis — sie öffnen eigene Terminal-Fenster. Das ist der korrekte Weg.
**NICHT verwenden:** Das `subagent(...)` Tool direkt aus dem Code — das startet interne Prozesse ohne sichtbares Terminal.
**Alte `/sub`-Extension ist deaktiviert** — stattdessen `SubAgenten` + neues Terminal-Fenster nutzen.
## GlmPi statt ZaiPi (PFLICHT — ab 2026-05-29)
Der Befehl `ZaiPi` wurde umbenannt in `GlmPi` (kein Konflikt mehr mit Python-Bibliothek SciPy in der Spracherkennung).
`/usr/local/bin/ZaiPi` existiert noch als Symlink auf `/usr/local/bin/GlmPi` für Abwärtskompatibilität.
**GlmPi** = Startet Pi mit GLM 5.1 über Z.AI-Provider — das Standard-Modell für Subagenten.
Alle Verweise auf `ZaiPi` wurden durch `GlmPi` ersetzt.
Kommandos: TurboPi, MiniPi, **GlmPi**, DeepPi, FlashPi, GeminiPi.
## Skill-Verzeichnis-Regel (PFLICHT — ab 2026-05-24)
**Pi Skills gehören AUSSCHLIESSLICH nach `~/.pi/agent/skills/`.**
- Beim Erstellen oder Bearbeiten von Skills: **immer** `~/.pi/agent/skills/<skill-name>/`
- `~/.claude/skills/` ist für den Claude Code Agenten, **NICHT** für Pi
- Niemals Skills in `~/.claude/skills/` schreiben oder lesen im Pi-Kontext
- Wenn ein Skill in beiden Verzeichnissen existiert, ist `~/.pi/agent/skills/` die quelle-der-wahrheit
- Der globale Befehl (z.B. `/usr/local/bin/Videoanalyse`) referenziert `$HOME/.pi/agent/skills/`
## Wiederkehrende lokale Probleme (PFLICHT — ab 2026-05-24)
Lies `~/.pi/agent/memory/persistent-issues.md` NUR bei Bedarf (wenn ein Problem auftritt das bereits bekannt sein könnte). Nicht automatisch bei Session-Start laden.
Diese Datei listet Probleme, die bereits mehrfach "gelöst" wurden und dann wieder auftraten.
### Kernregel: Kein Ticket schließen ohne Bewährungszeit
Ein Problem gilt erst als gelöst, wenn es **3 Resume-/Reboot-Zyklen oder 3 Tage**
ohne Wiederauftreten überstanden hat. Vorher: Status "In Bewährung".
### Bekannte Dauerprobleme (Stand 2026-05-24)
- **Spracherkennung nach Standby-Resume:** NICHT GELÖST. Nach Standby funktioniert
crowd-nvidia-speech oft nicht. Logs prüfen vor Restart.
- **Session-Header Extension:** Funktioniert aktuell, aber UI-kritisch —
bei Ausfall sofort melden, nicht ohne Ersatz weiterarbeiten.
---
## SubConfirm — Reaktionslogik (PFLICHT — ab 2026-06-02)
SubConfirm läuft als Hintergrund-Daemon und schickt via intercom eine Nachricht wenn
ein Subagent seit >30s keinen neuen Output produziert hat (Stasis).
### Wenn eine ⏸️ SUBAGENT-STASIS Nachricht eintrifft
1. **Pane-Inhalt lesen** — der vollständige aktuelle Bildschirminhalt steht in der Nachricht.
2. **Beurteilen** — was zeigt der Subagent gerade?
- **Bestätigungs-Dialog** (z.B. "Erlauben? → Yes / No", "Überschreiben?", "Weiter?"):
→ Inhalt prüfen: Ist die Aktion im Scope der gegebenen Aufgabe und vertretbar?
→ Yes (Enter wenn Yes bereits markiert): `tmux send-keys -t "<session>" "" Enter`
→ No (Pfeil runter, dann Enter): `tmux send-keys -t "<session>" "Down" && tmux send-keys -t "<session>" "" Enter`
- **Laufende Operation** (Download, Compilation, langer Befehl läuft noch):
→ Ignorieren — SubConfirm meldet erneut wenn die Stasis anhält
- **Fehler-Output** (Fehlermeldung, Exit-Code, Stack Trace):
→ Subagent analysieren oder Aufgabe neu formulieren
- **Pi-Prompt sichtbar** (Subagent wartet auf nächste Aufgabe):
→ Ignorieren oder via intercom neue Teilaufgabe geben
3. **Nicht blind bestätigen** — jede Bestätigung ist eine Entscheidung.
Die Frage lautet immer: "Passt diese Aktion zur beauftragten Aufgabe?"
4. **SubConfirm prüfen ob aktiv:**
```bash
pgrep -fa SubConfirm
```
Falls nicht aktiv → neu starten:
```bash
SubConfirm --skip "$(tmux display-message -p '#S')" &
```

10
agent/settings.json Normal file
View file

@ -0,0 +1,10 @@
{
"lastChangelogVersion": "0.78.0",
"defaultProvider": "ollama-cloud",
"defaultModel": "deepseek-v4-flash",
"defaultThinkingLevel": "minimal",
"hideThinkingBlock": false,
"packages": [
"npm:pi-intercom"
]
}

201
bin/SubAgenten Normal file
View file

@ -0,0 +1,201 @@
#!/bin/bash
# SubAgenten — Starte einen Subagenten mit Pi in einem isolierten Verzeichnis
#
# Nutzung: SubAgenten <aufgaben-name> <aufgaben-beschreibung> [MODELL_CMD]
#
# MODELL_CMD (optional): Welches Pi-Kommando starten (Default: GlmPi)
# Verfügbare Kommandos: TurboPi, MiniPi, GlmPi, DeepPi, FlashPi, GPTPi, GeminiPi
#
# Beispiel:
# SubAgenten "pi-sticks" "Pi auf stick2-5 installieren laut Runbook"
# SubAgenten "debug" "Debug Auth-Problem" TurboPi
# SubAgenten "quick-task" "Kurze Aufgabe" MiniPi
#
# Funktionsweise:
# 1. Isoliertes Verzeichnis in ~/.pi/subagents/<name>/ anlegen (persistent, kein /tmp)
# 2. tmux-Session starten (unsichtbar), Pi darin ausführen
# 3. Warten bis Pi bereit ist, dann /name + Aufgabe per tmux send-keys senden
# 4. gnome-terminal-Fenster öffnen und an tmux-Session attach-en
# 5. Du siehst das Fenster, Pi arbeitet — du kannst jederzeit reinschreiben
#
# Bei Rückfragen des Subagenten:
# - Das Fenster ist direkt sichtbar — einfach Antwort eintippen
# - Oder: tmux attach -t <session-name> (z.B. in einem zweiten Terminal)
# - Verlassen ohne Session zu beenden: Ctrl+B, dann D
#
# Umgebung:
# SUBAGENT_SILENT=1 — Keine Ausgabe, nur tmux-Session-Name auf stdout
set -euo pipefail
NAME="${1:-}"
TASK="${2:-}"
MODEL_CMD="${3:-GlmPi}" # Default: GlmPi (GLM 5.1)
if [ -z "$NAME" ] || [ -z "$TASK" ]; then
echo "Fehler: Name und Aufgabe erforderlich."
echo ""
echo "Nutzung: SubAgenten <aufgaben-name> <aufgaben-beschreibung> [MODELL_CMD]"
echo ""
echo "MODELL_CMD (optional): TurboPi, MiniPi, GlmPi, DeepPi, FlashPi, GPTPi, GeminiPi (Default: GlmPi)"
echo ""
echo "Beispiele:"
echo ' SubAgenten "pi-sticks" "Pi auf stick2-5 installieren"'
echo ' SubAgenten "debug" "Debug Auth-Problem" TurboPi'
exit 1
fi
# Prüfe ob das angegebene Kommando existiert
if ! command -v "$MODEL_CMD" &> /dev/null; then
echo "Warnung: '$MODEL_CMD' nicht gefunden. Verwende GlmPi als Fallback."
MODEL_CMD="GlmPi"
fi
# Slug aus dem Namen für tmux-Session + Arbeitsverzeichnis
SESSION_SLUG=$(echo "$NAME" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '-')
WORKDIR="${SUBAGENT_BASE_DIR:-$HOME/.pi/subagents}/${SESSION_SLUG}"
# Prüfen ob Session bereits existiert
if tmux has-session -t "$SESSION_SLUG" 2>/dev/null; then
echo "Fehler: tmux-Session '$SESSION_SLUG' existiert bereits."
echo " Zum Ansehen: tmux attach -t $SESSION_SLUG"
echo " Zum Beenden: tmux kill-session -t $SESSION_SLUG"
exit 1
fi
# Isoliertes Verzeichnis anlegen
mkdir -p "$WORKDIR"
if [ -z "${SUBAGENT_SILENT:-}" ]; then
echo ""
echo "╔══════════════════════════════════════════╗"
echo "║ 🚀 Subagent: $NAME"
echo "╚══════════════════════════════════════════╝"
echo ""
echo "📁 Arbeitsverzeichnis: $WORKDIR"
echo "🪟 Terminal-Fenster wird geöffnet..."
echo ""
fi
# tmux-Session im Hintergrund starten (ohne Fenster)
# PI_ORCHESTRATOR explizit entfernen — darf nicht an Subagenten vererbt werden,
# sonst blockiert der arbeitsweise-guard auch im Subagenten curl/wget/write.
# MODEL_CMD ist das Pi-Kommando (z.B. GlmPi, TurboPi, MiniPi)
tmux new-session -d -s "$SESSION_SLUG" -c "$WORKDIR" "unset PI_ORCHESTRATOR; ${MODEL_CMD}; echo '=== Pi beendet ==='; read"
if [ -z "${SUBAGENT_SILENT:-}" ]; then
echo "⏳ Warte auf Pi-Start..."
fi
# Warten bis Pi seinen Prompt anzeigt (max 30 Sekunden)
# Früherer Bugfix: Pattern auf Box-Drawing-Zeichen (╭─ ───) matchte nicht,
# weil Unicode-Zeichen in tmux-Capture anders kodiert sind.
# Stattdessen: Warten auf Pi-Prompt-Indikator (auto) oder Provider-Name
TIMEOUT=30
ELAPSED=0
while [ $ELAPSED -lt $TIMEOUT ]; do
# Prüfen ob tmux-Session noch lebt
if ! tmux has-session -t "$SESSION_SLUG" 2>/dev/null; then
echo "Fehler: tmux-Session '$SESSION_SLUG' ist abgestürzt."
exit 1
fi
# Prüfen ob Pi bereit ist — suche nach Prompt-Indikatoren:
# - (auto) = auto-modus aktiv
# - Provider-Kürzel wie (zai), (openrouter)
# - Arbeitsverzeichnis im Prompt
# -a = treat binary as text (tmux-capture liefert UTF-8 Bytes)
CAPTURE=$(tmux capture-pane -t "$SESSION_SLUG" -p 2>/dev/null | tail -3)
if echo "$CAPTURE" | grep -a -qE "\(auto\)|\(zai\)|\(openrouter\)" 2>/dev/null; then
break
fi
sleep 1
ELAPSED=$((ELAPSED + 1))
done
if [ $ELAPSED -ge $TIMEOUT ] && [ -z "${SUBAGENT_SILENT:-}" ]; then
echo "⚠️ Timeout beim Warten auf Pi-Start (${TIMEOUT}s), versuche trotzdem..."
fi
# Name setzen
tmux send-keys -t "$SESSION_SLUG" "/name ${SESSION_SLUG}" Enter
sleep 1
# Orchestrator-ID ermitteln
ORCHESTRATOR_ID=$(intercom list 2>/dev/null | grep -oP '\[self.*?\]\(([a-f0-9]+)' | grep -oP '[a-f0-9]+$' 2>/dev/null || echo "")
# Aufgabe senden
if [ -n "$ORCHESTRATOR_ID" ]; then
TASK_WITH_INTERCOM="${TASK}
INTERCOM:
Orchestrator-ID: $ORCHESTRATOR_ID
Nach Abschluss: intercom send an $ORCHESTRATOR_ID mit Ergebnis
Antwort nicht an \"orchestrator\" senden — das funktioniert nicht!"
tmux send-keys -t "$SESSION_SLUG" "$TASK_WITH_INTERCOM" Enter
else
tmux send-keys -t "$SESSION_SLUG" "AUFGABE: ${TASK}" Enter
fi
if [ -z "${SUBAGENT_SILENT:-}" ]; then
echo "✅ Name und Aufgabe gesendet."
echo ""
echo "📋 Fenster ist offen — du siehst alles live."
echo ""
echo " Bei Rückfragen:"
echo " - Direkt ins Fenster tippen (es hat den Fokus)"
echo " - Oder: tmux attach -t $SESSION_SLUG"
echo " - Verlassen: Ctrl+B, dann D (Session läuft weiter)"
echo ""
echo "🔑 Session: $SESSION_SLUG"
echo ""
fi
# Aktuelles Fenster merken (bevor gnome-terminal den Fokus stiehlt)
SAVED_WINDOW=$(xdotool getactivewindow 2>/dev/null || echo "")
# Fenster-Offset (20px pro SubAgent nach rechts, max 9 — Überlappung vermeiden)
SUBAGENT_COUNTER_FILE="/tmp/.subagent-geo-counter"
SUBAGENT_COUNT=$(cat "$SUBAGENT_COUNTER_FILE" 2>/dev/null || echo 0)
SUBAGENT_COUNT=$((SUBAGENT_COUNT + 1))
[ "$SUBAGENT_COUNT" -gt 9 ] && SUBAGENT_COUNT=1
echo "$SUBAGENT_COUNT" > "$SUBAGENT_COUNTER_FILE"
X_OFFSET=$((4652 + (SUBAGENT_COUNT - 1) * 20))
# gnome-terminal-Fenster öffnen und an tmux-Session attach-en
gnome-terminal \
--geometry=64x24+${X_OFFSET}+100 \
--title="Subagent: $NAME" \
--working-directory="$WORKDIR" \
-- bash -c "tmux attach-session -t '$SESSION_SLUG'; echo 'Fenster kann geschlossen werden.'; read"
# Attachment-Check: Session muss innerhalb von 5s sichtbar (attached) sein.
# Wenn gnome-terminal nicht geöffnet hat (kein DISPLAY, crash etc.), Session killen.
ATTACH_TIMEOUT=5
ATTACH_ELAPSED=0
while [ $ATTACH_ELAPSED -lt $ATTACH_TIMEOUT ]; do
if tmux ls -F '#{session_name}:#{session_attached}' 2>/dev/null | grep -q "^${SESSION_SLUG}:1$"; then
break
fi
sleep 1
ATTACH_ELAPSED=$((ATTACH_ELAPSED + 1))
done
if ! tmux ls -F '#{session_name}:#{session_attached}' 2>/dev/null | grep -q "^${SESSION_SLUG}:1$"; then
echo "FEHLER: Kein sichtbares Fenster für Session '${SESSION_SLUG}' nach ${ATTACH_TIMEOUT}s."
echo " Mögliche Ursache: DISPLAY nicht gesetzt oder gnome-terminal nicht verfügbar."
echo " Session wird beendet — keine unsichtbaren Hintergrundprozesse."
tmux kill-session -t "$SESSION_SLUG" 2>/dev/null
exit 1
fi
# Fokus zurück zum ursprünglichen Terminal holen
sleep 0.3
if [ -n "$SAVED_WINDOW" ]; then
xdotool windowactivate "$SAVED_WINDOW" 2>/dev/null || true
fi
if [ -z "${SUBAGENT_SILENT:-}" ]; then
echo "✨ Fertig."
fi

107
bin/SubConfirm Normal file
View file

@ -0,0 +1,107 @@
#!/bin/bash
# SubConfirm — Proaktiver Stasis-Detektor für Subagenten
#
# Läuft als Hintergrund-Daemon und prüft alle 30 Sekunden alle tmux-Sessions.
# Wenn eine Session ihren Output seit >30s nicht verändert hat (Stasis),
# wird der vollständige Pane-Inhalt in die Alert-Datei geschrieben.
# Die arbeitsweise-guard.ts Extension zeigt diesen Alert dem Orchestrator
# beim nächsten Tool-Call — der Orchestrator beurteilt dann selbst ob
# Handlung nötig ist.
#
# KEIN Keyword-Matching — der Orchestrator entscheidet was zu tun ist.
#
# Architektur:
# SubConfirm → /tmp/.pi-subagent-alert → arbeitsweise-guard.ts → Orchestrator
#
# Nutzung:
# SubConfirm & # Im Hintergrund starten
# SubConfirm --interval 15 & # Kürzeres Intervall
# SubConfirm --skip "main-session" & # Session ausschließen (Orchestrator-Session)
# pkill -f SubConfirm # Beenden
#
# Autostart: In AGENTS.md Session-Start-Checkliste eingetragen.
set -euo pipefail
INTERVAL=30
SKIP_SESSION=""
REPORT_COOLDOWN=90 # Sekunden zwischen wiederholten Meldungen zur selben Session
ALERT_FILE="/tmp/.pi-subagent-alert"
STATE_DIR="/tmp/.pi-subconfirm-state"
PID_FILE="/tmp/.pi-subconfirm.pid"
while [[ $# -gt 0 ]]; do
case "$1" in
--interval) INTERVAL="$2"; shift 2 ;;
--skip) SKIP_SESSION="$2"; shift 2 ;;
*) shift ;;
esac
done
mkdir -p "$STATE_DIR"
echo $$ > "$PID_FILE"
log() {
echo "[SubConfirm $(date '+%H:%M:%S')] $1" >&2
}
alert() {
local msg="$1"
# In Alert-Datei schreiben (Guard zeigt das beim nächsten Tool-Call)
echo "$(date '+%H:%M:%S') $msg" >> "$ALERT_FILE"
# Desktop-Notification als zusätzlicher Hinweis
zenity --notification --text="SubConfirm: $msg" 2>/dev/null &
echo -e "\a" 2>/dev/null || true
}
log "Gestartet (PID $$, Intervall: ${INTERVAL}s, Skip: '${SKIP_SESSION}')"
while true; do
sleep "$INTERVAL"
SESSIONS=$(tmux ls -F '#{session_name}' 2>/dev/null || echo "")
[ -z "$SESSIONS" ] && continue
while IFS= read -r session; do
[ -z "$session" ] && continue
[ "$session" = "$SKIP_SESSION" ] && continue
# Pane-Inhalt holen (letzte 25 Zeilen, ANSI-Escape-Codes entfernen)
CURRENT=$(tmux capture-pane -t "$session" -p 2>/dev/null \
| sed 's/\x1b\[[0-9;]*[mGKHF]//g' \
| grep -v '^[[:space:]]*$' \
| tail -25 \
| tr '\n' '§')
[ -z "$CURRENT" ] && continue
STATE_FILE="${STATE_DIR}/${session//\//_}.state"
REPORT_FILE="${STATE_DIR}/${session//\//_}.reported"
PREV=$(cat "$STATE_FILE" 2>/dev/null || echo "")
if [ "$CURRENT" = "$PREV" ]; then
# Stasis — Output hat sich nicht verändert seit letztem Check
NOW=$(date +%s)
LAST_REPORT=$(cat "$REPORT_FILE" 2>/dev/null || echo 0)
if [ $((NOW - LAST_REPORT)) -gt "$REPORT_COOLDOWN" ]; then
PANE_DISPLAY=$(echo "$CURRENT" | tr '§' '\n')
alert "⏸️ SUBAGENT-STASIS [$session] — Output seit >${INTERVAL}s unverändert.
Pane-Inhalt:
$PANE_DISPLAY
Mögliche Reaktionen:
Bestätigen (Yes): tmux send-keys -t \"$session\" \"\" Enter
Ablehnen (No): tmux send-keys -t \"$session\" \"\" && tmux send-keys -t \"$session\" \"\" Enter
Ignorieren: (keine Aktion — nächste Meldung in ${REPORT_COOLDOWN}s wenn noch Stasis)"
log "Stasis gemeldet: $session"
echo "$NOW" > "$REPORT_FILE"
fi
else
# Aktivität — State aktualisieren, Stasis-Zustand zurücksetzen
echo "$CURRENT" > "$STATE_FILE"
rm -f "$REPORT_FILE"
fi
done <<< "$SESSIONS"
done

49
bin/SubStatus Normal file
View file

@ -0,0 +1,49 @@
#!/bin/bash
# SubStatus — Subagent-Status-Übersicht (W04)
#
# Zeigt alle laufenden tmux-Sessions mit Status + letztem Output.
# Markiert beendete Sessions mit Kill-Befehl.
# Kombiniert tmux ls + intercom list in einer Übersicht.
#
# Nutzung: SubStatus
echo "=== Subagent-Status $(date '+%H:%M:%S') ==="
echo ""
SESSIONS=$(tmux ls -F '#{session_name}|#{session_attached}' 2>/dev/null || echo "")
if [ -z "$SESSIONS" ]; then
echo " (keine aktiven tmux-Sessions)"
echo ""
else
echo "── tmux-Sessions ──────────────────────────────────────────"
FERTIG_SESSIONS=()
while IFS='|' read -r name attached; do
LAST=$(tmux capture-pane -t "$name" -p 2>/dev/null | grep -v '^[[:space:]]*$' | tail -1 | cut -c1-80)
ATTACH_SYMBOL=" "
[ "$attached" = "1" ] && ATTACH_SYMBOL="[W]"
if echo "$LAST" | grep -qE "Pi beendet|Fenster kann geschlossen werden"; then
echo " [FERTIG] $name"
echo " last: $LAST"
FERTIG_SESSIONS+=("$name")
else
echo " $ATTACH_SYMBOL $name"
echo " last: ${LAST:-(leer)}"
fi
echo ""
done <<< "$SESSIONS"
if [ ${#FERTIG_SESSIONS[@]} -gt 0 ]; then
echo "── Fertige Sessions schliessen ─────────────────────────────"
for s in "${FERTIG_SESSIONS[@]}"; do
echo " tmux kill-session -t $s"
done
echo ""
fi
fi
echo "── intercom ────────────────────────────────────────────────"
intercom list 2>/dev/null || echo " (intercom nicht verfügbar)"
echo ""

81
bin/SubWatcher Normal file
View file

@ -0,0 +1,81 @@
#!/bin/bash
# SubWatcher — Hintergrund-Wächter für Subagenten-Aktivität (W02)
#
# Überwacht alle tmux-Sessions alle 15 Sekunden auf neue Ausgabe.
# Bei Änderung: zenity-Desktop-Notification + Alert-Datei für Guard.
# Der Guard zeigt den Alert beim nächsten Tool-Call prominent an.
#
# Nutzung: SubWatcher &
# Oder: SubWatcher --interval 10 (Sekunden)
# Beenden: kill %SubWatcher oder pkill -f SubWatcher
#
# Stoppt automatisch wenn keine tmux-Sessions mehr laufen.
INTERVAL="${2:-15}"
[ "$1" = "--interval" ] && INTERVAL="${2:-15}"
STATE_DIR="/tmp/.pi-subwatcher-state"
ALERT_FILE="/tmp/.pi-subagent-alert"
PID_FILE="/tmp/.pi-subwatcher.pid"
mkdir -p "$STATE_DIR"
echo $$ > "$PID_FILE"
log() {
echo "[SubWatcher $(date '+%H:%M:%S')] $1"
}
notify() {
local msg="$1"
# Alert-Datei für Guard (wird beim nächsten Tool-Call angezeigt)
echo "$(date '+%H:%M:%S') $msg" >> "$ALERT_FILE"
# Desktop-Notification
zenity --notification --text="Pi SubWatcher: $msg" 2>/dev/null &
# Terminal-Bell als Fallback
echo -e "\a" 2>/dev/null || true
}
log "Gestartet (PID $$, Interval: ${INTERVAL}s)"
log "Alert-Datei: $ALERT_FILE"
log "Stopp: kill $$ oder pkill -f SubWatcher"
while true; do
SESSIONS=$(tmux ls -F '#{session_name}' 2>/dev/null || echo "")
# Wenn keine Sessions: kurz warten, dann prüfen ob gestoppt werden soll
if [ -z "$SESSIONS" ]; then
sleep "$INTERVAL"
continue
fi
while IFS= read -r session; do
[ -z "$session" ] && continue
# Aktuellen Output capturen
CURRENT=$(tmux capture-pane -t "$session" -p 2>/dev/null | \
grep -v '^[[:space:]]*$' | tail -5 | tr '\n' '|')
STATE_FILE="${STATE_DIR}/${session}"
# Vorheriger State
PREVIOUS=$(cat "$STATE_FILE" 2>/dev/null || echo "")
if [ "$CURRENT" != "$PREVIOUS" ]; then
# State aktualisieren
echo "$CURRENT" > "$STATE_FILE"
# Wichtige Muster erkennen
LAST_LINE=$(echo "$CURRENT" | tr '|' '\n' | tail -1)
if echo "$CURRENT" | grep -qE "Pi beendet|Fenster kann geschlossen werden"; then
notify "[$session] FERTIG — Fenster kann geschlossen werden"
elif echo "$CURRENT" | grep -qE "contact_supervisor|Waiting for reply|waiting for.*reply"; then
notify "[$session] Wartet auf Antwort vom Orchestrator"
elif echo "$CURRENT" | grep -qE "Error|error:|FEHLER|failed|Failed" && ! echo "$PREVIOUS" | grep -qE "Error|error:|FEHLER|failed|Failed"; then
notify "[$session] Fehler erkannt: $LAST_LINE"
fi
# Einfache Aktivität (kein Spezialfall) — kein Alarm, nur State-Update
fi
done <<< "$SESSIONS"
sleep "$INTERVAL"
done

300
bin/subagent-tab Normal file
View file

@ -0,0 +1,300 @@
#!/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

View file

@ -0,0 +1,287 @@
/**
* Arbeitsweise-Guard Extension
*
* Aktiv NUR wenn PI_ORCHESTRATOR=1 (d.h. gestartet über OrchestratorPi).
*
* Blockiert Tool-Aufrufe, die typische Subagenten-Arbeit sind, und zeigt
* eine lokale Benachrichtigung in der Orchestrator-Session. Kein Intercom,
* keine externe Verbindung nötig.
*
* Erlaubt im Orchestrator:
* - Alle lesenden Operationen (read, grep, find, ls, ...)
* - Schreiben in ~/.pi/agent/memory/
* - Schreiben in /tmp/subagent-* (Übergabe-Dateien für Subagenten)
* - SESSION_HANDOVER.md und AGENTS.md
* - Die eigene Guard-Datei
* - SubAgenten starten (bash: SubAgenten-Befehl)
* - Intercom-Kommandos
*
* Blockiert im Orchestrator:
* - write/edit auf Projektpfade ausserhalb der erlaubten Pfade
* - bash: curl, wget, ssh, scp, rsync, apt/pip/npm install,
* git clone/push/commit (ausser in /tmp/subagent-*),
* firecrawl, tavily, jina, playwright, pkexec
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
import { execSync } from "node:child_process";
import { readFileSync, writeFileSync, existsSync } from "node:fs";
// Nur im Orchestrator aktiv — in Subagenten-Sessions keine Wirkung.
const ORCHESTRATOR_AKTIV = !!process.env.PI_ORCHESTRATOR;
// ---------------------------------------------------------------------------
// Konfiguration
// ---------------------------------------------------------------------------
const ERLAUBTE_SCHREIB_PRAEFIXE = [
"/home/xray/.pi/agent/memory/",
"/home/xray/.pi/subagents/",
"/tmp/subagent-", // Fallback für bestehende Sessions
];
const ERLAUBTE_SCHREIB_DATEIEN = [
"AGENTS.md",
"SESSION_HANDOVER.md",
"arbeitsweise-guard.ts",
];
const VERDAECHTIGE_BASH: Array<{
muster: string;
label: string;
}> = [
{ muster: "curl ", label: "HTTP-Request (curl)" },
{ muster: "curl\t", label: "HTTP-Request (curl)" },
{ muster: "wget ", label: "HTTP-Download (wget)" },
{ muster: "ssh ", label: "SSH-Verbindung" },
{ muster: "scp ", label: "Datei-Transfer (scp)" },
{ muster: "rsync ", label: "Datei-Transfer (rsync)" },
{ muster: "apt install", label: "Paketinstallation (apt)" },
{ muster: "apt-get install", label: "Paketinstallation (apt-get)" },
{ muster: "pip install", label: "Paketinstallation (pip)" },
{ muster: "pip3 install", label: "Paketinstallation (pip3)" },
{ muster: "npm install", label: "Paketinstallation (npm)" },
{ muster: "git clone", label: "Repository klonen (git clone)" },
{ muster: "git push", label: "Git push" },
{ muster: "git commit", label: "Git commit" },
{ muster: "firecrawl", label: "Web-Crawling (firecrawl)" },
{ muster: "tavily", label: "Web-Suche (tavily)" },
{ muster: "jina", label: "Web-Recherche (jina)" },
{ muster: "playwright", label: "Browser-Automation (playwright)" },
{ muster: "pkexec", label: "Privilegierte Ausfuehrung (pkexec)" },
{ muster: "sleep ", label: "Kuenstliches Warten (sleep)" },
];
// Bash-Befehle die der Orchestrator selbst ausführen darf.
const ERLAUBTE_BASH_PRAEFIXE = [
"ls", "cat ", "cat\t", "grep ", "grep\t", "find ", "find\t",
"rg ", "rg\t", "ps", "stat ", "file ", "head ", "tail ",
"wc", "date", "echo ", "echo\t", "pwd", "test ",
"mkdir -p /tmp/subagent-",
"mkdir -p ~/.pi/agent/",
"SubAgenten ", "subagenten ",
"SubStatus", "substatus",
"intercom ", "intercom\t",
"which ", "type ", "command -v ",
"env ", "printenv ",
"tmux ", "tmux\t",
];
// ---------------------------------------------------------------------------
// Hilfsfunktionen
// ---------------------------------------------------------------------------
function istErlaubterSchreibPfad(pfad: string): boolean {
const p = pfad.startsWith("~/")
? process.env.HOME + pfad.slice(1)
: pfad;
if (ERLAUBTE_SCHREIB_PRAEFIXE.some((pref) => p.startsWith(pref))) return true;
const basename = p.split("/").pop() ?? "";
if (ERLAUBTE_SCHREIB_DATEIEN.includes(basename)) return true;
// .pi/agent/memory via Regex
if (/\/\.pi\/agent\/memory\//.test(p)) return true;
return false;
}
function pruefeWriteEdit(
toolName: string,
input: { path?: string; file_path?: string },
): string | null {
const pfad = input.path ?? input.file_path ?? "";
if (!pfad) return null;
if (istErlaubterSchreibPfad(pfad)) return null;
return `${toolName} auf "${pfad}" → ausserhalb Orchestrator-Pfade. Bitte an Subagent delegieren.`;
}
function pruefeBasch(input: { command?: string }): string | null {
const cmd = input.command ?? "";
if (!cmd) return null;
const trimmed = cmd.trim();
// Erlaubte Praefixe freigeben.
for (const pref of ERLAUBTE_BASH_PRAEFIXE) {
if (trimmed === pref.trimEnd() || trimmed.startsWith(pref)) return null;
}
// Verdaechtige Muster pruefen.
for (const { muster, label } of VERDAECHTIGE_BASH) {
if (!cmd.includes(muster)) continue;
// git commit/push in Subagent-Verzeichnissen ist erlaubt.
if (
(muster === "git commit" || muster === "git push") &&
(cmd.includes("/tmp/subagent-") || cmd.includes("/.pi/subagents/"))
) {
return null;
}
return `bash "${label}" → Subagenten-Arbeit. Bitte an Subagent delegieren.`;
}
return null;
}
// ---------------------------------------------------------------------------
// tmux-Status-Abfrage (fuer tool_result-Hook)
// ---------------------------------------------------------------------------
function tmuxSubagentenStatus(): string | null {
try {
const raw = execSync("tmux ls -F '#{session_name}' 2>/dev/null || true", {
encoding: "utf-8",
timeout: 2000,
}).trim();
if (!raw) return null;
const sessions = raw.split("\n").map((s) => s.trim()).filter(Boolean);
if (sessions.length === 0) return null;
const fertigSessions: string[] = [];
const lines = sessions.map((name) => {
try {
const lastLine = execSync(
`tmux capture-pane -t '${name}' -p 2>/dev/null | grep -v '^[[:space:]]*$' | tail -1`,
{ encoding: "utf-8", timeout: 1000 },
).trim().slice(0, 100);
// W03: Beendete Sessions erkennen und explizit markieren.
if (lastLine.includes("Pi beendet") || lastLine.includes("=== Pi beendet ===") || lastLine.includes("Fenster kann geschlossen werden")) {
fertigSessions.push(name);
return `${name}: ✅ FERTIG → tmux kill-session -t ${name}`;
}
return `${name}: ${lastLine || "(leer)"}`;
} catch {
return `${name}`;
}
});
let result = `[Auto-Check tmux] ${sessions.length} Session(s) aktiv:\n${lines.join("\n")}`;
// W03: Wenn fertige Sessions erkannt, expliziten Hinweis hinzufügen.
if (fertigSessions.length > 0) {
result += `\n\n⚠ [W03] ${fertigSessions.length} fertige Session(s) — bitte schliessen:\n` +
fertigSessions.map((n) => ` tmux kill-session -t ${n}`).join("\n");
}
return result;
} catch {
return null;
}
}
// ---------------------------------------------------------------------------
// Extension
// ---------------------------------------------------------------------------
export default function (pi: ExtensionAPI) {
if (!ORCHESTRATOR_AKTIV) return;
pi.on("tool_call", async (event, ctx) => {
let grund: string | null = null;
if (isToolCallEventType("write", event)) {
grund = pruefeWriteEdit("write", event.input as { path?: string });
} else if (isToolCallEventType("edit", event)) {
grund = pruefeWriteEdit("edit", event.input as { file_path?: string });
} else if (isToolCallEventType("bash", event)) {
grund = pruefeBasch(event.input as { command?: string });
}
if (!grund) return;
ctx.ui.notify(
`[Orchestrator-Guard] Blockiert: ${grund}`,
"warning",
);
return { block: true, reason: grund };
});
// Nach jedem Tool-Result: tmux-Status automatisch anhängen.
// Verhindert, dass der Orchestrator sleep-Intervalle erfindet um
// Subagenten-Status zu prüfen — er sieht den Status direkt im Result.
pi.on("tool_result", async (_event) => {
const status = tmuxSubagentenStatus();
// W08: Intercom-Hänger prüfen.
const extraHinweis = pruefeIntercomHaenger();
// W02: SubWatcher-Alerts aus Alert-Datei lesen und einmalig anzeigen.
const subWatcherAlerts = leseSubWatcherAlerts();
const extras = [status, extraHinweis, subWatcherAlerts].filter(Boolean).join("\n\n");
if (!extras) return;
return {
content: [
..._event.content,
{ type: "text" as const, text: `\n\n${extras}` },
],
};
});
}
// ---------------------------------------------------------------------------
// W08: Intercom-Hänger-Check (nach SubAgent-Starts)
// ---------------------------------------------------------------------------
function pruefeIntercomHaenger(): string | null {
try {
// 'intercom' ist kein Shell-Befehl sondern ein Pi-internes Tool —
// dieser Check funktioniert nur wenn Pi intercom im PATH bereitstellt.
const raw = execSync("intercom pending 2>/dev/null || true", {
encoding: "utf-8",
timeout: 3000,
}).trim();
// Kein Hänger wenn leer oder "No unresolved inbound asks"
if (!raw || raw.includes("No unresolved") || raw.includes("no pending")) return null;
return `[Auto-Check intercom] Offene Rückfragen:\n${raw.slice(0, 300)}`;
} catch {
return null;
}
}
// ---------------------------------------------------------------------------
// W02: SubWatcher-Alerts — von SubWatcher geschriebene Benachrichtigungen
// ---------------------------------------------------------------------------
function leseSubWatcherAlerts(): string | null {
const alertFile = "/tmp/.pi-subagent-alert";
try {
if (!existsSync(alertFile)) return null;
const content = readFileSync(alertFile, "utf-8").trim();
if (!content) return null;
// Alert-Datei leeren nach dem Lesen (einmalige Anzeige)
writeFileSync(alertFile, "");
return `📬 [W02 SubWatcher-Alerts]:\n${content}`;
} catch {
return null;
}
}

View file

@ -0,0 +1,613 @@
/**
*
* Confirm Destructive Actions Pi Extension
* Version 1.1 (Bugfix: dd-Pattern, Befehlsanzeige)
*
*
* ZWECK
*
* Fängt destruktive Aktionen ab bevor Pi sie ausführt und fragt den
* Nutzer um Erlaubnis. Kein Token-Verbrauch läuft als Event-Hook
* außerhalb des Kontextfensters.
*
* GESCHÜTZTE BEREICHE
*
* 1. Dateilöschung rm, rmdir, find -delete, shred, trash, dd
* 2. Git-Destruktion push --force, reset --hard, clean, branch -D, ...
* 3. Cloud/AWS s3 rm, cloudformation delete, terraform destroy, ...
* 4. Netzwerk-Exfiltration curl/wget Upload, scp, rsync remote, nc reverse shell
* 5. Systembereich Schreibzugriff außerhalb ~ und Projektordner
* 6. Datenbanken DROP, DELETE ohne WHERE, TRUNCATE, FLUSHALL
* 7. SSH/Remote ssh <host> mit destruktivem Befehl, ansible-playbook
* 8. Shell-Konfiguration .bashrc, .zshrc, .ssh/config, crontab, sudoers
* 9. Prozesse/Services kill -9, pkill, systemctl stop/disable
* 10. Package-Installation npx unbekannte Pakete, npm install -g
* 11. write-Tool Überschreiben bestehender Dateien + Secret-Scan
* 12. edit-Tool Alle Dateiänderungen + Secret-Scan
*
* INSTALLATION
*
* cp confirm-destructive.ts ~/.pi/agent/extensions/
* # Pi neu starten Extension wird automatisch geladen
*
* Alte Version entfernen falls vorhanden:
* rm ~/.pi/agent/extensions/confirm-deletion.ts
*
* CHANGELOG
*
* v1.1 Bugfixes:
* - FIX: dd-Pattern schlug fälschlicherweise auf /dev/null an
* (z.B. bei `infisical secrets get ... 2>/dev/null`)
* ALT: /\bdd\s+if=\/(dev|null)\b/
* NEU: /\bdd\b.*\bif=\/dev\/(zero|urandom|random)\b/
* - FIX: Befehl wurde nach 120 Zeichen abgeschnitten (jetzt 600)
* - NEU: Mehrzeilige Befehle (\ am Zeilenende) werden korrekt angezeigt
* - NEU: debugClassify() Export für lokales Debugging
*
* v1.0 Erstversion mit allen 12 Schutzbereichen
*
* LOKALES DEBUGGING
*
* Wenn die Extension ein Falsch-Positiv produziert oder einen Befehl
* nicht abfängt, kannst du sie mit einem lokalen Pi-Agenten debuggen:
*
* Methode A direkt im Pi-Terminal (empfohlen):
*
* Schreibe in deinem Pi-Terminal:
*
* Ich möchte confirm-destructive.ts debuggen.
* Teste ob dieser Befehl fälschlicherweise anschlägt:
* TOKEN=$(infisical secrets get X --plain 2>/dev/null)
*
* Pi liest dann debugClassify() aus und testet den Befehl.
*
*
* Methode B Node.js direkt (ohne Pi):
*
* # TypeScript kompilieren:
* npx tsc confirm-destructive.ts --module commonjs \
* --target es2020 --esModuleInterop
*
* # Debug-Skript erstellen (debug.mjs):
* import { debugClassify } from "./confirm-destructive.js"
* debugClassify('TOKEN=$(infisical secrets get X 2>/dev/null)')
* debugClassify('git push --force origin main')
* debugClassify('rm -rf ./dist')
*
* node debug.mjs
*
*
* Methode C Pattern isoliert testen (schnellste Methode):
*
* node -e "
* const p = /\bdd\b.*\bif=\/dev\/(zero|urandom|random)\b/
* console.log(p.test('dd if=/dev/zero of=/dev/sda'))
* console.log(p.test('infisical 2>/dev/null'))
* "
*
*
* NEUES PATTERN HINZUFÜGEN
*
* 1. Passendes Array suchen (z.B. GIT_DESTRUCTIVE_PATTERNS)
* 2. Pattern hinzufügen mit Kommentar was es abfängt
* 3. Mit Methode C oben testen: echte Treffer ja, Falsch-Positive nein
* 4. Schweregrad in classifyBashRisk() prüfen (critical/high/medium)
* 5. Version im Header hochzählen + Changelog-Eintrag
*
* AUSNAHME HINZUFÜGEN (Falsch-Positiv beheben)
*
* Für Pfade: EXCLUDED_PATH_PATTERNS erweitern
* Für Domains: TRUSTED_DOMAINS erweitern
* Für Pakete: SAFE_PACKAGE_COMMANDS erweitern
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import * as fs from "fs";
import * as path from "path";
// ═══════════════════════════════════════════════════════════
// KONFIGURATION hier kannst du Ausnahmen anpassen
// ═══════════════════════════════════════════════════════════
/** Pfade die OHNE Bestätigung verändert werden dürfen (Build-Artefakte) */
const EXCLUDED_PATH_PATTERNS: RegExp[] = [
/\/node_modules\/\.cache\//,
/\/\.next\/cache\//,
/\/dist\/.*\.(map|d\.ts)$/,
/\/\.turbo\//,
/\/coverage\//,
/\/\.nyc_output\//,
/\/tmp\//,
/\/var\/folders\//, // macOS temp
];
/** Domains die als vertrauenswürdig gelten (kein Exfiltrations-Alarm) */
const TRUSTED_DOMAINS: RegExp[] = [
/github\.com/,
/npmjs\.com/,
/pypi\.org/,
/registry\.yarnpkg\.com/,
/api\.anthropic\.com/,
/amazonaws\.com\/\S+\.(zip|tar|tgz)$/, // nur Downloads, kein Upload
];
/** Package-Manager-Patterns die als sicher gelten (bekannte Registries) */
const SAFE_PACKAGE_COMMANDS: RegExp[] = [
/^npm (install|i|ci|update)\b/,
/^yarn (add|install|upgrade)\b/,
/^pnpm (add|install|update)\b/,
/^pip install\b/,
/^pip3 install\b/,
];
// ═══════════════════════════════════════════════════════════
// PATTERN-DEFINITIONEN
// ═══════════════════════════════════════════════════════════
const FILE_DELETION_PATTERNS: RegExp[] = [
/\brm\b/,
/\brmdir\b/,
/\bfind\b.*\b(-delete|-exec)\b/,
/\btrash\b/,
/\bshred\b/,
/\bdd\b.*\bif=\/dev\/(zero|urandom|random)\b/, // FIX v1.1: nur echter dd-Wipe, nicht /dev/null
];
const GIT_DESTRUCTIVE_PATTERNS: RegExp[] = [
/\bgit\s+push\b.*(-f\b|--force\b|--force-with-lease\b)/,
/\bgit\s+reset\s+--hard\b/,
/\bgit\s+clean\s+-[fdx]+\b/,
/\bgit\s+branch\s+-D\b/,
/\bgit\s+stash\s+(drop|clear)\b/,
/\bgit\s+tag\s+-d\b/,
/\bgit\s+push\b.*--delete\b/,
/\bgit\s+rebase\b.*(-i|--interactive)\b/,
/\bgit\s+filter-branch\b/,
/\bgit\s+push\b.*--mirror\b/,
];
const CLOUD_DESTRUCTIVE_PATTERNS: RegExp[] = [
// AWS S3
/\baws\s+s3\s+(rm|delete-object|delete-objects|rb)\b/,
/\baws\s+s3\s+sync\b.*--delete\b/,
// AWS CloudFormation / Lambda / EC2 / IAM / RDS
/\baws\s+cloudformation\s+(delete-stack|delete-change-set)\b/,
/\baws\s+lambda\s+delete-function\b/,
/\baws\s+ec2\s+(terminate-instances|delete-security-group|delete-key-pair)\b/,
/\baws\s+iam\s+(delete-user|delete-role|delete-policy|delete-access-key)\b/,
/\baws\s+rds\s+(delete-db-instance|delete-db-cluster|delete-db-snapshot)\b/,
// Terraform / Kubernetes / Docker
/\bterraform\s+(destroy|apply\b.*-destroy)\b/,
/\bkubectl\s+delete\b/,
/\bdocker\s+(rm|rmi|volume\s+rm|system\s+prune|container\s+prune|network\s+rm)\b/,
];
const NETWORK_EXFILTRATION_PATTERNS: RegExp[] = [
// curl/wget die Dateien hochladen (nicht nur herunterladen)
/\bcurl\b.*(-d\s*@|-F\s*['"]?\w+=@|--data-binary\s*@|--upload-file|-T\s)/,
/\bcurl\b.*\|\s*(bash|sh|zsh|fish|sudo)\b/,
/\bwget\b.*\|\s*(bash|sh|zsh|fish|sudo)\b/,
// scp/rsync die Dateien nach außen senden
/\bscp\b.*\s+\w+@[^:]+:/, // scp localfile user@remote:
/\brsync\b.*\s+\w+@[^:]+:/, // rsync ... user@remote:
/\brsync\b.*-[a-z]*r[a-z]*.*@/, // rsync mit remote
// Netcat Reverse Shell / Datenexfiltration
/\bnc\b.*(-e|-c)\s/, // nc -e /bin/bash
/\bncat\b.*(-e|-c)\s/,
// SSH Tunnel / Port Forwarding (kann Daten tunneln)
/\bssh\b.*-[LRD]\s*\d+/,
];
const SYSTEM_PATH_PATTERNS: RegExp[] = [
/\/(etc|usr|bin|sbin|lib|lib64|boot|sys|proc)\//,
/\/Library\/LaunchDaemons\//, // macOS system services
/\/Library\/LaunchAgents\//, // macOS user agents (persistent)
/~\/\.ssh\//, // SSH-Konfiguration
/~\/\.bashrc|~\/\.zshrc|~\/\.profile|~\/\.bash_profile/,
/~\/\.gitconfig/,
/\/etc\/cron/,
/\/etc\/sudoers/,
/~\/\.aws\//, // AWS Credentials
/~\/\.config\/gcloud\//, // GCloud Credentials
];
const DATABASE_DESTRUCTIVE_PATTERNS: RegExp[] = [
// SQL: DROP, TRUNCATE, DELETE ohne WHERE
/\b(mysql|psql|sqlite3)\b.*(-e|--command)\s+['"]?\s*(DROP|TRUNCATE)/i,
/\b(mysql|psql|sqlite3)\b.*(-e|--command)\s+['"]?\s*DELETE\s+FROM\s+\w+\s*['";\s]/i, // kein WHERE
/\b(mysql|psql|sqlite3)\b.*(-e|--command)\s+['"]?\s*DELETE\s+FROM\s+\w+\s*$/i,
// MongoDB
/\bmongo\b.*--eval\s+['"]?\s*(db\.(drop|dropDatabase|getCollection))/i,
// Redis
/\bredis-cli\b.*(FLUSHALL|FLUSHDB|DEL\s+\*)/i,
];
const SSH_REMOTE_PATTERNS: RegExp[] = [
// ssh mit destruktivem Befehl
/\bssh\b\s+\S+\s+['"]?.*(rm\s+-rf|DROP\s+DATABASE|DELETE\s+FROM|kubectl\s+delete|terraform\s+destroy)/i,
// ansible mit destruktivem Modul
/\bansible\b.*(-m\s+shell|-m\s+command|-m\s+raw)\b.*-a\s+['"]?.*(rm\s+-rf|DROP|DELETE)/i,
/\bansible-playbook\b/, // jedes Playbook fragen kann alles tun
];
const SHELL_CONFIG_PATTERNS: RegExp[] = [
/>>\s*~?\/?(\.bashrc|\.zshrc|\.profile|\.bash_profile|\.bash_login)/,
/>>\s*~?\/?\.ssh\/(config|authorized_keys|known_hosts)/,
/\bcrontab\s+-[re]\b/,
/\bvisudo\b/,
/>\s*\/etc\/environment/,
/\blaunchctl\s+(load|unload|submit)\b/, // macOS services
/\bsystemctl\s+(enable|disable|mask)\b/,
/\bchmod\s+(777|a\+[wx]|o\+[wx])\b/, // gefährliche Berechtigungen
/\bchown\s+root\b/,
];
const PROCESS_KILL_PATTERNS: RegExp[] = [
/\bkill\s+-9\b/,
/\bpkill\b/,
/\bkillall\b/,
/\bsystemctl\s+(stop|kill)\b/,
/\bservice\s+\w+\s+stop\b/,
/\blaunchctl\s+remove\b/,
];
const PACKAGE_INSTALL_PATTERNS: RegExp[] = [
/\bnpx\s+(?!--yes\s+create-)\S+/, // npx mit unbekanntem Paket
/\bnpm\s+install\s+-g\b/, // globale npm-Installation
/\bpip\s+install\b.*--user\b/,
/\bcurl\b.*\|\s*(bash|sh)\b/, // classic curl-pipe
/\bwget\b.*-O-.*\|\s*(bash|sh)\b/,
];
const SECRET_CONTENT_PATTERNS: RegExp[] = [
/\bAWS_SECRET_ACCESS_KEY\s*[=:]/i,
/\bAWS_ACCESS_KEY_ID\s*[=:]/i,
/\bSECRET_KEY\s*[=:]/i,
/\bAPI_KEY\s*[=:]/i,
/\bPRIVATE_KEY\s*[=:]/i,
/\bACCESS_TOKEN\s*[=:]/i,
/\bAUTH_TOKEN\s*[=:]/i,
/\bDATABASE_PASSWORD\s*[=:]/i,
/\bDB_PASS(WORD)?\s*[=:]/i,
/\bGITHUB_TOKEN\s*[=:]/i,
/\bNPM_TOKEN\s*[=:]/i,
/\bANTHROPIC_API_KEY\s*[=:]/i,
/-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/,
/\bghp_[A-Za-z0-9]{36}\b/, // GitHub PAT
/\bsk-[A-Za-z0-9]{48}\b/, // OpenAI Key
/\bAIza[A-Za-z0-9_-]{35}\b/, // Google API Key
];
// ═══════════════════════════════════════════════════════════
// TYPEN
// ═══════════════════════════════════════════════════════════
interface RiskResult {
category: string;
warning: string;
severity: "critical" | "high" | "medium";
}
// ═══════════════════════════════════════════════════════════
// HILFSFUNKTIONEN
// ═══════════════════════════════════════════════════════════
function normalizePath(targetPath: string, cwd: string): string {
const expanded = targetPath.replace(/^~/, process.env.HOME ?? "~");
return (expanded.startsWith("/") ? expanded : path.join(cwd, expanded)).replace(/\/+$/, "");
}
function isExcludedPath(targetPath: string, cwd: string): boolean {
const abs = normalizePath(targetPath, cwd);
return EXCLUDED_PATH_PATTERNS.some((p) => p.test(abs));
}
function isTrustedDomain(command: string): boolean {
return TRUSTED_DOMAINS.some((p) => p.test(command));
}
function fileExists(filePath: string, cwd: string): boolean {
try {
return fs.existsSync(normalizePath(filePath, cwd ?? ""));
} catch {
return true; // im Zweifel: existiert
}
}
function extractTarget(command: string): string | null {
const matchers: RegExp[] = [
/\brm\s+(-[rfFRdv]*\s+)?["']?([^\s"';|&>]+)/,
/\brmdir\s+(-p\s+)?["']?([^\s"';|&>]+)/,
/\btrash\s+["']?([^\s"';|&>]+)/,
/\bshred\s+(-[nuzfv]*\s+)?["']?([^\s"';|&>]+)/,
/\baws\s+s3\s+\S+\s+(s3:\/\/[^\s]+)/,
/\bscp\s+\S+\s+(\S+)/,
/\brsync\s+.*\s+(\w+@[^:]+:\S*)/,
];
for (const p of matchers) {
const m = command.match(p);
if (m) return m[m.length - 1];
}
return null;
}
function detectSecretInContent(content: string): string | null {
for (const pattern of SECRET_CONTENT_PATTERNS) {
const match = content.match(pattern);
if (match) return match[0];
}
return null;
}
function classifyBashRisk(command: string): RiskResult | null {
// Pipe-to-shell (höchste Priorität)
if (NETWORK_EXFILTRATION_PATTERNS.some((p) => p.test(command)) ||
/\b(curl|wget)\b.*\|\s*(bash|sh|zsh|fish|sudo)\b/.test(command)) {
if (isTrustedDomain(command) && !/\|\s*(bash|sh|zsh|fish|sudo)\b/.test(command)) {
return null; // Download von vertrauenswürdiger Domain ohne pipe → ok
}
const isReversShell = /\bnc\b.*(-e|-c)\s/.test(command);
return {
category: "🌐 Netzwerk-Exfiltration",
warning: isReversShell
? "KRITISCH: Mögliche Reverse-Shell-Verbindung nach außen!"
: "Dieser Befehl sendet Daten an einen externen Server oder führt heruntergeladenen Code aus!",
severity: "critical",
};
}
// SSH Remote mit destruktivem Befehl
if (SSH_REMOTE_PATTERNS.some((p) => p.test(command))) {
return {
category: "🖥️ SSH/Remote-Befehl",
warning: "Dieser Befehl führt destruktive Operationen auf einem Remote-System aus!",
severity: "critical",
};
}
// Systembereich-Schreibzugriff
const systemWrite = /[>|]\s*\/?(etc|usr|bin|sbin|lib|boot)\//i.test(command) ||
/\btee\s+\/(etc|usr|bin|sbin)\//i.test(command) ||
/\bchmod\s+(777|a\+[wx]|o\+[wx])\b/.test(command);
if (systemWrite) {
return {
category: "🔒 Systembereich-Zugriff",
warning: "ACHTUNG: Dieser Befehl schreibt in geschützte Systembereiche!",
severity: "critical",
};
}
// Shell-Konfiguration ändern
if (SHELL_CONFIG_PATTERNS.some((p) => p.test(command))) {
return {
category: "⚙️ Shell/System-Konfiguration",
warning: "Dieser Befehl ändert persistente Systemkonfiguration (Shell, SSH, Cron, Services).",
severity: "high",
};
}
// Git-Destruktion
if (GIT_DESTRUCTIVE_PATTERNS.some((p) => p.test(command))) {
const isForce = /--force\b|-f\b|--mirror\b/.test(command);
return {
category: "🔀 Git-Destruktion",
warning: isForce
? "ACHTUNG: Force-Push überschreibt den Remote-Stand unwiederbringlich!"
: "Dieser Git-Befehl löscht oder überschreibt Daten.",
severity: isForce ? "critical" : "high",
};
}
// Cloud/AWS-Ressourcen
if (CLOUD_DESTRUCTIVE_PATTERNS.some((p) => p.test(command))) {
const isTerraformDestroy = /terraform\s+destroy/.test(command);
return {
category: "☁️ Cloud-Ressource",
warning: isTerraformDestroy
? "KRITISCH: terraform destroy löscht ALLE verwalteten Infrastruktur-Ressourcen!"
: "Dieser Befehl löscht Cloud-Ressourcen (AWS/Terraform/Kubernetes/Docker).",
severity: isTerraformDestroy ? "critical" : "high",
};
}
// Datenbankoperationen
if (DATABASE_DESTRUCTIVE_PATTERNS.some((p) => p.test(command))) {
return {
category: "🗄️ Datenbank-Destruktion",
warning: "Dieser Befehl löscht Datenbank-Daten oder -Strukturen (DROP/TRUNCATE/DELETE/FLUSH)!",
severity: "critical",
};
}
// Dateilöschung
if (FILE_DELETION_PATTERNS.some((p) => p.test(command))) {
const isRecursive = /\brm\s+-[^-]*r/i.test(command);
return {
category: "🗑️ Dateilöschung",
warning: isRecursive
? "ACHTUNG: Rekursive Löschung kann nicht rückgängig gemacht werden!"
: "Dieser Befehl löscht Dateien oder Verzeichnisse.",
severity: isRecursive ? "high" : "medium",
};
}
// Prozesse/Services beenden
if (PROCESS_KILL_PATTERNS.some((p) => p.test(command))) {
return {
category: "⛔ Prozess/Service beenden",
warning: "Dieser Befehl beendet Prozesse oder Services.",
severity: "medium",
};
}
// Package-Installation (nur npx unbekannt + globale npm)
if (PACKAGE_INSTALL_PATTERNS.some((p) => p.test(command))) {
if (SAFE_PACKAGE_COMMANDS.some((p) => p.test(command))) return null;
return {
category: "📦 Package-Installation",
warning: "Dieses Package könnte Lifecycle-Scripts ausführen (postinstall).",
severity: "medium",
};
}
return null;
}
function buildBashMessage(command: string, risk: RiskResult): string {
const target = extractTarget(command);
const severityIcon = risk.severity === "critical" ? "🚨" :
risk.severity === "high" ? "⚠️ " : " ";
// FIX v1.1: Befehl vollständig anzeigen (war 120, jetzt 600 Zeichen)
// Mehrzeilige Befehle (\ am Zeilenende) werden eingerückt dargestellt
const displayCommand = command.length > 600
? command.slice(0, 597) + "..."
: command;
const formattedCommand = displayCommand
.split(/\\\n/)
.map((line, i) => i === 0 ? ` Befehl: ${line.trim()}` : ` ${line.trim()}`)
.join("\n");
return [
`${severityIcon} ${risk.category}`,
"",
formattedCommand,
target ? ` Ziel: ${target}` : "",
"",
risk.warning,
].filter(Boolean).join("\n");
}
// ─────────────────────────────────────────────
// DEBUG-HILFSFUNKTION
// Für lokales Debugging: pi confirm-destructive.ts --debug
// Zeigt welches Pattern auf welchen Befehl anschlägt.
// Verwendung im Pi-Terminal:
// /skill:debug-extension
// Oder direkt: node -e "require('./confirm-destructive.ts')" (nach tsc)
// ─────────────────────────────────────────────
export function debugClassify(command: string): void {
const result = classifyBashRisk(command);
if (!result) {
console.log(`✅ KEIN TREFFER: "${command}"`);
return;
}
console.log(`${result.severity.toUpperCase()} ${result.category}`);
console.log(` Befehl: ${command}`);
console.log(` Warnung: ${result.warning}`);
const target = extractTarget(command);
if (target) console.log(` Ziel: ${target}`);
}
// ═══════════════════════════════════════════════════════════
// EXTENSION
// ═══════════════════════════════════════════════════════════
export default function (pi: ExtensionAPI) {
pi.on("tool_call", async (event, ctx) => {
// ── 1. BASH ──────────────────────────────────────────────────────────────
if (event.toolName === "bash") {
const command = (event.input as { command: string }).command;
if (!command) return undefined;
const risk = classifyBashRisk(command);
if (!risk) return undefined;
// Ausnahme: bekannte Build-Artefakt-Pfade
const target = extractTarget(command);
if (target && ctx.cwd && isExcludedPath(target, ctx.cwd)) return undefined;
if (!ctx.hasUI) {
return { block: true, reason: `Destruktiver Befehl blockiert (kein UI): ${command}` };
}
const allowed = await ctx.ui.confirm(buildBashMessage(command, risk), "Erlauben?");
if (!allowed) return { block: true, reason: `Abgelehnt: ${risk.category}` };
return undefined;
}
// ── 2. WRITE ─────────────────────────────────────────────────────────────
if (event.toolName === "write") {
const input = event.input as { path?: string; content?: string };
const filePath = input.path;
const content = input.content ?? "";
if (!filePath || !ctx.hasUI) return undefined;
// Secret im Inhalt → immer blockieren
const secretMatch = detectSecretInContent(content);
if (secretMatch) {
const confirmed = await ctx.ui.confirm(
"🔑 Möglicher Secret/API-Key im Dateiinhalt",
`Gefunden: "${secretMatch}"\nZieldatei: ${filePath}\n\nTrotzdem schreiben?`,
);
if (!confirmed) return { block: true, reason: "Secret-Schreibvorgang abgelehnt" };
return undefined;
}
// Systembereich-Schreibzugriff
const absPath = normalizePath(filePath, ctx.cwd ?? "");
if (SYSTEM_PATH_PATTERNS.some((p) => p.test(absPath))) {
const confirmed = await ctx.ui.confirm(
"🔒 Schreibzugriff in Systembereich",
`Ziel: ${absPath}\n\nDiese Datei liegt außerhalb deines Projekts. Erlauben?`,
);
if (!confirmed) return { block: true, reason: "Systembereich-Schreibzugriff abgelehnt" };
return undefined;
}
// Bestehende Datei überschreiben
if (fileExists(filePath, ctx.cwd ?? "")) {
const confirmed = await ctx.ui.confirm(
"📝 Datei überschreiben",
`${filePath}\n\nDiese Datei existiert bereits. Inhalt wird vollständig ersetzt. Erlauben?`,
);
if (!confirmed) return { block: true, reason: "Überschreiben abgelehnt" };
}
return undefined;
}
// ── 3. EDIT ──────────────────────────────────────────────────────────────
if (event.toolName === "edit") {
const input = event.input as { path?: string; edits?: Array<{ oldText?: string; newText?: string }> };
const filePath = input.path;
const edits = input.edits ?? [];
if (!filePath || !ctx.hasUI) return undefined;
// Secret in neuem Inhalt
for (const edit of edits) {
const secretMatch = detectSecretInContent(edit.newText ?? "");
if (secretMatch) {
const confirmed = await ctx.ui.confirm(
"🔑 Möglicher Secret/API-Key in Änderung",
`Gefunden: "${secretMatch}"\nDatei: ${filePath}\n\nTrotzdem schreiben?`,
);
if (!confirmed) return { block: true, reason: "Secret-Edit abgelehnt" };
return undefined;
}
}
// Systembereich
const absPath = normalizePath(filePath, ctx.cwd ?? "");
if (SYSTEM_PATH_PATTERNS.some((p) => p.test(absPath))) {
const confirmed = await ctx.ui.confirm(
"🔒 Änderung in Systembereich",
`${absPath}\n\nDiese Datei liegt außerhalb deines Projekts. Erlauben?`,
);
if (!confirmed) return { block: true, reason: "Systembereich-Änderung abgelehnt" };
return undefined;
}
const confirmed = await ctx.ui.confirm(
"✏️ Datei ändern",
`${filePath}\n${edits.length} Änderung(en)\n\nErlauben?`,
);
if (!confirmed) return { block: true, reason: "Änderung abgelehnt" };
return undefined;
}
return undefined;
});
}

View file

@ -0,0 +1,78 @@
/**
* Default Model Extension
*
* Setzt bei jedem Session-Start das Modell zurück auf den in settings.json
* definierten defaultProvider/defaultModel.
*
* Dadurch bleibt das Standard-Modell stabil: Man kann in einer Session
* temporär ein anderes Modell wählen (via /model oder Ctrl+G). Nach
* Session-Ende, Reload oder neuer Session gilt wieder der Default.
*
* Konfiguration in ~/.pi/agent/settings.json:
* "defaultProvider": "anthropic",
* "defaultModel": "claude-sonnet-4-20250514",
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import * as fs from "node:fs";
import * as path from "node:path";
function getSettingsPath(): string {
return path.join(
process.env.HOME || "/home/xray",
".pi",
"agent",
"settings.json",
);
}
function readDefaultModel(): {
provider: string | null;
model: string | null;
} {
try {
const raw = fs.readFileSync(getSettingsPath(), "utf-8");
const settings = JSON.parse(raw);
return {
provider: settings.defaultProvider ?? null,
model: settings.defaultModel ?? null,
};
} catch {
return { provider: null, model: null };
}
}
export default function (pi: ExtensionAPI) {
pi.on("session_start", async (event, ctx) => {
if (!ctx.hasUI) return;
// Bei Startup (Programmstart) ist das Modell bereits korrekt gesetzt:
// entweder von CLI-Args (--provider/--model) oder aus settings.json.
// Nur bei Resume/Fork/Reload kann ein veraltetes Modell stammen.
if (event.reason === "startup") return;
const { provider, model } = readDefaultModel();
if (!provider || !model) return;
// Prüfen ob das aktuelle Modell bereits dem Default entspricht
const currentProvider = ctx.model?.provider;
const currentModel = ctx.model?.id;
if (currentProvider === provider && currentModel === model) return;
// Modell-Objekt aus dem Registry suchen
const modelObj = ctx.modelRegistry?.find?.(provider, model);
if (!modelObj) return;
try {
const success = await pi.setModel(modelObj);
if (!success) {
ctx.ui.notify(
`⚠️ Default-Modell ${provider}/${model} nicht verfügbar (API-Key?)`,
"warning",
);
}
} catch {
// Silent — Modell-Wechsel fehlgeschlagen, weitermachen
}
});
}

View file

@ -0,0 +1,169 @@
/**
* Session Header Extension
*
* Zeigt den erkannten Sitzungszweck (Intention) als gelbe Überschrift
* oberhalb des Editors an. Nutzt das LLM zur Zusammenfassung der ersten
* Benutzereingabe, statt sie 1:1 zu transkribieren.
*
* Farbe: Gelb (warning), damit es sich von anderen UI-Elementen abhebt.
* Fallback: Gekürzter Originaltext, wenn die LLM-Zusammenfassung fehlschlägt.
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { complete, getModel } from "@earendil-works/pi-ai";
const MAX_TITLE_LENGTH = 160;
const MAX_SUMMARY_TOKENS = 60;
function truncate(text: string): string {
const trimmed = text.trim();
if (trimmed.length <= MAX_TITLE_LENGTH) return trimmed;
return trimmed.slice(0, MAX_TITLE_LENGTH - 3) + "\u2026";
}
/**
* Ruft das aktuelle LLM auf, um aus der Benutzereingabe eine kurze
* Intention-Zusammenfassung (512 Wörter) zu extrahieren.
*/
async function generateIntentionSummary(
input: string,
ctx: any,
): Promise<string | null> {
try {
const provider = ctx.model?.provider;
const modelId = ctx.model?.id;
if (!provider || !modelId) return null;
const model = getModel(provider, modelId);
if (!model) return null;
const registry = ctx.modelRegistry;
const auth = registry
? await registry.getApiKeyAndHeaders(model)
: null;
if (!auth?.ok || !auth.apiKey) return null;
const prompt = [
"Du extrahierst aus der folgenden Benutzereingabe maximal 510 Wörter",
"als präzisen Session-Titel. Fasse die KERN-Intention zusammen,",
"nicht die wörtliche Formulierung. Antworte NUR mit dem Titel,",
"keinem weiteren Text.",
"",
"Eingabe:",
input,
].join("\n");
const response = await complete(
model,
{
messages: [
{
role: "user",
content: [{ type: "text" as const, text: prompt }],
timestamp: Date.now(),
},
],
},
{
apiKey: auth.apiKey,
headers: auth.headers,
maxTokens: MAX_SUMMARY_TOKENS,
},
);
const textParts = response.content
.filter((c: any): c is { type: "text"; text: string } => c.type === "text")
.map((c: any) => c.text)
.join(" ")
.trim();
if (!textParts) return null;
// Entferne mögliche Anführungszeichen, die das LLM setzt
return textParts.replace(/^["']|["']$/g, "").substring(0, MAX_TITLE_LENGTH);
} catch (err) {
// Bei Fehler (z.B. API nicht erreichbar, Modell nicht gefunden): Fallback auf truncate
return null;
}
}
function makeWidget(title: string | null, theme: any) {
return {
render(_width: number): string[] {
if (!title) {
return [theme.fg("warning", "\u25C6 Sitzung: (warte auf erste Eingabe\u2026)")];
}
const label = theme.fg("warning", theme.bold("\u25C6"));
const text = theme.fg("warning", ` ${title}`);
return [`${label}${text}`];
},
invalidate() {},
};
}
export default function (pi: ExtensionAPI) {
let sessionTitle: string | null = null;
pi.on("session_start", async (_event, ctx) => {
if (!ctx.hasUI) return;
// Bestehende Session: ersten User-Eintrag finden und zusammenfassen
const entries = ctx.sessionManager.getEntries();
for (const entry of entries) {
if (entry.type === "message" && (entry as any).message?.role === "user") {
const text = (entry as any).message.content
.filter((c: any) => c.type === "text")
.map((c: any) => c.text)
.join(" ");
if (text.trim()) {
// Bei Wiederaufnahme: versuche Zusammenfassung asynchron
generateIntentionSummary(text, ctx).then((summary) => {
if (summary) {
sessionTitle = summary;
pi.setSessionName(sessionTitle);
ctx.ui.setWidget(
"session-header",
makeWidget(sessionTitle, ctx.ui.theme),
);
}
}).catch(() => {
// Silent fail — truncate reicht als Fallback
});
// Interim: gekürzten Originaltext anzeigen, bis Summary da ist
sessionTitle = truncate(text);
pi.setSessionName(sessionTitle);
break;
}
}
}
ctx.ui.setWidget("session-header", makeWidget(sessionTitle, ctx.ui.theme));
});
// Erste Benutzereingabe: Intention erkennen statt 1:1 übernehmen
pi.on("input", async (event, ctx) => {
if (!ctx.hasUI) return;
if (sessionTitle) return { action: "continue" };
// Interim: sofort gekürzten Originaltext anzeigen
sessionTitle = truncate(event.text);
pi.setSessionName(sessionTitle);
ctx.ui.setWidget("session-header", makeWidget(sessionTitle, ctx.ui.theme));
// Async: LLM-Zusammenfassung nachladen
generateIntentionSummary(event.text, ctx).then((summary) => {
if (summary) {
sessionTitle = summary;
pi.setSessionName(sessionTitle);
ctx.ui.setWidget("session-header", makeWidget(sessionTitle, ctx.ui.theme));
}
}).catch(() => {
// Silent fail — truncate reicht als Fallback
});
return { action: "continue" };
});
pi.on("session_before_switch", async () => {
sessionTitle = null;
});
}

278
extensions/session-index.ts Normal file
View file

@ -0,0 +1,278 @@
/**
* Session Index Extension
*
* Pflegt ein projektübergreifendes Inhaltsverzeichnis der letzten 10 Sessions
* in ~/.pi/agent/SESSION_INDEX.md.
*
* Bei Session-Start:
* - Zeigt die Tabelle als Widget über dem Editor an
* - Prüft ob die Vorgänger-Session ein SESSION_HANDOVER.md hat fragt Benutzer
*
* Bei Session-Shutdown:
* - Aktualisiert den Index mit dem endgültigen Session-Namen
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
const AGENT_DIR = path.join(os.homedir(), ".pi", "agent");
const SESSIONS_DIR = path.join(AGENT_DIR, "sessions");
const INDEX_PATH = path.join(AGENT_DIR, "SESSION_INDEX.md");
const MAX_SESSIONS = 10;
interface SessionInfo {
name: string;
date: string;
project: string;
jsonlPath: string;
hasHandover: boolean;
handoverPath: string | null;
}
// ---------------------------------------------------------------------------
// Hilfsfunktionen
// ---------------------------------------------------------------------------
/** Alle JSONL-Session-Dateien rekursiv finden */
function findAllSessions(): string[] {
const results: string[] = [];
if (!fs.existsSync(SESSIONS_DIR)) return results;
const dirs = fs.readdirSync(SESSIONS_DIR);
for (const dir of dirs) {
const dirPath = path.join(SESSIONS_DIR, dir);
if (!fs.statSync(dirPath).isDirectory()) continue;
const files = fs.readdirSync(dirPath);
for (const file of files) {
if (file.endsWith(".jsonl")) {
results.push(path.join(dirPath, file));
}
}
}
return results;
}
/** Session-Namen aus dem JSONL extrahieren (prüft session_info und header) */
function getSessionName(jsonlPath: string): string | null {
try {
const content = fs.readFileSync(jsonlPath, "utf-8");
const lines = content.split("\n");
// Zuerst nach session_info suchen (enthält den tatsächlichen Namen)
for (const line of lines) {
if (!line.trim()) continue;
try {
const entry = JSON.parse(line);
if (entry.type === "session_info" && entry.name) {
return entry.name;
}
} catch { /* next line */ }
}
// Fallback: header.name
const firstLine = lines[0];
if (firstLine) {
const header = JSON.parse(firstLine);
if (header.name) return header.name;
}
return null;
} catch {
return null;
}
}
/** Timestamp aus dem Dateinamen extrahieren (Format: YYYY-MM-DDTHH-MM-SS...) */
function extractTimestamp(jsonlPath: string): string {
const basename = path.basename(jsonlPath, ".jsonl");
const match = basename.match(/^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})/);
return match ? match[1].replace("T", " ") : basename;
}
/** Projektname aus dem Verzeichnisnamen ableiten */
function extractProject(jsonlPath: string): string {
const dirName = path.basename(path.dirname(jsonlPath));
// Entferne führende/trailing "--"
return dirName.replace(/^--|--$/g, "").replace(/--/g, " / ");
}
/** CWD (Arbeitsverzeichnis) aus dem Session-Header der JSONL lesen */
function getSessionCwd(jsonlPath: string): string | null {
try {
const firstLine = fs.readFileSync(jsonlPath, "utf-8").split("\n")[0];
if (!firstLine) return null;
const header = JSON.parse(firstLine);
return header.cwd || null;
} catch {
return null;
}
}
/** Nach SESSION_HANDOVER.md im Projektverzeichnis suchen */
function findHandover(jsonlPath: string): { hasHandover: boolean; handoverPath: string | null } {
// CWD direkt aus dem JSONL-Header lesen (zuverlässig, keine Slug-Konvertierung nötig)
const cwd = getSessionCwd(jsonlPath);
if (cwd) {
const handoverPath = path.join(cwd, "SESSION_HANDOVER.md");
if (fs.existsSync(handoverPath)) {
return { hasHandover: true, handoverPath };
}
}
// Fallback: Versuche den neuesten JSONL im selben Projekt-Verzeichnis
const dirPath = path.dirname(jsonlPath);
const files = fs.readdirSync(dirPath)
.filter(f => f.endsWith(".jsonl"))
.sort()
.reverse();
for (const f of files) {
const c = getSessionCwd(path.join(dirPath, f));
if (c) {
const hp = path.join(c, "SESSION_HANDOVER.md");
if (fs.existsSync(hp)) {
return { hasHandover: true, handoverPath: hp };
}
}
}
return { hasHandover: false, handoverPath: null };
}
/** SESSION_INDEX.md schreiben */
function writeIndex(sessions: SessionInfo[]): void {
const lines: string[] = [
"# Session-Index (letzte Sessions)",
"",
`> **Stand:** ${new Date().toLocaleString('sv-SE', { timeZone: 'Europe/Berlin' }).slice(0, 19)}`,
`> **Pfad:** \`${INDEX_PATH}\``,
`> **Quelle:** \`~/.pi/agent/extensions/session-index.ts\``,
"",
"| # | Datum | Projekt | Session-Name | Handover |",
"|---|-------|---------|-------------|----------|",
];
for (let i = 0; i < sessions.length; i++) {
const s = sessions[i];
const handover = s.hasHandover ? `[✓](${s.handoverPath})` : "";
lines.push(`| ${i + 1} | ${s.date.slice(0, 16)} | ${s.project} | ${s.name.length > 70 ? s.name.slice(0, 67) + "..." : s.name} | ${handover} |`);
}
lines.push("", "---", "", "*Automatisch generiert von session-index.ts*", "");
try {
fs.mkdirSync(path.dirname(INDEX_PATH), { recursive: true });
fs.writeFileSync(INDEX_PATH, lines.join("\n"), "utf-8");
} catch (err) {
// Silent fail — Index ist optional, Widget reicht
}
}
/** Sessions laden, sortieren und Top-N filtern */
function loadSessionIndex(): SessionInfo[] {
const files = findAllSessions();
const sessions: SessionInfo[] = [];
for (const file of files) {
const name = getSessionName(file);
const { hasHandover, handoverPath } = findHandover(file);
sessions.push({
name: name || extractTimestamp(file) + " (unbenannt)",
date: extractTimestamp(file),
project: extractProject(file),
jsonlPath: file,
hasHandover,
handoverPath,
});
}
// Nach Datum absteigend sortieren
sessions.sort((a, b) => b.date.localeCompare(a.date));
return sessions.slice(0, MAX_SESSIONS);
}
// ---------------------------------------------------------------------------
// Widget
// ---------------------------------------------------------------------------
function makeIndexWidget(sessions: SessionInfo[], theme: any) {
return {
render(_width: number): string[] {
const lines: string[] = [];
lines.push(theme.fg("warning", theme.bold("═══ Letzte Sessions ═══")));
for (let i = 0; i < sessions.length && i < 5; i++) {
const s = sessions[i];
const date = s.date.slice(5, 16); // MM-DD HH:MM
const handover = s.hasHandover ? theme.fg("success", " ●") : "";
const entry = theme.fg("dim", `${date}`) + ` ${s.name.length > 60 ? s.name.slice(0, 57) + "..." : s.name}${handover}`;
lines.push(entry);
}
lines.push(theme.fg("dim", `Vollständiger Index: ${INDEX_PATH}`));
return lines;
},
invalidate() {},
};
}
// ---------------------------------------------------------------------------
// Extension
// ---------------------------------------------------------------------------
export default function (pi: ExtensionAPI) {
pi.on("session_start", async (_event, ctx) => {
if (!ctx.hasUI) return;
try {
const sessions = loadSessionIndex();
// Aktuellen Session-Namen ergänzen, falls schon gesetzt
const currentName = pi.getSessionName();
if (currentName && sessions.length > 0) {
sessions[0].name = currentName;
}
// Index-Datei schreiben
writeIndex(sessions);
// Widget anzeigen (oberhalb des Editors)
ctx.ui.setWidget("session-index", makeIndexWidget(sessions, ctx.ui.theme));
// Prüfen ob Vorgänger-Session einen Handover hat
if (sessions.length >= 2 && sessions[1].hasHandover) {
const prev = sessions[1];
const msg = `Handover gefunden: ${prev.name}\nPfad: ${prev.handoverPath}`;
const load = await ctx.ui.confirm(
"Handover laden?",
`${msg}\n\nSoll der Handover der vorherigen Session in den Kontext geladen werden?`,
);
if (load && prev.handoverPath) {
try {
const content = fs.readFileSync(prev.handoverPath, "utf-8");
// Als System-Prompt-Zusatz einfügen via Benutzernachricht
ctx.ui.notify(`Handover geladen: ${path.basename(prev.handoverPath)}`, "info");
pi.sendUserMessage(
`[Handover aus vorheriger Session]\n\n${content.slice(0, 3000)}`,
{ deliverAs: "nextTurn" },
);
} catch {
ctx.ui.notify("Fehler beim Laden des Handovers", "error");
}
}
}
} catch (err) {
// Silent fail — Index ist optional
}
});
pi.on("session_shutdown", async (_event, _ctx) => {
// Index beim Beenden nochmal aktualisieren (finaler Name)
try {
const sessions = loadSessionIndex();
writeIndex(sessions);
} catch {
// Silent fail
}
});
}

308
extensions/vision-proxy.ts Normal file
View file

@ -0,0 +1,308 @@
/**
* Vision-Proxy Extension v6 (Robuste Bildpfad-Erkennung)
*
* Änderungen v6 v5:
* - Erkennt Bildpfade ÜBERALL im Prompt (nicht nur /tmp/pi-clipboard-*)
* - Unterstützt: absolute Pfade, relative Pfade, file:// URLs
* - Erkennung per Dateiendung (.png, .jpg, .jpeg, .gif, .webp, .bmp)
* - Prüft Datei-Existenz vor Verarbeitung
* - Maximal 5 Bilder pro Nachricht (Schutz vor Overload)
* - Bessere Fehlerbehandlung und Logging
*
* v5 bleibt als vision-proxy-v5-backup.ts erhalten
*/
import { complete, type Message } from "@earendil-works/pi-ai";
import * as fs from "node:fs";
import * as path from "node:path";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
const LOG_FILE = "/tmp/vision-proxy.log";
const VISION_PROVIDER = "openrouter";
const VISION_MODEL_ID = "qwen/qwen3-vl-32b-instruct";
const MAX_IMAGES = 5;
function log(msg: string) {
const ts = new Date().toISOString().substring(11, 23);
const line = `[${ts}] ${msg}\n`;
try { fs.appendFileSync(LOG_FILE, line); } catch {}
}
const VISION_SYSTEM_PROMPT = `Du bist ein Bildanalyse-Assistent. Beschreibe das angehängte Bild detailliert und präzise.
Gib ALLES lesbar wieder jeden Text, jeden Code, jede Zahl exakt. Bei Fehlermeldungen: Zeichen-exakte Wiedergabe.
Bei Tabellen, Diagrammen oder Strukturen: Gib den Inhalt strukturiert wieder.
Keine Einleitung direkt den Inhalt beschreiben.`;
const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".tif"]);
function isImagePath(filePath: string): boolean {
const ext = path.extname(filePath).toLowerCase();
return IMAGE_EXTENSIONS.has(ext);
}
function detectMimeType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase();
const map: Record<string, string> = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
".bmp": "image/bmp",
".tiff": "image/tiff",
".tif": "image/tiff",
};
return map[ext] || "image/png";
}
/**
* Extrahiert Bildpfade aus dem Prompt-Text.
* Erkennt:
* - Absolute Pfade: /home/user/bild.png
* - pi-clipboard Pfade: /tmp/pi-clipboard-xxx.png
* - file:// URLs
* - Pfade in Anführungszeichen (mit Leerzeichen)
* - Relative Pfade die auf dem CWD existieren
*/
function extractImagePaths(prompt: string, cwd: string): string[] {
const found: string[] = [];
const seen = new Set<string>();
// Pattern 1: file:// URLs
const fileUrlPattern = /file:\/\/([^\s"'<>]+)/g;
let match;
while ((match = fileUrlPattern.exec(prompt)) !== null) {
const p = decodeURIComponent(match[1]);
if (!seen.has(p) && isImagePath(p)) {
seen.add(p);
found.push(p);
}
}
// Pattern 2: Absolute Pfade mit Bild-Endung (Linux/Mac)
const absPathPattern = /(\/[^\s"'<>]+\.(?:png|jpe?g|gif|webp|bmp|tiff?))/gi;
while ((match = absPathPattern.exec(prompt)) !== null) {
const p = match[1];
if (!seen.has(p)) {
seen.add(p);
found.push(p);
}
}
// Pattern 3: Pfade in Anführungszeichen (Leerzeichen)
const quotedPathPattern = /["']([^"']+\.(?:png|jpe?g|gif|webp|bmp|tiff?))["']/gi;
while ((match = quotedPathPattern.exec(prompt)) !== null) {
const p = match[1];
if (!seen.has(p) && p.startsWith("/")) {
seen.add(p);
found.push(p);
}
}
// Pattern 4: Prompt selbst ist ein Bildpfad (paste-only)
const trimmed = prompt.trim();
if (isImagePath(trimmed) && !seen.has(trimmed)) {
seen.add(trimmed);
found.push(trimmed);
}
// Pattern 5: Relative Pfade die auf CWD existieren
const relPathPattern = /([^\s"'<>]+\.(?:png|jpe?g|gif|webp|bmp|tiff?))/gi;
while ((match = relPathPattern.exec(prompt)) !== null) {
const p = match[1];
if (!seen.has(p) && !p.startsWith("/") && !p.startsWith("http")) {
const resolved = path.resolve(cwd, p);
if (!seen.has(resolved)) {
seen.add(resolved);
found.push(resolved);
}
}
}
return found;
}
export default function (pi: ExtensionAPI) {
log("=== Vision-Proxy v6 factory started ===");
pi.on("session_start", async (_event, ctx) => {
log("v6: session_start");
try {
ctx.ui.notify("🔧 Vision-Proxy v6 (robuste Bildpfad-Erkennung) geladen", "info");
} catch (e: any) {
log(`v6: session_start notify ERROR: ${e.message}`);
}
});
pi.on("before_agent_start", async (event, ctx) => {
log("--- v6: before_agent_start ---");
log(`prompt length: ${event.prompt?.length ?? "undefined"}`);
log(`prompt first 200: ${(event.prompt || "").substring(0, 200)}`);
try {
// Step 1: event.images prüfen (falls Pi diese korrekt übergibt)
const hasEventImages = event.images && event.images.length > 0;
log(`event.images: ${hasEventImages ? `array[${event.images!.length}]` : "none"}`);
// Step 2: Prompt nach Bilddateien scannen
const prompt = event.prompt || "";
const cwd = ctx.cwd || process.cwd();
const rawPaths = extractImagePaths(prompt, cwd);
const imagePaths = rawPaths.filter(p => {
try { return fs.existsSync(p) && fs.statSync(p).isFile(); } catch { return false; }
}).slice(0, MAX_IMAGES);
log(`Found ${rawPaths.length} path candidates, ${imagePaths.length} valid: ${JSON.stringify(imagePaths)}`);
// Step 3: Kurzschluss — keine Bilder
if (imagePaths.length === 0 && !hasEventImages) {
log("No images found — exiting");
return;
}
// Step 4: Vision-Modell suchen
let visionModel = ctx.modelRegistry.find(VISION_PROVIDER, VISION_MODEL_ID);
if (!visionModel) {
const alternatives = ["qwen3-vl:235b-instruct", "gemini-3-flash-preview", "kimi-k2.6"];
for (const alt of alternatives) {
visionModel = ctx.modelRegistry.find(VISION_PROVIDER, alt);
if (visionModel) { log(`Fallback vision model: ${visionModel.id}`); break; }
}
}
if (!visionModel) {
log("ERROR: No vision model found — exiting");
return;
}
log(`Using vision model: ${visionModel.id}`);
// Step 5: API-Key
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(visionModel);
if (!auth.ok || !auth.apiKey) {
log(`ERROR: No API key (error: ${auth.error || "none"}) — exiting`);
return;
}
log("Auth OK");
// Step 6: UI-Feedback
const theme = ctx.ui.theme;
const totalImages = imagePaths.length + (hasEventImages ? event.images!.length : 0);
const statusId = "vision-proxy";
ctx.ui.setStatus(statusId, theme.fg("warning", "🔍 Analysiere Bild..."));
ctx.ui.notify(theme.fg("warning", `🔍 ${totalImages} Bild(er) werden analysiert...`), "info");
// Step 7: Bilder analysieren
const descriptions: string[] = [];
// 7a: Datei-basierte Bilder
for (let i = 0; i < imagePaths.length; i++) {
const imgPath = imagePaths[i];
const label = totalImages > 1 ? `Bild ${i + 1} (${path.basename(imgPath)})` : "Bild";
log(`Processing file: ${imgPath}`);
try {
const imageBuffer = fs.readFileSync(imgPath);
const base64Data = imageBuffer.toString("base64");
const mimeType = detectMimeType(imgPath);
log(`Read ${imageBuffer.length} bytes, mime=${mimeType}`);
const userMessage: Message = {
role: "user",
content: [
{ type: "text", text: `Analysiere dieses Bild. Gib alle sichtbaren Texte, Code, Zahlen und Strukturen exakt wieder.` },
{ type: "image", data: base64Data, mimeType },
],
timestamp: Date.now(),
};
const response = await complete(
visionModel,
{ systemPrompt: VISION_SYSTEM_PROMPT, messages: [userMessage] },
{ apiKey: auth.apiKey, headers: auth.headers, signal: ctx.signal },
);
const descriptionText = response.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("\n");
log(`Description received (${descriptionText.length} chars)`);
descriptions.push(`### ${label}\n${descriptionText.trim()}`);
} catch (imgErr: any) {
log(`ERROR processing ${imgPath}: ${imgErr.message}`);
descriptions.push(`### ${label}\n⚠ Fehler: ${imgErr.message}`);
}
}
// 7b: Event-basierte Bilder (falls Pi diese eines Tages übergibt)
if (hasEventImages) {
for (let i = 0; i < event.images!.length; i++) {
const img = event.images![i];
const label = `Event-Bild ${descriptions.length + 1}`;
log(`Processing event image ${i + 1}`);
try {
const userMessage: Message = {
role: "user",
content: [
{ type: "text", text: `Analysiere dieses Bild. Gib alle sichtbaren Texte, Code, Zahlen und Strukturen exakt wieder.` },
img,
],
timestamp: Date.now(),
};
const response = await complete(
visionModel,
{ systemPrompt: VISION_SYSTEM_PROMPT, messages: [userMessage] },
{ apiKey: auth.apiKey, headers: auth.headers, signal: ctx.signal },
);
const descriptionText = response.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("\n");
descriptions.push(`### ${label}\n${descriptionText.trim()}`);
} catch (imgErr: any) {
log(`ERROR processing event image: ${imgErr.message}`);
descriptions.push(`### ${label}\n⚠ Fehler: ${imgErr.message}`);
}
}
}
// Step 8: Ergebnis zurückgeben
if (descriptions.length === 0) {
ctx.ui.setStatus(statusId, undefined);
log("No descriptions generated — exiting");
return;
}
ctx.ui.setStatus(statusId, undefined);
ctx.ui.notify(theme.fg("success", `✅ Bildanalyse abgeschlossen (${descriptions.length} Bild(er))`), "info");
const visionContext = [
`== VISION-PROXY v6: Bildanalyse ==`,
`${descriptions.length} Bild${descriptions.length > 1 ? "er wurden" : " wurde"} durch ${visionModel.id} analysiert.`,
``,
...descriptions,
`== ENDE VISION-PROXY ==`,
].join("\n");
log(`Returning vision context (${visionContext.length} chars)`);
return {
message: {
customType: "vision-proxy",
content: visionContext,
display: true,
details: { imageCount: descriptions.length },
},
};
} catch (err: any) {
try { ctx.ui.setStatus("vision-proxy", ctx.ui.theme.fg("error", "❌ Bildanalyse fehlgeschlagen")); } catch {}
try { ctx.ui.notify(ctx.ui.theme.fg("error", `❌ Bildanalyse fehlgeschlagen: ${err.message}`), "error"); } catch {}
log(`FATAL ERROR: ${err.message}\n${err.stack}`);
}
});
log("=== Vision-Proxy v6 factory complete ===");
}

175
install.sh Normal file
View file

@ -0,0 +1,175 @@
#!/bin/bash
# PiSystem install.sh — Deterministisches Setup für alle Maschinen
#
# Installiert alle Pi-Orchestrator-Infrastrukturkomponenten:
# - bin/Sub* → ~/bin/ + /usr/local/bin/
# - extensions/ → ~/.pi/agent/extensions/
# - memory/ → ~/.pi/agent/memory/
# - agent/AGENTS.md → ~/.pi/agent/AGENTS.md
#
# Idempotent: Kann beliebig oft ausgeführt werden.
# Erstellt Backups mit Timestamp vor jedem Überschreiben.
#
# Voraussetzungen:
# - pi (Pi Agent CLI) installiert
# - tmux installiert
# - intercom (pi-intercom npm package) installiert
#
# Nutzung:
# ./install.sh # Vollinstallation
# ./install.sh --dry-run # Zeigt was getan würde ohne Änderungen
set -euo pipefail
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
DRY_RUN=false
[ "${1:-}" = "--dry-run" ] && DRY_RUN=true
TS=$(date '+%Y-%m-%d-%H-%M')
BACKUP_SUFFIX=".bak-v${TS}"
green() { echo -e "\033[0;32m✓ $*\033[0m"; }
yellow() { echo -e "\033[0;33m⚠ $*\033[0m"; }
red() { echo -e "\033[0;31m✗ $*\033[0m"; }
info() { echo -e " $*"; }
install_file() {
local src="$1"
local dst="$2"
local executable="${3:-false}"
if [ ! -f "$src" ]; then
red "Quelldatei fehlt: $src"
return 1
fi
local dst_dir
dst_dir="$(dirname "$dst")"
if $DRY_RUN; then
info "[DRY-RUN] $src$dst"
return 0
fi
mkdir -p "$dst_dir"
# Backup falls Ziel existiert und sich unterscheidet
if [ -f "$dst" ] && ! diff -q "$src" "$dst" &>/dev/null; then
cp "$dst" "${dst}${BACKUP_SUFFIX}"
info "Backup: ${dst}${BACKUP_SUFFIX}"
fi
cp "$src" "$dst"
[ "$executable" = "true" ] && chmod +x "$dst"
green "$dst"
}
echo ""
echo "═══════════════════════════════════════════════════════"
echo " PiSystem Installation — $(date '+%Y-%m-%d %H:%M')"
$DRY_RUN && echo " MODUS: DRY-RUN (keine Änderungen)"
echo "═══════════════════════════════════════════════════════"
echo ""
# ─── Voraussetzungen prüfen ─────────────────────────────────
echo "── Voraussetzungen ────────────────────────────────────"
check_cmd() {
if command -v "$1" &>/dev/null; then
green "$1 vorhanden ($(command -v "$1"))"
else
red "$1 FEHLT — Installation erforderlich: $2"
MISSING=true
fi
}
MISSING=false
check_cmd "tmux" "apt install tmux"
check_cmd "pi" "Siehe CrowdBrain neue-maschine.md für Pi-Installation"
check_cmd "zenity" "apt install zenity (für Desktop-Notifications von SubConfirm)"
$MISSING && { echo ""; red "Fehlende Voraussetzungen — Abbruch."; exit 1; }
# pi-intercom als Pi-Package prüfen (kein Shell-Binary, daher separat)
if pi list-packages 2>/dev/null | grep -q "pi-intercom"; then
green "pi-intercom installiert"
else
yellow "pi-intercom nicht gefunden — in Pi ausführen: pi install npm:pi-intercom"
fi
echo ""
# ─── bin/ → ~/bin/ ──────────────────────────────────────────
echo "── bin/ → ~/bin/ ──────────────────────────────────────"
mkdir -p "$HOME/bin"
for f in "$REPO_DIR"/bin/*; do
[ -f "$f" ] || continue
install_file "$f" "$HOME/bin/$(basename "$f")" true
done
echo ""
# ─── bin/ → /usr/local/bin/ (nur Sub* Skripte) ──────────────
echo "── bin/Sub* → /usr/local/bin/ ─────────────────────────"
for f in "$REPO_DIR"/bin/Sub*; do
[ -f "$f" ] || continue
DST="/usr/local/bin/$(basename "$f")"
if $DRY_RUN; then
info "[DRY-RUN] $f$DST (sudo)"
elif sudo -n true 2>/dev/null; then
sudo cp "$f" "$DST" && sudo chmod +x "$DST"
green "$DST"
else
yellow "/usr/local/bin: sudo nötig — übersprungen (~/bin reicht)"
break
fi
done
echo ""
# ─── Extensions ─────────────────────────────────────────────
echo "── extensions/ → ~/.pi/agent/extensions/ ──────────────"
mkdir -p "$HOME/.pi/agent/extensions"
for f in "$REPO_DIR"/extensions/*; do
[ -f "$f" ] || continue
install_file "$f" "$HOME/.pi/agent/extensions/$(basename "$f")"
done
echo ""
# ─── Memory ─────────────────────────────────────────────────
echo "── memory/ → ~/.pi/agent/memory/ ──────────────────────"
mkdir -p "$HOME/.pi/agent/memory"
for f in "$REPO_DIR"/memory/*; do
[ -f "$f" ] || continue
install_file "$f" "$HOME/.pi/agent/memory/$(basename "$f")"
done
echo ""
# ─── AGENTS.md ──────────────────────────────────────────────
echo "── agent/ → ~/.pi/agent/ ───────────────────────────────"
install_file "$REPO_DIR/agent/AGENTS.md" "$HOME/.pi/agent/AGENTS.md"
install_file "$REPO_DIR/agent/settings.json" "$HOME/.pi/agent/settings.json"
echo ""
# ─── PATH prüfen ────────────────────────────────────────────
echo "── PATH-Check ──────────────────────────────────────────"
if echo "$PATH" | grep -q "$HOME/bin"; then
green "~/bin ist im PATH"
else
yellow "~/bin ist NICHT im PATH"
info "Folgendes in ~/.bashrc oder ~/.zshrc eintragen:"
info ' export PATH="$HOME/bin:$PATH"'
fi
echo ""
# ─── SubConfirm-Autostart Hinweis ───────────────────────────
echo "── SubConfirm ──────────────────────────────────────────"
if pgrep -f SubConfirm &>/dev/null; then
green "SubConfirm läuft bereits (PID: $(pgrep -f SubConfirm))"
else
yellow "SubConfirm läuft nicht"
info "Starten mit: SubConfirm --skip \"\$(tmux display-message -p '#S')\" &"
info "Für Autostart beim Pi-Session-Start: in AGENTS.md ist das bereits eingetragen."
fi
echo ""
echo "═══════════════════════════════════════════════════════"
$DRY_RUN && echo " DRY-RUN abgeschlossen — keine Änderungen vorgenommen" || echo " Installation abgeschlossen ✓"
echo "═══════════════════════════════════════════════════════"
echo ""

276
memory/arbeitsweise.md Normal file
View file

@ -0,0 +1,276 @@
# Arbeitsweise: Orchestrator + SubAgenten
> Stand: 2026-05-30 | Gültig für alle Sessions.
---
## 1. Grundprinzip
**Die erste Pi-Session = Orchestrator.**
Zwei Aufgaben:
1. **Mit dem Benutzer sprechen**
2. **Arbeiten an SubAgenten delegieren**
SubAgenten machen die Arbeit. Der Orchestrator greift nicht selbst in Projekte ein.
---
## 2. Rollen
| Rolle | Wer | Aufgabe |
|-------|-----|---------|
| 🧑 Benutzer | Mensch | Aufgaben, Entscheidungen, Rückfragen |
| 🎯 Orchestrator | Erste Pi-Session | Kommuniziert mit Benutzer, delegiert, nimmt Ergebnisse entgegen |
| 🤖 SubAgent | Eigene Pi-Session (gnome-terminal) | Arbeitet (programmieren, recherchieren, installieren, konfigurieren) |
---
## 3. SubAgenten — Output-Verzeichnis
Jeder SubAgent speichert Ergebnisse in einem **sprechenden Unterverzeichnis**:
```
/<projektverzeichnis>/<aufgaben-name>/
```
**Beispiel:**
```
/media/xray/NEU/Code/20260530/
├── offene-todos/offene-todos.html
├── widerrufsbutton-recherche/widerrufsbutton-recherche.md
├── vm-debian13/{vm-pi-setup.sh, test-results.log}
└── SESSION_HANDOVER.md
```
**Regeln:**
- Verzeichnisname = Aufgabenname (sprechend, kein Kürzel)
- Alle Ergebnisse in DIESEM Verzeichnis
- Nichts auf Projektebene (außer SESSION_HANDOVER.md)
- SubAgent erstellt Verzeichnis selbst (`mkdir -p`)
---
## 4. Kommunikation
| Weg | Zweck |
|-----|-------|
| Benutzer ↔ Orchestrator | Direkt im Chat |
| Orchestrator → SubAgent | Via `SubAgenten`-Skript + intercom |
| SubAgent → Orchestrator | Ergebnis via intercom |
| SubAgent → Benutzer | Nur via Orchestrator (`contact_supervisor`) |
---
## 5. Ablauf
1. Benutzer startet Pi → **Orchestrator**
2. Aufgabe → Orchestrator delegiert an **SubAgenten**
3. SubAgent arbeitet in eigenem Fenster → speichert in `/<projekt>/<aufgabe>/`
4. SubAgent meldet per **intercom** zurück
5. Orchestrator präsentiert Ergebnis
6. Nächste Aufgabe (parallel oder sequenziell)
---
## 6. VERBOT: Eigenmächtige Änderungen an funktionierenden Systemen (2026-05-30)
> **Regelverstoß 2026-05-30:** SubAgent "speech-cuda-fix" ohne Rückfrage gestartet,
> um eine funktionierende Lösung zu überschreiben. Gestoppt vor Änderung.
### Regel
**Orchestrator startet NIEMALS eigenmächtig einen SubAgenten, der eine funktionierende Lösung überschreibt, ersetzt oder modifiziert.**
Gilt insbesondere bei:
- System **läuft und erfüllt Aufgabe** (auch wenn suboptimal)
- Änderung **riskiert bestehende Funktionalität**
- **Alternativen vorhanden** (Diagnose vor Eingriff, Workaround statt Umbau)
### Erlaubtes Vorgehen bei Problemen
| Situation | Erlaubt | Verboten |
|-----------|---------|----------|
| Läuft, aber langsam | Diagnose-SubAgent (Status quo, nichts ändern) | SubAgent mit Änderungsauftrag |
| Fällt aus | Diagnose-SubAgent, Ergebnis präsentieren | SubAgent mit Reparatur-Auftrag |
| Läuft fehlerhaft | Ursachenforschung (Logs, Metriken) | Fix-Auftrag ohne Rückfrage |
| Benutzer sagt "mach mal" | ✅ Machen | ❌ Selber entscheiden |
### Korrekter Ablauf bei erkanntem Problem
1. **Diagnostizieren** (Logs, Status, Config lesen — ohne Rückfrage erlaubt)
2. **Befund präsentieren** (Was kaputt, welche Optionen)
3. **Benutzer entscheiden lassen** (Erst "mach das" → dann handeln)
4. **Delegieren** (SubAgent mit klarem Auftrag)
### Ausnahmen (nur mit expliziter Genehmigung)
- Akute Sicherheitslücke mit bekanntem Exploit
- System tot, Benutzer nicht erreichbar (minimalinvasiver Fix + Dokumentation)
Sonst: **Erst fragen, dann handeln.**
---
## 7. SubAgent-Interaktion: Terminals vs. intercom (2026-05-30)
> **Regelverstoß 2026-05-30:** intercom-Nachricht statt tmux für Yes/No-Prompt — Signal ging nicht an.
### Regel
**Interaktive Prompts (Bestätigungen, `Yes/No`-Auswahlen) im SubAgent-Terminal → `tmux send-keys`, NICHT per intercom.**
| Situation | Korrekt | Falsch |
|-----------|---------|--------|
| "Datei überschreiben?" | `tmux send-keys -t <session> Enter` | intercom "ja, mach" |
| SubAgent wartet auf Eingabe | Terminal-Input per tmux | intercom-Nachricht |
| SubAgent hat Rückfrage zum Inhalt | intercom | tmux (Prozess stören) |
### SubAgent-Status prüfen
```bash
tmux capture-pane -t <session> -p -S -10
```
`→ Yes / No` sichtbar → `tmux send-keys -t <session> Enter`
---
## 8. PRE-FLIGHT CHECKLIST — Vor JEDER Aktion (2026-05-30)
> **Wiederholte Verstöße:** Orchestrator arbeitet selbst statt zu delegieren.
**Vor JEDEM Tool-Call (bash, write, edit, read):**
- [ ] **1. Darf ich das selbst machen?**
- Erlaubt: `read`, intercom `list`/`pending`, tmux-Status
- Alles andere (Schreiben, Bearbeiten, Curl, Crawlen, Programmieren) → SubAgent
- [ ] **2. Gibt es schon einen laufenden SubAgent?**
- `intercom({action:"list"})` prüfen → Aufgabe per intercom geben
- [ ] **3. Änderung an funktionierendem System?**
- Ja → §6: **erst Benutzer fragen**
- [ ] **4. Internet / live-Webseiten?**
- Ja → **SubAgent muss das machen** (kein curl/requests/firecrawl direkt)
- [ ] **5. Benutzer informiert?**
- Vor jeder Aktion: Bescheid geben WAS passiert
### Bei Verstoß
Verstoß wird dokumentiert, bei nächsten Session geladen. Nach 3 Verstößen: Session-Handover + Neustart.
### Automatische Trigger
Checkliste feuert bei:
- Geplantem Tool-Call (write, edit, bash mit curl/requests)
- Eigener Idee was zu tun ist
- Rückfrage vom Benutzer
---
## 9. SUBAGENT-PROMPT-ÜBERWACHUNG (2026-05-30)
> **Problem:** SubAgenten hängen an Yes/No-Prompts, Orchestrator wartet unbemerkt.
### Kernregel
**Nach JEDER SubAgent-Interaktion SOFORT prüfen: Wartet er auf Eingabe?**
Gilt nach: intercom-Nachricht, Aufgaben-Übergabe, Status-Abfrage, Rückmeldung.
### Ausführung
```bash
# 1. Status prüfen
tmux capture-pane -t <session> -p -S -10
# 2. Yes/No-Prompt → bestätigen
tmux send-keys -t <session> Enter
# 3. Anderer Prompt → tmux send-keys (NICHT intercom!)
```
### Session-Namen ermitteln
```bash
tmux ls 2>/dev/null | grep -v "^π"
```
Hängende Prompts finden:
```bash
tmux capture-pane -t <session> -p -S -10 | grep -E "Yes|No|Erlauben|überschreiben|existiert"
```
### Prüf-Rhythmus
Während SubAgent arbeitet:
- Nach jeder eigenen Aktion → tmux-Check
- Stillstand >30s ohne Rückmeldung → tmux-Check
- Unerwartet lange Wartezeit → tmux-Check
### AUTO-CHECK-HOOK: ALLE SubAgenten auf einmal (2026-05-30)
> **Problem:** Bisher nur der letzte SubAgent geprüft, andere blieben hängen.
**Nach JEDEM SubAgent-Start und nach JEDER Antwort an den Benutzer** (wenn SubAgenten laufen):
```bash
tmux ls 2>/dev/null | grep -v "^π" | cut -d: -f1 | while read s; do
if tmux capture-pane -t "$s" -p -S -10 2>/dev/null | grep -qE "Erlauben|Yes.*No|überschreiben|existiert bereits|Erlauben Sie"; then
echo "⚠️ Prompt bei: $s → bestätige Enter"
tmux send-keys -t "$s" Enter
fi
done
```
**Trigger:** Nach:
1. JEDEM `SubAgenten`-Aufruf
2. JEDER Antwort an den Benutzer (während SubAgenten aktiv)
3. JEDER intercom-Rückmeldung (dann ALLE anderen auch prüfen!)
4. JEDEM Edit-Versuch eines SubAgenten (promptet immer "Erlauben?")
### Warum Deadlocks entstehen
SubAgent erstellt Datei → will aktualisieren → fragt "Erlauben?" → wartet auf Tastatureingabe → Orchestrator wartet auf intercom → **Deadlock**.
**Lösung:** Nach JEDER SubAgent-Kommunikation ist tmux-Check Pflicht (§8 gilt nicht nur vor Tool-Calls).
---
## 12. KEIN sleep() im Orchestrator oder SubAgenten (2026-05-31)
> **Regelverstoß 2026-05-31:** Orchestrator hat durch `sleep(45)` 45 Sekunden den Benutzer blockiert. SubAgent hat durch `sleep(300)` 5 Minuten blockiert, ohne auf intercom-Nachrichten zu reagieren.
### Regel
**Weder der Orchestrator noch SubAgenten verwenden `sleep()` zum Warten.**
| Situation | FALSCH | RICHTIG |
|-----------|--------|--------|
| Auf Ergebnis warten | `sleep(300)` | Kurzes Intervall prüfen (max 1 Befehl, sofort beim Benutzer antworten), eigenständig prüfen ohne Benutzer zu blockieren |
| Auf Download warten | `sleep(300)` | Container starten, in kurzer Schleife per curl prüfen (kein `sleep()` im Pi/Bash — Tool-Call-Intervall reicht) |
| Auf Modell-Ladung warten | `sleep(60)` | Nach Container-Start direkt per curl health checken, wiederholen bis bereit |
### Grund
Der Benutzer wird blockiert und kann während `sleep()` weder mit dem Orchestrator kommunizieren noch neue Aufgaben geben. intercom-Nachrichten werden während `sleep()` nicht verarbeitet.
### Konsequenz
**Jeder `sleep()`-Aufruf im Code/Bash ist ein Regelverstoß.**
---
## 10. SYSTEMWEIT VERBINDLICH
Alle Regeln hier:
- Gültig für JEDE Pi-Instanz, auf JEDEM Rechner
- Versioniert in `~/.pi/agent/memory/arbeitsweise.md`
- Gepflegt im pi-agent Repo: `/media/xray/NEU/Code/pi-agent/`
- Geladen bei jeder Session-Initialisierung
Änderungen:
1. In `~/.pi/agent/memory/arbeitsweise.md` speichern
2. Ins pi-agent Repo committen: `/media/xray/NEU/Code/pi-agent/`
3. Bei Bedarf auf andere Rechner deployen

278
memory/persistent-issues.md Normal file
View file

@ -0,0 +1,278 @@
# Wiederkehrende lokale Probleme (Persistent Issues)
**Erstellt:** 2026-05-24
**Standort:** `~/.pi/agent/memory/persistent-issues.md`
**Spiegel:** CrowdBrain `persistent-issues.md`
---
## Regel: Wann ist ein Problem WIRKLICH gelöst?
Ein Problem gilt erst dann als **erledigt**, wenn es **mehrere Tage hintereinander nicht mehr aufgetreten** ist.
Ein Fix, der einmal funktioniert, ist kein gelöster Bug — er ist eine Hypothese, die sich über Zeit beweisen muss.
**Vor jedem Schließen eines Tickets prüfen:**
1. Wie viele Resume-/Reboot-Zyklen hat der Fix überlebt? (Minimum: 3)
2. Ist das Problem in dieser Session reproduzierbar NICHT aufgetreten?
3. Gab es einen System-Update, der den Fix unwirksam machen könnte?
---
## 🟢 Problem 1: Spracherkennung nach Standby-Resume
**Status:** In Bewährung (Fix vom 2026-05-24)
**Status-Änderung (2026-05-24):**
- Aktueller Stand: Persistence Mode (GPU) aktiviert
- `Standby`-Skript modifiziert (stoppt Dienst vor Suspend)
- Auto-Resume-Trigger (`crowd-nvidia-speech-resume.service`) eingerichtet
- Implementierung eines „Wait-for-GPU“-Checks (`/usr/local/bin/Speech-Auto-Start`) anstelle fester Wartezeit (Test steht aus)
- Service-Korrektur: `Environment=CUDA_VISIBLE_DEVICES=0` (zwingt GPU-Nutzung)
**Bewährungsprüfung (Erfolgs-Kriterien):**
- [ ] 3 Suspend-/Resume-Zyklen hintereinander erfolgreich (Startet auf GPU, performant)?
- [ ] Keine CPU-Fallback-Warnungen im Log?
- [ ] Die neue Warte-Logik startet den Speech-Dienst zuverlässig erst NACH Treiber-Bereitschaft?
---
## 🟡 Problem 2: Session-Header Extension (UI-kritisch)
**Erstmals erstellt:** 2026-05-23
**Status:** ✅ Funktioniert aktuell (2026-05-24)
### Was es tut
Die Extension `~/.pi/agent/extensions/session-header.ts` zeigt in gelber Schrift eine vom LLM generierte Zusammenfassung der Session-Intention oberhalb des Pi-Editors an.
### Warum es hier gelistet ist
Dies ist ein **kritisches UI-Element**: Wenn es unbemerkt kaputtgeht, kann der Benutzer in der falschen Session die falschen Befehle eingeben. Es darf nicht stillschweigend ausfallen.
### Prüf-Checkliste (bei jeder Session)
- [ ] Gelbe Statusleiste sichtbar?
- [ ] Zeigt die korrekte Session-Intention?
- [ ] Kein „warte auf erste Eingabe…“-Fallback in einer laufenden Session?
### Was tun bei Ausfall?
1. Extension-Log prüfen
2. Fallback: `truncate(text)` zeigt den Originaltext an — das ist okay als Notlösung
3. Nicht ohne Ersatz weiterarbeiten
---
## 🔴 Problem 3: Tickets werden zu früh geschlossen (Metaprozess)
**Erstmals dokumentiert:** 2026-05-24
**Status:** ❌ SYSTEMISCHES PROBLEM
### Muster
1. Problem tritt auf → Ticket wird erstellt
2. Fix wird implementiert → Fix funktioniert einmal
3. Ticket wird als „erledigt“ markiert
4. Problem tritt Tage/Wochen später wieder auf
5. Ticket ist bereits geschlossen → wird nicht erneut geöffnet
6. Benutzer muss Problem erneut melden
### Bekannte Vorfälle
- Spracherkennung nach Standby (siehe Problem 1)
- Weitere? (Liste wächst mit jedem neuen Vorfall)
### Systemische Lösung
- **Mindest-Bewährungszeit:** 3 Resume-/Reboot-Zyklen oder 3 Tage ohne Wiederauftreten
- **Status „Bewährung“** statt „Erledigt“: Ein neuer Status zwischen „Offen“ und „Erledigt“
- **Diese Datei ist der Kanon**: Was hier als ungelöst steht, ist ungelöst — egal was in todos.md steht
---
## 🔴 Problem 4: Pi Skills landen im falschen Verzeichnis (strukturiert)
**Erstmals dokumentiert:** 2026-05-24
**Status:** ✅ GEFIXT (2026-05-24) — in Bewährung
### Symptom
Pi las seine Skills aus `~/.claude/skills/` (Verzeichnis des Claude-Code-Agenten) statt aus `~/.pi/agent/skills/`. Dadurch:
- Agenten schrieben Skills in das falsche Verzeichnis
- Andere Agenten oder globale Befehle lasen aus dem richtigen Verzeichnis → alter Stand
- Nach Session-Neustarts waren Änderungen "verschwunden" weil die falsche Datei gelesen wurde
- Dies war die **Root Cause** für mehrfache Fehlschläge bei Skill-Updates (Video-Analyse, Session-Header, etc.)
### Was passiert ist
In `~/.pi/agent/settings.json` war konfiguriert:
```json
"skills": ["~/.claude/skills"]
```
Das ist das Skills-Verzeichnis von Anthropic Claude Code (einem völlig anderen Tool). Pi hat dort gelesen und geschrieben.
### Fix (2026-05-24)
- `settings.json` korrigiert: `"skills": ["~/.pi/agent/skills"]`
- AGENTS.md um Regel ergänzt: Skills gehören AUSSCHLIESSLICH nach `~/.pi/agent/skills/`
- Video-Analyse-Skill neu geschrieben und in alle drei Orte synchronisiert
### Bewährungsprüfung
- [ ] Nächste Session: Prüfen ob Pi Skills aus `~/.pi/agent/skills/` liest
- [ ] Skill-Änderung überlebt Session-Neustart?
- [ ] Globale Befehle (Videoanalyse etc.) funktionieren mit dem neuen Stand?
- Minimum: 3 Session-Wechsel ohne Rückfall
### Verwandte Einträge
- AGENTS.md — Abschnitt "Skill-Verzeichnis-Regel"
- Problem 3 (Tickets zu früh geschlossen) — dieses Muster war eine Folge dieses Problems
---
## 🟡 Problem 5: Session-Index zeigt keine Handover-Links (für 20260524-Projekt)
**Erstmals dokumentiert:** 2026-05-24
**Status:** ❌ NICHT GELÖST
### Symptom
Die Extension `session-index.ts` erzeugt korrekt einen Rolling Index der letzten 10 Sessions (`SESSION_INDEX.md`). Aber:
- Alle Sessions heißen „(unbenannt)" — keine Session-Namen gesetzt
- Handover-Links werden nicht gefunden, obwohl `SESSION_HANDOVER.md` im Projektverzeichnis liegt
- Die Extension fragt beim Start NICHT ob das Handover geladen werden soll
- Ergebnis: Jede neue Session startet blind, Benutzer muss Kontext manuell rekonstruieren
### Betroffene Komponenten
- `~/.pi/agent/extensions/session-index.ts` — Rolling Session Index
- `~/.pi/agent/extensions/session-header.ts` — Session-Namen setzen
- `~/.pi/agent/SESSION_INDEX.md` — generierter Index
- `<projekt>/SESSION_HANDOVER.md` — Handover-Datei
### Vermutete Ursachen
1. **Handover-Pfad-Auflösung:** Die Funktion `findHandover()` rekonstruiert den Projektpfad aus dem Verzeichnis-Slug. Für `--media-xray-NEU-Code-20260524--` muss sie `/media/xray/NEU/Code/20260524/SESSION_HANDOVER.md` finden — das klappt nicht.
2. **Session-Namen:** `getSessionName()` sucht nach `header.name` — aber die Session-Header-Extension setzt den Namen möglicherweise zu spät oder in einem anderen Feld.
### Nächste Schritte
1. Debug: Warum wird der Handover für das 20260524-Projekt nicht gefunden?
2. Debug: Warum bleiben Session-Namen auf „(unbenannt)"?
3. Fix und testen
---
## 🔴 Problem 6: OpenRouter DeepSeek-V4-Pro Provider-Routing (nicht wie erwartet)
**Erstmals dokumentiert:** 2026-05-24
**Status:** ❌ NICHT GELÖST
### Symptom
OpenRouter listet den nativen DeepSeek-Provider für `deepseek/deepseek-v4-pro` (preis: $0.435/1M Input), aber Requests mit `provider.only: ["deepseek"]` oder `provider.order: ["deepseek"]` schlagen mit 404 fehl: "No endpoints available matching your guardrail restrictions and data policy."
Andere Provider (DeepInfra $1.30, SiliconFlow $0.858, Together) funktionieren.
### Vermutete Ursache (Hypothesen)
- Privacy/Guardrail-Einstellungen auf https://openrouter.ai/settings/privacy blockieren den DeepSeek-Provider
- Geografische Einschränkung (DeepSeek-Provider nur in bestimmten Regionen)
- Account-Tier reicht nicht für diesen Provider
### Workaround (aktiv)
- `openRouterRouting` aus der Model-Config entfernt → OpenRouter wählt automatisch den günstigsten verfügbaren Provider
### Nächste Schritte
1. OpenRouter Privacy-Einstellungen prüfen
2. Im OpenRouter Dashboard nachsehen, ob DeepSeek-Provider für diesen Account verfügbar ist
3. Ggf. OpenRouter-Support kontaktieren
---
## 🟡 Problem 7: Git-Remotes mit HTTPS-Tokens statt SSH (Hygiene)
**Erstmals dokumentiert:** 2026-05-24
**Status:** ❌ NICHT ERLEIDGT
### Symptom
~35 Repos in `/media/xray/NEU/Code/` nutzen HTTPS-Remotes mit eingebetteten Forgejo-Tokens:
```
https://xray:TOKEN@forgejo.ccpn.cc/xray/repo.git
```
### Risiko
- Nicht akut (HTTPS verschlüsselt den Transport)
- Aber: Token liegt lesbar in `.git/config`, könnte versehentlich committed werden
- Cloudflare könnte HTTPS auf `forgejo.ccpn.cc` blockieren wie bei Port 2220
### Ziel
Alle Remotes auf SSH via `forgejo-git` umstellen:
```
ssh://git@forgejo-git/xray/repo.git
```
### Nächste Schritte
1. Skript schreiben, das alle HTTPS-Remotes erkennt und umstellt
2. Nach der Umstellung Tokens in Forgejo rotieren
3. Alte Tokens aus Infisical entfernen
## 🔴 Problem 9: Orchestrator startet eigenmächtig Änderungs-SubAgenten (NEU 2026-05-30)
**Erstmals dokumentiert:** 2026-05-30
**Status:** ❌ NICHT GELÖST (Regel eingeführt, muss sich bewähren)
### Symptom
Der Orchestrator erkennt ein Problem an einem funktionierenden System (z.B. Spracherkennung läuft auf CPU statt CUDA) und startet eigenmächtig — **ohne Rückfrage beim Benutzer** — einen SubAgenten, der das System überschreiben/modifizieren soll.
### Konkreter Vorfall (2026-05-30)
- **System:** CrowdNVIDIASpeech (Spracherkennung) — lief auf CPU statt CUDA nach Neustart
- **Problem:** ctranslate2 war als CPU-only-Version installiert, CUDA-Device-Count = 0
- **Mein Fehler:** Ich startete SubAgent "speech-cuda-fix" mit dem Auftrag, ctranslate2 neu zu installieren
- **Warum falsch:** Die Spracherkennung FUNKTIONIERTE (auf CPU, wenn auch langsam). Ein Eingriff riskierte eine funktionierende Lösung ohne Not
- **Gestoppt:** Der SubAgent wurde gestoppt, bevor er Änderungen vornahm
### Ursache
- Keine klare Regel, wann der Orchestrator eigenständig handeln darf
- Falsche Priorisierung: "Läuft nicht optimal" ≠ "Muss sofort gefixt werden"
- Fehlende Reflexion: „Ist das System aktuell funktional und gebrauchsfähig?" wurde nicht gestellt
### Regel (eingeführt 2026-05-30 in arbeitsweise.md §6)
**Der Orchestrator startet NIEMALS eigenmächtig einen SubAgenten, der eine funktionierende Lösung überschreiben, ersetzen oder modifizieren soll.**
Korrekter Ablauf:
1. Diagnostizieren (erlaubt)
2. Befund präsentieren (erlaubt)
3. Benutzer entscheiden lassen (PFLICHT)
4. Dann delegieren
### Bewährungsprüfung
- [ ] Orchestrator fragt vor Änderungen an funktionierenden Systemen zurück
- [ ] Kein unerlaubter SubAgent-Start in dieser Session
- [ ] Regel ist im Arbeitsgedächtnis verankert (nicht nur in Datei)
---
## 🟡 Problem 8: Intercom-Rückmeldung von Subagenten schlägt fehl (Orchestrator-ID)
**Erstmals dokumentiert:** 2026-05-29
**Status:** ❌ NICHT GELÖST
### Symptom
Subagenten versuchen, sich nach Abschluss per intercom an "orchestrator" zu melden.
Die Nachricht kommt nicht an, weil die echte Orchestrator-Session eine zufällige ID hat (z.B. `3c56b8b8`).
Fehler: `Message to "orchestrator" was not delivered: Session not found`
### Ursache
Der Subagent hat im instruktionstext kein Wissen über die aktuelle Orchestrator-Session-ID.
Er rät "orchestrator" als Ziel, aber das ist kein gültiger Session-Name/ID.
### Fix (2026-05-29)
- AGENTS.md aktualisiert: Jede Subagenten-Aufgabe MUSS die aktuelle Orchestrator-ID enthalten
- Template erweitert um INTERCOM-Abschnitt mit ID
- `subagent-start` wird nicht geändert (bleibt generisch), stattdessen Aufgabe mit ID anreichern
### Nächste Schritte
1. Alle Subagenten-Aufgaben-Templates prüfen ob sie die Orchestrator-ID enthalten
2. Ggf. `subagent-start`-Skript erweitern, dass es automatisch die Orchestrator-ID in die Aufgabe einflicht
3. Bewährungszeit: 3 Subagenten-Durchläufe ohne Fehlschlag
### Betroffene Stellen
- AGENTS.md — Abschnitt "Korrektes Sub-Agent-Muster" (aktualisiert)
- `SubAgenten`-Skript (/home/xray/bin/SubAgenten)
- Jede manuelle Subagenten-Start-Aufgabe
---
## 📋 Prozess bei Wiederauftreten eines Problems
1. Dieses Dokument öffnen
2. Datum des Wiederauftretens eintragen
3. Symptome dokumentieren (Log-Auszüge, Fehlermeldungen)
4. Nächsten Schritt definieren
5. CrowdBrain `persistent-issues.md` synchron aktualisieren

View file

@ -0,0 +1,46 @@
# SubAgent Auto-Check System
> Stand: 2026-06-02 | Automatisiert durch SubConfirm
## Wie es funktioniert
**SubConfirm** läuft als Hintergrund-Daemon und übernimmt die proaktive Erkennung.
Manuelles Polling ist nicht mehr nötig.
SubConfirm prüft alle 30 Sekunden alle tmux-Sessions auf **Stasis** (kein neuer Output).
Bei Stasis: sendet den vollständigen Pane-Inhalt via intercom an den Orchestrator.
Der Orchestrator beurteilt und reagiert — kein Keyword-Matching, volle Situationsbeurteilung.
## SubConfirm starten (Session-Start)
```bash
SubConfirm --skip "$(tmux display-message -p '#S')" &
```
Prüfen ob läuft:
```bash
pgrep -fa SubConfirm
```
Stoppen:
```bash
pkill -f SubConfirm
```
## Wenn eine STASIS-Meldung kommt
Siehe AGENTS.md → "SubConfirm — Reaktionslogik"
Kurzfassung:
- Bestätigungs-Dialog → Inhalt beurteilen → `tmux send-keys -t "<session>" "" Enter` (Yes) oder `""` (No)
- Laufende Operation → ignorieren
- Fehler → analysieren oder Aufgabe neu stellen
- Pi-Prompt sichtbar → ignorieren oder neue Teilaufgabe geben
## Manueller Check (Fallback wenn SubConfirm nicht läuft)
```bash
for s in $(tmux ls 2>/dev/null | cut -d: -f1); do
echo "=== $s ==="; tmux capture-pane -t "$s" -p | tail -5; echo
done
```