From 3cf9ef928b43c262884aac59e70c094e37bb622c Mon Sep 17 00:00:00 2001 From: dschlueter Date: Mon, 15 Jun 2026 01:04:55 +0200 Subject: [PATCH] =?UTF-8?q?init:=20pi=5Fcoder=5Fv2=20=E2=80=94=20Single-Se?= =?UTF-8?q?rver=20(Option=20B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Beide Rollen (Coder + Judge) auf einem llama.cpp-Server (Port 8001). switchModel wechselt zwischen qwen3.5-coder und qwen3.5-judge via llama-cpp-single-Provider ohne Server-Neustart. start-single.sh startet den gemeinsamen Server, GPU wählbar per GPU_DEVICE-Env-Variable. Co-Authored-By: Claude Sonnet 4.6 --- BEDIENUNGSANLEITUNG.md | 948 ++++++++++++ README.md | 330 +++++ SINGLE_SERVER_PLAN.md | 150 ++ install_servers_and_pi_coder_extension.sh | 16 + models.json | 143 ++ pi-coder-judge-extension.ts | 1617 +++++++++++++++++++++ start-servers.sh | 30 + start-single.sh | 79 + status.sh | 34 + stop-servers.sh | 14 + 10 files changed, 3361 insertions(+) create mode 100644 BEDIENUNGSANLEITUNG.md create mode 100644 README.md create mode 100644 SINGLE_SERVER_PLAN.md create mode 100755 install_servers_and_pi_coder_extension.sh create mode 100644 models.json create mode 100644 pi-coder-judge-extension.ts create mode 100755 start-servers.sh create mode 100755 start-single.sh create mode 100755 status.sh create mode 100755 stop-servers.sh diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md new file mode 100644 index 0000000..7665547 --- /dev/null +++ b/BEDIENUNGSANLEITUNG.md @@ -0,0 +1,948 @@ +# Bedienungsanleitung: pi_coder + +pi_coder ist ein Werkzeug, das zwei lokale KI-Modelle als **Coder** und **Judge** einsetzt, +um Software automatisch zu schreiben, zu prüfen und zu verbessern — alles gesteuert über +einfache Slash-Kommandos in der pi-Agent-Oberfläche. + +--- + +## Inhaltsverzeichnis + +1. [Konzept: Coder und Judge](#1-konzept-coder-und-judge) +2. [Vorbereitung](#2-vorbereitung) +3. [Server starten und stoppen](#3-server-starten-und-stoppen) +4. [Neues Projekt anlegen](#4-neues-projekt-anlegen) +5. [Manueller Workflow: /coder → /judge → /fix → /shipit](#5-manueller-workflow) +6. [Automatischer Workflow: /optimize](#6-automatischer-workflow-optimize) (inkl. [Interactive-Modus](#interactive-modus)) +7. [Kleine Änderungen: /patch und /quick_check](#7-kleine-änderungen-patch-und-quick_check) +8. [Dokumentation generieren: /update_doku](#8-dokumentation-generieren-update_doku) +9. [Versionsverwaltung: /version](#9-versionsverwaltung-version) +10. [TASK.md verstehen und nutzen](#10-taskmd-verstehen-und-nutzen) +11. [Typische Anwendungsfälle](#11-typische-anwendungsfälle) +12. [Fehlermeldungen und Lösungen](#12-fehlermeldungen-und-lösungen) + +--- + +## 1. Konzept: Coder und Judge + +pi_coder verwendet zwei Rollen: + +**Coder** (Port 8001): Schreibt und repariert Code. Liest die Aufgabe aus `TASK.md`, +implementiert sie, führt Tests aus und erstellt Git-Commits. + +**Judge** (Port 8002): Überprüft den Code mit dem Blick eines skeptischen Senior-Entwicklers. +Prüft Korrektheit, Robustheit, Randfälle, Sicherheit und Produktionsreife. Gibt ein Urteil: +- `PASS` — Code ist in Ordnung +- `PASS WITH CONCERNS` — grundsätzlich akzeptabel, aber mit Anmerkungen +- `FAIL` — enthält Blocker, die behoben werden müssen + +Der Grundgedanke: Coder und Judge haben keine „Höflichkeitsschranke" zueinander — +der Judge kritisiert direkt und konkret, der Coder repariert ohne Widerspruch. + +--- + +## 2. Vorbereitung + +### Server starten + +```bash +cd ~/pi_coder +./start-servers.sh +``` + +Ausgabe bei Erfolg: +``` +[*] Starte beide Server parallel ... +[✓] Coder (:8001) bereit +[✓] Judge (:8002) bereit +``` + +Dauer: bis zu 5 Minuten (Modell wird in GPU-VRAM geladen; neuere llama.cpp-Versionen +prüfen beim Start die VRAM-Verfügbarkeit, was zusätzliche Zeit kostet). + +### Status prüfen + +```bash +./status.sh +``` + +``` +=== LLaMA-Server Status === +qwen36-27b-coder (Port 8001): Container=RUNNING HTTP=OK +qwen36-27b-judge (Port 8002): Container=RUNNING HTTP=OK +``` + +### pi agent öffnen + +pi agent im Projektverzeichnis starten — das ist das Verzeichnis, in dem dein Code liegt, +**nicht** `~/pi_coder`: + +```bash +cd ~/MeinProjekt +pi +``` + +--- + +## 3. Server starten und stoppen + +### GPU-Zuordnung (3-GPU-Setup) + +Bei einem System mit drei GPUs sind die Rollen fest in den Startskripten verankert — +**keine manuelle Konfiguration nötig:** + +| GPU | Modell | Rolle | +|-----|--------|-------| +| GPU 0 (NVIDIA T600) | — | Display / Monitor — wird von den Skripten nicht angetastet | +| GPU 1 (RTX 3090, 24 GB) | Qwen3.6-27B | **Coder** (Port 8001) | +| GPU 2 (RTX 3090, 24 GB) | Qwen3.6-27B | **Judge** (Port 8002) | + +Jeder Server bekommt seine eigene GPU exklusiv — so gibt es keinen VRAM-Konflikt, +auch wenn beide gleichzeitig laufen. + +**Wichtig:** Andere GPU-Prozesse (z. B. TTS-Dienste, Ollama) auf GPU 1 oder GPU 2 können +VRAM belegen und dazu führen, dass ein Server nicht startet. Im Zweifel prüfen: +```bash +nvidia-smi --query-gpu=index,name,memory.used,memory.free --format=csv +``` + +### Beide starten (empfohlen) + +```bash +cd ~/pi_coder +./start-servers.sh +``` + +Erwartete Ausgabe: +``` +[*] Starte beide Server parallel ... +[✓] Coder (:8001) bereit +[✓] Judge (:8002) bereit +``` + +Dauer: bis zu 5 Minuten. Danach sind beide Modelle im VRAM und sofort antwortbereit. + +### Einzelnen Server neu starten + +Z.B. wenn nur der Judge-Server abgestürzt ist: + +```bash +./start-judge.sh # Port 8002, GPU 2 +# oder: +./start-coder.sh # Port 8001, GPU 1 +``` + +### Beide stoppen + +```bash +./stop-servers.sh +``` + +### Status prüfen + +```bash +./status.sh +``` + +### Alternativer Modellpfad + +Falls die GGUF-Datei an einem anderen Ort liegt: + +```bash +HF_HOME=/mnt/daten/huggingface ./start-servers.sh +``` + +--- + +## 4. Neues Projekt anlegen + +### Kommando + +``` +/new_project +``` + +### Beispiel + +``` +/new_project ~/Python_Programs/mein_tool +``` + +Was passiert: +- Verzeichnis `~/Python_Programs/mein_tool` wird angelegt +- `git init` wird ausgeführt +- `.gitignore` wird mit Standardeinträgen angelegt und committed + +**Wichtig:** pi agent wechselt **nicht automatisch** in das neue Verzeichnis — +die Session bleibt im aktuellen Verzeichnis. Nach dem Anlegen: + +```bash +cd ~/Python_Programs/mein_tool +pi +``` + +Dann kannst du `/coder` oder `/optimize` mit dem neuen Projekt verwenden. + +--- + +## 5. Manueller Workflow + +Der manuelle Workflow gibt dir volle Kontrolle über jeden Schritt. + +### Schritt 1: /coder — Aufgabe übergeben + +``` +/coder +``` + +Der Coder: +1. Legt `TASK.md` im aktuellen Verzeichnis an (oder hängt an bestehende an) +2. Liest `TASK.md` und implementiert den Auftrag +3. Führt Tests oder Build-Checks aus +4. Erstellt einen Git-Commit + +**Beispiel:** + +``` +/coder Schreibe ein Python-Kommandozeilenprogramm 'textcount'. Es soll eine Textdatei als Argument nehmen und folgendes ausgeben: Anzahl Zeichen, Wörter, Zeilen und die 5 häufigsten Wörter (ohne Stoppwörter). +``` + +Typische Ausgabe des Coders: +``` +Implementierung abgeschlossen. +- src/textcount.py erstellt (Hauptprogramm) +- tests/test_textcount.py erstellt (Unit-Tests) +- requirements.txt angelegt (keine externen Abhängigkeiten) +- Alle 8 Tests bestanden +- Commit: feat: implement textcount CLI tool +Risiken: Stoppwortliste nur Deutsch/Englisch, keine Konfigurations-Option. +``` + +### Schritt 2: /judge — Code überprüfen lassen + +``` +/judge +``` + +Optionaler Fokus: +``` +/judge Besonderes Augenmerk auf Fehlerbehandlung und Edge Cases +``` + +Der Judge: +1. Liest `TASK.md` und prüft ob alle Anforderungen umgesetzt sind +2. Analysiert `git show HEAD` +3. Führt Tests aus +4. Gibt ein strukturiertes Urteil aus + +**Beispiel-Ausgabe PASS:** +``` +Urteil: PASS WITH CONCERNS + +Blocker: keine + +Major: +- Stoppwortliste ist hardcoded; große Projekte erwarten --stopwords-file Option + +Minor: +- Keine --version Flag +- Fehlermeldung bei nicht-existenter Datei gibt keinen Exit-Code 1 zurück + +Fehlende Tests: +- Test für leere Datei fehlt +- Test für Datei mit nur Leerzeichen fehlt + +Produktionsrisiken: +- Bei sehr großen Dateien (>1 GB) wird alles in den RAM geladen + +Konkrete Fix-Aufträge: +1. exit(1) bei FileNotFoundError +2. Test für leere Eingabedatei +``` + +**Beispiel-Ausgabe FAIL:** +``` +Urteil: FAIL + +Blocker: +- textcount.py importiert 'collections.Counter' aber das ist nicht installiert + (Counter ist stdlib, aber der Import-Fehler tritt bei Python < 3.9 auf) +- ./textcount.py existiert nicht — tests/test_textcount.py schlägt komplett fehl + +Major: ... +``` + +### Schritt 3: /fix — Kritik beheben + +``` +/fix +``` + +Optionaler Hinweis: +``` +/fix Den Major-Punkt mit der Stoppwortliste kannst du weglassen, das ist kein Produktionsprojekt +``` + +Der Coder arbeitet die Judge-Kritik ab (Blocker zuerst, dann Major, dann Minor) +und erstellt einen neuen Commit. + +### Schritt 4: /shipit — Finale Freigabe + +``` +/shipit +``` + +Der Judge gibt ein finales Urteil: +- `SHIP` — bereit für Produktion +- `NO-SHIP` — noch Probleme offen + +**Beispiel:** +``` +Urteil: SHIP + +Letzte Blocker: keine + +Restrisiken: +- Kein Streaming für sehr große Dateien (dokumentiert in README) + +Empfohlene Sofortmaßnahmen: keine +``` + +--- + +## 6. Automatischer Workflow: /optimize + +`/optimize` führt den gesamten Coder→Judge→Fix-Zyklus automatisch durch. + +### Syntax + +``` +/optimize [--rounds N] [--with-doku] [--continue] [--interactive] + [--no-tests] [--approve-concerns] [--test-cmd "cmd"] [--test-timeout N] +``` + +- `--rounds N` — maximale Anzahl Runden (Standard: 2) +- `--with-doku` — nach SHIP automatisch `/update_doku` ausführen +- `--continue` — überspringt die Implementierungsphase und startet direkt mit dem + Judge→Fix-Zyklus ab dem aktuellen Code-Stand. Nützlich wenn man bereits manuell + `/coder`, `/judge` und `/fix` durchgeführt hat und den Rest automatisieren möchte. + Im `--continue`-Modus werden Coder- und Judge-Server gleichzeitig geprüft. +- `--interactive` — pausiert nach erstem PASS für einen menschlichen Checkpoint. + Details: siehe [Interactive-Modus](#interactive-modus) weiter unten. +- `--no-tests` — überspringt die automatische Test-Erkennung. Sinnvoll wenn keine + Test-Suite vorhanden ist oder Tests über externe Infrastruktur laufen. +- `--approve-concerns` — behandelt „PASS WITH CONCERNS" wie „PASS": kein ShipIt-Call, + direktes SHIP. Für Projekte, bei denen du dem Judge-Urteil vertraust. +- `--test-cmd "befehl"` — überschreibt die automatische Test-Erkennung mit einem + eigenen Befehl (z.B. `--test-cmd "pytest tests/ -x"`). +- `--test-timeout N` — maximale Laufzeit pro Test-Befehl in Sekunden (Standard: 120). + +### Beispiel: einfacher Auftrag + +``` +/optimize Schreibe ein Rust-Programm 'genpw' das sichere Passwörter generiert. Optionen: --length N (Standard 16), --count N (Standard 1), --no-symbols, --no-numbers. +``` + +Was im Hintergrund passiert: +``` +Phase 1: Coder implementiert... +Phase 2: Runde 1/2: Quick-Check (kompakter Erstcheck)... + → Urteil: FAIL (2 Blocker) +Phase 3: Runde 1/2: Coder fixt... +Phase 4: Runde 2/2: Judge — TASK.md + letzter Commit + Tests... + → Urteil: PASS WITH CONCERNS +✓ PASS WITH CONCERNS nach Runde 2 +Finale ShipIt-Prüfung... (nur bei PASS WITH CONCERNS, nicht bei klarem PASS) + → SHIP +[Dialog: Version → v0.1.0 (empfohlen)] +``` + +**Runde 1 = Quick-Check:** Kompakter Prompt ohne TASK.md-Analyse — erkennt offensichtliche +Fehler schnell. Erst ab Runde 2 (oder bei `--continue`) kommt der vollständige Judge-Prompt. + +Bei klarem `PASS` entfällt die ShipIt-Runde — es wird direkt SHIP ausgelöst. +Mit `--approve-concerns` gilt das auch für `PASS WITH CONCERNS`. + +Während des Ablaufs zeigt die Statuszeile die aktuelle Phase — mit laufendem Timer, +der beweist, dass die LLM tatsächlich arbeitet (und nicht hängt): + +``` +◉ Coder implementiert: Login-Flow mit JWT [01:23] +◉○ Runde 1/2: Quick-Check [00:47] +●◉ Runde 2/2: Coder fixt — fehlendes Null-Check [01:05] +●● ✓ PASS nach Runde 2/2 — ShipIt… +🚀 SHIP – produktionsreif +``` + +Steht der Timer still, hängt der Prozess — dann `/cancel` verwenden. + +### Beispiel: mehr Runden + +``` +/optimize Implementiere einen vollständigen REST-API-Client für die GitHub API in Python mit Rate-Limiting, Retry-Logic und Caching --rounds 5 +``` + +### Beispiel: mit automatischer Dokumentation + +``` +/optimize Schreibe ein Go-Tool 'logfilter' das Logdateien nach Regex-Muster filtert --with-doku +``` + +Nach SHIP werden automatisch ausgeführt: +1. Code-Kommentare einfügen +2. README.md schreiben +3. BEDIENUNGSANLEITUNG.md schreiben + +### Vom manuellen Workflow in den automatischen wechseln + +Du hast bereits `/coder`, `/judge` und `/fix` manuell durchgeführt und möchtest +den Rest automatisch ablaufen lassen: + +``` +/optimize --continue +``` + +``` +/optimize --continue --rounds 5 +``` + +``` +/optimize --continue --with-doku +``` + +Die Implementierungsphase wird übersprungen — der Judge prüft sofort den aktuellen +Stand und der Fix-Zyklus läuft automatisch bis PASS oder max. N Runden. + +### Loop-Erkennung + +Wenn zweimal hintereinander genau dieselben Blocker auftreten, bricht `/optimize` ab: +``` +⚠ Derselbe Blocker tritt erneut auf – Schleife abgebrochen. Bitte manuell prüfen. +``` + +In diesem Fall: `/judge` manuell ausführen, Blocker lesen, mit `/fix` manuell eingreifen. + +### Max. Runden ohne PASS + +``` +⚠ 2 Runden durchlaufen ohne PASS. Bitte manuell prüfen. +``` + +Dann: `/judge` und `/fix` manuell für gezielte Eingriffe. +Mit `--rounds N` kann die Grenze hochgesetzt werden, z.B. `--rounds 5` für komplexe Aufgaben. + +### Interactive-Modus + +Mit `--interactive` pausiert `/optimize` nach dem ersten PASS und wartet auf menschliches +Feedback — bevor das abschließende SHIP ausgelöst wird. + +``` +/optimize Implementiere Feature X --interactive +``` + +Typischer Ablauf: +``` +Phase 1: Coder implementiert... +Phase 2: Judge prüft... + → Urteil: PASS +⏸ PASS erreicht. Weitere Features? /continue "Zusatzauftrag" — oder /continue zum Shippern. +``` + +Jetzt hast du drei Optionen: + +**Option A: Direkt shippern** +``` +/continue +``` +→ ShipIt wird gestartet, Version-Dialog erscheint. + +**Option B: Zusatzauftrag hinzufügen** +``` +/continue "Füge außerdem eine --verbose Option hinzu" +``` +→ Coder implementiert den Zusatz, dann läuft der Judge-Loop erneut an. +→ Nach erneutem PASS erscheint der Checkpoint wieder — du kannst beliebig viele + Iterationen anhängen, bevor du mit `/continue` zum SHIP gehst. + +**Option C: Abbrechen** +``` +/cancel +``` +→ Loop wird abgebrochen, kein SHIP. + +**Timeout:** Wenn du 30 Minuten lang nichts eingibst, bricht `/optimize` automatisch ab. + +**Wann ist `--interactive` sinnvoll?** +- Wenn der Auftrag aus mehreren voneinander abhängigen Features besteht +- Wenn du nach jeder fertigen Stufe entscheiden möchtest, ob du weitermachst +- Wenn du sicherstellen willst, dass nichts unbeabsichtigt committed wird + +--- + +## 7. Kleine Änderungen: /patch und /quick_check + +Für minimale Korrekturen — kein voller Review-Zyklus, keine TASK.md-Änderungen. + +### /patch — kleine Änderung umsetzen + +``` +/patch +``` + +Der Coder ändert **ausschließlich** das Beschriebene, prüft ob es noch kompiliert/startet +und erstellt einen Commit. + +**Beispiele:** + +``` +/patch Mindestpasswortlänge von 4 auf 8 Zeichen erhöhen +``` + +``` +/patch Fehlermeldung bei ungültigem Argument von stderr auf stdout umleiten +``` + +``` +/patch Versionsnummer in Cargo.toml von 0.1.0 auf 0.2.0 erhöhen +``` + +``` +/patch Die Funktion parse_args() soll bei fehlendem --input-Argument eine sinnvolle Hilfsnachricht ausgeben statt zu paniken +``` + +### /quick_check — Änderung schnell prüfen lassen + +``` +/quick_check [was geprüft werden soll] +``` + +Der Judge schaut sich `git show HEAD` an und gibt nur `OK` oder `PROBLEM` zurück. + +**Beispiele:** + +``` +/quick_check +``` + +``` +/quick_check Prüfe ob die Mindestlängen-Änderung korrekt umgesetzt ist und keine Randfälle fehlen +``` + +**Typische Ausgaben:** + +``` +Urteil: OK + +Die Änderung in src/main.rs Zeile 47 ist korrekt. Mindestlänge wird jetzt +sowohl bei --length als auch im Standardfall geprüft. +``` + +``` +Urteil: PROBLEM + +src/lib.rs Zeile 23: Der neue Mindestwert von 8 wird nur bei --length geprüft, +nicht beim Standardwert (16). Wenn jemand --length 6 übergibt, schlägt die +Validierung korrekt fehl, aber der Standardfall ist nicht abgedeckt. +Fix: Validierung in die Funktion generate_password() verschieben statt in parse_args(). +``` + +### Typischer /patch + /quick_check Workflow + +``` +/patch Timeout bei HTTP-Requests von 30 auf 10 Sekunden setzen +``` +*(Coder ändert, committet)* + +``` +/quick_check Prüfe ob der Timeout auch bei Retry-Versuchen korrekt gilt +``` +*(Judge gibt OK oder zeigt konkretes Problem)* + +--- + +## 8. Dokumentation generieren: /update_doku + +Nach Abschluss der Entwicklung (nach `/shipit` oder `/optimize`) erstellt `/update_doku` +drei Dinge automatisch: + +1. **Code-Kommentare** — erklärt das WARUM in den Quelldateien (Deutsch) +2. **README.md** — Entwicklerperspektive: Installation, Build, Verwendung +3. **BEDIENUNGSANLEITUNG.md** — Endnutzerperspektive: einfach, ohne Jargon + +``` +/update_doku +``` + +### Inkrementelles Update + +`/update_doku` merkt sich via Git-Tags welche Dateien seit dem letzten Lauf geändert wurden. +Nur geänderte Quelldateien werden neu kommentiert — unveränderte bleiben unangetastet. + +``` +Code-Kommentare: keine Änderungen seit letztem Lauf – übersprungen. +README.md: 2 Datei(en) geändert – wird geprüft +BEDIENUNGSANLEITUNG.md: 2 Datei(en) geändert – wird geprüft +``` + +### Zusammen mit /optimize + +``` +/optimize Implementiere Feature X --with-doku +``` + +Führt nach SHIP automatisch `/update_doku` aus. + +--- + +## 9. Versionsverwaltung: /version + +pi_coder verwaltet Versionsnummern im SemVer-Format (`vMAJOR.MINOR.PATCH`) automatisch — +basierend auf den Commit-Messages des generierten Codes. + +### Wie Commit-Messages die Version bestimmen + +Der Coder verwendet standardmäßig das Conventional-Commits-Format: + +| Commit-Prefix | Beispiel | Bump-Typ | +|---|---|---| +| `feat!:` oder `BREAKING CHANGE` | `feat!: API komplett überarbeitet` | major (v1.0.0 → v2.0.0) | +| `feat:` | `feat: CSV-Export hinzugefügt` | minor (v1.0.0 → v1.1.0) | +| `fix:`, `chore:`, andere | `fix: Crash bei leerer Datei` | patch (v1.0.0 → v1.0.1) | + +### Automatisch nach SHIP + +Nach einem erfolgreichen SHIP-Verdikt in `/optimize` oder `/shipit` erscheint automatisch +ein Dialog: + +``` +┌─ Version ──────────────────────────────────────────────┐ +│ Aktuelle Version: v1.2.3. Commits seit letztem Tag: │ +│ minor-Bump erkannt. │ +│ │ +│ patch → v1.2.4 │ +│ minor → v1.3.0 (empfohlen) │ +│ major → v2.0.0 │ +│ Überspringen │ +└─────────────────────────────────────────────────────────┘ +``` + +Du kannst den empfohlenen Wert bestätigen oder manuell einen anderen wählen. + +### Manuell aufrufen + +``` +/version +``` + +Nützlich wenn du den Tag nachträglich setzen möchtest oder nach manuellen Commits. + +### Was passiert nach der Auswahl + +1. Die Versionsnummer wird in die Projekt-Manifest-Datei geschrieben (falls vorhanden): + - `package.json` → `npm version --no-git-tag-version X.Y.Z` + - `Cargo.toml` → `version = "X.Y.Z"` in `[package]` + - `pyproject.toml` → `version = "X.Y.Z"` in `[project]` + - `VERSION` → Dateiinhalt `vX.Y.Z` +2. Commit: `chore: bump version to vX.Y.Z` +3. Git-Tag: `vX.Y.Z` wird gesetzt + +Wenn keine der genannten Dateien vorhanden ist, wird nur der Git-Tag gesetzt. + +### Erstes Mal — kein Tag vorhanden + +``` +┌─ Version ──────────────────────────────────────────────┐ +│ Noch kein Versions-Tag vorhanden. │ +│ │ +│ patch → v0.0.1 │ +│ minor → v0.1.0 (empfohlen) │ +│ major → v1.0.0 │ +│ Überspringen │ +└─────────────────────────────────────────────────────────┘ +``` + +Empfehlung: `v0.1.0` für ein frisches, funktionierendes Projekt; `v1.0.0` wenn es +sofort produktionsreif ist. + +--- + +## 10. TASK.md verstehen und nutzen + +`TASK.md` ist die persistente Aufgabenbeschreibung im Projektverzeichnis. Sie wird von +allen Kommandos als Referenz gelesen. + +### Erstellt von /coder und /optimize + +Beim ersten `/coder`-Aufruf: +```markdown +# Aufgabe + +Schreibe ein Python-Kommandozeilenprogramm 'textcount'... + +## Erstellt +2026-05-19T14:30:00.000Z + +## Status +- [ ] Implementierung +- [x] Review bestanden (PASS) +- [ ] Produktionsreif (SHIP) +``` + +### Zusatzauftrag hinzufügen + +Wenn du später `/coder` mit einer neuen Aufgabe aufrufst, wird TASK.md erweitert statt überschrieben: + +``` +/coder Füge zusätzlich eine --csv-Option hinzu, die das Ergebnis als CSV ausgibt +``` + +```markdown +# Aufgabe + +[...ursprüngliche Aufgabe...] + +--- + +## Zusatzauftrag + +2026-05-19T15:45:00.000Z + +Füge zusätzlich eine --csv-Option hinzu... + +## Status +- [ ] Implementierung +- [ ] Review bestanden (PASS) +- [ ] Produktionsreif (SHIP) +``` + +### Status-Checkboxen + +Die Checkboxen werden automatisch abgehakt: +- `[x] Implementierung` — nach erfolgreichem `/coder` oder `Phase 1` von `/optimize` +- `[x] Review bestanden (PASS)` — nach PASS durch `/judge` oder in `/optimize` +- `[x] Produktionsreif (SHIP)` — nach SHIP durch `/shipit` oder `/update_doku` + +--- + +## 11. Typische Anwendungsfälle + +### Neues Rust-Programm von Null + +```bash +# 1. Verzeichnis anlegen +/new_project ~/Rust_Programs/mein_tool + +# 2. Terminal: in Verzeichnis wechseln und pi neu starten +# cd ~/Rust_Programs/mein_tool && pi + +# 3. In pi: vollautomatisch implementieren + dokumentieren +/optimize Schreibe ein Rust-CLI-Tool 'csvfilter' das CSV-Dateien zeilenweise filtert. Optionen: --column NAME, --value WERT, --regex. Ausgabe auf stdout. --with-doku +``` + +### Bestehendes Projekt verbessern + +```bash +# In pi, im Projektverzeichnis: +/coder Refaktoriere die Datenbankschicht: ersetze das raw-SQL durch sqlx mit typsicheren Queries. Alle Tests müssen danach noch laufen. +/judge +/fix +/shipit +``` + +### Schnelle Bugfixes + +```bash +/patch Die Funktion split_csv() schlägt bei Feldern mit eingebetteten Kommas fehl (RFC 4180 nicht implementiert) +/quick_check +``` + +### Repo ohne Test-Suite oder mit externer CI + +```bash +# Test-Erkennung überspringen — Judge bewertet nur den Code +/optimize "Implementiere Feature X" --no-tests + +# Externe Test-Suite explizit angeben +/optimize "Implementiere Feature X" --test-cmd "make integration-test" +``` + +### Schneller Loop ohne ShipIt-Runde + +```bash +# Für Projekte wo "PASS WITH CONCERNS" ausreicht: +/optimize "Kleines Refactoring" --approve-concerns + +# Kombination: kein Test, kein ShipIt bei Concerns, 1 Runde +/optimize "Typo-Fix in Fehlermeldungen" --rounds 1 --no-tests --approve-concerns +``` + +### Versionsnummer nach der Entwicklung setzen + +```bash +# Nach SHIP: Dialog erscheint automatisch +/optimize Neues Feature X --rounds 2 +# → SHIP → Dialog → "minor → v1.1.0" wählen → Tag gesetzt + +# Oder manuell: +/version +``` + +### Kommentarlosen Legacy-Code dokumentieren + +```bash +# Nur Kommentare und Dokumentation, kein Code ändern: +/update_doku +``` + +### Schrittweise mit manuellem Review + +```bash +/coder Implementiere OAuth2-Login mit GitHub +# → Code lesen, verstehen +/judge Besonderes Augenmerk auf Token-Speicherung und CSRF-Schutz +# → Judge-Bericht lesen +/fix Ignoriere den Minor-Punkt mit der Logging-Verbosität, das ist Absicht +/shipit +``` + +### Experiment: mehrere Runden explizit + +```bash +/optimize Schreibe einen vollständigen Markdown-Parser mit AST in Python --rounds 5 +``` + +### Schrittweise Features hinzufügen mit --interactive + +```bash +# Erst Grundgerüst implementieren und PASS abwarten: +/optimize Schreibe ein CLI-Tool 'filewatch' das Dateiänderungen überwacht --interactive + +# Nach PASS erscheint: ⏸ PASS – warte auf /continue… + +# Option A: Genug, direkt shippern: +/continue + +# Option B: Weiteres Feature anhängen: +/continue "Füge außerdem einen --filter GLOB-Parameter hinzu" +# → Coder implementiert, Judge prüft erneut, PASS → Checkpoint wieder aktiv + +# Nochmal erweitern: +/continue "Füge --output-log DATEI hinzu um Änderungen zu protokollieren" + +# Fertig → shippern: +/continue +``` + +--- + +## 12. Fehlermeldungen und Lösungen + +### "Modell-Datei nicht gefunden" + +``` +[!] Modell-Datei nicht gefunden: /home/.../models/qwen3/Qwen3.6-27B-Uncensored-...gguf +``` + +**Ursache:** Die GGUF-Datei liegt nicht am erwarteten Ort. + +**Lösung:** +```bash +# Pfad prüfen: +ls $HF_HOME/models/qwen3/ + +# Oder mit explizitem Pfad starten: +HF_HOME=/korrekter/pfad ./start-servers.sh +``` + +### Server startet nicht / HTTP nicht erreichbar + +``` +[!] HTTP-Server wurde nicht rechtzeitig erreichbar. +``` + +**Ursachen und Lösungen:** + +1. Zu wenig VRAM — Container bricht beim Laden ab: + ```bash + docker logs qwen36-27b-coder | tail -50 + # Suche nach: "CUDA out of memory" oder "failed to allocate" + ``` + → Kontext reduzieren: `-c 32768` statt `-c 131072` + +2. GPU nicht verfügbar: + ```bash + nvidia-smi # alle drei GPUs sichtbar? + # GPU 1 (Coder) testen: + docker run --gpus '"device=1"' --rm nvidia/cuda:12.0-base nvidia-smi + # GPU 2 (Judge) testen: + docker run --gpus '"device=2"' --rm nvidia/cuda:12.0-base nvidia-smi + ``` + +3. Port bereits belegt: + ```bash + ss -tlnp | grep 800[12] + docker ps -a # alter Container noch vorhanden? + ./stop-servers.sh + ./start-servers.sh + ``` + +### "Agent is already processing a prompt" + +**Ursache:** Ein Kommando wurde aufgerufen während pi agent noch auf eine Antwort wartet. + +**Lösung:** Warten bis die aktuelle Antwort fertig ist, dann das Kommando wiederholen. +Bei `/optimize` passiert das automatisch — der interne Mechanismus wartet auf `idle`. + +### "edits[n] ... oldText must match exactly" + +**Ursache:** Der interne pi-agent-Edit-Mechanismus hat beim Anwenden mehrerer Änderungen +an derselben Datei versagt. + +**Was pi_coder dagegen tut:** Ein `tool_call`-Hook in der Extension sortiert +Mehrfach-Edits automatisch von hinten nach vorne (Bottom-up-Reordering), sodass +frühere Edits spätere Positionen nicht verschieben. Zusätzlich steht das `apply_patch`-Tool +bereit, das GNU `patch -p1` mit Fuzzy-Matching nutzt. + +**Falls es trotzdem auftritt:** Das Modell manuell anweisen: +``` +Lies die Datei neu ein und wende die Änderungen als unified diff mit apply_patch an. +``` + +### "N Runden ohne PASS" / Loop-Erkennung schlägt an + +``` +⚠ Derselbe Blocker tritt erneut auf – Schleife abgebrochen. +``` + +**Ursache:** Der Coder kann einen bestimmten Blocker nicht beheben — z.B. weil die +Aufgabe einen Widerspruch enthält oder ein externes System fehlt. + +**Lösung:** Manuell eingreifen: +``` +/judge ← Judge-Bericht lesen +``` +Dann den Blocker analysieren und entweder: +- `/fix Ignoriere Blocker X, das ist nicht Teil dieser Aufgabe` +- Den Code selbst anpassen und dann `/fix` aufrufen +- Die Aufgabe in TASK.md präzisieren +- Bei komplexen Aufgaben mit mehr Runden wiederholen: `/optimize --continue --rounds 5` + +### Server läuft, aber pi wechselt nicht das Modell + +**Ursache:** `models.json` wurde nach einer Änderung nicht neu deployt. + +**Lösung:** +```bash +cd ~/pi_coder +./install_servers_and_pi_coder_extension.sh +# Dann /reload in pi agent +``` + +### "Neues Projekt" wechselt nicht das Verzeichnis + +Das ist gewollt — pi-Sessions sind an ihr Startverzeichnis gebunden. +Nach `/new_project ` im Terminal: +```bash +cd +pi +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..d5755b7 --- /dev/null +++ b/README.md @@ -0,0 +1,330 @@ +# pi_coder — Automatisierter Coder/Judge-Workflow für pi agent + +Dieses Repository enthält die Konfiguration und Skripte für einen automatisierten +Coding-Workflow mit zwei lokalen LLaMA-Modellen: ein Coder-Modell und ein Judge-Modell, +gesteuert über [pi agent](https://github.com/earendil-works/pi). + +--- + +## Überblick + +``` +Nutzer gibt Auftrag + │ + ▼ + /coder → qwen3.5-coder (:8001) → Implementierung + git commit + │ + ▼ + /judge → qwen3.5-judge (:8002) → Review: PASS / FAIL + Blocker + │ + FAIL? ▼ + /fix → qwen3.5-coder (:8001) → Fixes + git commit + │ + PASS? ▼ + /shipit → qwen3.5-judge (:8002) → Finale Freigabe: SHIP / NO-SHIP + (nur bei "PASS WITH CONCERNS" — klares PASS → direkt SHIP) + + /optimize = Coder→Judge→Fix-Schleife automatisch (bis PASS oder max. N Runden) + --interactive: pausiert nach PASS für menschlichen Checkpoint + optionale Zusatzaufträge +``` + +Beide Modelle laufen als **separate llama.cpp-Docker-Container** und sprechen eine +OpenAI-kompatible API (`/v1/chat/completions`). pi agent wechselt automatisch zwischen +den Endpunkten wenn du ein `/judge`-, `/fix`- oder `/coder`-Kommando aufrufst. + +--- + +## Modelle + +| Rolle | Modell | Port | Container | Alias | GPU | +|--------|---------------------------------------------------|------|------------------|----------------|----------| +| Coder | Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS | 8001 | qwen36-27b-coder | qwen3.5-coder | device=1 | +| Judge | Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS | 8002 | qwen36-27b-judge | qwen3.5-judge | device=2 | + +Beide Container verwenden dasselbe GGUF-Datei, aber mit unterschiedlichen +Serverparametern (Kontext, Temperatur, Parallelität). + +--- + +## Voraussetzungen + +- Docker mit NVIDIA-GPU-Support: + ```bash + # NVIDIA Container Toolkit installieren (falls nicht vorhanden) + distribution=$(. /etc/os-release; echo $ID$VERSION_ID) + curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add - + curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list \ + | sudo tee /etc/apt/sources.list.d/nvidia-docker.list + sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit + sudo systemctl restart docker + ``` +- NVIDIA-GPUs: empfohlen 2 × RTX 3090 (je 24 GB VRAM) für Coder und Judge parallel. + Bei 3 GPUs: GPU 0 für Display reservieren, GPU 1 → Coder, GPU 2 → Judge. +- GGUF-Modell vorhanden unter: + `$HF_HOME/models/qwen3/Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS.gguf` + - Standard-Pfad: `HF_HOME=/home/dschlueter/nvme2n1p7_home/huggingface` + - Überschreibbar: `HF_HOME=/anderer/pfad ./start-servers.sh` +- [pi agent](https://github.com/earendil-works/pi) installiert (`~/.pi/`) + +--- + +## Installation + +```bash +# 1. Repository klonen +git clone https://kitux.de/forgejo/dschlueter/pi_coder.git ~/pi_coder +cd ~/pi_coder + +# 2. Extension und Modell-Config nach ~/.pi/agent/ deployen +./install_servers_and_pi_coder_extension.sh + +# 3. pi agent neu laden (in der pi-Oberfläche) +# /reload + +# 4. Server starten +./start-servers.sh +``` + +Nach späteren Änderungen an `pi-coder-judge-extension.ts` oder `models.json`: +```bash +./install_servers_and_pi_coder_extension.sh # kopiert nach ~/.pi/agent/ +# dann /reload in pi agent +``` + +--- + +## Server starten / stoppen / status + +```bash +# Beide Server parallel starten (empfohlen — dauert 1–3 Minuten) +./start-servers.sh + +# Einzeln starten (z.B. nur einen neu starten) +./start-coder.sh # Port 8001 +./start-judge.sh # Port 8002 + +# Beide stoppen +./stop-servers.sh + +# Status beider Server prüfen +./status.sh +``` + +`start-servers.sh` startet beide Container gleichzeitig und wartet bis beide +HTTP-ready sind (max. 5 Minuten — die neue llama.cpp-Version prüft beim Start, ob +Modell + KV-Cache in den VRAM passen, was zusätzliche Zeit kostet). Logs werden +getrennt gesammelt und nur bei Fehler ausgegeben. + +Wenn Server bereits laufen und du `start-servers.sh` (oder ein Einzelskript) +aufrufst, werden die laufenden Container zuerst per `docker rm -f` gestoppt +und dann neu gestartet — ein laufender Inference-Request wird dabei abgebrochen. + +--- + +## llama.cpp-Serverparameter im Detail + +### Gemeinsame Parameter + +| Parameter | Wert | Bedeutung | +|---|---|---| +| `--jinja` | — | Verwendet das im GGUF eingebettete Jinja-Chat-Template (Qwen-Format). Notwendig für korrekte `<\|im_start\|>`-Tokens. | +| `--reasoning on` | — | Aktiviert das interne Thinking-Budget des Modells (Qwen3-spezifisch). Ersetzt das frühere `--chat-template-kwargs '{"enable_thinking":true}'`. | +| `--no-context-shift` | — | Kontextfenster wird **nicht** verschoben wenn es voll ist — stattdessen Fehler. Verhindert stille Datenverluste. | +| `--repeat-penalty 1.05` | — | Leichte Penalty für Wiederholungen. Wert > 1.0 unterdrückt Loops. | +| `--top-k 20` | — | Nur die 20 wahrscheinlichsten nächsten Tokens werden berücksichtigt. | +| `--min-p 0.01` | — | Tokens mit Wahrscheinlichkeit < 1 % des wahrscheinlichsten Tokens werden ausgeschlossen. | +| `-ngl 999` | — | Alle Layer auf die GPU laden (999 = „alle"). Bei zu wenig VRAM reduzieren. | +| `-fa on` | — | Flash Attention — schnellere Attention-Berechnung, weniger VRAM für den Attention-Pass. | +| `--kv-unified` | — | Einheitlicher KV-Cache über alle Schichten. Effizienter bei langen Kontexten. | +| `--cache-type-k q4_0` | — | KV-Cache Keys in 4-Bit quantisiert. Spart ~75 % VRAM gegenüber fp16 — nötig für 256K Kontext auf einer 24-GB-GPU. | +| `--cache-type-v q4_0` | — | KV-Cache Values ebenfalls 4-Bit quantisiert. | +| `--cont-batching` | — | Continuous Batching: neue Anfragen werden in laufende Batches eingefügt — höherer Durchsatz bei mehreren parallelen Anfragen. | +| `--main-gpu 0` | — | GPU-Index (0 = erste des Containers) für Nicht-Tensor-Operationen. | +| `--gpus '"device=1"'` (Coder) | — | Docker: nur GPU 1 dem Coder-Container zuweisen. | +| `--gpus '"device=2"'` (Judge) | — | Docker: nur GPU 2 dem Judge-Container zuweisen. | + +### Coder-Server (Port 8001) — optimiert für Coding-Aufgaben + +| Parameter | Wert | Erklärung / Wirkung | +|---|---|---| +| `-c 262144` | 256K Tokens | Sehr großes Kontextfenster: gesamte Codebasis + langer Gesprächsverlauf passt rein. **Sehr hoher VRAM-Bedarf.** Reduziere auf `65536` wenn VRAM knapp. | +| `-n 16384` | 16K Tokens | Maximale Ausgabelänge pro Anfrage. Für Kommentieraufgaben (`/update_doku`) nötig. | +| `--temp 0.2` | — | Niedrige Temperatur: deterministisch, konsistenter Code. Erhöhe auf `0.4–0.6` für kreativere Lösungsansätze. | +| `--top-p 0.95` | — | Nucleus Sampling: 95 % der Wahrscheinlichkeitsmasse. Passend zu temp 0.2. | +| `--batch-size 1024` | — | Prompt-Verarbeitungs-Batch. Größer = schnelleres Einlesen langer Dateien. | +| `--ubatch-size 512` | — | Micro-Batch für GPU-Kernel. Muss ≤ batch-size sein. | +| `--parallel 2` | — | 2 gleichzeitige Request-Slots. Nützlich wenn pi agent schnell Folgeanfragen schickt. | + +### Judge-Server (Port 8002) — optimiert für Reviews + +| Parameter | Wert | Erklärung / Wirkung | +|---|---|---| +| `-c 262144` | 256K Tokens | Großes Kontextfenster: nötig bei langen /optimize-Runden, wo der Gesprächsverlauf stark anwächst. | +| `-n 16384` | 16K Tokens | Lange Reviews und Begründungen passen vollständig in die Ausgabe. | +| `--temp 0.1` | — | Sehr niedrige Temperatur: maximale Konsistenz und Reproduzierbarkeit der Urteile. | +| `--top-p 0.9` | — | Etwas enger als beim Coder — weniger Variation im Urteil gewünscht. | +| `--batch-size 512` | — | Kleiner als beim Coder — Judge bekommt selten sehr lange Prompts. | +| `--ubatch-size 256` | — | Entsprechend kleiner. | +| `--parallel 1` | — | Judge-Aufgaben sind immer sequenziell im Workflow, daher 1 Slot ausreichend. | + +--- + +## GPU-Konfiguration und VRAM + +### Aktuelles Setup (1 GPU pro Server) + +Jeder Server bekommt eine dedizierte GPU — dadurch kein VRAM-Konflikt: + +``` +GPU 0 NVIDIA T600 → Display / Monitor (nicht für KI genutzt) +GPU 1 RTX 3090 (24 GB) → qwen36-27b-coder (Port 8001) +GPU 2 RTX 3090 (24 GB) → qwen36-27b-judge (Port 8002) +``` + +Die Skripte verwenden `--gpus '"device=1"'` (Coder) bzw. `'"device=2"'` (Judge). +Innerhalb des Containers ist dies stets `CUDA:0`, daher `--main-gpu 0` ohne `--tensor-split`. + +### VRAM-Abschätzung für das 27B IQ4_XS-Modell (eine GPU) + +| Komponente | Größe (ca.) | +|---|---| +| Modell-Gewichte (IQ4_XS, 27B) | ~14 GB | +| KV-Cache bei 262 144 Tokens (q4_0) | ~8–10 GB | +| KV-Cache bei 131 072 Tokens (q4_0) | ~4–5 GB | +| KV-Cache bei 65 536 Tokens (q4_0) | ~2–3 GB | +| **Summe (262k Kontext)** | **~22–24 GB** | + +Bei einer **24-GB-GPU** ist 262k Kontext knapp kalkuliert. Wichtig: keine anderen Prozesse +dürfen nennenswert VRAM belegen (z. B. TTS-Dienste, Ollama). Prüfen mit: +```bash +nvidia-smi --query-gpu=index,memory.used,memory.free --format=csv +``` + +Falls der VRAM nicht reicht: Kontext in den Startskripten auf `131072` oder `65536` reduzieren. + +### Anpassung für andere GPU-Konfigurationen + +```bash +# 2 GPUs (beide für KI, keine Display-GPU): + --gpus '"device=0"' # Coder + --gpus '"device=1"' # Judge + +# 1 GPU (beide Server auf gleicher GPU — nicht empfohlen, VRAM-Konflikt): + --gpus '"device=0"' # beide Server + --tensor-split 0.5,0.5 # Modell hälftig aufteilen + -c 65536 # Kontext stark reduzieren + +# 2 GPUs für einen Server (wenn sehr großer Kontext nötig): + --gpus '"device=0,1"' \ + --tensor-split 0.5,0.5 \ + --main-gpu 0 \ + -c 262144 +``` + +Bei einer **16-GB-GPU** ist die Modellgröße allein schon grenzwertig. +Entweder ein kleineres Modell verwenden oder die Quantisierung erhöhen (IQ3_XS, Q4_K_M). + +--- + +## Parameter-Tuning-Guide + +### Temperatur (`--temp`) + +| Wert | Eignung | +|---|---| +| `0.0–0.1` | Maximale Reproduzierbarkeit. Gut für Judge/Review. | +| `0.1–0.3` | Guter Kompromiss für Coding. **Empfohlen für Coder.** | +| `0.4–0.6` | Kreativere Lösungen, mehr Varianz. Sinnvoll für Prototyping. | +| `0.7–1.0` | Kreativschreiben, Brainstorming. Für Coding meist zu viel Rauschen. | + +### Kontextgröße (`-c`) + +Je größer der Kontext, desto mehr VRAM braucht der KV-Cache. +Faustregel: KV-Cache ≈ `context_size × layers × head_dim × 2 × bytes_per_element`. +Bei q8_0 (1 Byte/Element) und Qwen3-27B (28 Schichten, 128 Head-Dim, 32 Heads): +KV-Cache ≈ `context_size × 28 × 128 × 32 × 2 × 1 Byte ≈ context_size × 0,23 MB` + +| Kontext | KV-Cache (q4_0) | Empfehlung | +|---|---|---| +| 32 768 | ~1,9 GB | 1 × 16-GB-GPU | +| 65 536 | ~3,7 GB | 1 × 24-GB-GPU | +| 131 072 | ~7,5 GB | 2 × 16-GB-GPU | +| 262 144 | ~8–10 GB | 1 × 24-GB-GPU (q4_0) — **aktuell gesetzt** | + +### KV-Cache-Quantisierung + +| `--cache-type-k/v` | VRAM | Qualität | +|---|---|---| +| `f16` | 100 % (Basis) | Referenz | +| `q8_0` | ~50 % | Kaum merklich schlechter | +| `q4_0` | ~25 % | Merklicher Qualitätsverlust bei langen Kontexten — aber nötig für 256K Kontext auf 2× 24 GB. **Aktuell gesetzt.** | + +### Parallelität (`--parallel`) + +Mehr parallele Slots erhöhen den Durchsatz bei gleichzeitigen Anfragen, aber jeder Slot +reserviert Speicher im KV-Cache. Im pi-coder-Workflow sind echte Parallelaufrufe selten, +daher ist `--parallel 1` für den Judge ausreichend. Coder `--parallel 2` bietet Puffer +wenn pi agent Folgeanfragen schnell hintereinander schickt. + +--- + +## Dateien + +| Datei | Zweck | +|---|---| +| `pi-coder-judge-extension.ts` | pi agent Extension (Kommandos, Tools, Hooks) | +| `models.json` | Provider- und Modell-Konfiguration für pi agent | +| `test-utils.ts` | Unit-Tests für Hilfsfunktionen der Extension | +| `run-tests.sh` | Unit-Tests ausführen (TypeScript-Stripping + node) | +| `start-servers.sh` | Beide Server parallel starten (empfohlen) | +| `start-coder.sh` | Nur Coder-Container starten (Port 8001) | +| `start-judge.sh` | Nur Judge-Container starten (Port 8002) | +| `stop-servers.sh` | Beide Container stoppen | +| `status.sh` | Laufstatus beider Server anzeigen | +| `install_servers_and_pi_coder_extension.sh` | Extension + models.json nach `~/.pi/agent/` kopieren | +| `examples/` | Demo-Projekte (Python, Rust, Go, C, Bash, HTML/JS) für Live-Demos | +| `examples/restore-all.sh` | Examples nach Demo-Lauf in Ausgangszustand zurücksetzen | + +--- + +## pi-Kommandos (Kurzübersicht) + +| Kommando | Modell | Beschreibung | +|---|---|---| +| `/coder ` | Coder | TASK.md anlegen, Implementierung starten | +| `/judge [fokus]` | Judge | Code-Review gegen TASK.md + letzten Commit | +| `/fix [hinweis]` | Coder | Judge-Kritik beheben, committen | +| `/shipit` | Judge | Finale Freigabeprüfung | +| `/optimize [--rounds N] [--with-doku] [--continue] [--interactive]` | beide | Vollautomatische Schleife bis PASS (Standard: 2 Runden, Runde 1: Quick-Judge) | +| `/optimize ... [--no-tests] [--approve-concerns] [--test-cmd "cmd"] [--test-timeout N]` | beide | Test-Erkennung überspringen / PASS WITH CONCERNS direkt shippern | +| `/patch <änderung>` | Coder | Gezielte Minimaländerung ohne Review | +| `/quick_check [was]` | Judge | Schnelle Prüfung der letzten Änderung | +| `/version` | — | Versionsnummer erhöhen (SemVer + Git-Tag) | +| `/update_doku` | Coder | Code kommentieren + README + Bedienungsanleitung | +| `/plan ` | Coder | Implementierungsplan in PLAN.md (kein Code) | +| `/continue` | Coder | Unterbrochenen Prozess fortsetzen | +| `/cancel` | — | Laufenden Loop nach aktuellem Schritt abbrechen | +| `/new_project ` | — | Neues Projektverzeichnis + git init | + +Ausführliche Beschreibung aller Kommandos mit Beispielen: siehe **BEDIENUNGSANLEITUNG.md**. + +--- + +## Live-Aktivitätsstatus + +Während der Ausführung zeigt pi_coder in der Statuszeile, was gerade passiert — +inklusive eines laufenden `[MM:SS]`-Timers während jeder LLM-Inference-Phase: + +| Situation | Anzeige | +|---|---| +| Coder implementiert | `◉ Coder implementiert: Login-Flow mit JWT [01:23]` | +| edit-Tool aktiv | `Editiere src/main.py…` | +| git commit | `Git-Commit…` | +| Judge (Quick-Check, Runde 1) | `◉○ Runde 1/2: Quick-Check [00:47]` | +| Judge (voller Review, Runde 2) | `●◉ Runde 2/2: Judge — TASK.md + letzter Commit + Tests [02:15]` | +| Tests laufen | `●○ Runde 1/2: Tests laufen (pytest, max. 120s)…` | +| Fix-Phase | `●◉ Runde 2/2: Coder fixt — fehlendes Null-Check [01:05]` | +| ShipIt | `●●◉ ShipIt — finale Freigabe [00:33]` | +| Interactive-Pause (--interactive) | `⏸ PASS – warte auf /continue…` | + +Der Timer beweist, dass die LLM tatsächlich arbeitet. Steht er still, hängt der Prozess. diff --git a/SINGLE_SERVER_PLAN.md b/SINGLE_SERVER_PLAN.md new file mode 100644 index 0000000..fc59ec9 --- /dev/null +++ b/SINGLE_SERVER_PLAN.md @@ -0,0 +1,150 @@ +# Plan: pi_coder_v2 — Single-Server (Option B) + +## Kontext + +pi_coder verwendet zwei llama.cpp-Server für Coder (GPU 1) und Judge (GPU 2), +die aber dasselbe GGUF-Modell ausführen. Die Rollentrennung entsteht ausschließlich +durch System-Prompts. Wenn nur eine GPU verfügbar ist, kann ein einziger Server +beide Rollen übernehmen — Rollenwechsel per Prompt statt per Server-Neustart. + +**pi_coder bleibt unverändert.** Die Umsetzung erfolgt im neuen Verzeichnis `~/pi_coder_v2`. + +--- + +## Schritt 1: Verzeichnis anlegen und Dateien kopieren + +```bash +mkdir ~/pi_coder_v2 +cp ~/pi_coder/pi-coder-judge-extension.ts ~/pi_coder_v2/ +cp ~/pi_coder/models.json ~/pi_coder_v2/ +cp ~/pi_coder/start-coder.sh ~/pi_coder_v2/ +cp ~/pi_coder/start-judge.sh ~/pi_coder_v2/ +cp ~/pi_coder/start-servers.sh ~/pi_coder_v2/ +cp ~/pi_coder/stop-servers.sh ~/pi_coder_v2/ +cp ~/pi_coder/status.sh ~/pi_coder_v2/ +cp ~/pi_coder/install_servers_and_pi_coder_extension.sh ~/pi_coder_v2/ +cp ~/pi_coder/README.md ~/pi_coder_v2/ +cp ~/pi_coder/BEDIENUNGSANLEITUNG.md ~/pi_coder_v2/ +cp ~/pi_coder/CLAUDE.md ~/pi_coder_v2/ +# Plan-Datei ebenfalls kopieren +cp ~/pi_coder_v2/SINGLE_SERVER_PLAN.md +# git init +cd ~/pi_coder_v2 && git init && git add -A && git commit -m "init: kopiert aus pi_coder" +``` + +Nicht kopieren: `run-tests.sh`, `test-utils.ts`, `examples/` (nicht relevant für Kernfunktion). + +--- + +## Schritt 2: `start-single.sh` anlegen + +Basiert auf `start-coder.sh` mit folgenden Änderungen: + +| Parameter | Alt (coder) | Neu (single) | +|---|---|---| +| `CONTAINER_NAME` | `qwen36-27b-coder` | `qwen36-27b-single` | +| `HOST_PORT` | 8001 | 8001 | +| `MODEL_ALIAS` | `qwen3.5-coder` | `qwen3.5-single` | +| `--temp` | 0.6 | 0.65 (Kompromiss) | +| `--gpus` | `"device=1"` | `"device=1"` (oder konfigurierbar) | + +Der Server registriert sich unter dem Alias `qwen3.5-single`. + +--- + +## Schritt 3: `models.json` anpassen + +Neuen Provider `llama-cpp-single` hinzufügen mit **beiden** Modell-IDs auf Port 8001: + +```json +"llama-cpp-single": { + "baseUrl": "http://127.0.0.1:8001/v1", + "api": "openai-completions", + "apiKey": "none", + "compat": { + "supportsDeveloperRole": false, + "supportsReasoningEffort": false, + "maxTokensField": "max_tokens", + "thinkingFormat": "qwen-chat-template" + }, + "models": [ + { + "id": "qwen3.5-coder", + "name": "Qwen3.6 27B Single-Server Coder (llama.cpp :8001)", + "reasoning": true, + "input": ["text"], + "contextWindow": 262144, + "maxTokens": 16384, + "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } + }, + { + "id": "qwen3.5-judge", + "name": "Qwen3.6 27B Single-Server Judge (llama.cpp :8001)", + "reasoning": true, + "input": ["text"], + "contextWindow": 262144, + "maxTokens": 16384, + "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } + } + ] +} +``` + +Die bestehenden Provider `llama-cpp-coder` und `llama-cpp-judge` bleiben erhalten +(für Kompatibilität), werden aber nicht aktiv genutzt. + +--- + +## Schritt 4: `pi-coder-judge-extension.ts` — switchModel-Aufrufe + +Alle Aufrufe von `switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder")` +und `switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge")` werden auf +`"llama-cpp-single"` umgestellt. Der Modell-ID-Parameter (`"qwen3.5-coder"` / +`"qwen3.5-judge"`) bleibt unverändert — so bleiben die Registry-Wechsel erhalten, +sind aber sofort (kein Server-Neustart). + +Betroffene Stellen (ca. 8): +- `/coder` handler +- `/judge` handler +- `/fix` handler +- `/shipit` handler +- `/optimize` loop (Coder-Kickoff, Judge-Phase, Fix-Phase, ShipIt-Phase) +- `/patch` handler +- `/quick_check` handler +- `/plan` handler + +--- + +## Schritt 5: `install_servers_and_pi_coder_extension.sh` anpassen + +Pfade auf `pi_coder_v2` anpassen (falls nötig — Skript referenziert vermutlich `$REPO` +als relatives Verzeichnis, was automatisch korrekt ist). + +--- + +## Schritt 6: `waitUntilModelReady` im `--continue`-Modus + +Im `--continue`-Modus prüft die Extension beide Server parallel: +```typescript +const [coderReady, judgeReady] = await Promise.all([ + waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder"), + waitUntilModelReady(pi, ctx, 8002, "qwen3.5-judge"), +]); +``` + +Dies muss auf einen einzigen Check umgestellt werden: +```typescript +const ready = await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single"); +``` + +--- + +## Verifikation + +1. `cd ~/pi_coder_v2 && ./start-single.sh` → Server startet auf Port 8001 +2. `nvidia-smi` → nur eine GPU belegt, ~20 GB VRAM +3. `./install_servers_and_pi_coder_extension.sh` → Extension in `~/.pi/agent/` deployen +4. In pi: `/reload` → Extension neu laden +5. `/optimize ` → Loop läuft durch +6. In der Statuszeile: `switchModel` wechselt ohne spürbare Pause +7. Qualitätscheck: Judge-Urteil und Coder-Fix verhalten sich wie erwartet diff --git a/install_servers_and_pi_coder_extension.sh b/install_servers_and_pi_coder_extension.sh new file mode 100755 index 0000000..72a334f --- /dev/null +++ b/install_servers_and_pi_coder_extension.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Kopiert die versionierten Dateien aus dem Repo nach ~/.pi/agent/. +# Nach jeder Änderung im Repo ausführen, damit pi agent die neue Version lädt. +set -euo pipefail +REPO="$(cd "$(dirname "$0")" && pwd)" + +mkdir -p ~/.pi/agent/extensions + +cp "$REPO/pi-coder-judge-extension.ts" ~/.pi/agent/extensions/pi-coder-judge-extension.ts +echo "Kopiert: pi-coder-judge-extension.ts → ~/.pi/agent/extensions/" + +cp "$REPO/models.json" ~/.pi/agent/models.json +echo "Kopiert: models.json → ~/.pi/agent/" + +echo "" +echo "Fertig. Bitte /reload in pi agent ausführen." diff --git a/models.json b/models.json new file mode 100644 index 0000000..2e10757 --- /dev/null +++ b/models.json @@ -0,0 +1,143 @@ +{ + "providers": { + "ollama": { + "baseUrl": "http://localhost:11434/v1", + "api": "openai-completions", + "apiKey": "ollama", + "compat": { + "supportsDeveloperRole": false, + "supportsReasoningEffort": false + }, + "models": [ + { "id": "qwen2.5-coder:7b", "name": "Qwen2.5 Coder 7B (schnell)" }, + { "id": "qwen3-coder-30b-gpu:latest", "name": "Qwen3 Coder 30B GPU (Standard)" }, + { "id": "mistral-small3.2:24b", "name": "Mistral Small 3.2 24B" }, + { "id": "deepseek-r1:32b", "name": "DeepSeek R1 32B (Reasoning)" } + ] + }, + + "llama-cpp": { + "baseUrl": "http://127.0.0.1:8000/v1", + "api": "openai-completions", + "apiKey": "none", + "compat": { + "supportsDeveloperRole": false, + "supportsReasoningEffort": false, + "maxTokensField": "max_tokens", + "thinkingFormat": "qwen-chat-template" + }, + "models": [ + { + "id": "qwen35b-uncensored", + "name": "Qwen3.6 35B Uncensored (llama.cpp :8000)" + }, + { + "id": "qwen35b-moe-tools", + "name": "Qwen3.6 35B MoE Tools (llama.cpp :8000)" + } + ] + }, + + "llama-cpp-single": { + "baseUrl": "http://127.0.0.1:8001/v1", + "api": "openai-completions", + "apiKey": "none", + "compat": { + "supportsDeveloperRole": false, + "supportsReasoningEffort": false, + "maxTokensField": "max_tokens", + "thinkingFormat": "qwen-chat-template" + }, + "models": [ + { + "id": "qwen3.5-coder", + "name": "Qwen3.6 27B Single-Server Coder (llama.cpp :8001)", + "reasoning": true, + "input": ["text"], + "contextWindow": 262144, + "maxTokens": 16384, + "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } + }, + { + "id": "qwen3.5-judge", + "name": "Qwen3.6 27B Single-Server Judge (llama.cpp :8001)", + "reasoning": true, + "input": ["text"], + "contextWindow": 262144, + "maxTokens": 16384, + "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } + } + ] + }, + + "llama-cpp-coder": { + "baseUrl": "http://127.0.0.1:8001/v1", + "api": "openai-completions", + "apiKey": "none", + "compat": { + "supportsDeveloperRole": false, + "supportsReasoningEffort": false, + "maxTokensField": "max_tokens", + "thinkingFormat": "qwen-chat-template" + }, + "models": [ + { + "id": "qwen3.5-coder", + "name": "Qwen3.6 27B Coder (llama.cpp :8001)", + "reasoning": true, + "input": ["text"], + "contextWindow": 262144, + "maxTokens": 16384, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + } + ] + }, + + "llama-cpp-judge": { + "baseUrl": "http://127.0.0.1:8002/v1", + "api": "openai-completions", + "apiKey": "none", + "compat": { + "supportsDeveloperRole": false, + "supportsReasoningEffort": false, + "maxTokensField": "max_tokens", + "thinkingFormat": "qwen-chat-template" + }, + "models": [ + { + "id": "qwen3.5-judge", + "name": "Qwen3.6 27B Judge (llama.cpp :8002)", + "reasoning": true, + "input": ["text"], + "contextWindow": 262144, + "maxTokens": 16384, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + } + ] + }, + + "openrouter": { + "models": [ + { "id": "qwen/qwen3-235b-a22b:free", "name": "Qwen3 235B (Free)" }, + { "id": "deepseek/deepseek-r1:free", "name": "DeepSeek R1 (Free)" }, + { "id": "google/gemini-2.5-pro-exp-03-25:free", "name": "Gemini 2.5 Pro (Free)" }, + { "id": "meta-llama/llama-4-maverick:free", "name": "Llama 4 Maverick (Free)" }, + { "id": "microsoft/phi-4:free", "name": "Phi-4 (Free)" }, + { "id": "qwen/qwen-2.5-coder-32b-instruct", "name": "Qwen2.5 Coder 32B (günstig)" }, + { "id": "deepseek/deepseek-r1", "name": "DeepSeek R1 Full (Reasoning)" }, + { "id": "qwen/qwen3-235b-a22b", "name": "Qwen3 235B Full" } + ] + } + } +} + diff --git a/pi-coder-judge-extension.ts b/pi-coder-judge-extension.ts new file mode 100644 index 0000000..c091399 --- /dev/null +++ b/pi-coder-judge-extension.ts @@ -0,0 +1,1617 @@ +// pi-coder-judge-extension.ts +// Automatisierter Coder-Judge-Fix-Workflow für AI-Coding-Assistenten +// Modelle: qwen3.5-coder + qwen3.5-judge auf Single-Server (Port 8001) + +import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; +import { Type } from "typebox"; + +// ── Prompt-Bausteine ──────────────────────────────────────────────────────── + +function coderKickoff(task: string): string { + return [ + "Du bist der Coding-Agent.", + "Lies TASK.md für die vollständige Aufgabenbeschreibung.", + "Halte dich strikt an die dort beschriebenen Anforderungen.", + "Arbeite sorgfältig, konkret und produktionsorientiert.", + "Lies jede Datei unmittelbar vor dem Editieren neu ein.", + "Wenn du mehrere Stellen in derselben Datei änderst: Erzeuge einen unified diff und nutze das apply_patch-Tool — nicht mehrere edit-Aufrufe.", + "Fallback: Datei komplett neu schreiben wenn apply_patch nicht möglich.", + "", + "Git-Pflichten:", + "- Falls noch kein git-Repository existiert, initialisiere es mit 'git init'.", + "- Führe nach der Implementierung einen Commit durch: git add -A && git commit -m 'feat: ...'", + "", + "Führe nach der Implementierung passende Tests oder Checks aus.", + "Melde knapp, was du geändert hast, welche Risiken bleiben und welche Tests du ausgeführt hast.", + "", + "Auftrag:", + task + ].join("\n"); +} + +function judgePrompt(extra: string): string { + const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : ""; + return [ + "Du bist ein pingeliger, skeptischer Senior-Reviewer und QA-Ingenieur.", + "Deine Aufgabe ist NICHT, nett zu sein, sondern Fehler, Risiken, Randfälle und Produktionsprobleme zu finden.", + "Arbeite reproduzierbar und konkret.", + "", + "Pflichten:", + "0. Lies TASK.md und prüfe, ob alle dort beschriebenen Anforderungen vollständig umgesetzt sind.", + "1. Sieh dir den letzten Commit an: 'git log -1 --stat' und 'git show HEAD'.", + "2. Führe relevante Tests, Linter oder Startchecks aus.", + "3. Versuche Fehler aktiv zu finden.", + "4. Bewerte Korrektheit, Robustheit, Fehlerbehandlung, Sicherheit, Logging, Wartbarkeit und Produktionsreife.", + "5. Wenn etwas fehlt, sage es klar und direkt.", + "", + "Ausgabeformat:", + "- Urteil: PASS | PASS WITH CONCERNS | FAIL", + "- Blocker", + "- Major", + "- Minor", + "- Fehlende Tests", + "- Produktionsrisiken", + "- Konkrete Fix-Aufträge an den Coder", + "", + "Wenn du etwas behauptest, nenne die Datei, den Befehl, den Test oder den Reproduktionshinweis." + ].join("\n") + suffix; +} + +// Kompakter Ersteindruck-Prompt für Runde 1: kein TASK.md, nur Diff-Review. +// Reduziert Inference-Zeit wenn der Code offensichtlich gut ist. +// Bei FAIL → Runde 2 mit vollem judgePrompt() für detaillierte Analyse. +function quickJudgePrompt(extra: string): string { + const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : ""; + return [ + "Schneller Code-Review — erster Eindruck.", + "Du bist ein skeptischer Senior-Reviewer. Sei direkt und knapp.", + "", + "1. Sieh dir 'git show HEAD' an.", + "2. Führe relevante Tests aus, falls vorhanden.", + "3. Gibt es offensichtliche Blocker? (Bugs, fehlende Fehlerbehandlung, Sicherheitslücken, kaputte Imports)", + "4. Wenn alles offensichtlich in Ordnung ist: PASS.", + "5. Bei Zweifeln oder Lücken: FAIL — konkrete Blocker benennen.", + "", + "Ausgabeformat (kompakt):", + "- Urteil: PASS | PASS WITH CONCERNS | FAIL", + "- Blocker (falls vorhanden)", + "- Konkrete Fix-Aufträge (falls FAIL)" + ].join("\n") + suffix; +} + +// Quick-Variante für Runde 1 mit bereits vorliegendem Test-Output. +function quickJudgeWithTestsPrompt(testOutput: string, extra: string): string { + const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : ""; + return [ + "Schneller Code-Review — erster Eindruck.", + "Du bist ein skeptischer Senior-Reviewer. Sei direkt und knapp.", + "", + "Die Test-Suite wurde bereits extern ausgeführt. Führe KEINE weiteren Tests aus.", + "", + "1. Sieh dir 'git show HEAD' an.", + "2. Analysiere das folgende Test-Ergebnis:", + "```", + testOutput, + "```", + "3. Gibt es offensichtliche Blocker? (Test-Failures, Bugs, Sicherheitslücken)", + "4. Wenn alles offensichtlich in Ordnung ist: PASS.", + "5. Bei Zweifeln: FAIL — konkrete Blocker benennen.", + "", + "Ausgabeformat (kompakt):", + "- Urteil: PASS | PASS WITH CONCERNS | FAIL", + "- Blocker (falls vorhanden)", + "- Konkrete Fix-Aufträge (falls FAIL)" + ].join("\n") + suffix; +} + +// Wie judgePrompt, aber Tests werden NICHT vom Judge ausgeführt — +// die Extension hat sie bereits extern gestartet und übergibt den Output. +function judgeWithTestsPrompt(testOutput: string, extra: string): string { + const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : ""; + return [ + "Du bist ein pingeliger, skeptischer Senior-Reviewer und QA-Ingenieur.", + "Deine Aufgabe ist NICHT, nett zu sein, sondern Fehler, Risiken, Randfälle und Produktionsprobleme zu finden.", + "", + "Die Test-Suite wurde bereits extern ausgeführt. Das Ergebnis steht unten.", + "Führe KEINE weiteren Tests aus — weder dieselben noch andere.", + "", + "Pflichten:", + "0. Lies TASK.md und prüfe, ob alle dort beschriebenen Anforderungen vollständig umgesetzt sind.", + "1. Sieh dir den letzten Commit an: 'git log -1 --stat' und 'git show HEAD'.", + "2. Analysiere das folgende Test-Ergebnis und leite daraus Blocker/Major/Minor ab:", + "```", + testOutput, + "```", + "3. Versuche weitere Fehler im Code aktiv zu finden (Randfälle, Sicherheit, Robustheit).", + "4. Wenn du etwas behauptest, nenne die Datei, die Zeile oder den Reproduktionshinweis.", + "", + "Ausgabeformat:", + "- Urteil: PASS | PASS WITH CONCERNS | FAIL", + "- Blocker", + "- Major", + "- Minor", + "- Fehlende Tests", + "- Produktionsrisiken", + "- Konkrete Fix-Aufträge an den Coder", + ].join("\n") + suffix; +} + +function fixPrompt(extra: string): string { + const suffix = extra?.trim() ? "\n\nZusätzlicher User-Hinweis:\n" + extra.trim() : ""; + return [ + "Wechsle in den Reparaturmodus.", + "Lies TASK.md als Referenz — stelle sicher, dass nach den Fixes alle Anforderungen erfüllt bleiben.", + "Nutze den letzten Judge-Bericht als verbindliche Aufgabenliste.", + "Behebe zuerst Blocker, dann Major, dann Minor.", + "Lies jede betroffene Datei unmittelbar vor dem Editieren erneut ein.", + "Wenn du mehrere Stellen in derselben Datei änderst: Erzeuge einen unified diff und nutze das apply_patch-Tool — nicht mehrere edit-Aufrufe.", + "Fallback: Datei komplett neu schreiben wenn apply_patch nicht möglich.", + "Führe nach den Fixes passende Tests aus.", + "Führe danach einen Commit durch: git add -A && git commit -m 'fix: ...'", + "Wenn ein Punkt nicht sinnvoll umsetzbar ist, begründe das präzise.", + "Liefere am Ende nur:", + "- Was geändert wurde", + "- Welche Judge-Punkte geschlossen wurden", + "- Welche Punkte offen bleiben", + "- Welche Tests ausgeführt wurden" + ].join("\n") + suffix; +} + +function shipitPrompt(extra: string): string { + return [ + "Führe die finale Freigabeprüfung durch.", + "Lies TASK.md und prüfe, ob alle dort beschriebenen Anforderungen im finalen Stand enthalten sind.", + "Lies die relevanten geänderten Dateien und 'git log --oneline -10'.", + "Führe sinnvolle Tests, Linter und Startchecks aus.", + "Beurteile, ob der Stand produktionsreif ist.", + "", + "Ausgabeformat:", + "- Urteil: SHIP | NO-SHIP", + "- Letzte Blocker", + "- Restrisiken", + "- Empfohlene Sofortmaßnahmen vor Deployment" + ].join("\n") + (extra?.trim() ? "\n\nZusätzlicher Fokus:\n" + extra.trim() : ""); +} + +function patchPrompt(change: string): string { + return [ + "Du machst eine kleine, gezielte Codeänderung. Nichts weiter.", + "Ändere AUSSCHLIESSLICH das Folgende:", + change, + "", + "Regeln:", + "- Kein Refactoring, keine weiteren Verbesserungen, keine Umbenennungen", + "- Lies die Datei unmittelbar vor dem Editieren neu ein", + "- Wenn du mehrere Stellen in derselben Datei änderst: Erzeuge einen unified diff und nutze das apply_patch-Tool", + "- Fallback: Datei komplett neu schreiben", + "- Finde die betroffene Stelle direkt (grep oder gezielte Datei-Suche)", + "- Ändere nur die notwendigen Zeilen", + "- Prüfe danach nur: Kompiliert/startet es noch?", + "- Commit: git add -A && git commit -m 'fix: '", + "- Melde: Datei, Zeile(n), was geändert wurde. Fertig." + ].join("\n"); +} + +function quickCheckPrompt(what: string): string { + const focus = what?.trim() ? "\n\nZu prüfende Änderung:\n" + what.trim() : ""; + return [ + "Schnelle Prüfung einer kleinen Codeänderung.", + "Lies 'git show HEAD' um zu sehen was geändert wurde.", + "Prüfe NUR:", + "- Ist die Änderung korrekt umgesetzt?", + "- Gibt es offensichtliche Fehler oder Randfälle die übersehen wurden?", + "- Kompiliert/startet der Code?", + "", + "Ausgabeformat (kurz):", + "- Urteil: OK | PROBLEM", + "- Falls PROBLEM: konkret was falsch ist und wie zu fixen", + "", + "Kein vollständiger Review, keine Stilkritik, kein Refactoring-Vorschlag." + ].join("\n") + focus; +} + +function commentCodePrompt(): string { + return [ + "Lies TASK.md und alle Quelldateien des Projekts.", + "Füge wartungsfreundliche Kommentare ein, die Entwicklern ohne Vorkenntnis helfen, den Code zu verstehen und zu warten.", + "", + "Regeln:", + "- Kommentiere das WARUM, nicht das WAS (kein 'x += 1 // increment x')", + "- Erkläre nicht-offensichtliche Algorithmen, Randfälle und Design-Entscheidungen", + "- Füge Modul-/Datei-Level-Kommentare ein, die den Gesamtzweck der Datei erklären", + "- Keine trivialen Kommentare, die nur den Code wiederholen", + "- Sprache der Kommentare: Deutsch", + "", + "Wenn du mehrere Stellen in derselben Datei kommentierst: Erzeuge einen unified diff und nutze das apply_patch-Tool — nicht mehrere edit-Aufrufe.", + "Fallback: Datei komplett neu schreiben wenn apply_patch nicht möglich.", + "", + "Führe danach einen Build/Test aus um sicherzustellen, dass die Kommentare nichts kaputt gemacht haben.", + "Melde welche Dateien du kommentiert hast." + ].join("\n"); +} + +function readmeMdPrompt(): string { + return [ + "Lies TASK.md und alle Quelldateien des Projekts.", + "Schreibe oder aktualisiere README.md aus Entwicklerperspektive.", + "", + "Pflichtabschnitte:", + "- Projektbeschreibung (Was macht das Programm? Warum?)", + "- Voraussetzungen (Dependencies, Toolchain)", + "- Installation und Build", + "- Verwendung (alle Kommandozeilenoptionen und Flags)", + "- Beispiele mit konkreter Ausgabe", + "- Projektstruktur (Dateien und ihre Aufgabe)", + "", + "Halte es technisch präzise und korrekt. Sprache: Deutsch." + ].join("\n"); +} + +function bedienungsanleitungPrompt(): string { + return [ + "Lies TASK.md und README.md.", + "Schreibe oder aktualisiere BEDIENUNGSANLEITUNG.md aus Endnutzer-Perspektive.", + "Setze kein Entwicklerwissen voraus — die Zielgruppe sind normale Anwender.", + "", + "Pflichtabschnitte:", + "- Zweck des Programms (was kann der Nutzer damit tun?)", + "- Installation für Endnutzer (Schritt für Schritt)", + "- Erste Schritte / Schnellstart", + "- Alle Optionen mit verständlicher Erklärung und Beispielen", + "- Typische Anwendungsfälle", + "- Fehlermeldungen und ihre Lösung", + "", + "Sprache: Deutsch. Einfach, klar, ohne Jargon." + ].join("\n"); +} + +function commentCodePromptIncremental(files: string[]): string { + const fileList = files.map(f => ` - ${f}`).join("\n"); + return [ + "Kommentiere NUR die folgenden Quelldateien — sie haben sich seit dem letzten Kommentar-Update geändert:", + fileList, + "Alle anderen Dateien haben bereits aktuelle Kommentare — lass sie vollständig unberührt.", + "", + "Regeln:", + "- Kommentiere das WARUM, nicht das WAS (kein 'x += 1 // increment x')", + "- Erkläre nicht-offensichtliche Algorithmen, Randfälle und Design-Entscheidungen", + "- Füge Modul-/Datei-Level-Kommentare ein, die den Gesamtzweck der Datei erklären", + "- Keine trivialen Kommentare, die nur den Code wiederholen", + "- Sprache der Kommentare: Deutsch", + "", + "Wenn du mehrere Stellen in derselben Datei kommentierst: Erzeuge einen unified diff und nutze das apply_patch-Tool — nicht mehrere edit-Aufrufe.", + "Fallback: Datei komplett neu schreiben wenn apply_patch nicht möglich.", + "", + "Führe danach einen Build/Test aus um sicherzustellen, dass die Kommentare nichts kaputt gemacht haben.", + "Melde welche Dateien du kommentiert hast." + ].join("\n"); +} + +function readmeMdPromptIncremental(files: string[]): string { + const fileList = files.map(f => ` - ${f}`).join("\n"); + return [ + "Folgende Quelldateien haben sich seit dem letzten README-Update geändert:", + fileList, + "", + "Prüfe: Haben diese Änderungen Auswirkungen auf Installation, Verwendung, Optionen oder Projektstruktur?", + "- Falls JA: Lies README.md und aktualisiere NUR die betroffenen Abschnitte.", + "- Falls NEIN: Antworte nur mit dem Satz: 'README.md ist aktuell – keine Änderung nötig.'", + "", + "Wenn du mehrere Abschnitte in README.md aktualisierst: Erzeuge einen unified diff und nutze das apply_patch-Tool.", + "Halte es technisch präzise und korrekt. Sprache: Deutsch." + ].join("\n"); +} + +function bedienungsanleitungPromptIncremental(files: string[]): string { + const fileList = files.map(f => ` - ${f}`).join("\n"); + return [ + "Folgende Quelldateien haben sich seit dem letzten Bedienungsanleitung-Update geändert:", + fileList, + "", + "Prüfe: Haben diese Änderungen Auswirkungen auf die Benutzung des Programms durch Endnutzer?", + "(z.B. neue Optionen, geändertes Verhalten, neue Fehlermeldungen)", + "- Falls JA: Lies BEDIENUNGSANLEITUNG.md und aktualisiere NUR die betroffenen Abschnitte.", + "- Falls NEIN: Antworte nur mit dem Satz: 'BEDIENUNGSANLEITUNG.md ist aktuell – keine Änderung nötig.'", + "", + "Wenn du mehrere Abschnitte in BEDIENUNGSANLEITUNG.md aktualisierst: Erzeuge einen unified diff und nutze das apply_patch-Tool.", + "Sprache: Deutsch. Einfach, klar, ohne Jargon." + ].join("\n"); +} + +function planPrompt(task: string): string { + return [ + "Du bist ein erfahrener Software-Architekt im PLANUNGSMODUS.", + "", + "ABSOLUTE VERBOTE — du darfst NICHT:", + "- Dateien editieren, schreiben oder löschen (kein edit, write, apply_patch)", + "- Git-Commits durchführen", + "- Tests oder Skripte ausführen die Seiteneffekte haben", + "", + "ERLAUBT:", + "- Dateien lesen (read, cat, grep, find)", + "- Git-History lesen (git log, git show, git diff)", + "- PLAN.md anlegen oder überschreiben (das ist dein Ausgabe-Dokument)", + "", + "Analysiere den Auftrag gründlich und erstelle einen konkreten Implementierungsplan.", + "", + "Auftrag:", + task, + "", + "Struktur deiner Ausgabe:", + "1. IST-Analyse (relevante Dateien, Architektur, Abhängigkeiten)", + "2. Implementierungsplan (nummerierte Schritte, konkret und umsetzbar)", + "3. Kritische Entscheidungen (Alternativen + Empfehlung)", + "4. Risiken und offene Fragen", + "5. Geschätzte Komplexität: einfach / mittel / komplex", + "", + "Schreibe den vollständigen Plan in PLAN.md.", + "Schließe ab mit: 'Plan bereit. Starte Umsetzung mit /coder oder /optimize --continue'", + ].join("\n"); +} + +// ── Hilfsfunktionen ───────────────────────────────────────────────────────── + +// Legt TASK.md neu an oder hängt einen Zusatzauftrag an. +async function writeTaskMd( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + task: string +): Promise { + const check = await pi.exec("bash", ["-c", "test -f TASK.md && echo exists"], { cwd: ctx.cwd }); + const exists = check.stdout.trim() === "exists"; + + let content: string; + if (exists) { + content = [ + "", + "---", + "", + "## Zusatzauftrag", + "", + new Date().toISOString(), + "", + task, + "", + "## Status", + "- [ ] Implementierung", + "- [ ] Review bestanden (PASS)", + "- [ ] Produktionsreif (SHIP)", + ].join("\n") + "\n"; + } else { + content = [ + "# Aufgabe", + "", + task, + "", + "## Erstellt", + new Date().toISOString(), + "", + "## Status", + "- [ ] Implementierung", + "- [ ] Review bestanden (PASS)", + "- [ ] Produktionsreif (SHIP)", + ].join("\n") + "\n"; + } + + const redirect = exists ? ">>" : ">"; + await pi.exec("bash", ["-c", `printf "%s" "$1" ${redirect} TASK.md`, "_", content], { cwd: ctx.cwd }); + ctx.ui.notify(exists ? "TASK.md erweitert" : "TASK.md angelegt", "info"); +} + +// Hakt einen Status-Eintrag in TASK.md ab. +// label: exakt wie in der Checkbox, z.B. "Implementierung" +async function tickTaskMdStatus( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + label: string +): Promise { + const check = await pi.exec("bash", ["-c", "test -f TASK.md && echo exists"], { cwd: ctx.cwd }); + if (check.stdout.trim() !== "exists") return; + // Python übernimmt den String-Ersatz — kein Shell-Escaping-Problem + await pi.exec( + "python3", + ["-c", + "import sys; f=open('TASK.md','r'); c=f.read(); f.close(); " + + "c=c.replace('- [ ] '+sys.argv[1], '- [x] '+sys.argv[1]); " + + "f=open('TASK.md','w'); f.write(c); f.close()", + label + ], + { cwd: ctx.cwd } + ); +} + +async function switchModel( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + provider: string, + modelId: string +): Promise { + const key = `${provider}/${modelId}`; + if (key === currentModelKey) return true; + const model = ctx.modelRegistry.find(provider, modelId); + if (!model) { + ctx.ui.notify(`Modell ${provider}/${modelId} nicht gefunden`, "error"); + return false; + } + const ok = await pi.setModel(model); + if (ok !== false) currentModelKey = key; + if (!ok) ctx.ui.notify(`Kein API-Key für ${modelId}`, "warning"); + return ok !== false; +} + +// Sendet eine Nachricht und wartet bis der Agent fertig ist. +// Retry-Schleife fängt "Agent is already processing" ab — tritt auf wenn +// waitForIdle() zu früh zurückkehrt (Race Condition im pi-Agent). +async function sendAndWait( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + content: string +): Promise { + await ctx.waitForIdle(); + for (let attempt = 1; attempt <= 5; attempt++) { + try { + pi.sendUserMessage(content, { deliverAs: "followUp" }); + break; + } catch (e: any) { + if (attempt === 5) throw e; + // Exponentieller Backoff: 500ms, 1s, 2s, 4s + await new Promise(r => setTimeout(r, 500 * Math.pow(2, attempt - 1))); + await ctx.waitForIdle(); + } + } + await new Promise(r => setTimeout(r, 150)); + await ctx.waitForIdle(); +} + +// Prüft via POST /v1/chat/completions ob das Modell im VRAM bereit ist. +// /health und /v1/models antworten bereits während des GPU-Ladevorgangs — nur +// ein echter Completion-Request liefert zuverlässig HTTP 200 wenn das Modell ready ist. +async function waitUntilModelReady( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + port: number, + modelAlias: string, + maxWaitMs = 180_000 +): Promise { + const deadline = Date.now() + maxWaitMs; + const body = JSON.stringify({ + model: modelAlias, + messages: [{ role: "user", content: "ping" }], + max_tokens: 1, temperature: 0.0, stream: false, + }); + // Body als Datei — verhindert Shell-Injection wenn modelAlias Sonderzeichen enthält + const tmpBody = `/tmp/pi_ready_${Date.now()}_${Math.random().toString(36).slice(2)}.json`; + await pi.exec("bash", ["-c", `printf "%s" "$1" > "${tmpBody}"`, "_", body], { cwd: ctx.cwd }); + let notified = false; + try { + while (Date.now() < deadline) { + const r = await pi.exec("bash", ["-c", + `curl -s -o /dev/null -w "%{http_code}" --max-time 5 ` + + `-X POST http://localhost:${port}/v1/chat/completions ` + + `-H "Content-Type: application/json" ` + + `-d "@${tmpBody}"` + ], { cwd: ctx.cwd }); + if (r.stdout?.trim() === "200") return true; + if (!notified) { + ctx.ui.notify(`Modell-Server (Port ${port}) lädt noch — warte bis zu 3 min…`, "info"); + notified = true; + } + await new Promise(res => setTimeout(res, 3000)); + } + return false; + } finally { + await pi.exec("bash", ["-c", `rm -f "${tmpBody}"`], { cwd: ctx.cwd }); + } +} + +// Führt einen Shell-Befehl aus und gibt stdout+stderr zurück (max. 6000 Zeichen). +// Erkennt Test-Suiten im Projektverzeichnis anhand von Framework-Markern. +// Alle Checks laufen parallel — konservativ, keine False Positives. +async function detectTestCommands( + pi: ExtensionAPI, + ctx: ExtensionCommandContext +): Promise { + const [hasPytest, hasNpm, hasCargo, hasGo, hasMake] = await Promise.all([ + pi.exec("bash", ["-c", + "test -f pytest.ini || test -f conftest.py || " + + "(test -f pyproject.toml && grep -q 'pytest' pyproject.toml) || " + + "find . -maxdepth 4 \\( -name 'test_*.py' -o -name '*_test.py' \\) 2>/dev/null | grep -q ." + ], { cwd: ctx.cwd }), + pi.exec("bash", ["-c", + "test -f package.json && " + + "grep -q '\"test\"' package.json && " + + "! grep -q 'no test' package.json" + ], { cwd: ctx.cwd }), + pi.exec("bash", ["-c", "test -f Cargo.toml"], { cwd: ctx.cwd }), + pi.exec("bash", ["-c", + "test -f go.mod && find . -maxdepth 4 -name '*_test.go' 2>/dev/null | grep -q ." + ], { cwd: ctx.cwd }), + pi.exec("bash", ["-c", + "test -f Makefile && grep -qE '^test[[:space:]]*:' Makefile" + ], { cwd: ctx.cwd }), + ]); + return ([ + hasPytest.code === 0 ? "pytest -x -q 2>&1" : null, + hasNpm.code === 0 ? "npm test 2>&1" : null, + hasCargo.code === 0 ? "cargo test 2>&1" : null, + hasGo.code === 0 ? "go test ./... 2>&1" : null, + hasMake.code === 0 ? "make test 2>&1" : null, + ] as (string | null)[]).filter((c): c is string => c !== null); +} + +// Führt mehrere Test-Befehle parallel als CPU-Prozesse aus und liefert einen +// kombinierten Output-Block für judgeWithTestsPrompt(). +async function runTestsParallel( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + cmds: string[], + timeoutSecs: number = 120 +): Promise { + const results = await Promise.all( + // timeout-Wrapper: verhindert hängende Tests (Exit 124 = Timeout) + cmds.map(cmd => pi.exec( + "bash", + ["-c", `timeout ${timeoutSecs} bash -c ${JSON.stringify(cmd)}`], + { cwd: ctx.cwd } + )) + ); + const MAX_PER = Math.max(1000, Math.floor(6000 / cmds.length)); + return results.map((r, i) => { + const raw = (r.stdout + (r.stderr ? "\n" + r.stderr : "")).trim(); + const out = raw.length > MAX_PER + ? raw.slice(0, MAX_PER) + `\n[… gekürzt, ${raw.length} Zeichen]` + : raw || "(kein Output)"; + const status = r.code === 0 ? "✓ OK" + : r.code === 124 ? `✗ Timeout (>${timeoutSecs}s)` + : `✗ Exit ${r.code}`; + return `=== ${cmds[i]} [${status}] ===\n${out}`; + }).join("\n\n"); +} + +// Liest den Text der letzten Assistenten-Antwort aus dem Session-Branch. +function getLastAssistantText(ctx: ExtensionCommandContext): string { + const entries = ctx.sessionManager.getBranch(); + for (let i = entries.length - 1; i >= 0; i--) { + const entry = entries[i]; + if (entry.type === "message") { + const msg = (entry as any).message; + if (msg?.role === "assistant" && Array.isArray(msg.content)) { + return msg.content + .filter((c: any) => c.type === "text") + .map((c: any) => c.text as string) + .join("\n"); + } + } + } + return ""; +} + +// Extrahiert das Urteil aus einer Judge-Antwort. +// "UNREADABLE" wenn kein Urteil erkennbar — unterscheidbar von einem expliziten FAIL. +// Normalisiert Blocker-Text für die Loop-Erkennung — verhindert False-Negatives +// durch minimale Formulierungsunterschiede im Judge-Output (Whitespace, Satzzeichen). +function normalizeForComparison(s: string): string { + return s.trim().replace(/\s+/g, " ").replace(/[.,;:!?]+$/g, "").toLowerCase(); +} + +function parseVerdict(text: string): string { + const m = text.match(/Urteil:\s*(PASS WITH CONCERNS|PASS|FAIL)/i); + return m ? m[1].toUpperCase() : "UNREADABLE"; +} + +// Extrahiert den Blocker-Abschnitt für die Loop-Erkennung. +// Erkennt Bullet-Listen (- / – / *), Bold (**Blocker**) und Headings (## Blocker). +function parseBlockers(text: string): string { + const m = text.match( + /(?:\*\*Blocker\*\*|##\s*Blocker|[-–*]\s*Blocker)[:\n]([\s\S]*?)(?:\n(?:\*\*Major\*\*|##\s*Major|[-–*]\s*Major)|\n(?:\*\*Minor\*\*|##\s*Minor|[-–*]\s*Minor)|$)/i + ); + return m ? m[1].trim() : ""; +} + +// Gibt geänderte Quelldateien seit einem Git-Tag zurück. +// null = Tag existiert nicht (erster Lauf) → alles verarbeiten +// [] = nichts geändert → Phase überspringen +// [...] = nur diese Dateien verarbeiten +async function getFilesSinceTag( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + tagName: string +): Promise { + const tagCheck = await pi.exec("bash", ["-c", `git tag -l "${tagName}"`], { cwd: ctx.cwd }); + if (!tagCheck.stdout.trim()) return null; + + const diff = await pi.exec( + "bash", + ["-c", `git diff "${tagName}" --name-only 2>/dev/null`], + { cwd: ctx.cwd } + ); + + // Bei git-Fehler alles verarbeiten (sicherer als stilles Überspringen) + if (diff.code !== 0) return null; + + return diff.stdout.trim() + .split("\n") + .filter(f => + f.length > 0 && + !f.endsWith(".md") && + !f.endsWith(".lock") && + !f.endsWith(".toml") && + !f.startsWith("target/") && + !f.endsWith(".gitignore") + ); +} + +// Dokumentations-Phase: inkrementell via Git-Tags, nur geänderte Dateien werden verarbeitet. +// Wird von /update_doku und /optimize --with-doku genutzt. +async function runUpdateDoku(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise { + if (!await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder")) { + ctx.ui.notify("Coder-Modell nicht verfügbar — Dokumentations-Phase abgebrochen", "error"); + return; + } + + // Jede Phase läuft unabhängig — Fehler in Phase 1 blockieren nicht Phase 2/3. + // Tag wird nur NACH erfolgreichem sendAndWait gesetzt. + + // Phase 1: Code-Kommentare + try { + const commentFiles = await getFilesSinceTag(pi, ctx, "docs-last-commented"); + if (commentFiles === null) { + ctx.ui.setStatus("update_doku", "1/3: Code wird kommentiert (alle Dateien)…"); + currentActivity = "Coder kommentiert Code…"; + await sendAndWait(pi, ctx, commentCodePrompt()); + await pi.exec("bash", ["-c", "git tag -f docs-last-commented"], { cwd: ctx.cwd }); + } else if (commentFiles.length === 0) { + ctx.ui.notify("Code-Kommentare: keine Änderungen seit letztem Lauf – übersprungen.", "info"); + } else { + ctx.ui.setStatus("update_doku", `1/3: Code wird kommentiert (${commentFiles.length} Datei(en))…`); + currentActivity = "Coder kommentiert Code…"; + await sendAndWait(pi, ctx, commentCodePromptIncremental(commentFiles)); + await pi.exec("bash", ["-c", "git tag -f docs-last-commented"], { cwd: ctx.cwd }); + } + } catch (e: any) { + ctx.ui.notify(`1/3 Code-Kommentare fehlgeschlagen: ${String(e?.message ?? e)}`, "error"); + } + + // Phase 2: README.md + try { + const readmeFiles = await getFilesSinceTag(pi, ctx, "docs-last-readme"); + if (readmeFiles === null) { + ctx.ui.setStatus("update_doku", "2/3: README.md wird geschrieben…"); + currentActivity = "Coder schreibt README…"; + await sendAndWait(pi, ctx, readmeMdPrompt()); + await pi.exec("bash", ["-c", "git tag -f docs-last-readme"], { cwd: ctx.cwd }); + } else if (readmeFiles.length === 0) { + ctx.ui.notify("README.md: keine Änderungen seit letztem Lauf – übersprungen.", "info"); + } else { + ctx.ui.setStatus("update_doku", `2/3: README.md wird geprüft (${readmeFiles.length} Datei(en) geändert)…`); + currentActivity = "Coder schreibt README…"; + await sendAndWait(pi, ctx, readmeMdPromptIncremental(readmeFiles)); + await pi.exec("bash", ["-c", "git tag -f docs-last-readme"], { cwd: ctx.cwd }); + } + } catch (e: any) { + ctx.ui.notify(`2/3 README.md fehlgeschlagen: ${String(e?.message ?? e)}`, "error"); + } + + // Phase 3: BEDIENUNGSANLEITUNG.md + try { + const bedFiles = await getFilesSinceTag(pi, ctx, "docs-last-bedienungsanleitung"); + if (bedFiles === null) { + ctx.ui.setStatus("update_doku", "3/3: BEDIENUNGSANLEITUNG.md wird geschrieben…"); + currentActivity = "Coder schreibt Bedienungsanleitung…"; + await sendAndWait(pi, ctx, bedienungsanleitungPrompt()); + await pi.exec("bash", ["-c", "git tag -f docs-last-bedienungsanleitung"], { cwd: ctx.cwd }); + } else if (bedFiles.length === 0) { + ctx.ui.notify("BEDIENUNGSANLEITUNG.md: keine Änderungen seit letztem Lauf – übersprungen.", "info"); + } else { + ctx.ui.setStatus("update_doku", `3/3: BEDIENUNGSANLEITUNG.md wird geprüft (${bedFiles.length} Datei(en) geändert)…`); + currentActivity = "Coder schreibt Bedienungsanleitung…"; + await sendAndWait(pi, ctx, bedienungsanleitungPromptIncremental(bedFiles)); + await pi.exec("bash", ["-c", "git tag -f docs-last-bedienungsanleitung"], { cwd: ctx.cwd }); + } + } catch (e: any) { + ctx.ui.notify(`3/3 BEDIENUNGSANLEITUNG.md fehlgeschlagen: ${String(e?.message ?? e)}`, "error"); + } + + // Abschließender Dokumentations-Commit (immer, auch bei Teilfehlern) + await pi.exec( + "bash", + ["-c", "git add -A && git commit -m 'docs: update comments, README, BEDIENUNGSANLEITUNG' || true"], + { cwd: ctx.cwd } + ); + + // TASK.md: Produktionsreif abhaken + await tickTaskMdStatus(pi, ctx, "Produktionsreif (SHIP)"); + + ctx.ui.setStatus("update_doku", "✓ Dokumentation abgeschlossen"); + ctx.ui.notify("Dokumentations-Phase abgeschlossen. Commit angelegt.", "info"); +} + +// ── Versions-Verwaltung (SemVer + Git-Tags) ────────────────────────────────── + +// Liest den höchsten vX.Y.Z-Tag via `git tag -l`. Gibt null zurück wenn kein Tag existiert. +async function getCurrentVersion( + pi: ExtensionAPI, + ctx: ExtensionCommandContext +): Promise<[number, number, number] | null> { + const res = await pi.exec("bash", ["-c", "git tag -l 'v*' | sort -V | tail -1"], { cwd: ctx.cwd }); + const raw = (res.stdout ?? "").trim(); + const m = raw.match(/^v?(\d+)\.(\d+)\.(\d+)$/); + return m ? [+m[1], +m[2], +m[3]] : null; +} + +// Analysiert Commit-Subjects seit dem letzten Tag nach Conventional Commits. +// feat! / BREAKING CHANGE → major, feat: → minor, alles andere → patch. +async function analyzeBumpType( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + since?: string +): Promise<"major" | "minor" | "patch"> { + const range = since ? `${since}..HEAD` : "HEAD"; + const res = await pi.exec("bash", ["-c", `git log ${range} --format="%s" 2>/dev/null`], { cwd: ctx.cwd }); + const lines = (res.stdout ?? "").split("\n"); + if (lines.some(l => /^feat!:|BREAKING CHANGE/.test(l))) return "major"; + if (lines.some(l => /^feat(\(.+\))?:/.test(l))) return "minor"; + return "patch"; +} + +// Findet die erste vorhandene Versions-Manifest-Datei im Arbeitsverzeichnis. +async function detectVersionFile( + pi: ExtensionAPI, + ctx: ExtensionCommandContext +): Promise<"package.json" | "Cargo.toml" | "pyproject.toml" | "VERSION" | null> { + for (const f of ["package.json", "Cargo.toml", "pyproject.toml"]) { + const r = await pi.exec("bash", ["-c", `test -f ${f}`], { cwd: ctx.cwd }); + if (r.exitCode === 0) return f as "package.json" | "Cargo.toml" | "pyproject.toml"; + } + const r = await pi.exec("bash", ["-c", "test -f VERSION"], { cwd: ctx.cwd }); + return r.exitCode === 0 ? "VERSION" : null; +} + +// Schreibt die neue Version in die Manifest-Datei und erstellt einen chore-Commit. +async function applyVersionBump( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + manifest: string, + version: string +): Promise { + let cmd: string; + if (manifest === "package.json") { + cmd = `npm version --no-git-tag-version ${version}`; + } else if (manifest === "Cargo.toml") { + cmd = `sed -i 's/^version = ".*"/version = "${version}"/' Cargo.toml`; + } else if (manifest === "pyproject.toml") { + cmd = `sed -i 's/^version = ".*"/version = "${version}"/' pyproject.toml`; + } else { + cmd = `printf 'v%s\\n' '${version}' > VERSION`; + } + await pi.exec("bash", ["-c", cmd], { cwd: ctx.cwd }); + await pi.exec( + "bash", + ["-c", `git add ${manifest} && git commit -m "chore: bump version to v${version}"`], + { cwd: ctx.cwd } + ); +} + +// Hauptfunktion: ermittelt aktuelle Version, analysiert Commits, zeigt Dialog, setzt Tag. +async function runVersionBump(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise { + // Early exit wenn kein git-Repo vorhanden + const gitCheck = await pi.exec("bash", ["-c", "git rev-parse --is-inside-work-tree 2>/dev/null"], { cwd: ctx.cwd }); + if (gitCheck.exitCode !== 0) return; + + const current = await getCurrentVersion(pi, ctx); + const tag = current ? `v${current[0]}.${current[1]}.${current[2]}` : undefined; + const bump = await analyzeBumpType(pi, ctx, tag); + + const [maj, min, pat] = current ?? [0, 0, 0]; + const initial = !current; + const versions: Record<"patch" | "minor" | "major", string> = initial + ? { patch: "v0.0.1", minor: "v0.1.0", major: "v1.0.0" } + : { patch: `v${maj}.${min}.${pat + 1}`, minor: `v${maj}.${min + 1}.0`, major: `v${maj + 1}.0.0` }; + + const recommended: "patch" | "minor" | "major" = initial ? "minor" : bump; + const labels = (["patch", "minor", "major"] as const).map( + t => `${t} → ${versions[t]}${t === recommended ? " (empfohlen)" : ""}` + ); + + const choice = await ctx.ui.select({ + title: "Version", + message: current + ? `Aktuelle Version: ${tag}. Commits seit letztem Tag: ${bump}-Bump erkannt.` + : "Noch kein Versions-Tag vorhanden.", + options: [...labels, "Überspringen"], + }); + + if (!choice || choice.startsWith("Überspringen")) return; + + const chosen = (["patch", "minor", "major"] as const).find(t => choice.startsWith(t))!; + const newVersion = versions[chosen].replace(/^v/, ""); + const newTag = `v${newVersion}`; + + const manifest = await detectVersionFile(pi, ctx); + if (manifest) { + await applyVersionBump(pi, ctx, manifest, newVersion); + } + + const tagResult = await pi.exec("bash", ["-c", `git tag ${newTag}`], { cwd: ctx.cwd }); + if (tagResult.exitCode !== 0) { + ctx.ui.notify(`Tag ${newTag} existiert bereits — manuell löschen mit: git tag -d ${newTag}`, "error"); + return; + } + ctx.ui.notify(`Version ${newTag} getaggt.`, "info"); +} + +// Committed alle ungespeicherten Änderungen nach SHIP — Sicherheitsnetz falls der LLM es vergessen hat. +async function autoCommitIfDirty(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise { + const status = await pi.exec("bash", ["-c", "git status --porcelain"], { cwd: ctx.cwd }); + if ((status.stdout ?? "").trim()) { + await pi.exec( + "bash", + ["-c", "git add -A && git commit -m 'chore: Abschluss-Commit (produktionsreif)'"], + { cwd: ctx.cwd } + ); + } +} + +// Zeigt die abschließende Erfolgsmeldung nach SHIP. +// "info" ist der einzige verfügbare positive Notification-Level in der pi-API. +function notifyShipSuccess(ctx: ExtensionCommandContext): void { + ctx.ui.notify( + "✅ Fertig! Das Programm ist jetzt produktionsreif und committed.", + "info" + ); +} + +// Prominente Abschluss-Notification + Widget-Update mit Uhrzeit und Ergebnis. +function finalNotify( + ctx: ExtensionCommandContext, + verdict: string, + detail: string +): void { + const timestamp = new Date().toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }); + const level = verdict.startsWith("🚀") ? "info" + : verdict.includes("NO-SHIP") || verdict.startsWith("⛔") ? "error" + : verdict.includes("⚠") ? "warning" + : "info"; + ctx.ui.notify(`${verdict}: ${detail}`, level); + ctx.ui.setWidget("coder-judge", [ + `Letzter Lauf: ${verdict} — ${detail} (${timestamp})`, + "/optimize [--rounds N] [--with-doku] [--continue] [--test-cmd \"cmd\"]", + "/fix · /judge · /shipit · /cancel · /continue · /help", + ]); +} + +// ── Extension ──────────────────────────────────────────────────────────────── + +let cancelRequested = false; +let currentModelKey = ""; // Cache für switchModel() — verhindert redundante setModel()-Aufrufe +let interactivePauseActive = false; +let interactiveContinueRequested = false; +let interactivePauseTask = ""; +let currentActivity = ""; // Working-Message für den aktuellen Command-Kontext +let phaseTimerHandle: ReturnType | null = null; + +// Startet einen Sekundentakt, der [MM:SS] in die Statuszeile schreibt — zeigt dem Nutzer, +// dass die LLM-Inference läuft und nicht hängt. Muss mit stopPhaseTimer() beendet werden. +function startPhaseTimer(ctx: ExtensionCommandContext, statusKey: string, label: string): void { + stopPhaseTimer(); + const start = Date.now(); + ctx.ui.setStatus(statusKey, `${label} [00:00]`); + phaseTimerHandle = setInterval(() => { + const s = Math.round((Date.now() - start) / 1000); + const mm = String(Math.floor(s / 60)).padStart(2, "0"); + const ss = String(s % 60).padStart(2, "0"); + ctx.ui.setStatus(statusKey, `${label} [${mm}:${ss}]`); + }, 1000); +} + +function stopPhaseTimer(): void { + if (phaseTimerHandle !== null) { + clearInterval(phaseTimerHandle); + phaseTimerHandle = null; + } +} + +// Erzeugt eine knappe Statuszeile aus Tool-Name und Argumenten. +function toolExecutionLabel(toolName: string, args: Record): string { + switch (toolName) { + case "edit": + return `Editiere ${args.path ?? "Datei"}…`; + case "write": + return `Schreibe ${args.path ?? "Datei"} neu…`; + case "read": + return `Lese ${args.path ?? "Datei"}…`; + case "grep": + return `Suche in ${args.path ?? args.pattern ?? "Dateien"}…`; + case "find": + return `Suche Dateien: ${args.pattern ?? ""}…`; + case "ls": + return `Verzeichnis: ${args.path ?? "."}…`; + case "bash": { + const cmd = String(args.command ?? "").trim().replace(/\n[\s\S]*/s, ""); + if (/git\s+commit/.test(cmd)) return "Git-Commit…"; + if (/git\s+add/.test(cmd)) return "Stage Änderungen…"; + if (/git\s+tag/.test(cmd)) return "Git-Tag setzen…"; + if (/pytest|npm test|cargo test|go test|make test/.test(cmd)) return "Tests laufen…"; + if (/git\s+(diff|log|show|tag -l)/.test(cmd)) return "Git-History lesen…"; + if (/patch\s+-p1/.test(cmd)) return "Wende Patch an…"; + if (/curl/.test(cmd)) return "HTTP-Request…"; + return `Shell: ${cmd.slice(0, 55)}${cmd.length > 55 ? "…" : ""}`; + } + case "apply_patch": + return "Wende Patch an…"; + default: + return ""; + } +} + +export default function (pi: ExtensionAPI) { + pi.on("session_start", async function (_event, ctx) { + ctx.ui.setWidget("coder-judge", [ + "/optimize [--rounds N] [--with-doku] [--continue] [--test-cmd \"cmd\"]", + "/fix · /judge · /shipit · /cancel · /continue · /help", + ]); + }); + + // ── Live-Aktivitätsstatus ──────────────────────────────────────────────── + // turn_start: Working-Text auf aktuellen Command-Kontext setzen + pi.on("turn_start", function (_event, ctx) { + if (currentActivity) ctx.ui.setWorkingMessage(currentActivity); + }); + + // tool_execution_start: präzise Statuszeile während Tool-Ausführung + pi.on("tool_execution_start", function (event, ctx) { + const label = toolExecutionLabel(event.toolName, (event as any).args ?? {}); + if (label) ctx.ui.setStatus("agent", label); + }); + + // tool_execution_end: Statuszeile löschen + pi.on("tool_execution_end", function (_event, ctx) { + ctx.ui.setStatus("agent", undefined); + }); + + // agent_end: Working-Text und Statuszeile zurücksetzen + pi.on("agent_end", function (_event, ctx) { + ctx.ui.setWorkingMessage(); + ctx.ui.setStatus("agent", undefined); + currentActivity = ""; + }); + + // ── Robustes edit: Bottom-up-Reordering via tool_call-Hook ───────────── + // Behebt "edits[n] doesn't match": Mehrere Edits auf dieselbe Datei werden + // von hinten nach vorne sortiert, damit frühere Edits spätere Positionen nicht verschieben. + + pi.on("tool_call", async function (event, ctx) { + if (event.toolName !== "edit") return; + + const input = event.input as { + path: string; + edits: Array<{ oldText: string; newText: string }>; + }; + + if (!input?.edits || input.edits.length <= 1) return; + + const readResult = await pi.exec( + "bash", + ["-c", `cat "$1"`, "_", input.path], + { cwd: ctx.cwd } + ); + if (readResult.code !== 0) return; + + const content = readResult.stdout; + + const positioned = input.edits.map(edit => ({ + edit, + idx: content.indexOf(edit.oldText) + })); + + // Nicht gefundene Einträge (idx === -1) ans Ende — sie schlagen sowieso fehl + positioned.sort((a, b) => b.idx - a.idx); + + input.edits.splice(0, input.edits.length, ...positioned.map(p => p.edit)); + }); + + // ── Manuelle Kommandos ─────────────────────────────────────────────────── + + pi.registerCommand("coder", { + description: "Implementiert ohne Review-Loop → qwen3.5-coder (:8001).", + handler: async function (args: string, ctx: ExtensionCommandContext) { + const task = (args || "").trim(); + if (!task) { + ctx.ui.notify("Benutzung: /coder ", "error"); + return; + } + if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) { + ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error"); + return; + } + await writeTaskMd(pi, ctx, task); + await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder"); + currentActivity = "Coder implementiert…"; + await sendAndWait(pi, ctx, coderKickoff(task)); + } + }); + + pi.registerCommand("judge", { + description: "Review gegen TASK.md + git show HEAD → qwen3.5-judge (:8001).", + handler: async function (args: string, ctx: ExtensionCommandContext) { + if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) { + ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error"); + return; + } + await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-judge"); + currentActivity = "Judge reviewt…"; + await sendAndWait(pi, ctx, judgePrompt(args || "")); + } + }); + + pi.registerCommand("fix", { + description: "Fixt Judge-Kritik, committet Ergebnis → qwen3.5-coder (:8001).", + handler: async function (args: string, ctx: ExtensionCommandContext) { + if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) { + ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error"); + return; + } + await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder"); + currentActivity = "Coder fixt Judge-Kritik…"; + await sendAndWait(pi, ctx, fixPrompt(args || "")); + } + }); + + pi.registerCommand("shipit", { + description: "Finale Freigabe gegen TASK.md + git log → qwen3.5-judge (:8001).", + handler: async function (args: string, ctx: ExtensionCommandContext) { + if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) { + ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error"); + return; + } + await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-judge"); + ctx.ui.notify("Judge prüft finale Freigabe — Ergebnis erscheint im Chat (SHIP / NO-SHIP)", "info"); + currentActivity = "Judge: finale Freigabe…"; + await sendAndWait(pi, ctx, shipitPrompt(args || "")); + const shipText = getLastAssistantText(ctx); + const shipVerdict = shipText.match(/Urteil:\s*(SHIP|NO-SHIP)/i)?.[1]?.toUpperCase() ?? ""; + if (shipVerdict === "SHIP") { + await autoCommitIfDirty(pi, ctx); + notifyShipSuccess(ctx); + } else if (shipVerdict === "NO-SHIP") { + ctx.ui.notify("NO-SHIP — noch Blocker offen. Bitte /fix aufrufen.", "error"); + } + } + }); + + // ── Automatische Optimierungsschleife ──────────────────────────────────── + + pi.registerCommand("optimize", { + description: "Coder→Judge→Fix-Schleife bis PASS (default 2 Runden, Runde 1: Quick-Judge). Klares PASS → direkt SHIP; PASS WITH CONCERNS → ShipIt-Runde (oder --approve-concerns zum Überspringen). --interactive: Checkpoint nach PASS. --no-tests: Test-Erkennung überspringen. /optimize [--rounds N] [--with-doku] [--continue] [--interactive] [--no-tests] [--approve-concerns] [--test-cmd \"override\"] [--test-timeout N]", + handler: async function (args: string, ctx: ExtensionCommandContext) { + const roundsMatch = (args || "").match(/--rounds\s+(\d+)/); + const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 2; + const withDoku = /--with-doku/.test(args || ""); + const continueMode = /--continue/.test(args || ""); + const interactive = /--interactive/.test(args || ""); + const noTests = /--no-tests/.test(args || ""); + const approveConcerns = /--approve-concerns/.test(args || ""); + const testCmdMatch = (args || "").match(/--test-cmd\s+"([^"]+)"|--test-cmd\s+'([^']+)'|--test-cmd\s+(\S+)/); + const testCmd: string | null = testCmdMatch ? (testCmdMatch[1] ?? testCmdMatch[2] ?? testCmdMatch[3]) : null; + const testTimeoutMatch = (args || "").match(/--test-timeout\s+(\d+)/); + const testTimeout = testTimeoutMatch ? Math.max(1, parseInt(testTimeoutMatch[1], 10)) : 120; + const task = (args || "") + .replace(/--rounds\s+\d+/, "") + .replace(/--test-timeout\s+\d+/, "") + .replace(/--with-doku/, "") + .replace(/--continue/, "") + .replace(/--interactive/, "") + .replace(/--no-tests/, "") + .replace(/--approve-concerns/, "") + .replace(/--test-cmd\s+"[^"]*"/, "") + .replace(/--test-cmd\s+\S+/, "") + .trim(); + + if (!continueMode && !task) { + ctx.ui.notify("Benutzung: /optimize [--rounds N] [--with-doku] [--continue] [--test-cmd \"befehl\"]", "error"); + return; + } + + try { + if (continueMode) { + // --continue: Implementierungsphase überspringen, direkt in Judge→Fix-Schleife + // Erweiterter Auftrag wird als Zusatzauftrag in TASK.md eingetragen (falls angegeben) + if (task) await writeTaskMd(pi, ctx, task); + ctx.ui.setStatus("optimize", `Setze fort (max ${maxRounds} Runden Judge→Fix)…`); + const continueMsg = task + ? `--continue: Zusatzauftrag in TASK.md eingetragen, überspringe Implementierung.` + : `--continue: Überspringe Implementierung, starte direkt mit Judge-Prüfung.`; + ctx.ui.notify(continueMsg, "info"); + + // Im --continue-Modus: Single-Server prüfen (Port 8001 — Coder + Judge in einem). + ctx.ui.setStatus("optimize", "Single-Server wird geprüft (Port 8001)…"); + const serverReady = await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single"); + if (!serverReady) { + finalNotify(ctx, "⛔ Server nicht erreichbar", "Port 8001 — kein HTTP 200 nach 3 min. start-single.sh ausführen"); + return; + } + } else { + // TASK.md anlegen und Implementierung starten + await writeTaskMd(pi, ctx, task); + ctx.ui.setStatus("optimize", `Starte Optimierung (max ${maxRounds} Runden)…`); + const taskPreview = task.length > 55 ? task.slice(0, 52) + "…" : task; + ctx.ui.setStatus("optimize", `◉ Coder liest Anforderungen + implementiert: ${taskPreview}`); + if (!await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder")) { + finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar"); + return; + } + currentActivity = "Coder implementiert…"; + startPhaseTimer(ctx, "optimize", `◉ Coder implementiert: ${taskPreview}`); + await sendAndWait(pi, ctx, coderKickoff(task)); + stopPhaseTimer(); + await tickTaskMdStatus(pi, ctx, "Implementierung"); + if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", "Nach Implementierung"); return; } + + // Judge-Bereitschaft via Completion-Check — /health antwortet bereits während des + // GPU-Ladevorgangs und ist kein verlässliches Signal. Nur HTTP 200 auf einen + // echten Completion-Request bedeutet: Modell ist im VRAM und bereit. + ctx.ui.setStatus("optimize", "Single-Server wird geprüft…"); + if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) { + finalNotify(ctx, "⛔ Server nicht erreichbar", "Port 8001 — kein HTTP 200 nach 3 min. start-single.sh ausführen"); + return; + } + } + + // Test-Suiten ermitteln: --no-tests überspringt alles, --test-cmd überschreibt Auto-Erkennung. + // Läuft nach Coder, damit neu angelegte Test-Dateien bereits erkannt werden. + let autoTestCmds: string[] = []; + if (noTests) { + ctx.ui.notify("--no-tests: Test-Erkennung übersprungen.", "info"); + } else { + ctx.ui.setStatus("optimize", "Test-Suiten werden erkannt…"); + autoTestCmds = testCmd ? [testCmd] : await detectTestCommands(pi, ctx); + if (autoTestCmds.length > 0) { + const label = autoTestCmds.map(c => c.split(" ")[0]).join(", "); + ctx.ui.notify( + `${autoTestCmds.length} Test-Suite${autoTestCmds.length > 1 ? "n" : ""} erkannt: ${label}`, + "info" + ); + } else { + ctx.ui.notify("Keine Test-Suiten erkannt — Judge führt Tests selbst aus.", "info"); + } + } + + let lastBlockers = ""; + let verdict = ""; + let keepGoing = true; + + // Äußere Schleife für --interactive: nach PASS pausieren, Zusatzaufträge ermöglichen. + while (keepGoing) { + keepGoing = false; + verdict = ""; + lastBlockers = ""; + + // Schleife: Judge → (PASS? fertig : Fix → nächste Runde) + for (let round = 1; round <= maxRounds; round++) { + const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round); + if (!await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-judge")) { + finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar"); + return; + } + + // Runde 1 ohne --continue: Quick-Judge (kein TASK.md, kürzerer Prompt). + // Bei FAIL folgt Runde 2 mit vollem judgePrompt für detaillierte Analyse. + const useQuickJudge = round === 1 && !continueMode; + if (autoTestCmds.length > 0) { + const label = autoTestCmds.length === 1 + ? autoTestCmds[0].split(" ")[0] + : `${autoTestCmds.length} Suiten parallel`; + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${label}, max. ${testTimeout}s)…`); + const testOutput = await runTestsParallel(pi, ctx, autoTestCmds, testTimeout); + const judgeLabel = useQuickJudge ? "Quick-Check" : "Judge analysiert"; + currentActivity = `Judge Runde ${round}/${maxRounds}${useQuickJudge ? " (Quick)" : ""}…`; + startPhaseTimer(ctx, "optimize", `${prog} Runde ${round}/${maxRounds}: ${judgeLabel} Test-Ergebnis`); + await sendAndWait(pi, ctx, useQuickJudge + ? quickJudgeWithTestsPrompt(testOutput, "") + : judgeWithTestsPrompt(testOutput, "")); + stopPhaseTimer(); + } else { + const judgeLabel = useQuickJudge ? "Quick-Check" : "Judge — TASK.md + letzter Commit + Tests"; + currentActivity = `Judge Runde ${round}/${maxRounds}${useQuickJudge ? " (Quick)" : ""}…`; + startPhaseTimer(ctx, "optimize", `${prog} Runde ${round}/${maxRounds}: ${judgeLabel}`); + await sendAndWait(pi, ctx, useQuickJudge ? quickJudgePrompt("") : judgePrompt("")); + stopPhaseTimer(); + } + if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Judge Runde ${round}`); return; } + + const judgeText = getLastAssistantText(ctx); + verdict = parseVerdict(judgeText); + + if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") { + await tickTaskMdStatus(pi, ctx, "Review bestanden (PASS)"); + const nextStep = interactive ? "warte auf /continue…" : "ShipIt…"; + ctx.ui.setStatus("optimize", `${"●".repeat(round)} ✓ ${verdict} nach Runde ${round}/${maxRounds} — ${nextStep}`); + break; + } + + // Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen. + // Normalisierung verhindert False-Negatives durch minimale Formulierungsunterschiede. + const currentBlockers = parseBlockers(judgeText); + if (currentBlockers && normalizeForComparison(currentBlockers) === normalizeForComparison(lastBlockers)) { + ctx.ui.setStatus("optimize", `${prog} ⚠ Gleicher Blocker in Runde ${round} – manuelle Intervention nötig`); + finalNotify(ctx, "⚠ Schleife", "Gleicher Blocker zweimal – manuelle Intervention nötig"); + return; + } + lastBlockers = currentBlockers; + + if (round === maxRounds) { + ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)} ⚠ Max. ${maxRounds} Runden ohne PASS`); + if (verdict === "UNREADABLE") { + finalNotify(ctx, "⚠ Urteil unklar", `${maxRounds} Runden – Judge-Urteil nicht erkennbar, Antwort im Chat prüfen`); + } else { + finalNotify(ctx, "⚠ Kein PASS", `${maxRounds} Runden ohne PASS – bitte /judge und /fix manuell`); + } + return; + } + + // Fix-Phase: Blocker-Preview aus Judge-Bericht anzeigen + const blockerHint = currentBlockers + ? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers) + : "Kritikpunkte aus Judge-Bericht"; + if (!await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder")) { + finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar"); + return; + } + currentActivity = `Coder fixt: ${blockerHint}`; + startPhaseTimer(ctx, "optimize", `${prog} Runde ${round}/${maxRounds}: Coder fixt — ${blockerHint}`); + await sendAndWait(pi, ctx, fixPrompt("")); + stopPhaseTimer(); + if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Fix Runde ${round}`); return; } + } + + // Interactive-Modus: nach PASS pausieren und auf /continue warten (max 30 min). + // /continue ohne Args → direkt ShipIt. /continue "Zusatz" → Coder implementiert, Loop nochmal. + if (interactive && (verdict === "PASS" || verdict === "PASS WITH CONCERNS")) { + interactivePauseActive = true; + interactiveContinueRequested = false; + interactivePauseTask = ""; + + ctx.ui.setStatus("optimize", `⏸ ${verdict} – warte auf /continue…`); + ctx.ui.notify( + `✅ ${verdict} erreicht. Weitere Features? /continue "Zusatzauftrag" — oder /continue zum Shippern.`, + "info" + ); + + const waitStart = Date.now(); + while (!interactiveContinueRequested && !cancelRequested) { + if (Date.now() - waitStart > 30 * 60 * 1000) { + interactivePauseActive = false; + finalNotify(ctx, "⚠ Timeout", "30 min ohne /continue — abgebrochen"); + return; + } + await new Promise(r => setTimeout(r, 500)); + } + interactivePauseActive = false; + + if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", "Im Interactive-Modus"); return; } + + if (interactivePauseTask) { + // Zusatzauftrag: Coder implementiert, dann Judge-Loop erneut + const addPreview = interactivePauseTask.length > 50 + ? interactivePauseTask.slice(0, 47) + "…" + : interactivePauseTask; + ctx.ui.setStatus("optimize", `◉ Coder implementiert Zusatzauftrag: ${addPreview}`); + await writeTaskMd(pi, ctx, interactivePauseTask); + if (!await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder")) { + finalNotify(ctx, "⛔ Modell-Fehler", "Coder-Modell nicht verfügbar"); + return; + } + currentActivity = "Coder implementiert Zusatzauftrag…"; + startPhaseTimer(ctx, "optimize", `◉ Coder implementiert Zusatzauftrag: ${addPreview}`); + await sendAndWait(pi, ctx, coderKickoff(interactivePauseTask)); + stopPhaseTimer(); + await tickTaskMdStatus(pi, ctx, "Implementierung"); + if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", "Nach Zusatz-Implementierung"); return; } + keepGoing = true; + continue; + } + // /continue ohne Args → direkt zu ShipIt (verdict bleibt PASS) + } + } + + // Finaler SHIP-Schritt: klares PASS → direkt SHIP ohne zweiten Inference-Aufruf. + // "PASS WITH CONCERNS" + --approve-concerns → direkt SHIP (ShipIt-Runde überspringen). + // "PASS WITH CONCERNS" ohne Flag → ShipIt-Runde als finale Abwägung. + if (verdict === "PASS" || (verdict === "PASS WITH CONCERNS" && approveConcerns)) { + ctx.ui.setStatus("optimize", "🚀 SHIP – produktionsreif"); + await autoCommitIfDirty(pi, ctx); + notifyShipSuccess(ctx); + finalNotify(ctx, "🚀 SHIP", "Programm ist produktionsreif"); + await runVersionBump(pi, ctx); + if (withDoku) { + await runUpdateDoku(pi, ctx); + } else { + ctx.ui.notify("Nächster Schritt: /update_doku für Code-Kommentare, README.md und BEDIENUNGSANLEITUNG.md", "info"); + } + } else if (verdict === "PASS WITH CONCERNS") { + ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)}◉ ShipIt — PASS WITH CONCERNS, finale Freigabe?…`); + if (!await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-judge")) { + finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar"); + return; + } + currentActivity = "Judge: finale Freigabe…"; + startPhaseTimer(ctx, "optimize", `${"●".repeat(maxRounds)}◉ ShipIt — finale Freigabe`); + await sendAndWait(pi, ctx, shipitPrompt("")); + stopPhaseTimer(); + + const shipText = getLastAssistantText(ctx); + const shipVerdict = shipText.match(/Urteil:\s*(SHIP|NO-SHIP)/i)?.[1]?.toUpperCase() ?? ""; + + if (shipVerdict === "SHIP") { + ctx.ui.setStatus("optimize", "🚀 SHIP – produktionsreif"); + await autoCommitIfDirty(pi, ctx); + notifyShipSuccess(ctx); + finalNotify(ctx, "🚀 SHIP", "Programm ist produktionsreif"); + await runVersionBump(pi, ctx); + if (withDoku) { + await runUpdateDoku(pi, ctx); + } else { + ctx.ui.notify("Nächster Schritt: /update_doku für Code-Kommentare, README.md und BEDIENUNGSANLEITUNG.md", "info"); + } + } else if (shipVerdict === "NO-SHIP") { + ctx.ui.setStatus("optimize", "⛔ NO-SHIP – noch nicht bereit"); + finalNotify(ctx, "⛔ NO-SHIP", "Noch Blocker offen – bitte /judge und /fix manuell"); + } else { + ctx.ui.setStatus("optimize", "ShipIt abgeschlossen"); + finalNotify(ctx, "ShipIt", "Kein klares Urteil – Antwort im Chat prüfen"); + } + } + } catch (e: any) { + finalNotify(ctx, "⛔ Fehler", String(e?.message ?? e)); + } finally { + stopPhaseTimer(); + // Sicherstellen dass keine Zustandsvariable in späteren /optimize-Aufruf leckt + cancelRequested = false; + currentModelKey = ""; + interactivePauseActive = false; + interactiveContinueRequested = false; + interactivePauseTask = ""; + } + } + }); + + // ── Schlanke Kommandos für kleine Änderungen ───────────────────────────── + + pi.registerCommand("patch", { + description: "Gezielte Minimaländerung ohne Refactoring, committet → qwen3.5-coder (:8001).", + handler: async function (args: string, ctx: ExtensionCommandContext) { + const change = (args || "").trim(); + if (!change) { + ctx.ui.notify("Benutzung: /patch ", "error"); + return; + } + if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) { + ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error"); + return; + } + await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder"); + currentActivity = "Coder patcht…"; + await sendAndWait(pi, ctx, patchPrompt(change)); + } + }); + + pi.registerCommand("quick_check", { + description: "Schnelle OK/PROBLEM-Prüfung einer kleinen Codeänderung → qwen3.5-judge (:8001).", + handler: async function (args: string, ctx: ExtensionCommandContext) { + if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) { + ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error"); + return; + } + await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-judge"); + currentActivity = "Judge: Schnellcheck…"; + await sendAndWait(pi, ctx, quickCheckPrompt(args || "")); + } + }); + + // ── Dokumentations-Phase ───────────────────────────────────────────────── + + pi.registerCommand("update_doku", { + description: "Inkrementelle Code-Kommentare + README.md + BEDIENUNGSANLEITUNG.md via Git-Tags.", + handler: async function (_args: string, ctx: ExtensionCommandContext) { + await runUpdateDoku(pi, ctx); + } + }); + + // ── Robustes Editieren via GNU patch ───────────────────────────────────── + + pi.registerTool({ + name: "apply_patch", + label: "Patch anwenden", + description: [ + "Wendet einen unified diff (git-Format) auf Dateien an.", + "Zuverlässiger als das edit-Tool bei mehrfachen Änderungen an derselben Datei.", + "Format: --- a/pfad/datei +++ b/pfad/datei @@ -n,m +n,m @@ ...", + "Verwende dieses Tool wenn du mehrere Stellen in einer Datei änderst." + ].join(" "), + parameters: Type.Object({ + patch: Type.String({ + description: "Unified diff im git-Format mit --- a/... und +++ b/... Headern." + }), + }), + async execute(_id, params, _signal, _onUpdate, ctx) { + const tmpFile = `/tmp/pi_patch_${Date.now()}_${Math.random().toString(36).slice(2)}.diff`; + await pi.exec( + "bash", + ["-c", `printf "%s" "$1" > "${tmpFile}"`, "_", params.patch], + { cwd: ctx.cwd } + ); + // -p1 entfernt führende a/ b/ Präfixe (git-Standard) + const result = await pi.exec( + "bash", + ["-c", `patch -p1 < "${tmpFile}"; rm -f "${tmpFile}"`], + { cwd: ctx.cwd } + ); + if (result.code !== 0) { + return { + content: [{ type: "text", text: `Patch fehlgeschlagen:\n${result.stderr}\n${result.stdout}` }], + isError: true + }; + } + return { content: [{ type: "text", text: result.stdout || "Patch erfolgreich angewendet." }] }; + } + }); + + // ── Planungsmodus ──────────────────────────────────────────────────────── + + pi.registerCommand("plan", { + description: "Erstellt Implementierungsplan in PLAN.md ohne Dateiänderungen → qwen3.5-coder.", + handler: async function (args: string, ctx: ExtensionCommandContext) { + const task = (args || "").trim(); + if (!task) { + ctx.ui.notify("Benutzung: /plan ", "error"); + return; + } + if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) { + ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error"); + return; + } + await writeTaskMd(pi, ctx, task); + await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder"); + ctx.ui.setStatus("plan", "Analysiere und plane (keine Dateiänderungen)…"); + currentActivity = "Coder plant (kein Code)…"; + await sendAndWait(pi, ctx, planPrompt(task)); + ctx.ui.setStatus("plan", ""); + finalNotify(ctx, "📋 Plan", "Analyse abgeschlossen — PLAN.md + Chat"); + } + }); + + pi.registerCommand("version", { + description: "Versionsnummer des Projekts erhöhen (SemVer + Git-Tag). Analysiert Commits seit letztem Tag.", + handler: async function (_args: string, ctx: ExtensionCommandContext) { + await runVersionBump(pi, ctx); + } + }); + + pi.registerCommand("help", { + description: "Zeigt alle Kommandos der pi-coder-judge-Extension.", + handler: async function (_args: string, ctx: ExtensionCommandContext) { + ctx.ui.notify([ + "── Kern-Workflow ─────────────────────────────────────────", + "/optimize [--rounds N] [--with-doku] [--continue]", + " [--test-cmd \"cmd\"] [--test-timeout N]", + " Coder→Judge→Fix-Schleife bis PASS (empfohlener Einstieg)", + "/fix [kommentar] Fixt Judge-Kritik, committet → Coder", + "/judge [kommentar] Review gegen TASK.md + HEAD → Judge", + "/shipit [kommentar] Finale Freigabe (SHIP/NO-SHIP) → Judge", + "", + "── Steuerung ─────────────────────────────────────────────", + "/continue Unterbrochenen Prozess fortsetzen", + "/cancel Laufenden Loop nach aktuellem Schritt abbrechen", + "", + "── Erweiterte Kommandos (immer tippbar, nicht im Menü) ───", + "/coder Nur Implementierung ohne Review-Loop → Coder", + "/patch <änderung> Gezielte Minimaländerung → Coder", + "/quick_check [was] Schnelle OK/PROBLEM-Prüfung → Judge", + "/plan Implementierungsplan in PLAN.md → Coder", + "/update_doku Code-Kommentare + README.md + BEDIENUNGSANLEITUNG.md", + "/version Versionsnummer erhöhen (SemVer + Git-Tag)", + "/discard Verwirft PLAN.md", + "/new_project Projektverzeichnis + git init + .gitignore", + ].join("\n"), "info"); + } + }); + + pi.registerCommand("cancel", { + description: "Bricht laufenden Optimize-Loop nach dem aktuellen Schritt ab.", + handler: async function (_args: string, ctx: ExtensionCommandContext) { + cancelRequested = true; + ctx.ui.notify("Abbruch angefordert — wird nach aktuellem Schritt gestoppt", "warning"); + } + }); + + pi.registerCommand("discard", { + description: "Löscht PLAN.md und verwirft den aktuellen Plan.", + handler: async function (_args: string, ctx: ExtensionCommandContext) { + await pi.exec("bash", ["-c", "rm -f PLAN.md"], { cwd: ctx.cwd }); + ctx.ui.notify("PLAN.md gelöscht — Plan verworfen", "info"); + finalNotify(ctx, "🗑 Plan verworfen", "Neu starten mit /plan oder /coder"); + } + }); + + pi.registerCommand("continue", { + description: "Im --interactive-Modus: bestätigt PASS und geht zu ShipIt — oder /continue \"Zusatz\" für weiteren Auftrag. Sonst: nimmt unterbrochenen Prozess wieder auf.", + handler: async function (args: string, ctx: ExtensionCommandContext) { + // Interactive-Pause-Handler: Signal an laufenden /optimize-Loop + if (interactivePauseActive) { + interactivePauseTask = (args || "").trim(); + interactiveContinueRequested = true; + const msg = interactivePauseTask + ? `Zusatzauftrag eingetragen: "${interactivePauseTask}" — Coder startet` + : "Fortfahren — ShipIt wird gestartet"; + ctx.ui.notify(msg, "info"); + return; + } + + // Standard-Verhalten: unterbrochenen Prozess wieder aufnehmen + if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) { + ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error"); + return; + } + await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder"); + ctx.ui.setStatus("continue", "Analysiere unterbrochenen Prozess…"); + currentActivity = "Coder analysiert Stand…"; + await sendAndWait(pi, ctx, [ + "Ein Prozess wurde unterbrochen. Analysiere den aktuellen Stand und führe ihn sinnvoll fort:", + "1. Lies TASK.md für den Auftrag", + "2. Lies PLAN.md falls vorhanden (war ein Plan in Arbeit?)", + "3. Führe 'git log --oneline -5' aus um zu sehen was bereits committed wurde", + "4. Entscheide: Muss noch implementiert werden? Ist ein Review fällig? Müssen Fixes nachgezogen werden?", + "5. Fahre direkt mit dem nächsten sinnvollen Schritt fort — kein langer Bericht, einfach weitermachen.", + ].join("\n")); + ctx.ui.setStatus("continue", ""); + } + }); + + // ── Projekt-Scaffolding ────────────────────────────────────────────────── + + pi.registerCommand("new_project", { + description: "Legt Projektverzeichnis, git-Repo und .gitignore an.", + handler: async function (args: string, ctx: ExtensionCommandContext) { + const rawPath = (args || "").trim(); + if (!rawPath) { + ctx.ui.notify("Benutzung: /new_project ", "error"); + return; + } + + // ~ expandieren + const projectPath = rawPath.startsWith("~/") + ? rawPath.replace("~/", (process.env.HOME || "") + "/") + : rawPath; + + // Verzeichnis anlegen + const mkResult = await pi.exec("bash", ["-c", 'mkdir -p "$1"', "_", projectPath], { cwd: ctx.cwd }); + if (mkResult.code !== 0) { + ctx.ui.notify(`Fehler: ${mkResult.stderr}`, "error"); + return; + } + + // git init (nur wenn noch kein Repo vorhanden) + const gitCheck = await pi.exec("bash", ["-c", "test -d .git && echo exists"], { cwd: projectPath }); + if (gitCheck.stdout.trim() !== "exists") { + await pi.exec("bash", ["-c", "git init"], { cwd: projectPath }); + } + + // .gitignore anlegen + const gitignore = "target/\n*.o\n*.d\n*.swp\n.env\n.DS_Store\n"; + await pi.exec("bash", ["-c", 'printf "%s" "$1" > .gitignore', "_", gitignore], { cwd: projectPath }); + await pi.exec( + "bash", + ["-c", "git add .gitignore && git commit -m 'chore: init project' || true"], + { cwd: projectPath } + ); + + ctx.ui.notify(`Projekt angelegt: ${projectPath}`, "info"); + ctx.ui.notify( + `⚠ Pi läuft noch in: ${ctx.cwd} — Session-Verzeichnis kann nicht gewechselt werden.\n` + + `Neues Projekt starten: cd ${projectPath} && pi`, + "warning" + ); + ctx.ui.setStatus("new_project", `Neues Projekt → cd ${projectPath} && pi`); + } + }); +} diff --git a/start-servers.sh b/start-servers.sh new file mode 100755 index 0000000..c6f424f --- /dev/null +++ b/start-servers.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +LOG_CODER=$(mktemp /tmp/coder_XXXXXX.log) +LOG_JUDGE=$(mktemp /tmp/judge_XXXXXX.log) + +echo "[*] Starte beide Server parallel ..." +bash "$SCRIPT_DIR/start-coder.sh" > "$LOG_CODER" 2>&1 & +PID_CODER=$! +bash "$SCRIPT_DIR/start-judge.sh" > "$LOG_JUDGE" 2>&1 & +PID_JUDGE=$! + +wait_result() { + local PID="$1" NAME="$2" LOG="$3" + if wait "$PID"; then + echo "[✓] $NAME bereit" + else + echo "[✗] $NAME fehlgeschlagen — Log:" + cat "$LOG" + return 1 + fi +} + +RC=0 +wait_result "$PID_CODER" "Coder (:8001)" "$LOG_CODER" || RC=1 +wait_result "$PID_JUDGE" "Judge (:8002)" "$LOG_JUDGE" || RC=1 + +rm -f "$LOG_CODER" "$LOG_JUDGE" +exit $RC diff --git a/start-single.sh b/start-single.sh new file mode 100755 index 0000000..553b14d --- /dev/null +++ b/start-single.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail + +HF_HOME="${HF_HOME:-/home/dschlueter/nvme2n1p7_home/huggingface}" +MODEL_REL_PATH="models/qwen3/Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS.gguf" +IMAGE="ghcr.io/ggml-org/llama.cpp:server-cuda" +CONTAINER_NAME="qwen36-27b-single" +HOST_PORT=8001 +CONTAINER_PORT=8000 +MODEL_ALIAS="qwen3.5-single" +GPU_DEVICE="${GPU_DEVICE:-1}" + +echo "[*] Verwende HF_HOME = $HF_HOME" +echo "[*] GPU device = $GPU_DEVICE (überschreibbar: GPU_DEVICE=2 ./start-single.sh)" +if [ ! -f "$HF_HOME/$MODEL_REL_PATH" ]; then + echo "[!] Modell-Datei nicht gefunden: $HF_HOME/$MODEL_REL_PATH" >&2 + exit 1 +fi + +if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}\$"; then + echo "[*] Stoppe existierenden Container $CONTAINER_NAME ..." + docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true +fi + +echo "[*] Starte llama.cpp-Single-Server (Coder + Judge in einem) ..." +docker run -d \ + --gpus "\"device=${GPU_DEVICE}\"" \ + --name "$CONTAINER_NAME" \ + --restart unless-stopped \ + -e HF_HOME="/hf_home" \ + -v "$HF_HOME:/hf_home:ro" \ + -p "${HOST_PORT}:${CONTAINER_PORT}" \ + "$IMAGE" \ + -m "/hf_home/${MODEL_REL_PATH}" \ + --alias "${MODEL_ALIAS}" \ + -c 262144 \ + -n 16384 \ + --jinja \ + --reasoning on \ + --no-context-shift \ + --temp 0.65 \ + --top-p 0.80 \ + --top-k 20 \ + --min-p 0.01 \ + --repeat-penalty 1.05 \ + --main-gpu 0 \ + -ngl 999 \ + -fa on \ + --kv-unified \ + --cache-type-k q4_0 \ + --cache-type-v q4_0 \ + --batch-size 1024 \ + --ubatch-size 512 \ + --parallel 1 \ + --cont-batching \ + --host 0.0.0.0 \ + --port "$CONTAINER_PORT" + +echo "[*] Warte auf Modell-Bereitschaft (Completion-Check, max. 300 s) ..." +MODEL_READY=0 +for i in {1..150}; do + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 \ + -X POST "http://localhost:${HOST_PORT}/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -d "{\"model\":\"${MODEL_ALIAS}\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":1,\"temperature\":0.0,\"stream\":false}") + if [ "$HTTP_CODE" = "200" ]; then MODEL_READY=1; break; fi + echo " [${i}/150] HTTP ${HTTP_CODE:-000} — Modell lädt noch, warte 2s ..." + sleep 2 +done + +if [ "$MODEL_READY" -ne 1 ]; then + echo "[!] Modell wurde nicht rechtzeitig bereit (kein HTTP 200 auf Completion)." >&2 + docker logs --tail 200 "$CONTAINER_NAME" || true + exit 1 +fi + +echo "[*] Modell bereit — erster Completion-Request erfolgreich (HTTP 200)." +echo "[*] Server läuft auf http://0.0.0.0:${HOST_PORT} (Coder + Judge)" +echo "[*] Stoppen mit: docker rm -f ${CONTAINER_NAME}" diff --git a/status.sh b/status.sh new file mode 100755 index 0000000..9eacbf7 --- /dev/null +++ b/status.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +check_server() { + local NAME="$1" + local PORT="$2" + local ALIAS="$3" + + printf "%-28s" "$NAME (Port $PORT):" + + # Docker-Status + if docker ps --format '{{.Names}}' | grep -q "^${NAME}\$"; then + printf " Container=\033[32mRUNNING\033[0m" + elif docker ps -a --format '{{.Names}}' | grep -q "^${NAME}\$"; then + printf " Container=\033[33mSTOPPED\033[0m" + else + printf " Container=\033[31mNOT FOUND\033[0m" + echo + return + fi + + # HTTP-Erreichbarkeit + if curl -s --max-time 3 "http://localhost:${PORT}/health" >/dev/null 2>&1 || \ + curl -s --max-time 3 "http://localhost:${PORT}/v1/models" >/dev/null 2>&1; then + printf " HTTP=\033[32mOK\033[0m" + else + printf " HTTP=\033[31mNOT READY\033[0m" + fi + + echo +} + +echo "=== LLaMA-Server Status ===" +check_server "qwen36-27b-coder" 8001 "qwen3.5-coder" +check_server "qwen36-27b-judge" 8002 "qwen3.5-judge" diff --git a/stop-servers.sh b/stop-servers.sh new file mode 100755 index 0000000..1d25229 --- /dev/null +++ b/stop-servers.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +CODER="qwen36-27b-coder" +JUDGE="qwen36-27b-judge" + +for NAME in "$CODER" "$JUDGE"; do + if docker ps -a --format '{{.Names}}' | grep -q "^${NAME}\$"; then + docker rm -f "$NAME" >/dev/null + echo "[*] Gestoppt: $NAME" + else + echo "[-] Nicht gefunden: $NAME" + fi +done