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:
commit
fb3daab33f
18 changed files with 3662 additions and 0 deletions
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Keine Backups ins Repo
|
||||||
|
*.bak*
|
||||||
|
*.backup*
|
||||||
|
*.disabled
|
||||||
|
|
||||||
|
# Keine Secrets
|
||||||
|
auth.json
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
397
agent/AGENTS.md
Normal file
397
agent/AGENTS.md
Normal 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 3–4 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
10
agent/settings.json
Normal 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
201
bin/SubAgenten
Normal 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
107
bin/SubConfirm
Normal 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
49
bin/SubStatus
Normal 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
81
bin/SubWatcher
Normal 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
300
bin/subagent-tab
Normal 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
|
||||||
287
extensions/arbeitsweise-guard.ts
Normal file
287
extensions/arbeitsweise-guard.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
613
extensions/confirm-deletion.ts
Normal file
613
extensions/confirm-deletion.ts
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
78
extensions/default-model.ts
Normal file
78
extensions/default-model.ts
Normal 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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
169
extensions/session-header.ts
Normal file
169
extensions/session-header.ts
Normal 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 (5–12 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 5–10 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
278
extensions/session-index.ts
Normal 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
308
extensions/vision-proxy.ts
Normal 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
175
install.sh
Normal 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
276
memory/arbeitsweise.md
Normal 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
278
memory/persistent-issues.md
Normal 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
|
||||||
46
memory/subagent-autocheck.md
Normal file
46
memory/subagent-autocheck.md
Normal 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
|
||||||
|
```
|
||||||
Loading…
Add table
Reference in a new issue