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