From fb3daab33f7744a2ff92103491ff5960085d9421 Mon Sep 17 00:00:00 2001 From: Raimund Bauer Date: Tue, 2 Jun 2026 11:53:37 +0200 Subject: [PATCH] feat/init: PiSystem Infrastruktur-Repo mit SubConfirm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .gitignore | 9 + agent/AGENTS.md | 397 ++++++++++++++++++++ agent/settings.json | 10 + bin/SubAgenten | 201 ++++++++++ bin/SubConfirm | 107 ++++++ bin/SubStatus | 49 +++ bin/SubWatcher | 81 ++++ bin/subagent-tab | 300 +++++++++++++++ extensions/arbeitsweise-guard.ts | 287 +++++++++++++++ extensions/confirm-deletion.ts | 613 +++++++++++++++++++++++++++++++ extensions/default-model.ts | 78 ++++ extensions/session-header.ts | 169 +++++++++ extensions/session-index.ts | 278 ++++++++++++++ extensions/vision-proxy.ts | 308 ++++++++++++++++ install.sh | 175 +++++++++ memory/arbeitsweise.md | 276 ++++++++++++++ memory/persistent-issues.md | 278 ++++++++++++++ memory/subagent-autocheck.md | 46 +++ 18 files changed, 3662 insertions(+) create mode 100644 .gitignore create mode 100644 agent/AGENTS.md create mode 100644 agent/settings.json create mode 100644 bin/SubAgenten create mode 100644 bin/SubConfirm create mode 100644 bin/SubStatus create mode 100644 bin/SubWatcher create mode 100644 bin/subagent-tab create mode 100644 extensions/arbeitsweise-guard.ts create mode 100644 extensions/confirm-deletion.ts create mode 100644 extensions/default-model.ts create mode 100644 extensions/session-header.ts create mode 100644 extensions/session-index.ts create mode 100644 extensions/vision-proxy.ts create mode 100644 install.sh create mode 100644 memory/arbeitsweise.md create mode 100644 memory/persistent-issues.md create mode 100644 memory/subagent-autocheck.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..100856d --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Keine Backups ins Repo +*.bak* +*.backup* +*.disabled + +# Keine Secrets +auth.json +*.key +*.pem diff --git a/agent/AGENTS.md b/agent/AGENTS.md new file mode 100644 index 0000000..ac8723a --- /dev/null +++ b/agent/AGENTS.md @@ -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.** + +``` +// +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- +``` + +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: + 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: +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-/` anlegen (isoliert vom Hauptprojekt) +2. `gnome-terminal --title="Subagent: "` 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 +``` +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- + ``` +2. **Terminal-Fenster öffnen (im isolierten Verzeichnis):** + ```bash + gnome-terminal --title="Subagent: " --working-directory=/tmp/subagent- -- 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 \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: + +SCOPE: +- +- + +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: + 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//` +- `~/.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 "" "" Enter` + → No (Pfeil runter, dann Enter): `tmux send-keys -t "" "Down" && tmux send-keys -t "" "" 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')" & + ``` diff --git a/agent/settings.json b/agent/settings.json new file mode 100644 index 0000000..97aa902 --- /dev/null +++ b/agent/settings.json @@ -0,0 +1,10 @@ +{ + "lastChangelogVersion": "0.78.0", + "defaultProvider": "ollama-cloud", + "defaultModel": "deepseek-v4-flash", + "defaultThinkingLevel": "minimal", + "hideThinkingBlock": false, + "packages": [ + "npm:pi-intercom" + ] +} \ No newline at end of file diff --git a/bin/SubAgenten b/bin/SubAgenten new file mode 100644 index 0000000..c7a8f79 --- /dev/null +++ b/bin/SubAgenten @@ -0,0 +1,201 @@ +#!/bin/bash +# SubAgenten — Starte einen Subagenten mit Pi in einem isolierten Verzeichnis +# +# Nutzung: SubAgenten [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// 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 (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 [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 \ No newline at end of file diff --git a/bin/SubConfirm b/bin/SubConfirm new file mode 100644 index 0000000..b2cf468 --- /dev/null +++ b/bin/SubConfirm @@ -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 diff --git a/bin/SubStatus b/bin/SubStatus new file mode 100644 index 0000000..60c42a9 --- /dev/null +++ b/bin/SubStatus @@ -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 "" diff --git a/bin/SubWatcher b/bin/SubWatcher new file mode 100644 index 0000000..2f37730 --- /dev/null +++ b/bin/SubWatcher @@ -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 diff --git a/bin/subagent-tab b/bin/subagent-tab new file mode 100644 index 0000000..07783be --- /dev/null +++ b/bin/subagent-tab @@ -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 [aufgabe] — Neuen Pane mit pi +# subagent-tab list — Alle Panes anzeigen +# subagent-tab kill — 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 < [aufgabe] Neuen Pane mit pi + subagent-tab spawn --provider P --model M [aufgabe] + subagent-tab list Alle Panes anzeigen + subagent-tab kill 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 ${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 diff --git a/extensions/arbeitsweise-guard.ts b/extensions/arbeitsweise-guard.ts new file mode 100644 index 0000000..c72c78c --- /dev/null +++ b/extensions/arbeitsweise-guard.ts @@ -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; + } +} diff --git a/extensions/confirm-deletion.ts b/extensions/confirm-deletion.ts new file mode 100644 index 0000000..12224ff --- /dev/null +++ b/extensions/confirm-deletion.ts @@ -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 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; + }); +} diff --git a/extensions/default-model.ts b/extensions/default-model.ts new file mode 100644 index 0000000..4cecbcc --- /dev/null +++ b/extensions/default-model.ts @@ -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 + } + }); +} \ No newline at end of file diff --git a/extensions/session-header.ts b/extensions/session-header.ts new file mode 100644 index 0000000..5a410bd --- /dev/null +++ b/extensions/session-header.ts @@ -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 { + 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; + }); +} diff --git a/extensions/session-index.ts b/extensions/session-index.ts new file mode 100644 index 0000000..86bbaf3 --- /dev/null +++ b/extensions/session-index.ts @@ -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 + } + }); +} diff --git a/extensions/vision-proxy.ts b/extensions/vision-proxy.ts new file mode 100644 index 0000000..073e2a5 --- /dev/null +++ b/extensions/vision-proxy.ts @@ -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 = { + ".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(); + + // 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 ==="); +} diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..35d7a54 --- /dev/null +++ b/install.sh @@ -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 "" diff --git a/memory/arbeitsweise.md b/memory/arbeitsweise.md new file mode 100644 index 0000000..b1cd112 --- /dev/null +++ b/memory/arbeitsweise.md @@ -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**: + +``` +/// +``` + +**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 `///` +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 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 -p -S -10 +``` + +`→ Yes / No` sichtbar → `tmux send-keys -t 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 -p -S -10 +# 2. Yes/No-Prompt → bestätigen +tmux send-keys -t 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 -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 diff --git a/memory/persistent-issues.md b/memory/persistent-issues.md new file mode 100644 index 0000000..d68ac0b --- /dev/null +++ b/memory/persistent-issues.md @@ -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 +- `/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 diff --git a/memory/subagent-autocheck.md b/memory/subagent-autocheck.md new file mode 100644 index 0000000..22ecb5e --- /dev/null +++ b/memory/subagent-autocheck.md @@ -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 "" "" 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 +```