diff --git a/.gitignore b/.gitignore deleted file mode 100644 index c1bfaae..0000000 --- a/.gitignore +++ /dev/null @@ -1,14 +0,0 @@ -*.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 deleted file mode 100644 index 9adb373..0000000 --- a/BEDIENUNGSANLEITUNG.md +++ /dev/null @@ -1,896 +0,0 @@ -# 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 deleted file mode 100644 index cde1fd7..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,80 +0,0 @@ -# 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 17e3c52..22fdde5 100644 --- a/README.md +++ b/README.md @@ -1,317 +1,3 @@ -# pi_coder — Automatisierter Coder/Judge-Workflow für pi agent +# pi_coder -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. +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 diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 93f647c..0000000 --- a/examples/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# 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 deleted file mode 100644 index 939ddd9..0000000 --- a/examples/c-linkedlist/PROTOKOLL.md +++ /dev/null @@ -1,35 +0,0 @@ -# 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 deleted file mode 100644 index 7861652..0000000 --- a/examples/c-linkedlist/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# 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 deleted file mode 100644 index e8d62ca..0000000 --- a/examples/c-linkedlist/linked_list.c +++ /dev/null @@ -1,43 +0,0 @@ -#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 deleted file mode 100644 index 9dc46ac..0000000 --- a/examples/c-linkedlist/linked_list.h +++ /dev/null @@ -1,16 +0,0 @@ -#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 deleted file mode 100644 index 1f2f3ad..0000000 --- a/examples/c-linkedlist/main.c +++ /dev/null @@ -1,16 +0,0 @@ -#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 deleted file mode 100644 index ef9a63b..0000000 --- a/examples/go-fibonacci/PROTOKOLL.md +++ /dev/null @@ -1,37 +0,0 @@ -# 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 deleted file mode 100644 index b3fd085..0000000 --- a/examples/go-fibonacci/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# 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 deleted file mode 100644 index 0c91c44..0000000 --- a/examples/go-fibonacci/fib_bench_test.go +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 18dcbe5..0000000 --- a/examples/go-fibonacci/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module fibonacci - -go 1.21 diff --git a/examples/go-fibonacci/main.go b/examples/go-fibonacci/main.go deleted file mode 100644 index 4e08244..0000000 --- a/examples/go-fibonacci/main.go +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 621cd25..0000000 --- a/examples/go-fibonacci/main_test.go +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 43ae0e2..0000000 --- a/examples/python-calculator/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -__pycache__/ -*.py[cod] diff --git a/examples/python-calculator/PROTOKOLL.md b/examples/python-calculator/PROTOKOLL.md deleted file mode 100644 index b7e8b9f..0000000 --- a/examples/python-calculator/PROTOKOLL.md +++ /dev/null @@ -1,19 +0,0 @@ -# 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 deleted file mode 100644 index 23853d0..0000000 --- a/examples/python-calculator/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# 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 deleted file mode 100644 index 34fc2f0..0000000 --- a/examples/python-calculator/calculator.py +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 20b9369..0000000 --- a/examples/python-calculator/test_calculator.py +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100755 index ce7fde2..0000000 --- a/examples/restore-all.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/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 deleted file mode 100644 index d408a53..0000000 --- a/examples/rust-wordcount/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -target/ -*.rlib -*.pdb diff --git a/examples/rust-wordcount/Cargo.toml b/examples/rust-wordcount/Cargo.toml deleted file mode 100644 index 2845fab..0000000 --- a/examples/rust-wordcount/Cargo.toml +++ /dev/null @@ -1,4 +0,0 @@ -[package] -name = "wordcount" -version = "0.1.0" -edition = "2021" diff --git a/examples/rust-wordcount/PROTOKOLL.md b/examples/rust-wordcount/PROTOKOLL.md deleted file mode 100644 index 762f940..0000000 --- a/examples/rust-wordcount/PROTOKOLL.md +++ /dev/null @@ -1,29 +0,0 @@ -# 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 deleted file mode 100644 index 69a6c10..0000000 --- a/examples/rust-wordcount/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# 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 deleted file mode 100644 index 257cc19..0000000 --- a/examples/rust-wordcount/src/main.rs +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100755 index 72a334f..0000000 --- a/install.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/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 deleted file mode 100644 index 470e986..0000000 Binary files a/llama_cpp_parameter_uebersicht_2xRTX_3090.pdf and /dev/null differ diff --git a/llama_cpp_parameter_uebersicht_RTX_2080TI.pdf b/llama_cpp_parameter_uebersicht_RTX_2080TI.pdf deleted file mode 100644 index 6b4253b..0000000 Binary files a/llama_cpp_parameter_uebersicht_RTX_2080TI.pdf and /dev/null differ diff --git a/models.json b/models.json deleted file mode 100644 index 3ffe010..0000000 --- a/models.json +++ /dev/null @@ -1,111 +0,0 @@ -{ - "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 deleted file mode 100644 index 950949c..0000000 --- a/pi-coder-judge-extension.ts +++ /dev/null @@ -1,1592 +0,0 @@ -// 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 deleted file mode 100755 index 344b718..0000000 --- a/run-tests.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/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 deleted file mode 100755 index 0032fc4..0000000 --- a/start-coder.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/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 deleted file mode 100755 index 40c138e..0000000 --- a/start-judge.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/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 deleted file mode 100755 index c6f424f..0000000 --- a/start-servers.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/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 deleted file mode 100755 index 9eacbf7..0000000 --- a/status.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/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 deleted file mode 100755 index 1d25229..0000000 --- a/stop-servers.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/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 deleted file mode 100644 index 3199b52..0000000 --- a/test-utils.ts +++ /dev/null @@ -1,516 +0,0 @@ -// 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); -}