diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c1bfaae --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.bak +node_modules/ +.DS_Store + +# pi-coder Laufzeitartefakte +TASK.md +examples/**/TASK.md + +# Build-Artefakte (Examples) +examples/rust-wordcount/target/ +examples/**/__pycache__/ +examples/**/.pytest_cache/ +examples/**/Cargo.lock +examples/**/ll_demo diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md new file mode 100644 index 0000000..9adb373 --- /dev/null +++ b/BEDIENUNGSANLEITUNG.md @@ -0,0 +1,896 @@ +# 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: 1–3 Minuten (Modell wird in GPU-VRAM geladen). + +### 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 + +### Beide starten (empfohlen) + +```bash +./start-servers.sh +``` + +### Einzelnen Server neu starten + +Z.B. wenn nur der Coder-Server abgestürzt ist: + +```bash +./start-coder.sh +``` + +### Beide stoppen + +```bash +./stop-servers.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 immer die aktuelle Aktivität: +`Coder implementiert…` → `Quick-Check…` → `Coder fixt Blocker…` → `Judge reviewt (Runde 2/2)…` + +### 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 # GPUs sichtbar? + docker run --gpus '"device=1,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.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/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..cde1fd7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,80 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Was ist dieses Repo? + +Eine **TypeScript-Extension** für den [`pi`-Coding-Agent](https://github.com/earendil-works/pi) (`@earendil-works/pi-coding-agent`). Sie implementiert einen automatisierten **Coder → Judge → Fix**-Loop mit zwei lokalen llama.cpp-Servern. + +Nach jeder Änderung an `pi-coder-judge-extension.ts` oder `models.json` muss deployed werden: + +```bash +./install.sh # kopiert nach ~/.pi/agent/extensions/ und ~/.pi/agent/ +# dann in pi agent: /reload +``` + +## Server-Lifecycle + +```bash +./start-servers.sh # beide Container parallel starten (empfohlen, ~1–3 min) +./start-coder.sh # nur Coder :8001 +./start-judge.sh # nur Judge :8002 +./stop-servers.sh # beide stoppen +./status.sh # Container- und HTTP-Status beider Server +``` + +Die Start-Skripte stoppen existierende Container automatisch vor dem Neustart. + +## Architektur der Extension + +`pi-coder-judge-extension.ts` ist die einzige Logikdatei. Sie registriert alle Commands, das `apply_patch`-Custom-Tool und zwei Event-Hooks beim pi-Agent. + +**Zwei LLM-Rollen:** + +| Rolle | Port | Container | Alias | +|-------|------|-----------|-------| +| Coder (Implementierung, Fixes, Doku) | 8001 | `qwen36-27b-coder` | `qwen3.5-coder` | +| Judge (Review, ShipIt, QuickCheck) | 8002 | `qwen36-27b-judge` | `qwen3.5-judge` | + +Beide nutzen dasselbe GGUF (`Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS.gguf`), aber unterschiedliche Serverparameter. + +**Zentraler Ablauf in `/optimize`:** +1. `writeTaskMd()` → TASK.md anlegen +2. `--continue`-Modus: Coder- und Judge-Server **parallel** via `Promise.all(waitUntilModelReady×2)` prüfen +3. Coder: `coderKickoff()` → implementiert + committet +4. Äußere `while(keepGoing)`-Schleife (für `--interactive`-Zusatzaufträge) +5. Loop (max. N Runden, Standard 2): + - Runde 1 (ohne `--continue`): `quickJudgePrompt()` / `quickJudgeWithTestsPrompt()` — kurzer Erstcheck + - Runde 2+: `judgePrompt()` / `judgeWithTestsPrompt()` — vollständige Analyse mit TASK.md + - `parseVerdict()` → PASS? → break. FAIL? → `parseBlockers()` → `normalizeForComparison()` → Loop-Check → Fix → nächste Runde +6. Bei PASS + `--interactive`: Polling auf `interactiveContinueRequested`. Zusatzauftrag → `coderKickoff()` → `keepGoing = true` +7. SHIP-Schritt: `PASS` oder (`PASS WITH CONCERNS` + `--approve-concerns`) → direkt SHIP. `PASS WITH CONCERNS` sonst → `shipitPrompt()` → SHIP/NO-SHIP +8. Loop-Erkennung: `normalizeForComparison(currentBlockers) === normalizeForComparison(lastBlockers)` → Abbruch +9. Optional: `runUpdateDoku()` bei `--with-doku` + +**`tool_call`-Hook (edit-Reordering):** Sortiert Multi-Edit-Aufrufe auf dieselbe Datei von hinten nach vorne. Verhindert den Fehler „edits[n] doesn't match" wenn mehrere Stellen einer Datei auf einmal geändert werden. + +**`apply_patch`-Tool:** Wendet unified diffs via `patch -p1` an — robuster als mehrfache `edit`-Aufrufe bei umfangreichen Änderungen. + +**Inkrementelle Dokumentation (`runUpdateDoku`):** Git-Tags (`docs-last-commented`, `docs-last-readme`, `docs-last-bedienungsanleitung`) markieren den letzten Dokumentationslauf. Nur Dateien, die sich seitdem geändert haben, werden neu verarbeitet. + +## Modell-Konfiguration (`models.json`) + +Fünf Provider: `ollama` (lokale Ollama-Instanz), `llama-cpp` (:8000), `llama-cpp-coder` (:8001), `llama-cpp-judge` (:8002), `openrouter`. Die beiden llama-cpp-\*-Provider werden von der Extension via `switchModel()` automatisch gewechselt — nie manuell setzen wenn die Extension läuft. + +Kritische Felder bei llama-cpp-Providern: `contextWindow` muss mit dem `-c`-Parameter im Start-Skript übereinstimmen (aktuell 262144). `maxTokens` begrenzt die Ausgabelänge pro Request. + +## Wichtige Invarianten + +- **`cancelRequested`** ist eine modulare Variable — sie wird von `/cancel` gesetzt und nach jedem Loop-Schritt in `/optimize` geprüft und zurückgesetzt. +- **`currentModelKey`** — Cache für `switchModel()`: speichert `"provider/modelId"` des zuletzt gesetzten Modells. Bei identischem Key wird `pi.setModel()` übersprungen. Wird im `finally`-Block auf `""` resettet. +- **`normalizeForComparison(s)`** — Hilfsfunktion für die Loop-Erkennung: normalisiert Whitespace und Satzzeichen vor dem String-Vergleich, verhindert False-Negatives. +- **`quickJudgePrompt()` / `quickJudgeWithTestsPrompt()`** — kompakte Prompt-Varianten für Runde 1 (ohne `--continue`): kein TASK.md, nur Diff + Testergebnis. Bei FAIL folgt Runde 2 mit `judgePrompt()`. +- **`interactivePauseActive` / `interactiveContinueRequested` / `interactivePauseTask`** — drei modulare Variablen für den `--interactive`-Modus. `interactivePauseActive` wird vom `/continue`-Command geprüft, um zwischen Interactive-Pause-Signal und normalem Fortsetzen zu unterscheiden. Alle drei werden im `finally`-Block zurückgesetzt. +- **`sendAndWait()`** wartet erst auf `idle`, dann `deliverAs: "followUp"` — verhindert „Agent is already processing". +- **`tickTaskMdStatus()`** nutzt Python3 für den String-Ersatz in TASK.md (kein Shell-Escaping-Problem). +- Beide Start-Skripte warten bis zu 90×2 s auf HTTP-Erreichbarkeit und führen dann einen Smoke-Test-Completion durch. + +## GPU-Setup + +Hardware: 2× RTX 3090 (device=1,2), tensor-split 0.5,0.5. KV-Cache: `q4_0` (25 % des fp16-VRAM — nötig für 262k Kontext auf 2× 24 GB). Für andere GPU-Konfigurationen: README.md Abschnitt „Anpassung". diff --git a/README.md b/README.md index 22fdde5..17e3c52 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,317 @@ -# pi_coder +# pi_coder — Automatisierter Coder/Judge-Workflow für pi agent -Automatischer Programm-Generator mit Pi Agent und zwei KI-Modelle als Coder und Judge, der solange Selbstoptimierungsscheifen dreht bis das Programm Produktionsniveau hat. \ No newline at end of file +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 | +|--------|---------------------------------------------------|------|------------------|----------------| +| Coder | Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS | 8001 | qwen36-27b-coder | qwen3.5-coder | +| Judge | Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS | 8002 | qwen36-27b-judge | qwen3.5-judge | + +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 + ``` +- Mindestens eine NVIDIA-GPU (empfohlen: zwei GPUs mit je ≥ 16 GB VRAM) +- 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 ~/pi_coder +cd ~/pi_coder + +# 2. Extension und Modell-Config nach ~/.pi/agent/ deployen +./install.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.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. 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. | +| `--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 40` | — | Nur die 40 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 2× 24 GB. | +| `--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 der übergebenen GPUs) für Nicht-Tensor-Operationen. | +| `--tensor-split 0.5,0.5` | — | Modell-Gewichte 50/50 auf zwei GPUs aufteilen. | +| `--gpus '"device=1,2"'` | — | Docker-Argument: GPU 1 und GPU 2 dem Container übergeben. | + +### 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. | + +--- + +## Anpassung für eine einzelne GPU + +Mit einer GPU läuft das Modell vollständig auf dieser GPU statt verteilt. +Anpassungen in `start-coder.sh` und `start-judge.sh`: + +```bash +# Vorher (2 GPUs, device 1 und 2): + --gpus '"device=1,2"' \ + --main-gpu 0 \ + --tensor-split 0.5,0.5 \ + +# Nachher (1 GPU, z.B. device 0): + --gpus '"device=0"' \ + --main-gpu 0 \ +# --tensor-split ← diese Zeile komplett entfernen +``` + +### VRAM-Abschätzung für das 27B IQ4_XS-Modell + +| Komponente | Größe (ca.) | +|---|---| +| Modell-Gewichte (IQ4_XS, 27B) | ~14,5 GB | +| KV-Cache bei 128K Kontext (q8_0) | ~14 GB | +| KV-Cache bei 64K Kontext (q8_0) | ~7 GB | +| KV-Cache bei 32K Kontext (q8_0) | ~3,5 GB | + +Bei einer **24-GB-GPU** ist nur ein Server gleichzeitig sinnvoll betreibbar: +- Modell-Gewichte: ~14,5 GB +- KV-Cache bei 32K Kontext: ~3,5 GB +- Summe: ~18 GB → passt mit Puffer + +**Empfehlung für eine 24-GB-GPU:** +```bash +# Coder — Kontext reduzieren +-c 32768 # statt 131072 +-n 8192 # statt 16384 + +# Judge — Kontext reduzieren +-c 32768 # statt 131072 +``` + +Bei einer **16-GB-GPU** ist die Modellgröße allein schon grenzwertig. +Entweder ein kleineres Modell verwenden oder die Quantisierung weiter erhöhen (IQ3_XS, Q4_K_M). + +### Beide Server auf einer GPU betreiben + +Technisch möglich, aber beide Server laden das Modell gleichzeitig → doppelter VRAM-Bedarf. +Auf einer 24-GB-GPU daher **nicht empfohlen**. Alternativen: + +- Nur einen Server gleichzeitig starten (manuell umschalten) +- Kleinere Quantisierung wählen (IQ3_XS: ~11 GB) +- `ollama` als Alternative — lädt Modelle bei Bedarf und entlädt sie wieder + +--- + +## 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 | ~15 GB | 2 × 24-GB-GPU — **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 | +| `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.sh` | Extension + models.json nach `~/.pi/agent/` kopieren | + +--- + +## 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: + +| Situation | Anzeige | +|---|---| +| Coder implementiert | `Coder implementiert…` | +| edit-Tool aktiv | `Editiere src/main.py…` | +| git commit | `Git-Commit…` | +| Judge reviewt (Runde 2/2) | `Judge reviewt (Runde 2/2)…` | +| Tests laufen | `Tests laufen…` | +| Fix-Phase | `Coder fixt Blocker…` | +| Interactive-Pause (--interactive) | `⏸ PASS – warte auf /continue…` | + +So ist jederzeit erkennbar, in welcher Phase sich der automatische Loop befindet. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..93f647c --- /dev/null +++ b/examples/README.md @@ -0,0 +1,74 @@ +# pi-coder Beispielprojekte + +Vier kleine, eigenständige Projekte als Demonstrationsgrundlage für die pi-coder-Features. +Jedes Projekt startet bewusst unvollständig — genau der Ausgangspunkt, für den pi-coder gebaut ist. + +## Übersicht + +| Verzeichnis | Sprache | Demonstriert | +|---|---|---| +| `python-calculator/` | Python | `/optimize` mit `--test-cmd pytest` | +| `rust-wordcount/` | Rust | `/optimize` mit `--test-cmd "cargo test"` + `/version` | +| `go-fibonacci/` | Go | `/optimize --interactive` + `/continue` + `/shipit` | +| `c-linkedlist/` | C | `/quick_check` + `/fix` + `/patch` | + +## Demo-Workflow + +### Schritt 1 — Vorbereitung: Sub-Repos anlegen + +Jedes Example braucht ein eigenes git-Repo, damit pi-coder commit-basierte +Features nutzen kann (Loop-Erkennung, Diff-Anzeige, `/version`): + +```bash +for dir in python-calculator rust-wordcount go-fibonacci c-linkedlist; do + cd examples/$dir + git init && git add -A && git commit -m "feat: initial $dir" + cd ../.. +done +``` + +Für `/version` im rust-wordcount-Beispiel zusätzlich: + +```bash +cd examples/rust-wordcount && git tag v0.1.0 +``` + +### Schritt 2 — Demo ausführen + +In pi das jeweilige Unterverzeichnis als Arbeitsverzeichnis öffnen. +Die genauen Befehle stehen im README.md des jeweiligen Examples. +Zeitmessung: Systemuhr notieren oder Terminal-Kommando `time` nutzen. + +### Schritt 3 — Protokoll ausfüllen + +Jedes Example enthält eine `PROTOKOLL.md`. +Startzeit, Endzeit, Rundenanzahl und Endergebnis eintragen. + +### Schritt 4 — Ausgangszustand wiederherstellen + +```bash +bash examples/restore-all.sh +``` + +Das Skript löscht Sub-Repos, restauriert alle Quelldateien aus dem Haupt-Repo +und bereinigt Build-Artefakte (`target/`, `__pycache__` etc.). + +--- + +## Empfohlene Demo-Reihenfolge + +| # | Beispiel | Geschätzte Dauer | Highlights | +|---|---|---|---| +| 1 | `python-calculator` | ~5–10 min | Einstieg, Test-Loop | +| 2 | `c-linkedlist` | ~5 min | `/quick_check` + `/fix`, kein Loop | +| 3 | `rust-wordcount` | ~10–15 min | Loop + `/version` | +| 4 | `go-fibonacci` | ~15–20 min | `--interactive` + `/shipit` | + +--- + +## Weitere Details + +[python-calculator](python-calculator/README.md) · +[rust-wordcount](rust-wordcount/README.md) · +[go-fibonacci](go-fibonacci/README.md) · +[c-linkedlist](c-linkedlist/README.md) diff --git a/examples/c-linkedlist/PROTOKOLL.md b/examples/c-linkedlist/PROTOKOLL.md new file mode 100644 index 0000000..939ddd9 --- /dev/null +++ b/examples/c-linkedlist/PROTOKOLL.md @@ -0,0 +1,35 @@ +# Demo-Protokoll: c-linkedlist + +## Lauf 1 + +**Datum:** + +**Befehl /quick_check:** +``` +/quick_check "Gibt es Speicherlecks oder sonstige Probleme in diesem C-Projekt?" +``` +**Startzeit:** +**Endzeit:** +**Dauer (min):** +**Ergebnis:** OK / PROBLEM (Kurzbeschreibung): + +**Befehl /fix:** +``` +/fix "Implementiere list_free() korrekt, sodass valgrind --leak-check=full sauber ist." +``` +**Startzeit:** +**Endzeit:** +**Dauer (min):** +**Ergebnis:** erledigt / fehlgeschlagen + +**Befehl /patch (optional):** +``` +/patch "Ergänze list_search(head, value) in Header und Implementierung. + Gibt den ersten Node* mit dem gesuchten Wert zurück, oder NULL." +``` +**Startzeit:** +**Endzeit:** +**Dauer (min):** +**Besonderheiten / Beobachtungen:** + +--- diff --git a/examples/c-linkedlist/README.md b/examples/c-linkedlist/README.md new file mode 100644 index 0000000..7861652 --- /dev/null +++ b/examples/c-linkedlist/README.md @@ -0,0 +1,48 @@ +# C Linked List + +Vollständige einfach-verkettete Liste — bis auf `list_free()`, das als leerer Stub vorliegt. +Jeder Programmlauf leckt den gesamten Listen-Speicher. + +## Aktueller Stand + +``` +linked_list.h Interface: node_new, list_prepend, list_append, list_print, list_free, list_length +linked_list.c Alles implementiert — außer list_free() (Stub, tut nichts) +main.c Baut Liste 1–5, gibt sie aus, ruft list_free() auf (ohne Wirkung) +``` + +## Demo 1: `/quick_check` als Diagnose + +``` +/quick_check "Gibt es Speicherlecks oder sonstige Probleme in diesem C-Projekt?" +``` + +Der Judge analysiert den Code und identifiziert das leere `list_free()` als Speicherleck-Quelle. + +## Demo 2: `/fix` für gezieltes Nacharbeiten + +``` +/fix "Implementiere list_free() korrekt, sodass valgrind --leak-check=full sauber ist." +``` + +Coder implementiert die Funktion, committet. Kein vollständiger Judge-Loop — +ideal für kleine, klar abgegrenzte Fixes. + +## Demo 3: `/patch` für Minimal-Erweiterungen + +``` +/patch "Ergänze list_search(head, value) in Header und Implementierung. + Gibt den ersten Node* mit dem gesuchten Wert zurück, oder NULL." +``` + +pi-coder wendet einen unified diff an (`apply_patch`-Tool), ohne den vollständigen Loop. + +## Manueller Build + +```bash +gcc -Wall -Wextra -o ll_demo linked_list.c main.c +./ll_demo + +# Mit Leak-Check: +valgrind --leak-check=full ./ll_demo +``` diff --git a/examples/c-linkedlist/linked_list.c b/examples/c-linkedlist/linked_list.c new file mode 100644 index 0000000..e8d62ca --- /dev/null +++ b/examples/c-linkedlist/linked_list.c @@ -0,0 +1,43 @@ +#include +#include +#include "linked_list.h" + +Node *node_new(int value) { + Node *n = malloc(sizeof(Node)); + n->value = value; + n->next = NULL; + return n; +} + +Node *list_prepend(Node *head, int value) { + Node *n = node_new(value); + n->next = head; + return n; +} + +Node *list_append(Node *head, int value) { + Node *n = node_new(value); + if (!head) return n; + Node *cur = head; + while (cur->next) cur = cur->next; + cur->next = n; + return head; +} + +void list_print(const Node *head) { + for (const Node *cur = head; cur; cur = cur->next) + printf("%d ", cur->value); + printf("\n"); +} + +/* BUG: Speicher wird nicht freigegeben — valgrind meldet Leaks. */ +void list_free(Node *head) { + (void)head; /* TODO: implementieren */ +} + +int list_length(const Node *head) { + int len = 0; + for (const Node *cur = head; cur; cur = cur->next) + len++; + return len; +} diff --git a/examples/c-linkedlist/linked_list.h b/examples/c-linkedlist/linked_list.h new file mode 100644 index 0000000..9dc46ac --- /dev/null +++ b/examples/c-linkedlist/linked_list.h @@ -0,0 +1,16 @@ +#ifndef LINKED_LIST_H +#define LINKED_LIST_H + +typedef struct Node { + int value; + struct Node *next; +} Node; + +Node *node_new(int value); +Node *list_prepend(Node *head, int value); +Node *list_append(Node *head, int value); +void list_print(const Node *head); +void list_free(Node *head); +int list_length(const Node *head); + +#endif diff --git a/examples/c-linkedlist/main.c b/examples/c-linkedlist/main.c new file mode 100644 index 0000000..1f2f3ad --- /dev/null +++ b/examples/c-linkedlist/main.c @@ -0,0 +1,16 @@ +#include +#include "linked_list.h" + +int main(void) { + Node *list = NULL; + + for (int i = 1; i <= 5; i++) + list = list_append(list, i); + + printf("Liste: "); + list_print(list); + printf("Länge: %d\n", list_length(list)); + + list_free(list); /* leckt wegen unvollständigem TODO */ + return 0; +} diff --git a/examples/go-fibonacci/PROTOKOLL.md b/examples/go-fibonacci/PROTOKOLL.md new file mode 100644 index 0000000..ef9a63b --- /dev/null +++ b/examples/go-fibonacci/PROTOKOLL.md @@ -0,0 +1,37 @@ +# Demo-Protokoll: go-fibonacci + +## Lauf 1 + +**Datum:** +**Befehl:** +``` +/optimize "Ersetze die naive Rekursion durch Memoization. + fib(50) soll in unter 1ms abgeschlossen sein. + Bestehende Tests müssen weiterhin grün bleiben." \ + --test-cmd "go test ./..." --interactive +``` +**Startzeit:** +**Ende Loop (PASS):** +**Dauer Loop (min):** +**Runden:** +**Endergebnis Loop:** PASS / PASS WITH CONCERNS + +**Befehl im --interactive-Checkpoint:** +``` +/continue "Gib zusätzlich die Berechnungszeit in Mikrosekunden aus." +``` +*(oder: `/continue` ohne Zusatzauftrag)* + +**Startzeit /continue:** +**Ende /continue:** + +**Befehl /shipit:** +``` +/shipit +``` +**Startzeit /shipit:** +**Endzeit /shipit:** +**Endergebnis /shipit:** SHIP / NO-SHIP +**Besonderheiten / Beobachtungen:** + +--- diff --git a/examples/go-fibonacci/README.md b/examples/go-fibonacci/README.md new file mode 100644 index 0000000..b3fd085 --- /dev/null +++ b/examples/go-fibonacci/README.md @@ -0,0 +1,49 @@ +# Go Fibonacci + +Naive rekursive Fibonacci-Implementierung — korrekt, aber exponentiell langsam. +`fib(45)` dauert mehrere Sekunden; `fib(50)` läuft praktisch nicht durch. + +## Aktueller Stand + +``` +main.go fib(n) — rekursiv, O(2^n) +main_test.go TestFib mit 5 Tabellen-Tests (alle grün) +``` + +## Demo 1: `/optimize --interactive` + +``` +/optimize "Ersetze die naive Rekursion durch einfache Memoization mit einer map[int]int. + Kein Mutex, kein Goroutine-Overhead — Single-Threaded reicht. + fib(50) soll in unter 1ms abgeschlossen sein. + Bestehende Tests müssen weiterhin grün bleiben." \ + --test-cmd "go test ." --interactive +``` + +Nach dem ersten PASS hält pi-coder im **interaktiven Checkpoint** an. +Hier kann ein Zusatzauftrag erteilt werden: + +``` +/continue "Gib zusätzlich die Berechnungszeit in Mikrosekunden aus." +``` + +Oder einfach bestätigen: + +``` +/continue +``` + +## Demo 2: Abschluss mit `/shipit` + +``` +/shipit +``` + +Der Judge prüft nochmals explizit auf Produktionsreife und gibt SHIP oder NO-SHIP zurück. + +## Manueller Test + +```bash +go test . +go run main.go +``` diff --git a/examples/go-fibonacci/fib_bench_test.go b/examples/go-fibonacci/fib_bench_test.go new file mode 100644 index 0000000..0c91c44 --- /dev/null +++ b/examples/go-fibonacci/fib_bench_test.go @@ -0,0 +1,9 @@ +package main + +import "testing" + +func BenchmarkFib50(b *testing.B) { + for i := 0; i < b.N; i++ { + fib(50) + } +} diff --git a/examples/go-fibonacci/go.mod b/examples/go-fibonacci/go.mod new file mode 100644 index 0000000..18dcbe5 --- /dev/null +++ b/examples/go-fibonacci/go.mod @@ -0,0 +1,3 @@ +module fibonacci + +go 1.21 diff --git a/examples/go-fibonacci/main.go b/examples/go-fibonacci/main.go new file mode 100644 index 0000000..4e08244 --- /dev/null +++ b/examples/go-fibonacci/main.go @@ -0,0 +1,18 @@ +package main + +import "fmt" + +// fib berechnet die n-te Fibonacci-Zahl rekursiv. +// Korrekt, aber für n > 40 sehr langsam (exponentiell). +func fib(n int) int { + if n <= 1 { + return n + } + return fib(n-1) + fib(n-2) +} + +func main() { + for i := 0; i <= 10; i++ { + fmt.Printf("fib(%2d) = %d\n", i, fib(i)) + } +} diff --git a/examples/go-fibonacci/main_test.go b/examples/go-fibonacci/main_test.go new file mode 100644 index 0000000..621cd25 --- /dev/null +++ b/examples/go-fibonacci/main_test.go @@ -0,0 +1,20 @@ +package main + +import "testing" + +func TestFib(t *testing.T) { + cases := []struct { + n, want int + }{ + {0, 0}, + {1, 1}, + {2, 1}, + {5, 5}, + {10, 55}, + } + for _, c := range cases { + if got := fib(c.n); got != c.want { + t.Errorf("fib(%d) = %d, want %d", c.n, got, c.want) + } + } +} diff --git a/examples/python-calculator/.gitignore b/examples/python-calculator/.gitignore new file mode 100644 index 0000000..43ae0e2 --- /dev/null +++ b/examples/python-calculator/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.py[cod] diff --git a/examples/python-calculator/PROTOKOLL.md b/examples/python-calculator/PROTOKOLL.md new file mode 100644 index 0000000..b7e8b9f --- /dev/null +++ b/examples/python-calculator/PROTOKOLL.md @@ -0,0 +1,19 @@ +# Demo-Protokoll: python-calculator + +## Lauf 1 + +**Datum:** +**Befehl:** +``` +/optimize "Ergänze multiply, divide (wirft ZeroDivisionError bei 0) und power. + Schreibe pytest-Tests für alle neuen Funktionen." \ + --test-cmd "pytest test_calculator.py -v" +``` +**Startzeit:** +**Endzeit:** +**Dauer (min):** +**Runden:** +**Endergebnis:** PASS / PASS WITH CONCERNS / SHIP / NO-SHIP +**Besonderheiten / Beobachtungen:** + +--- diff --git a/examples/python-calculator/README.md b/examples/python-calculator/README.md new file mode 100644 index 0000000..23853d0 --- /dev/null +++ b/examples/python-calculator/README.md @@ -0,0 +1,51 @@ +# Python Calculator + +Einfacher Taschenrechner mit `add()` und `subtract()`. +Multiply, divide, power und Fehlerbehandlung fehlen noch. + +## Aktueller Stand + +``` +calculator.py add(), subtract() +test_calculator.py 5 pytest-Tests (alle grün) +``` + +## Demo: `/optimize` mit Test-Integration + +``` +/optimize "Ergänze multiply, divide (wirft ZeroDivisionError bei 0) und power. + Schreibe pytest-Tests für alle neuen Funktionen." \ + --test-cmd "pytest test_calculator.py -v" +``` + +**Was pi-coder hier zeigt:** +- Coder implementiert, committet +- Extension führt `pytest` aus und übergibt das Ergebnis an den Judge +- Judge bewertet Korrektheit anhand der Testergebnisse +- Bei FAIL: Coder fixt, nächste Runde + +## Voraussetzungen + +```bash +pip install pytest +``` + +## Weitere Demo-Befehle nach dem `/optimize`-Lauf + +``` +/quick_check "Sind alle Randfälle (negative Zahlen, floats) korrekt behandelt?" +``` +Schnelle Einzel-Beurteilung ohne neuen Fix-Loop. + +``` +/update_doku +``` +Lässt den Coder Code-Kommentare ergänzen, README aktualisieren und eine +Bedienungsanleitung erzeugen. + +## Manueller Test + +```bash +pytest test_calculator.py -v +python calculator.py +``` diff --git a/examples/python-calculator/calculator.py b/examples/python-calculator/calculator.py new file mode 100644 index 0000000..34fc2f0 --- /dev/null +++ b/examples/python-calculator/calculator.py @@ -0,0 +1,11 @@ +def add(a, b): + return a + b + + +def subtract(a, b): + return a - b + + +if __name__ == "__main__": + print(add(3, 4)) # 7 + print(subtract(10, 3)) # 7 diff --git a/examples/python-calculator/test_calculator.py b/examples/python-calculator/test_calculator.py new file mode 100644 index 0000000..20b9369 --- /dev/null +++ b/examples/python-calculator/test_calculator.py @@ -0,0 +1,18 @@ +import pytest +from calculator import add, subtract + + +def test_add_positive(): + assert add(2, 3) == 5 + +def test_add_negative(): + assert add(-1, 1) == 0 + +def test_add_zero(): + assert add(0, 0) == 0 + +def test_subtract_basic(): + assert subtract(5, 3) == 2 + +def test_subtract_negative_result(): + assert subtract(3, 5) == -2 diff --git a/examples/restore-all.sh b/examples/restore-all.sh new file mode 100755 index 0000000..ce7fde2 --- /dev/null +++ b/examples/restore-all.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Stellt den Ausgangszustand aller Examples wieder her. +# Löscht erzeugte Sub-Repos, restauriert Quelldateien aus dem Haupt-Repo +# und bereinigt Build-Artefakte. +# +# Optionen: +# --reset-protokoll Setzt auch PROTOKOLL.md auf leere Templates zurück. +# Standard: PROTOKOLL.md bleibt unangetastet. + +set -euo pipefail + +ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)" +EXAMPLES="$ROOT/examples" +RESET_PROTOKOLL=false + +for arg in "$@"; do + [ "$arg" = "--reset-protokoll" ] && RESET_PROTOKOLL=true +done + +echo "Stelle Examples-Ausgangszustand wieder her..." + +for dir in python-calculator rust-wordcount go-fibonacci c-linkedlist; do + path="$EXAMPLES/$dir" + if [ -d "$path/.git" ]; then + rm -rf "$path/.git" + echo " ✓ Sub-Repo entfernt: $dir" + fi + # Quelldateien restaurieren — PROTOKOLL.md standardmäßig ausnehmen + while IFS= read -r file; do + git -C "$ROOT" checkout -- "$file" + done < <(git -C "$ROOT" ls-files "examples/$dir/" \ + | grep -v '/PROTOKOLL\.md$') + if $RESET_PROTOKOLL; then + git -C "$ROOT" checkout -- "examples/$dir/PROTOKOLL.md" + echo " ✓ Dateien restauriert: $dir (inkl. PROTOKOLL.md)" + else + echo " ✓ Dateien restauriert: $dir (PROTOKOLL.md behalten)" + fi +done + +# Build-Artefakte und pi-coder-Laufzeitartefakte bereinigen +rm -rf "$EXAMPLES/rust-wordcount/target" +find "$EXAMPLES" -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true +find "$EXAMPLES" -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true +find "$EXAMPLES" -name "ll_demo" -delete 2>/dev/null || true +find "$EXAMPLES" -name "TASK.md" -delete 2>/dev/null || true + +echo "" +echo "Fertig. Alle Examples sind im Ausgangszustand." +if $RESET_PROTOKOLL; then + echo "PROTOKOLL.md-Dateien wurden auf leere Templates zurückgesetzt." +else + echo "PROTOKOLL.md-Dateien wurden nicht verändert." + echo "Für leere Templates: $0 --reset-protokoll" +fi diff --git a/examples/rust-wordcount/.gitignore b/examples/rust-wordcount/.gitignore new file mode 100644 index 0000000..d408a53 --- /dev/null +++ b/examples/rust-wordcount/.gitignore @@ -0,0 +1,3 @@ +target/ +*.rlib +*.pdb diff --git a/examples/rust-wordcount/Cargo.toml b/examples/rust-wordcount/Cargo.toml new file mode 100644 index 0000000..2845fab --- /dev/null +++ b/examples/rust-wordcount/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "wordcount" +version = "0.1.0" +edition = "2021" diff --git a/examples/rust-wordcount/PROTOKOLL.md b/examples/rust-wordcount/PROTOKOLL.md new file mode 100644 index 0000000..762f940 --- /dev/null +++ b/examples/rust-wordcount/PROTOKOLL.md @@ -0,0 +1,29 @@ +# Demo-Protokoll: rust-wordcount + +## Lauf 1 + +**Datum:** +**Befehl:** +``` +/optimize "Ergänze --lines (Zeilenzählung) und --chars (Zeichenzählung) als CLI-Flags. + Ohne Flag: Standardausgabe wie bisher (Wörter). + Schreibe Tests für alle drei Modi." \ + --test-cmd "cargo test" +``` +**Startzeit:** +**Endzeit:** +**Dauer /optimize (min):** +**Runden:** +**Endergebnis /optimize:** PASS / PASS WITH CONCERNS / SHIP / NO-SHIP + +**Befehl /version:** +``` +/version +``` +**Startzeit /version:** +**Endzeit /version:** +**Gewählter Bump:** patch / minor / major +**Gesetzter Tag:** +**Besonderheiten / Beobachtungen:** + +--- diff --git a/examples/rust-wordcount/README.md b/examples/rust-wordcount/README.md new file mode 100644 index 0000000..69a6c10 --- /dev/null +++ b/examples/rust-wordcount/README.md @@ -0,0 +1,50 @@ +# Rust Word Counter + +Liest stdin und gibt die Anzahl der Wörter aus. +Zeilen- und Zeichenzählung sowie CLI-Flags fehlen noch. + +## Aktueller Stand + +``` +src/main.rs count_words() — nur Wortzählung, kein Argument-Parsing +Cargo.toml Version 0.1.0 +``` + +## Demo 1: `/optimize` mit Cargo-Test-Integration + +``` +/optimize "Ergänze --lines (Zeilenzählung) und --chars (Zeichenzählung) als CLI-Flags. + Ohne Flag: Standardausgabe wie bisher (Wörter). + Schreibe Tests für alle drei Modi." \ + --test-cmd "cargo test" +``` + +**Was pi-coder hier zeigt:** +- Rust-Toolchain wird automatisch erkannt +- `cargo test`-Output geht an den Judge +- Mehrere Compile-Test-Fix-Zyklen möglich + +## Demo 2: `/version` nach dem Feature + +**Voraussetzung:** Das Verzeichnis muss ein git-Repo mit mindestens einem Commit sein. +Falls noch kein Repo existiert, vorher einmalig: + +```bash +git init && git add -A && git commit -m "feat: initial wordcount" +git tag v0.1.0 +``` + +``` +/version +``` + +Analysiert die Commits seit `v0.1.0`, erkennt `feat:`-Commits → schlägt `minor`-Bump vor +und setzt den Git-Tag `v0.2.0`. + +## Manueller Test + +```bash +cargo test +echo "Hallo Welt" | cargo run +echo -e "Zeile 1\nZeile 2" | cargo run -- --lines +``` diff --git a/examples/rust-wordcount/src/main.rs b/examples/rust-wordcount/src/main.rs new file mode 100644 index 0000000..257cc19 --- /dev/null +++ b/examples/rust-wordcount/src/main.rs @@ -0,0 +1,36 @@ +use std::io::{self, Read}; + +fn count_words(text: &str) -> usize { + text.split_whitespace().count() +} + +fn main() { + let mut input = String::new(); + io::stdin().read_to_string(&mut input).unwrap(); + println!("{} words", count_words(&input)); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_empty_input() { + assert_eq!(count_words(""), 0); + } + + #[test] + fn test_single_word() { + assert_eq!(count_words("hallo"), 1); + } + + #[test] + fn test_multiple_words() { + assert_eq!(count_words("eins zwei drei"), 3); + } + + #[test] + fn test_extra_whitespace() { + assert_eq!(count_words(" a b "), 2); + } +} diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..72a334f --- /dev/null +++ b/install.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/llama_cpp_parameter_uebersicht_2xRTX_3090.pdf b/llama_cpp_parameter_uebersicht_2xRTX_3090.pdf new file mode 100644 index 0000000..470e986 Binary files /dev/null and b/llama_cpp_parameter_uebersicht_2xRTX_3090.pdf differ diff --git a/llama_cpp_parameter_uebersicht_RTX_2080TI.pdf b/llama_cpp_parameter_uebersicht_RTX_2080TI.pdf new file mode 100644 index 0000000..6b4253b Binary files /dev/null and b/llama_cpp_parameter_uebersicht_RTX_2080TI.pdf differ diff --git a/models.json b/models.json new file mode 100644 index 0000000..3ffe010 --- /dev/null +++ b/models.json @@ -0,0 +1,111 @@ +{ + "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-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..950949c --- /dev/null +++ b/pi-coder-judge-extension.ts @@ -0,0 +1,1592 @@ +// pi-coder-judge-extension.ts +// Automatisierter Coder-Judge-Fix-Workflow für AI-Coding-Assistenten +// Modelle: qwen3.5-coder (Port 8001), qwen3.5-judge (Port 8002) + +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-coder", "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 + +// 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-coder")) { + ctx.ui.notify("Coder-Server nicht bereit (Port 8001) — start-coder.sh ausführen", "error"); + return; + } + await writeTaskMd(pi, ctx, task); + await switchModel(pi, ctx, "llama-cpp-coder", "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 (:8002).", + handler: async function (args: string, ctx: ExtensionCommandContext) { + if (!await waitUntilModelReady(pi, ctx, 8002, "qwen3.5-judge")) { + ctx.ui.notify("Judge-Server nicht bereit (Port 8002) — start-judge.sh ausführen", "error"); + return; + } + await switchModel(pi, ctx, "llama-cpp-judge", "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-coder")) { + ctx.ui.notify("Coder-Server nicht bereit (Port 8001) — start-coder.sh ausführen", "error"); + return; + } + await switchModel(pi, ctx, "llama-cpp-coder", "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 (:8002).", + handler: async function (args: string, ctx: ExtensionCommandContext) { + if (!await waitUntilModelReady(pi, ctx, 8002, "qwen3.5-judge")) { + ctx.ui.notify("Judge-Server nicht bereit (Port 8002) — start-judge.sh ausführen", "error"); + return; + } + await switchModel(pi, ctx, "llama-cpp-judge", "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: beide Server parallel prüfen — spart bis zu 3 min bei Kaltstart. + ctx.ui.setStatus("optimize", "Coder- und Judge-Server werden geprüft (parallel)…"); + const [coderReady, judgeReady] = await Promise.all([ + waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder"), + waitUntilModelReady(pi, ctx, 8002, "qwen3.5-judge"), + ]); + if (!coderReady) { + finalNotify(ctx, "⛔ Coder nicht erreichbar", "Port 8001 — kein HTTP 200 nach 3 min. start-coder.sh ausführen"); + return; + } + if (!judgeReady) { + finalNotify(ctx, "⛔ Judge nicht erreichbar", "Port 8002 — kein HTTP 200 nach 3 min. start-judge.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-coder", "qwen3.5-coder")) { + finalNotify(ctx, "⛔ Modell-Fehler", "Coder-Modell (llama-cpp-coder) nicht verfügbar"); + return; + } + currentActivity = "Coder implementiert…"; + await sendAndWait(pi, ctx, coderKickoff(task)); + 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", "Judge-Server wird geprüft…"); + if (!await waitUntilModelReady(pi, ctx, 8002, "qwen3.5-judge")) { + finalNotify(ctx, "⛔ Judge nicht erreichbar", "Port 8002 — kein HTTP 200 nach 3 min. start-judge.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-judge", "qwen3.5-judge")) { + finalNotify(ctx, "⛔ Modell-Fehler", "Judge-Modell (llama-cpp-judge) 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"; + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: ${judgeLabel} Test-Ergebnis…`); + currentActivity = `Judge reviewt (Runde ${round}/${maxRounds})…`; + await sendAndWait(pi, ctx, useQuickJudge + ? quickJudgeWithTestsPrompt(testOutput, "") + : judgeWithTestsPrompt(testOutput, "")); + } else { + const judgeLabel = useQuickJudge ? "Quick-Check" : "Judge — TASK.md + letzter Commit + Tests"; + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: ${judgeLabel}…`); + currentActivity = `Judge reviewt (Runde ${round}/${maxRounds})…`; + await sendAndWait(pi, ctx, useQuickJudge ? quickJudgePrompt("") : judgePrompt("")); + } + 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"; + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Coder fixt — ${blockerHint}`); + if (!await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder")) { + finalNotify(ctx, "⛔ Modell-Fehler", "Coder-Modell (llama-cpp-coder) nicht verfügbar"); + return; + } + currentActivity = "Coder fixt Blocker…"; + await sendAndWait(pi, ctx, fixPrompt("")); + 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-coder", "qwen3.5-coder")) { + finalNotify(ctx, "⛔ Modell-Fehler", "Coder-Modell nicht verfügbar"); + return; + } + currentActivity = "Coder implementiert Zusatzauftrag…"; + await sendAndWait(pi, ctx, coderKickoff(interactivePauseTask)); + 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-judge", "qwen3.5-judge")) { + finalNotify(ctx, "⛔ Modell-Fehler", "Judge-Modell (llama-cpp-judge) nicht verfügbar"); + return; + } + currentActivity = "Judge: finale Freigabe…"; + await sendAndWait(pi, ctx, shipitPrompt("")); + + 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 { + // 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-coder")) { + ctx.ui.notify("Coder-Server nicht bereit (Port 8001) — start-coder.sh ausführen", "error"); + return; + } + await switchModel(pi, ctx, "llama-cpp-coder", "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 (:8002).", + handler: async function (args: string, ctx: ExtensionCommandContext) { + if (!await waitUntilModelReady(pi, ctx, 8002, "qwen3.5-judge")) { + ctx.ui.notify("Judge-Server nicht bereit (Port 8002) — start-judge.sh ausführen", "error"); + return; + } + await switchModel(pi, ctx, "llama-cpp-judge", "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-coder")) { + ctx.ui.notify("Coder-Server nicht bereit (Port 8001) — start-coder.sh ausführen", "error"); + return; + } + await writeTaskMd(pi, ctx, task); + await switchModel(pi, ctx, "llama-cpp-coder", "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-coder")) { + ctx.ui.notify("Coder-Server nicht bereit (Port 8001) — start-coder.sh ausführen", "error"); + return; + } + await switchModel(pi, ctx, "llama-cpp-coder", "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/run-tests.sh b/run-tests.sh new file mode 100755 index 0000000..344b718 --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Unit-Tests für pi-coder-judge-extension.ts +# Keine Abhängigkeiten außer node — entfernt TypeScript-Annotationen on-the-fly. + +set -euo pipefail + +TS_FILE="$(dirname "$0")/test-utils.ts" + +if ! command -v node &>/dev/null; then + echo "❌ node nicht gefunden" >&2 + exit 1 +fi + +# TypeScript-Annotationen entfernen: `: string`, `: unknown`, `: void`, `: boolean` +# und `function f(a: T, b: T)` → `function f(a, b)` (einfache Parameterlisten) +node --input-type=module < <( + sed \ + -e 's/: string\b//g' \ + -e 's/: unknown\b//g' \ + -e 's/: void\b//g' \ + -e 's/: boolean\b//g' \ + -e 's/: number\b//g' \ + -e 's/(s: )/(s)/g' \ + -e 's/(text: )/(text)/g' \ + -e 's/(actual, expected: unknown, label: string)/( actual, expected, label)/g' \ + "$TS_FILE" +) diff --git a/start-coder.sh b/start-coder.sh new file mode 100755 index 0000000..0032fc4 --- /dev/null +++ b/start-coder.sh @@ -0,0 +1,78 @@ +#!/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-coder" +HOST_PORT=8001 +CONTAINER_PORT=8000 +MODEL_ALIAS="qwen3.5-coder" + +echo "[*] Verwende HF_HOME = $HF_HOME" +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-Server für Coder ..." +docker run -d \ + --gpus '"device=1,2"' \ + --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 \ + --chat-template-kwargs '{"enable_thinking":true}' \ + --no-context-shift \ + --temp 0.6 \ + --top-p 0.80 \ + --top-k 20 \ + --min-p 0.01 \ + --repeat-penalty 1.05 \ + --main-gpu 0 \ + --tensor-split 0.5,0.5 \ + -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. 180 s) ..." +MODEL_READY=0 +for i in {1..90}; 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}/90] 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}" +echo "[*] Stoppen mit: docker rm -f ${CONTAINER_NAME}" diff --git a/start-judge.sh b/start-judge.sh new file mode 100755 index 0000000..40c138e --- /dev/null +++ b/start-judge.sh @@ -0,0 +1,78 @@ +#!/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-judge" +HOST_PORT=8002 +CONTAINER_PORT=8000 +MODEL_ALIAS="qwen3.5-judge" + +echo "[*] Verwende HF_HOME = $HF_HOME" +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-Server für Judge ..." +docker run -d \ + --gpus '"device=1,2"' \ + --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 \ + --chat-template-kwargs '{"enable_thinking":true}' \ + --no-context-shift \ + --temp 0.7 \ + --top-p 0.80 \ + --top-k 20 \ + --min-p 0.01 \ + --repeat-penalty 1.05 \ + --main-gpu 0 \ + --tensor-split 0.5,0.5 \ + -ngl 999 \ + -fa on \ + --kv-unified \ + --cache-type-k q4_0 \ + --cache-type-v q4_0 \ + --batch-size 512 \ + --ubatch-size 256 \ + --parallel 1 \ + --cont-batching \ + --host 0.0.0.0 \ + --port "$CONTAINER_PORT" + +echo "[*] Warte auf Modell-Bereitschaft (Completion-Check, max. 180 s) ..." +MODEL_READY=0 +for i in {1..90}; 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}/90] 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}" +echo "[*] Stoppen mit: docker rm -f ${CONTAINER_NAME}" 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/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 diff --git a/test-utils.ts b/test-utils.ts new file mode 100644 index 0000000..3199b52 --- /dev/null +++ b/test-utils.ts @@ -0,0 +1,516 @@ +// Unit-Tests für reine Hilfsfunktionen aus pi-coder-judge-extension.ts +// +// Ausführung (TypeScript): +// npx ts-node test-utils.ts +// +// Ausführung ohne ts-node (schneller): +// node --input-type=module < <(sed 's/: string//g; s/: unknown//g; s/: void//g; s/: boolean//g' test-utils.ts) +// +// Oder: Funktionen aus dieser Datei kopieren und als .js ausführen. + +// ── Funktionen (aus Extension kopiert, kein pi-API-Import nötig) ───────────── + +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"; +} + +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() : ""; +} + +// ── Test-Harness ────────────────────────────────────────────────────────────── + +let passed = 0; +let failed = 0; + +function expect(actual: unknown, expected: unknown, label: string): void { + if (actual === expected) { + console.log(` ✅ ${label}`); + passed++; + } else { + console.error(` ❌ ${label}`); + console.error(` erwartet: ${JSON.stringify(expected)}`); + console.error(` erhalten: ${JSON.stringify(actual)}`); + failed++; + } +} + +// ── normalizeForComparison ──────────────────────────────────────────────────── + +console.log("\nnormalizeForComparison()"); + +expect(normalizeForComparison(" Foo Bar "), "foo bar", + "trimmt führende/nachfolgende Leerzeichen"); + +expect(normalizeForComparison("Foo.\n"), "foo", + "entfernt trailing Punkt + Newline"); + +expect(normalizeForComparison("A B"), "a b", + "kollabiert mehrfache Leerzeichen"); + +expect(normalizeForComparison("Foo:"), "foo", + "entfernt trailing Doppelpunkt"); + +expect(normalizeForComparison("Foo;"), "foo", + "entfernt trailing Semikolon"); + +expect(normalizeForComparison("Foo!"), "foo", + "entfernt trailing Ausrufezeichen"); + +expect(normalizeForComparison("Foo?"), "foo", + "entfernt trailing Fragezeichen"); + +expect(normalizeForComparison("UPPER CASE"), "upper case", + "konvertiert zu Kleinbuchstaben"); + +// Loop-Detection: gleiche Blocker nach Normalisierung erkannt +expect( + normalizeForComparison("missing error handling.") === + normalizeForComparison("missing error handling"), + true, + "Loop-Detection: trailing Punkt macht keinen Unterschied" +); + +expect( + normalizeForComparison("null check missing\n") === + normalizeForComparison("null check missing"), + true, + "Loop-Detection: Newline am Ende macht keinen Unterschied" +); + +expect( + normalizeForComparison("Fehler bei Import.") === + normalizeForComparison("Fehler bei Import"), + true, + "Loop-Detection: mehrfache Leerzeichen + Punkt machen keinen Unterschied" +); + +expect( + normalizeForComparison("Blocker A") === normalizeForComparison("Blocker B"), + false, + "Loop-Detection: verschiedene Blocker werden NICHT als gleich erkannt" +); + +// ── parseVerdict ────────────────────────────────────────────────────────────── + +console.log("\nparseVerdict()"); + +expect(parseVerdict("Urteil: PASS"), "PASS", + "erkennt PASS"); + +expect(parseVerdict("Urteil: PASS WITH CONCERNS"), "PASS WITH CONCERNS", + "erkennt PASS WITH CONCERNS (vor PASS gematcht)"); + +expect(parseVerdict("Urteil: FAIL"), "FAIL", + "erkennt FAIL"); + +expect(parseVerdict("kein Urteil hier"), "UNREADABLE", + "gibt UNREADABLE zurück wenn kein Urteil"); + +expect(parseVerdict("urteil: pass"), "PASS", + "case-insensitiv: 'urteil: pass'"); + +expect(parseVerdict("urteil: Pass With Concerns"), "PASS WITH CONCERNS", + "case-insensitiv: gemischte Groß-/Kleinschreibung"); + +expect(parseVerdict("Das ist mein Urteil: PASS — und mehr Text dahinter"), "PASS", + "ignoriert Text nach dem Urteil"); + +expect(parseVerdict("Urteil:PASS"), "PASS", + "toleriert fehlenden Leerzeichen nach Doppelpunkt"); + +expect(parseVerdict(""), "UNREADABLE", + "leerer String → UNREADABLE"); + +// ── parseBlockers ───────────────────────────────────────────────────────────── + +console.log("\nparseBlockers()"); + +expect( + parseBlockers("**Blocker**:\n- fehlende Validierung\n**Major**:\n- anderes Problem"), + "- fehlende Validierung", + "erkennt **Blocker** mit Bold-Syntax" +); + +expect( + parseBlockers("## Blocker\nNull-Check fehlt\n## Major\nanderes"), + "Null-Check fehlt", + "erkennt ## Blocker mit Heading-Syntax" +); + +expect( + parseBlockers("- Blocker:\n- fehlender Import\n- Minor:\n- Stil"), + "- fehlender Import", + "erkennt - Blocker mit Bullet-Syntax" +); + +expect( + parseBlockers("– Blocker\nKein Logging\n- Minor\nKleinigkeit"), + "Kein Logging", + "erkennt – Blocker (Gedankenstrich)" +); + +expect( + parseBlockers("Urteil: PASS\n\nAlles ok."), + "", + "gibt leeren String zurück wenn kein Blocker-Abschnitt" +); + +expect( + parseBlockers("**Blocker**:\nkeine\n**Minor**:\n- Stil"), + "keine", + "extrahiert 'keine' als Blocker-Text" +); + +// Mehrzeiliger Blocker +const multilineInput = `**Blocker**: +- Import fehlt +- Funktion nicht definiert +**Major**: +- weitere Sache`; +const multilineResult = parseBlockers(multilineInput); +expect( + multilineResult.includes("Import fehlt") && multilineResult.includes("Funktion nicht definiert"), + true, + "extrahiert mehrzeiligen Blocker vollständig" +); + +// ── toolExecutionLabel ──────────────────────────────────────────────────────── + +function toolExecutionLabel(toolName, args) { + 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 ""; + } +} + +console.log("\ntoolExecutionLabel()"); + +expect(toolExecutionLabel("edit", { path: "src/main.ts" }), "Editiere src/main.ts…", + "edit: gibt Pfad zurück"); +expect(toolExecutionLabel("edit", {}), "Editiere Datei…", + "edit: Fallback 'Datei' wenn kein Pfad"); +expect(toolExecutionLabel("write", { path: "README.md" }), "Schreibe README.md neu…", + "write: gibt Pfad zurück"); +expect(toolExecutionLabel("read", { path: "foo.py" }), "Lese foo.py…", + "read: gibt Pfad zurück"); +expect(toolExecutionLabel("bash", { command: "git commit -m 'fix'" }), "Git-Commit…", + "bash: git commit → Git-Commit"); +expect(toolExecutionLabel("bash", { command: "git add -A" }), "Stage Änderungen…", + "bash: git add → Stage Änderungen"); +expect(toolExecutionLabel("bash", { command: "git tag v1.0.0" }), "Git-Tag setzen…", + "bash: git tag → Git-Tag setzen"); +expect(toolExecutionLabel("bash", { command: "pytest tests/" }), "Tests laufen…", + "bash: pytest → Tests laufen"); +expect(toolExecutionLabel("bash", { command: "cargo test" }), "Tests laufen…", + "bash: cargo test → Tests laufen"); +expect(toolExecutionLabel("bash", { command: "git log --oneline" }), "Git-History lesen…", + "bash: git log → Git-History lesen"); +expect(toolExecutionLabel("bash", { command: "patch -p1 < foo.patch" }), "Wende Patch an…", + "bash: patch -p1 → Wende Patch an"); +expect(toolExecutionLabel("bash", { command: "curl https://api.example.com" }), "HTTP-Request…", + "bash: curl → HTTP-Request"); +expect(toolExecutionLabel("bash", { command: "ls ." }), "Shell: ls .", + "bash: unbekannter Befehl kurz → kein abschließendes …"); +expect(toolExecutionLabel("bash", { command: "a".repeat(60) }), `Shell: ${"a".repeat(55)}…`, + "bash: Befehl > 55 Zeichen → abgeschnitten mit …"); +expect(toolExecutionLabel("apply_patch", {}), "Wende Patch an…", + "apply_patch → Wende Patch an"); +expect(toolExecutionLabel("unknown_tool", {}), "", + "unbekanntes Tool → leerer String"); + +// ── getLastAssistantText ────────────────────────────────────────────────────── + +function getLastAssistantText(ctx) { + 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.message; + if (msg?.role === "assistant" && Array.isArray(msg.content)) { + return msg.content + .filter((c) => c.type === "text") + .map((c) => c.text) + .join("\n"); + } + } + } + return ""; +} + +function makeCtx(entries) { + return { sessionManager: { getBranch: () => entries } }; +} +function makeMsg(role, content) { + return { type: "message", message: { role, content } }; +} + +console.log("\ngetLastAssistantText()"); + +expect( + getLastAssistantText(makeCtx([])), + "", + "leere Session → leerer String" +); +expect( + getLastAssistantText(makeCtx([ + makeMsg("assistant", [{ type: "text", text: "Hallo" }]), + ])), + "Hallo", + "eine assistant-Nachricht → deren Text" +); +expect( + getLastAssistantText(makeCtx([ + makeMsg("user", [{ type: "text", text: "Frage" }]), + makeMsg("assistant", [{ type: "text", text: "Antwort" }]), + ])), + "Antwort", + "user + assistant → gibt assistant-Text zurück" +); +expect( + getLastAssistantText(makeCtx([ + makeMsg("assistant", [{ type: "text", text: "Erste" }]), + makeMsg("assistant", [{ type: "text", text: "Letzte" }]), + ])), + "Letzte", + "mehrere assistant-Nachrichten → gibt die letzte zurück" +); +expect( + getLastAssistantText(makeCtx([ + makeMsg("assistant", [{ type: "tool_use", id: "x", name: "bash", input: {} }]), + ])), + "", + "assistant-Nachricht ohne text-Content → leerer String" +); +expect( + getLastAssistantText(makeCtx([ + makeMsg("assistant", [ + { type: "text", text: "Teil 1" }, + { type: "tool_use", id: "x", name: "edit", input: {} }, + { type: "text", text: "Teil 2" }, + ]), + ])), + "Teil 1\nTeil 2", + "gemischte text/tool_use-Inhalte → nur text-Teile mit \\n verbunden" +); + +// ── detectBumpType ──────────────────────────────────────────────────────────── + +function detectBumpType(lines) { + if (lines.some(l => /^feat!:|BREAKING CHANGE/.test(l))) return "major"; + if (lines.some(l => /^feat(\(.+\))?:/.test(l))) return "minor"; + return "patch"; +} + +console.log("\ndetectBumpType()"); + +expect(detectBumpType(["feat!: neue API"]), "major", + "feat!: → major"); +expect(detectBumpType(["BREAKING CHANGE: auth umgebaut"]), "major", + "BREAKING CHANGE → major"); +expect(detectBumpType(["feat: CSV-Export"]), "minor", + "feat: → minor"); +expect(detectBumpType(["feat(parser): neues Feature"]), "minor", + "feat(scope): → minor"); +expect(detectBumpType(["fix: Crash behoben"]), "patch", + "fix: → patch"); +expect(detectBumpType(["chore: cleanup"]), "patch", + "chore: → patch"); +expect(detectBumpType(["feat: kleine Änderung", "feat!: breaking"]), "major", + "major hat Vorrang vor minor in gemischter Liste"); +expect(detectBumpType([]), "patch", + "leere Commit-Liste → patch"); + +// ── parseSemVer ─────────────────────────────────────────────────────────────── + +function parseSemVer(tag) { + const m = tag.match(/^v?(\d+)\.(\d+)\.(\d+)$/); + return m ? [+m[1], +m[2], +m[3]] : null; +} + +function expectDeep(actual, expected, label) { + expect(JSON.stringify(actual), JSON.stringify(expected), label); +} + +console.log("\nparseSemVer()"); + +expectDeep(parseSemVer("v1.2.3"), [1, 2, 3], "v1.2.3 → [1, 2, 3]"); +expectDeep(parseSemVer("1.2.3"), [1, 2, 3], "1.2.3 ohne v → [1, 2, 3]"); +expectDeep(parseSemVer("v0.0.1"), [0, 0, 1], "v0.0.1 → [0, 0, 1]"); +expectDeep(parseSemVer("v10.20.300"), [10, 20, 300], "v10.20.300 → dreistellige Zahlen"); +expectDeep(parseSemVer("nicht-semver"), null, "ungültiger Tag → null"); +expectDeep(parseSemVer("v1.2"), null, "unvollständig v1.2 → null"); +expectDeep(parseSemVer(""), null, "leerer String → null"); + +// ── stripOptimizeFlags ──────────────────────────────────────────────────────── + +function stripOptimizeFlags(args) { + return (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(); +} + +console.log("\nstripOptimizeFlags()"); + +expect(stripOptimizeFlags("mein Auftrag --rounds 3"), "mein Auftrag", + "--rounds N wird entfernt"); +expect(stripOptimizeFlags("--with-doku Auftrag"), "Auftrag", + "--with-doku wird entfernt"); +expect(stripOptimizeFlags("Auftrag --no-tests --approve-concerns"), "Auftrag", + "--no-tests und --approve-concerns werden entfernt"); +expect(stripOptimizeFlags('Auftrag --test-cmd "pytest tests/"'), "Auftrag", + '--test-cmd "..." wird entfernt'); +expect(stripOptimizeFlags("Auftrag --test-timeout 60"), "Auftrag", + "--test-timeout N wird entfernt"); +expect(stripOptimizeFlags("--continue --interactive Auftrag"), "Auftrag", + "--continue und --interactive werden entfernt"); +expect(stripOptimizeFlags("Nur ein Auftrag"), "Nur ein Auftrag", + "keine Flags → Auftrag unverändert"); + +// ── parseOptimizeOptions ───────────────────────────────────────────────────── + +function parseOptimizeOptions(args) { + const roundsMatch = (args || "").match(/--rounds\s+(\d+)/); + const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 2; + const testCmdMatch = (args || "").match(/--test-cmd\s+"([^"]+)"|--test-cmd\s+'([^']+)'|--test-cmd\s+(\S+)/); + const testCmd = 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; + return { maxRounds, testCmd, testTimeout }; +} + +console.log("\nparseOptimizeOptions()"); + +expect(parseOptimizeOptions("--rounds 5").maxRounds, 5, + "--rounds 5 → maxRounds = 5"); +expect(parseOptimizeOptions("--rounds 0").maxRounds, 1, + "--rounds 0 → maxRounds = 1 (Math.max-Clamp)"); +expect(parseOptimizeOptions("--rounds abc").maxRounds, 2, + "--rounds abc → Regex matcht nicht → Default 2"); +expect(parseOptimizeOptions("").maxRounds, 2, + "kein --rounds → Default 2"); +expect(parseOptimizeOptions("--test-timeout 30").testTimeout, 30, + "--test-timeout 30 → testTimeout = 30"); +expect(parseOptimizeOptions("--test-timeout 0").testTimeout, 1, + "--test-timeout 0 → testTimeout = 1 (Math.max-Clamp)"); +expect(parseOptimizeOptions("").testTimeout, 120, + "kein --test-timeout → Default 120"); +expect(parseOptimizeOptions('--test-cmd "pytest -v"').testCmd, "pytest -v", + '--test-cmd "..." → testCmd ohne Anführungszeichen'); +expect(parseOptimizeOptions("--test-cmd pytest").testCmd, "pytest", + "--test-cmd ohne Anführungszeichen → testCmd = 'pytest'"); +expect(parseOptimizeOptions("auftrag --rounds 3 --test-timeout 60").maxRounds, 3, + "mehrere Flags kombiniert: maxRounds korrekt"); + +// ── calcVersionStrings ──────────────────────────────────────────────────────── + +function calcVersionStrings(current, bump) { + const [maj, min, pat] = current ?? [0, 0, 0]; + const initial = !current; + const versions = 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 = initial ? "minor" : bump; + const labels = ["patch", "minor", "major"].map( + t => `${t} → ${versions[t]}${t === recommended ? " (empfohlen)" : ""}` + ); + return { versions, recommended, labels }; +} + +console.log("\ncalcVersionStrings()"); + +expectDeep( + calcVersionStrings(null, "patch").versions, + { patch: "v0.0.1", minor: "v0.1.0", major: "v1.0.0" }, + "initial (null): alle drei Standard-Startwerte" +); +expect(calcVersionStrings(null, "patch").recommended, "minor", + "initial: recommended ist immer minor (unabhängig vom Bump)"); +expectDeep( + calcVersionStrings([1, 2, 3], "patch").versions, + { patch: "v1.2.4", minor: "v1.3.0", major: "v2.0.0" }, + "[1,2,3]: patch/minor/major korrekt hochgezählt" +); +expect(calcVersionStrings([1, 2, 3], "patch").recommended, "patch", + "[1,2,3] patch-Bump → recommended = patch"); +expect(calcVersionStrings([1, 2, 3], "minor").recommended, "minor", + "[1,2,3] minor-Bump → recommended = minor"); +expect(calcVersionStrings([1, 2, 3], "major").recommended, "major", + "[1,2,3] major-Bump → recommended = major"); + +{ + const labels = calcVersionStrings([1, 2, 3], "minor").labels; + expect(labels[1], "minor → v1.3.0 (empfohlen)", + "empfohlenes Label trägt Suffix '(empfohlen)'"); + expect(labels[0], "patch → v1.2.4", + "nicht-empfohlenes Label hat keinen Suffix"); +} + +expectDeep( + calcVersionStrings([0, 9, 9], "major").versions, + { patch: "v0.9.10", minor: "v0.10.0", major: "v1.0.0" }, + "[0,9,9]: zweistellige Zahlen korrekt hochgezählt" +); + +// ── parseShipVerdict ────────────────────────────────────────────────────────── + +function parseShipVerdict(text) { + return text.match(/Urteil:\s*(SHIP|NO-SHIP)/i)?.[1]?.toUpperCase() ?? ""; +} + +console.log("\nparseShipVerdict()"); + +expect(parseShipVerdict("Urteil: SHIP"), "SHIP", + "erkennt SHIP"); +expect(parseShipVerdict("Urteil: NO-SHIP"), "NO-SHIP", + "erkennt NO-SHIP"); +expect(parseShipVerdict("urteil: ship"), "SHIP", + "case-insensitiv: 'urteil: ship'"); +expect(parseShipVerdict("kein Urteil hier"), "", + "kein Urteil → leerer String"); +expect(parseShipVerdict("Urteil: PASS"), "", + "PASS ist kein gültiges SHIP-Token → leerer String"); +expect(parseShipVerdict(""), "", + "leerer String → leerer String"); + +// ── Ergebnis ────────────────────────────────────────────────────────────────── + +console.log(`\n${"─".repeat(50)}`); +console.log(`Gesamt: ${passed + failed} Tests — ${passed} bestanden, ${failed} fehlgeschlagen`); + +if (failed > 0) { + process.exit(1); +}