Compare commits

...

33 commits

Author SHA1 Message Date
e31b514533 chore: merge Forgejo initial commit, behalte lokale README.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 21:23:18 +02:00
433d3970fb chore: PROTOKOLL.md-Dateien auf leere Templates zurücksetzen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 21:06:07 +02:00
a86d8b39ad fix: restore-all.sh bewahrt PROTOKOLL.md standardmäßig
Neues Flag --reset-protokoll für expliziten Reset auf leere Templates.
Ohne Flag bleiben ausgefüllte Protokolle erhalten.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 20:53:01 +02:00
c6f2f1f8e0 docs: Demo-Protokolle ausgefüllt (automatisierter Lauf Fr 29. Mai 2026)
python-calculator: PASS, 1:15 min, 1 Runde
c-linkedlist:      /quick_check 0:21 + /fix 0:50 + /patch 0:31
rust-wordcount:    PASS WITH CONCERNS, 1:40 min, 1 Runde + /version v0.2.0
go-fibonacci:      PASS + SHIP, ca. 3 min gesamt

Hinweise: --interactive nicht mit --print kombinierbar;
/version benötigt interaktiven UI-Dialog.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 20:48:25 +02:00
0956f3f569 chore: Examples finalisieren — Benchmark, .gitignore, TASK.md-Cleanup
- fib_bench_test.go: von pi-coder erzeugter Benchmark übernommen
- .gitignore in python-calculator und rust-wordcount: verhindert
  Commit von Build-Artefakten in Demo-Sub-Repos
- TASK.md zu globalem .gitignore hinzugefügt (pi-coder-Laufzeitartefakt)
- restore-all.sh: bereinigt jetzt auch TASK.md-Dateien

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 20:26:44 +02:00
f5d2a5d66e fix: go-fibonacci ./... → . und Memoization-Aufgabe auf Single-Threaded einschränken
./... schlägt bei Single-Package-Modulen ohne Unterverzeichnisse fehl.
Mutex-Hinweis verhindert Deadlock durch rekursive Lock-Acquisition.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 20:24:00 +02:00
64c2b7f0fd feat: Demo-Examples (Python/Rust/Go/C) mit Protokoll-Templates und Restore-Skript
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 19:06:36 +02:00
fb4e96919a test: Unit-Tests auf 97 erweitern (toolExecutionLabel, getLastAssistantText, SemVer, Flags, ShipVerdict)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 19:06:29 +02:00
7b13c4996d test: Unit-Tests für normalizeForComparison, parseVerdict, parseBlockers
28 Tests für die drei reinen Hilfsfunktionen aus pi-coder-judge-extension.ts.
run-tests.sh führt test-utils.ts ohne ts-node aus (sed-basiertes TS→JS-Stripping).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:14:03 +02:00
cba70b67a1 docs: neue Flags, Quick-Judge und parallele Server-Checks dokumentiert
- --no-tests und --approve-concerns in Syntax, Beschreibungen und Beispielen
- Quick-Check für Runde 1 im Ablaufbeispiel erklärt
- --continue: Hinweis auf parallele Server-Bereitschaftsprüfung
- Abschnitt 11 (Anwendungsfälle): neue Beispiele für --no-tests / --approve-concerns
- CLAUDE.md: currentModelKey-Cache, normalizeForComparison, quickJudgePrompt dokumentiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:08:00 +02:00
482d98fb63 perf: Quick-Judge Runde 1, switchModel-Cache, Blocker-Normalisierung + weitere TAT-Optimierungen
A) quickJudgePrompt()/quickJudgeWithTestsPrompt(): Runde 1 ohne --continue nutzt einen
   kompakten Prompt ohne TASK.md — spart 15-30% Inference-Zeit bei direktem PASS
B) switchModel()-Caching via currentModelKey: Überspringt setModel() wenn Modell
   bereits korrekt gesetzt ist; currentModelKey wird im finally-Block resettet
C) normalizeForComparison() für Loop-Detection: Whitespace/Satzzeichen-Normalisierung
   verhindert False-Negatives bei minimalen Formulierungsunterschieden im Judge-Output
D) Parallele Server-Bereitschaftsprüfung im --continue-Modus via Promise.all:
   Spart bis zu 3 min bei Kaltstart beider Server
E) --no-tests Flag: überspringt detectTestCommands() und autoTestCmds-Befüllung
F) --approve-concerns Flag: behandelt "PASS WITH CONCERNS" wie "PASS" (kein ShipIt-Call)
H) sendAndWait() settle-Delay 400ms → 150ms: ~1-2 s weniger Wartezeit pro Durchlauf

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:03:56 +02:00
11ac46e565 feat: --interactive-Checkpoint, direktes SHIP bei PASS, default rounds 2
- /optimize --interactive pausiert nach erstem PASS; /continue setzt fort,
  /continue "Zusatz" hängt weiteren Auftrag an und wiederholt den Judge-Loop
- Klares PASS → direkt SHIP ohne zweiten ShipIt-Inference-Call (1-3 min gespart)
- PASS WITH CONCERNS → ShipIt-Runde weiterhin als finale Abwägung
- Default --rounds 3→2 (~30 % schnellere Durchläufe für typische Tasks)
- /continue-Command erkennt interactivePauseActive und leitet Signal weiter
- Alle drei Interactive-Zustandsvariablen werden im finally-Block resettet
- Dokumentation (README, BEDIENUNGSANLEITUNG, CLAUDE.md) vollständig aktualisiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:51:54 +02:00
5dee5f25e4 feat: Erfolgsmeldung + Auto-Commit nach SHIP
Nach SHIP-Verdikt (sowohl in /optimize als auch /shipit):
- autoCommitIfDirty(): committet uncommitted Änderungen via git add -A + commit,
  falls der LLM keinen abschließenden Commit gemacht hat (Sicherheitsnetz)
- notifyShipSuccess(): zeigt prominente Meldung
  " Fertig! Das Programm ist jetzt produktionsreif und committed."

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 01:36:02 +02:00
8aadb317e5 docs: /version, Live-Status, korrekte Serverparameter in Doku
- README: /version, /plan, /continue, /cancel in Kommando-Tabelle ergänzt
- README: --test-cmd/--test-timeout für /optimize dokumentiert
- README: Live-Aktivitätsstatus-Tabelle mit Beispielen
- README: Serverparameter korrigiert: -c 262144, -n 16384, --cache-type q4_0
- README: VRAM-Tabelle auf q4_0-Basis aktualisiert (256K-Zeile ergänzt)
- BEDIENUNGSANLEITUNG: neuer Abschnitt 9 "Versionsverwaltung: /version"
- BEDIENUNGSANLEITUNG: Dialog-Beispiele + Manifest-Update-Logik erklärt
- BEDIENUNGSANLEITUNG: Live-Status-Hinweis in /optimize-Ablauf integriert
- BEDIENUNGSANLEITUNG: Versionsbeispiel in Typische Anwendungsfälle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 00:23:23 +02:00
e13e9382ff feat: automatische SemVer-Versionierung nach SHIP + /version-Command
Neuer /version-Command und automatischer Trigger nach SHIP-Verdikt in /optimize:
- getCurrentVersion() liest höchsten vX.Y.Z-Tag (git tag -l | sort -V)
- analyzeBumpType() klassifiziert Commits (feat! → major, feat: → minor, fix: → patch)
- detectVersionFile() findet package.json / Cargo.toml / pyproject.toml / VERSION
- applyVersionBump() schreibt Version in Manifest + chore-Commit
- runVersionBump() zeigt ctx.ui.select()-Dialog mit empfohlenem Bump-Typ

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 23:49:53 +02:00
a6f7f968b5 fix: Test-Timeout verhindert hängenden Loop
runTestsParallel() wrappte jede Test-Suite mit 'timeout N bash -c ...'
(Standard: 120s). Exit 124 wird als Timeout erkannt und im Output markiert.

Neues Flag --test-timeout N für Integration-Tests die länger brauchen:
  /optimize "..." --test-timeout 300

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 23:47:06 +02:00
14c47ecd01 fix: sendAndWait-Retry + Judge-Server-Bereitschaftscheck vor Loop
- sendAndWait(): fängt "Agent is already processing" mit exponentiellem
  Backoff ab (5 Versuche: 500ms, 1s, 2s, 4s). Race Condition zwischen
  waitForIdle() und sendUserMessage() wird damit toleriert.
- /optimize: prüft Port 8002 vor dem Loop (20× alle 3s = max. 60s).
  Bei 503 "Loading model" wird gewartet statt sofort zu scheitern.
  Ist der Server nach 60s nicht erreichbar: Abbruch mit Hinweis.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 21:44:37 +02:00
2c07fb9d1c feat: automatische Test-Erkennung + parallele Ausführung in /optimize
Tests werden jetzt von der Extension selbst erkannt und als parallele
CPU-Prozesse gestartet — Judge bekommt den fertigen Output und führt
keine Tests mehr selbst aus.

- detectTestCommands(): erkennt pytest, npm test, cargo, go test, make test
  anhand von Framework-Markern (alle Checks parallel via Promise.all)
- runTestsParallel(): startet alle erkannten Suiten gleichzeitig, kombiniert
  Output mit Status-Header pro Suite (max. 6000 Zeichen gesamt)
- /optimize: Auto-Erkennung läuft einmalig nach Coder-Phase, vor dem Loop
- --test-cmd bleibt als Override für Sonderfälle erhalten
- Fallback: kein Framework erkannt → Judge führt Tests wie bisher selbst aus

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 21:39:21 +02:00
d5a2c10fa6 fix: --test-cmd in finalNotify-Widget ergänzt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 21:14:01 +02:00
6afcd6a271 feat: --test-cmd für /optimize — Tests laufen in Extension, nicht im Judge
Neues Flag: /optimize <auftrag> --test-cmd "pytest -x"

Die Extension führt den Test-Befehl vor jedem Judge-Call selbst aus (pi.exec).
Judge bekommt den Output fertig übergeben und muss keine Tests mehr starten.
Das entkoppelt Test-Laufzeit vom LLM-Call und spart Judge-Inferenz-Zeit.

- judgeWithTestsPrompt(): wie judgePrompt, aber mit Test-Output im Prompt,
  explizites Verbot weitere Tests zu starten
- runTests(): führt Shell-Befehl aus, kürzt Output auf 6000 Zeichen
- Ohne --test-cmd: bisheriges Verhalten unverändert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 21:10:25 +02:00
0fb0ec9e5d fix: /optimize --continue schreibt Zusatzauftrag in TASK.md
Wenn --continue mit einem Auftragstext kombiniert wird, wurde writeTaskMd()
bisher nicht aufgerufen — der Text wurde ignoriert. Jetzt wird er als
Zusatzauftrag angehängt, bevor die Judge→Fix-Schleife startet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 21:07:16 +02:00
8bdf73b4ce docs: CLAUDE.md — Architektur, Deploy-Workflow, GPU-Setup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:03:34 +02:00
4a31535b76 feat: /plan, /cancel, /continue, /discard + Context 262144 + KV-Cache q4_0
- Neue Befehle: /plan (Planungsmodus, nur PLAN.md), /cancel (Loop-Abbruch),
  /continue (Resume nach Unterbrechung), /discard (PLAN.md verwerfen)
- contextWindow in models.json und llama.cpp-Servern: 131072 → 262144
- KV-Cache: q8_0 → q4_0 (weniger VRAM, passt zu 262k-Kontext auf 2× 3090)
- parallel: 2 → 1 beim Coder (stabiler bei großem Kontext)
- Optimize-Status mit ASCII-Fortschrittsbalken + Blocker-Preview
- cancelRequested-Flag prüft nach jedem Loop-Schritt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:02:20 +02:00
b19c189e2e feat: prominente Abschluss-Notifications + Widget-Update für /optimize und /shipit 2026-05-20 02:08:09 +02:00
7cb299ff66 feat: /optimize --continue überspringt Implementierungsphase 2026-05-20 01:42:26 +02:00
aa00a8282e fix: Judge contextWindow in models.json auf 131072 korrigiert 2026-05-20 01:27:52 +02:00
1da712f0b8 fix: Judge-Kontext auf 131072 erhöht (war 65536, zu klein bei langen Optimize-Runden) 2026-05-20 01:20:07 +02:00
120f223c9b docs: umfassende README + BEDIENUNGSANLEITUNG mit llama.cpp-Parametern und Beispielen 2026-05-19 21:22:02 +02:00
8dddd0eabd docs: start-servers.sh in README ergänzt 2026-05-19 19:11:15 +02:00
da961e65f6 feat: start-servers.sh startet Coder und Judge parallel 2026-05-19 19:05:29 +02:00
59b16059cc feat: stop-servers.sh, status.sh, README.md 2026-05-19 18:57:31 +02:00
6c128f5cf6 fix: install.sh kopiert statt Symlinks zu setzen 2026-05-19 18:51:07 +02:00
4074e10c1a chore: init pi_coder repository
Pi agent extension, model config, and LLaMA server startup scripts
for the coder/judge workflow (ports 8001/8002).
2026-05-19 18:21:34 +02:00
39 changed files with 4448 additions and 2 deletions

14
.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
*.bak
node_modules/
.DS_Store
# pi-coder Laufzeitartefakte
TASK.md
examples/**/TASK.md
# Build-Artefakte (Examples)
examples/rust-wordcount/target/
examples/**/__pycache__/
examples/**/.pytest_cache/
examples/**/Cargo.lock
examples/**/ll_demo

896
BEDIENUNGSANLEITUNG.md Normal file
View file

@ -0,0 +1,896 @@
# Bedienungsanleitung: pi_coder
pi_coder ist ein Werkzeug, das zwei lokale KI-Modelle als **Coder** und **Judge** einsetzt,
um Software automatisch zu schreiben, zu prüfen und zu verbessern — alles gesteuert über
einfache Slash-Kommandos in der pi-Agent-Oberfläche.
---
## Inhaltsverzeichnis
1. [Konzept: Coder und Judge](#1-konzept-coder-und-judge)
2. [Vorbereitung](#2-vorbereitung)
3. [Server starten und stoppen](#3-server-starten-und-stoppen)
4. [Neues Projekt anlegen](#4-neues-projekt-anlegen)
5. [Manueller Workflow: /coder → /judge → /fix → /shipit](#5-manueller-workflow)
6. [Automatischer Workflow: /optimize](#6-automatischer-workflow-optimize) (inkl. [Interactive-Modus](#interactive-modus))
7. [Kleine Änderungen: /patch und /quick_check](#7-kleine-änderungen-patch-und-quick_check)
8. [Dokumentation generieren: /update_doku](#8-dokumentation-generieren-update_doku)
9. [Versionsverwaltung: /version](#9-versionsverwaltung-version)
10. [TASK.md verstehen und nutzen](#10-taskmd-verstehen-und-nutzen)
11. [Typische Anwendungsfälle](#11-typische-anwendungsfälle)
12. [Fehlermeldungen und Lösungen](#12-fehlermeldungen-und-lösungen)
---
## 1. Konzept: Coder und Judge
pi_coder verwendet zwei Rollen:
**Coder** (Port 8001): Schreibt und repariert Code. Liest die Aufgabe aus `TASK.md`,
implementiert sie, führt Tests aus und erstellt Git-Commits.
**Judge** (Port 8002): Überprüft den Code mit dem Blick eines skeptischen Senior-Entwicklers.
Prüft Korrektheit, Robustheit, Randfälle, Sicherheit und Produktionsreife. Gibt ein Urteil:
- `PASS` — Code ist in Ordnung
- `PASS WITH CONCERNS` — grundsätzlich akzeptabel, aber mit Anmerkungen
- `FAIL` — enthält Blocker, die behoben werden müssen
Der Grundgedanke: Coder und Judge haben keine „Höflichkeitsschranke" zueinander —
der Judge kritisiert direkt und konkret, der Coder repariert ohne Widerspruch.
---
## 2. Vorbereitung
### Server starten
```bash
cd ~/pi_coder
./start-servers.sh
```
Ausgabe bei Erfolg:
```
[*] Starte beide Server parallel ...
[✓] Coder (:8001) bereit
[✓] Judge (:8002) bereit
```
Dauer: 13 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 <pfad>
```
### 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 <auftrag>
```
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 <auftrag> [--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 <beschreibung der änderung>
```
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 <pfad>` im Terminal:
```bash
cd <pfad>
pi
```

80
CLAUDE.md Normal file
View file

@ -0,0 +1,80 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Was ist dieses Repo?
Eine **TypeScript-Extension** für den [`pi`-Coding-Agent](https://github.com/earendil-works/pi) (`@earendil-works/pi-coding-agent`). Sie implementiert einen automatisierten **Coder → Judge → Fix**-Loop mit zwei lokalen llama.cpp-Servern.
Nach jeder Änderung an `pi-coder-judge-extension.ts` oder `models.json` muss deployed werden:
```bash
./install.sh # kopiert nach ~/.pi/agent/extensions/ und ~/.pi/agent/
# dann in pi agent: /reload
```
## Server-Lifecycle
```bash
./start-servers.sh # beide Container parallel starten (empfohlen, ~13 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".

318
README.md
View file

@ -1,3 +1,317 @@
# pi_coder
# pi_coder — Automatisierter Coder/Judge-Workflow für pi agent
Automatischer Programm-Generator mit Pi Agent und zwei KI-Modelle als Coder und Judge, der solange Selbstoptimierungsscheifen dreht bis das Programm Produktionsniveau hat.
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 <repo-url> ~/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 13 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.40.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.00.1` | Maximale Reproduzierbarkeit. Gut für Judge/Review. |
| `0.10.3` | Guter Kompromiss für Coding. **Empfohlen für Coder.** |
| `0.40.6` | Kreativere Lösungen, mehr Varianz. Sinnvoll für Prototyping. |
| `0.71.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 <auftrag>` | 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 <auftrag> [--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 <auftrag>` | Coder | Implementierungsplan in PLAN.md (kein Code) |
| `/continue` | Coder | Unterbrochenen Prozess fortsetzen |
| `/cancel` | — | Laufenden Loop nach aktuellem Schritt abbrechen |
| `/new_project <pfad>` | — | 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.

74
examples/README.md Normal file
View file

@ -0,0 +1,74 @@
# pi-coder Beispielprojekte
Vier kleine, eigenständige Projekte als Demonstrationsgrundlage für die pi-coder-Features.
Jedes Projekt startet bewusst unvollständig — genau der Ausgangspunkt, für den pi-coder gebaut ist.
## Übersicht
| Verzeichnis | Sprache | Demonstriert |
|---|---|---|
| `python-calculator/` | Python | `/optimize` mit `--test-cmd pytest` |
| `rust-wordcount/` | Rust | `/optimize` mit `--test-cmd "cargo test"` + `/version` |
| `go-fibonacci/` | Go | `/optimize --interactive` + `/continue` + `/shipit` |
| `c-linkedlist/` | C | `/quick_check` + `/fix` + `/patch` |
## Demo-Workflow
### Schritt 1 — Vorbereitung: Sub-Repos anlegen
Jedes Example braucht ein eigenes git-Repo, damit pi-coder commit-basierte
Features nutzen kann (Loop-Erkennung, Diff-Anzeige, `/version`):
```bash
for dir in python-calculator rust-wordcount go-fibonacci c-linkedlist; do
cd examples/$dir
git init && git add -A && git commit -m "feat: initial $dir"
cd ../..
done
```
Für `/version` im rust-wordcount-Beispiel zusätzlich:
```bash
cd examples/rust-wordcount && git tag v0.1.0
```
### Schritt 2 — Demo ausführen
In pi das jeweilige Unterverzeichnis als Arbeitsverzeichnis öffnen.
Die genauen Befehle stehen im README.md des jeweiligen Examples.
Zeitmessung: Systemuhr notieren oder Terminal-Kommando `time` nutzen.
### Schritt 3 — Protokoll ausfüllen
Jedes Example enthält eine `PROTOKOLL.md`.
Startzeit, Endzeit, Rundenanzahl und Endergebnis eintragen.
### Schritt 4 — Ausgangszustand wiederherstellen
```bash
bash examples/restore-all.sh
```
Das Skript löscht Sub-Repos, restauriert alle Quelldateien aus dem Haupt-Repo
und bereinigt Build-Artefakte (`target/`, `__pycache__` etc.).
---
## Empfohlene Demo-Reihenfolge
| # | Beispiel | Geschätzte Dauer | Highlights |
|---|---|---|---|
| 1 | `python-calculator` | ~510 min | Einstieg, Test-Loop |
| 2 | `c-linkedlist` | ~5 min | `/quick_check` + `/fix`, kein Loop |
| 3 | `rust-wordcount` | ~1015 min | Loop + `/version` |
| 4 | `go-fibonacci` | ~1520 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)

View file

@ -0,0 +1,35 @@
# Demo-Protokoll: c-linkedlist
## Lauf 1
**Datum:**
**Befehl /quick_check:**
```
/quick_check "Gibt es Speicherlecks oder sonstige Probleme in diesem C-Projekt?"
```
**Startzeit:**
**Endzeit:**
**Dauer (min):**
**Ergebnis:** OK / PROBLEM (Kurzbeschreibung):
**Befehl /fix:**
```
/fix "Implementiere list_free() korrekt, sodass valgrind --leak-check=full sauber ist."
```
**Startzeit:**
**Endzeit:**
**Dauer (min):**
**Ergebnis:** erledigt / fehlgeschlagen
**Befehl /patch (optional):**
```
/patch "Ergänze list_search(head, value) in Header und Implementierung.
Gibt den ersten Node* mit dem gesuchten Wert zurück, oder NULL."
```
**Startzeit:**
**Endzeit:**
**Dauer (min):**
**Besonderheiten / Beobachtungen:**
---

View file

@ -0,0 +1,48 @@
# C Linked List
Vollständige einfach-verkettete Liste — bis auf `list_free()`, das als leerer Stub vorliegt.
Jeder Programmlauf leckt den gesamten Listen-Speicher.
## Aktueller Stand
```
linked_list.h Interface: node_new, list_prepend, list_append, list_print, list_free, list_length
linked_list.c Alles implementiert — außer list_free() (Stub, tut nichts)
main.c Baut Liste 15, 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
```

View file

@ -0,0 +1,43 @@
#include <stdlib.h>
#include <stdio.h>
#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;
}

View file

@ -0,0 +1,16 @@
#ifndef LINKED_LIST_H
#define LINKED_LIST_H
typedef struct Node {
int value;
struct Node *next;
} Node;
Node *node_new(int value);
Node *list_prepend(Node *head, int value);
Node *list_append(Node *head, int value);
void list_print(const Node *head);
void list_free(Node *head);
int list_length(const Node *head);
#endif

View file

@ -0,0 +1,16 @@
#include <stdio.h>
#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;
}

View file

@ -0,0 +1,37 @@
# Demo-Protokoll: go-fibonacci
## Lauf 1
**Datum:**
**Befehl:**
```
/optimize "Ersetze die naive Rekursion durch Memoization.
fib(50) soll in unter 1ms abgeschlossen sein.
Bestehende Tests müssen weiterhin grün bleiben." \
--test-cmd "go test ./..." --interactive
```
**Startzeit:**
**Ende Loop (PASS):**
**Dauer Loop (min):**
**Runden:**
**Endergebnis Loop:** PASS / PASS WITH CONCERNS
**Befehl im --interactive-Checkpoint:**
```
/continue "Gib zusätzlich die Berechnungszeit in Mikrosekunden aus."
```
*(oder: `/continue` ohne Zusatzauftrag)*
**Startzeit /continue:**
**Ende /continue:**
**Befehl /shipit:**
```
/shipit
```
**Startzeit /shipit:**
**Endzeit /shipit:**
**Endergebnis /shipit:** SHIP / NO-SHIP
**Besonderheiten / Beobachtungen:**
---

View file

@ -0,0 +1,49 @@
# Go Fibonacci
Naive rekursive Fibonacci-Implementierung — korrekt, aber exponentiell langsam.
`fib(45)` dauert mehrere Sekunden; `fib(50)` läuft praktisch nicht durch.
## Aktueller Stand
```
main.go fib(n) — rekursiv, O(2^n)
main_test.go TestFib mit 5 Tabellen-Tests (alle grün)
```
## Demo 1: `/optimize --interactive`
```
/optimize "Ersetze die naive Rekursion durch einfache Memoization mit einer map[int]int.
Kein Mutex, kein Goroutine-Overhead — Single-Threaded reicht.
fib(50) soll in unter 1ms abgeschlossen sein.
Bestehende Tests müssen weiterhin grün bleiben." \
--test-cmd "go test ." --interactive
```
Nach dem ersten PASS hält pi-coder im **interaktiven Checkpoint** an.
Hier kann ein Zusatzauftrag erteilt werden:
```
/continue "Gib zusätzlich die Berechnungszeit in Mikrosekunden aus."
```
Oder einfach bestätigen:
```
/continue
```
## Demo 2: Abschluss mit `/shipit`
```
/shipit
```
Der Judge prüft nochmals explizit auf Produktionsreife und gibt SHIP oder NO-SHIP zurück.
## Manueller Test
```bash
go test .
go run main.go
```

View file

@ -0,0 +1,9 @@
package main
import "testing"
func BenchmarkFib50(b *testing.B) {
for i := 0; i < b.N; i++ {
fib(50)
}
}

View file

@ -0,0 +1,3 @@
module fibonacci
go 1.21

View file

@ -0,0 +1,18 @@
package main
import "fmt"
// fib berechnet die n-te Fibonacci-Zahl rekursiv.
// Korrekt, aber für n > 40 sehr langsam (exponentiell).
func fib(n int) int {
if n <= 1 {
return n
}
return fib(n-1) + fib(n-2)
}
func main() {
for i := 0; i <= 10; i++ {
fmt.Printf("fib(%2d) = %d\n", i, fib(i))
}
}

View file

@ -0,0 +1,20 @@
package main
import "testing"
func TestFib(t *testing.T) {
cases := []struct {
n, want int
}{
{0, 0},
{1, 1},
{2, 1},
{5, 5},
{10, 55},
}
for _, c := range cases {
if got := fib(c.n); got != c.want {
t.Errorf("fib(%d) = %d, want %d", c.n, got, c.want)
}
}
}

2
examples/python-calculator/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
__pycache__/
*.py[cod]

View file

@ -0,0 +1,19 @@
# Demo-Protokoll: python-calculator
## Lauf 1
**Datum:**
**Befehl:**
```
/optimize "Ergänze multiply, divide (wirft ZeroDivisionError bei 0) und power.
Schreibe pytest-Tests für alle neuen Funktionen." \
--test-cmd "pytest test_calculator.py -v"
```
**Startzeit:**
**Endzeit:**
**Dauer (min):**
**Runden:**
**Endergebnis:** PASS / PASS WITH CONCERNS / SHIP / NO-SHIP
**Besonderheiten / Beobachtungen:**
---

View file

@ -0,0 +1,51 @@
# Python Calculator
Einfacher Taschenrechner mit `add()` und `subtract()`.
Multiply, divide, power und Fehlerbehandlung fehlen noch.
## Aktueller Stand
```
calculator.py add(), subtract()
test_calculator.py 5 pytest-Tests (alle grün)
```
## Demo: `/optimize` mit Test-Integration
```
/optimize "Ergänze multiply, divide (wirft ZeroDivisionError bei 0) und power.
Schreibe pytest-Tests für alle neuen Funktionen." \
--test-cmd "pytest test_calculator.py -v"
```
**Was pi-coder hier zeigt:**
- Coder implementiert, committet
- Extension führt `pytest` aus und übergibt das Ergebnis an den Judge
- Judge bewertet Korrektheit anhand der Testergebnisse
- Bei FAIL: Coder fixt, nächste Runde
## Voraussetzungen
```bash
pip install pytest
```
## Weitere Demo-Befehle nach dem `/optimize`-Lauf
```
/quick_check "Sind alle Randfälle (negative Zahlen, floats) korrekt behandelt?"
```
Schnelle Einzel-Beurteilung ohne neuen Fix-Loop.
```
/update_doku
```
Lässt den Coder Code-Kommentare ergänzen, README aktualisieren und eine
Bedienungsanleitung erzeugen.
## Manueller Test
```bash
pytest test_calculator.py -v
python calculator.py
```

View file

@ -0,0 +1,11 @@
def add(a, b):
return a + b
def subtract(a, b):
return a - b
if __name__ == "__main__":
print(add(3, 4)) # 7
print(subtract(10, 3)) # 7

View file

@ -0,0 +1,18 @@
import pytest
from calculator import add, subtract
def test_add_positive():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, 1) == 0
def test_add_zero():
assert add(0, 0) == 0
def test_subtract_basic():
assert subtract(5, 3) == 2
def test_subtract_negative_result():
assert subtract(3, 5) == -2

55
examples/restore-all.sh Executable file
View file

@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Stellt den Ausgangszustand aller Examples wieder her.
# Löscht erzeugte Sub-Repos, restauriert Quelldateien aus dem Haupt-Repo
# und bereinigt Build-Artefakte.
#
# Optionen:
# --reset-protokoll Setzt auch PROTOKOLL.md auf leere Templates zurück.
# Standard: PROTOKOLL.md bleibt unangetastet.
set -euo pipefail
ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
EXAMPLES="$ROOT/examples"
RESET_PROTOKOLL=false
for arg in "$@"; do
[ "$arg" = "--reset-protokoll" ] && RESET_PROTOKOLL=true
done
echo "Stelle Examples-Ausgangszustand wieder her..."
for dir in python-calculator rust-wordcount go-fibonacci c-linkedlist; do
path="$EXAMPLES/$dir"
if [ -d "$path/.git" ]; then
rm -rf "$path/.git"
echo " ✓ Sub-Repo entfernt: $dir"
fi
# Quelldateien restaurieren — PROTOKOLL.md standardmäßig ausnehmen
while IFS= read -r file; do
git -C "$ROOT" checkout -- "$file"
done < <(git -C "$ROOT" ls-files "examples/$dir/" \
| grep -v '/PROTOKOLL\.md$')
if $RESET_PROTOKOLL; then
git -C "$ROOT" checkout -- "examples/$dir/PROTOKOLL.md"
echo " ✓ Dateien restauriert: $dir (inkl. PROTOKOLL.md)"
else
echo " ✓ Dateien restauriert: $dir (PROTOKOLL.md behalten)"
fi
done
# Build-Artefakte und pi-coder-Laufzeitartefakte bereinigen
rm -rf "$EXAMPLES/rust-wordcount/target"
find "$EXAMPLES" -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
find "$EXAMPLES" -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
find "$EXAMPLES" -name "ll_demo" -delete 2>/dev/null || true
find "$EXAMPLES" -name "TASK.md" -delete 2>/dev/null || true
echo ""
echo "Fertig. Alle Examples sind im Ausgangszustand."
if $RESET_PROTOKOLL; then
echo "PROTOKOLL.md-Dateien wurden auf leere Templates zurückgesetzt."
else
echo "PROTOKOLL.md-Dateien wurden nicht verändert."
echo "Für leere Templates: $0 --reset-protokoll"
fi

3
examples/rust-wordcount/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
target/
*.rlib
*.pdb

View file

@ -0,0 +1,4 @@
[package]
name = "wordcount"
version = "0.1.0"
edition = "2021"

View file

@ -0,0 +1,29 @@
# Demo-Protokoll: rust-wordcount
## Lauf 1
**Datum:**
**Befehl:**
```
/optimize "Ergänze --lines (Zeilenzählung) und --chars (Zeichenzählung) als CLI-Flags.
Ohne Flag: Standardausgabe wie bisher (Wörter).
Schreibe Tests für alle drei Modi." \
--test-cmd "cargo test"
```
**Startzeit:**
**Endzeit:**
**Dauer /optimize (min):**
**Runden:**
**Endergebnis /optimize:** PASS / PASS WITH CONCERNS / SHIP / NO-SHIP
**Befehl /version:**
```
/version
```
**Startzeit /version:**
**Endzeit /version:**
**Gewählter Bump:** patch / minor / major
**Gesetzter Tag:**
**Besonderheiten / Beobachtungen:**
---

View file

@ -0,0 +1,50 @@
# Rust Word Counter
Liest stdin und gibt die Anzahl der Wörter aus.
Zeilen- und Zeichenzählung sowie CLI-Flags fehlen noch.
## Aktueller Stand
```
src/main.rs count_words() — nur Wortzählung, kein Argument-Parsing
Cargo.toml Version 0.1.0
```
## Demo 1: `/optimize` mit Cargo-Test-Integration
```
/optimize "Ergänze --lines (Zeilenzählung) und --chars (Zeichenzählung) als CLI-Flags.
Ohne Flag: Standardausgabe wie bisher (Wörter).
Schreibe Tests für alle drei Modi." \
--test-cmd "cargo test"
```
**Was pi-coder hier zeigt:**
- Rust-Toolchain wird automatisch erkannt
- `cargo test`-Output geht an den Judge
- Mehrere Compile-Test-Fix-Zyklen möglich
## Demo 2: `/version` nach dem Feature
**Voraussetzung:** Das Verzeichnis muss ein git-Repo mit mindestens einem Commit sein.
Falls noch kein Repo existiert, vorher einmalig:
```bash
git init && git add -A && git commit -m "feat: initial wordcount"
git tag v0.1.0
```
```
/version
```
Analysiert die Commits seit `v0.1.0`, erkennt `feat:`-Commits → schlägt `minor`-Bump vor
und setzt den Git-Tag `v0.2.0`.
## Manueller Test
```bash
cargo test
echo "Hallo Welt" | cargo run
echo -e "Zeile 1\nZeile 2" | cargo run -- --lines
```

View file

@ -0,0 +1,36 @@
use std::io::{self, Read};
fn count_words(text: &str) -> usize {
text.split_whitespace().count()
}
fn main() {
let mut input = String::new();
io::stdin().read_to_string(&mut input).unwrap();
println!("{} words", count_words(&input));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_input() {
assert_eq!(count_words(""), 0);
}
#[test]
fn test_single_word() {
assert_eq!(count_words("hallo"), 1);
}
#[test]
fn test_multiple_words() {
assert_eq!(count_words("eins zwei drei"), 3);
}
#[test]
fn test_extra_whitespace() {
assert_eq!(count_words(" a b "), 2);
}
}

16
install.sh Executable file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Kopiert die versionierten Dateien aus dem Repo nach ~/.pi/agent/.
# Nach jeder Änderung im Repo ausführen, damit pi agent die neue Version lädt.
set -euo pipefail
REPO="$(cd "$(dirname "$0")" && pwd)"
mkdir -p ~/.pi/agent/extensions
cp "$REPO/pi-coder-judge-extension.ts" ~/.pi/agent/extensions/pi-coder-judge-extension.ts
echo "Kopiert: pi-coder-judge-extension.ts → ~/.pi/agent/extensions/"
cp "$REPO/models.json" ~/.pi/agent/models.json
echo "Kopiert: models.json → ~/.pi/agent/"
echo ""
echo "Fertig. Bitte /reload in pi agent ausführen."

Binary file not shown.

Binary file not shown.

111
models.json Normal file
View file

@ -0,0 +1,111 @@
{
"providers": {
"ollama": {
"baseUrl": "http://localhost:11434/v1",
"api": "openai-completions",
"apiKey": "ollama",
"compat": {
"supportsDeveloperRole": false,
"supportsReasoningEffort": false
},
"models": [
{ "id": "qwen2.5-coder:7b", "name": "Qwen2.5 Coder 7B (schnell)" },
{ "id": "qwen3-coder-30b-gpu:latest", "name": "Qwen3 Coder 30B GPU (Standard)" },
{ "id": "mistral-small3.2:24b", "name": "Mistral Small 3.2 24B" },
{ "id": "deepseek-r1:32b", "name": "DeepSeek R1 32B (Reasoning)" }
]
},
"llama-cpp": {
"baseUrl": "http://127.0.0.1:8000/v1",
"api": "openai-completions",
"apiKey": "none",
"compat": {
"supportsDeveloperRole": false,
"supportsReasoningEffort": false,
"maxTokensField": "max_tokens",
"thinkingFormat": "qwen-chat-template"
},
"models": [
{
"id": "qwen35b-uncensored",
"name": "Qwen3.6 35B Uncensored (llama.cpp :8000)"
},
{
"id": "qwen35b-moe-tools",
"name": "Qwen3.6 35B MoE Tools (llama.cpp :8000)"
}
]
},
"llama-cpp-coder": {
"baseUrl": "http://127.0.0.1:8001/v1",
"api": "openai-completions",
"apiKey": "none",
"compat": {
"supportsDeveloperRole": false,
"supportsReasoningEffort": false,
"maxTokensField": "max_tokens",
"thinkingFormat": "qwen-chat-template"
},
"models": [
{
"id": "qwen3.5-coder",
"name": "Qwen3.6 27B Coder (llama.cpp :8001)",
"reasoning": true,
"input": ["text"],
"contextWindow": 262144,
"maxTokens": 16384,
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
}
}
]
},
"llama-cpp-judge": {
"baseUrl": "http://127.0.0.1:8002/v1",
"api": "openai-completions",
"apiKey": "none",
"compat": {
"supportsDeveloperRole": false,
"supportsReasoningEffort": false,
"maxTokensField": "max_tokens",
"thinkingFormat": "qwen-chat-template"
},
"models": [
{
"id": "qwen3.5-judge",
"name": "Qwen3.6 27B Judge (llama.cpp :8002)",
"reasoning": true,
"input": ["text"],
"contextWindow": 262144,
"maxTokens": 16384,
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
}
}
]
},
"openrouter": {
"models": [
{ "id": "qwen/qwen3-235b-a22b:free", "name": "Qwen3 235B (Free)" },
{ "id": "deepseek/deepseek-r1:free", "name": "DeepSeek R1 (Free)" },
{ "id": "google/gemini-2.5-pro-exp-03-25:free", "name": "Gemini 2.5 Pro (Free)" },
{ "id": "meta-llama/llama-4-maverick:free", "name": "Llama 4 Maverick (Free)" },
{ "id": "microsoft/phi-4:free", "name": "Phi-4 (Free)" },
{ "id": "qwen/qwen-2.5-coder-32b-instruct", "name": "Qwen2.5 Coder 32B (günstig)" },
{ "id": "deepseek/deepseek-r1", "name": "DeepSeek R1 Full (Reasoning)" },
{ "id": "qwen/qwen3-235b-a22b", "name": "Qwen3 235B Full" }
]
}
}
}

1592
pi-coder-judge-extension.ts Normal file

File diff suppressed because it is too large Load diff

27
run-tests.sh Executable file
View file

@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Unit-Tests für pi-coder-judge-extension.ts
# Keine Abhängigkeiten außer node — entfernt TypeScript-Annotationen on-the-fly.
set -euo pipefail
TS_FILE="$(dirname "$0")/test-utils.ts"
if ! command -v node &>/dev/null; then
echo "❌ node nicht gefunden" >&2
exit 1
fi
# TypeScript-Annotationen entfernen: `: string`, `: unknown`, `: void`, `: boolean`
# und `function f(a: T, b: T)` → `function f(a, b)` (einfache Parameterlisten)
node --input-type=module < <(
sed \
-e 's/: string\b//g' \
-e 's/: unknown\b//g' \
-e 's/: void\b//g' \
-e 's/: boolean\b//g' \
-e 's/: number\b//g' \
-e 's/(s: )/(s)/g' \
-e 's/(text: )/(text)/g' \
-e 's/(actual, expected: unknown, label: string)/( actual, expected, label)/g' \
"$TS_FILE"
)

78
start-coder.sh Executable file
View file

@ -0,0 +1,78 @@
#!/usr/bin/env bash
set -euo pipefail
HF_HOME="${HF_HOME:-/home/dschlueter/nvme2n1p7_home/huggingface}"
MODEL_REL_PATH="models/qwen3/Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS.gguf"
IMAGE="ghcr.io/ggml-org/llama.cpp:server-cuda"
CONTAINER_NAME="qwen36-27b-coder"
HOST_PORT=8001
CONTAINER_PORT=8000
MODEL_ALIAS="qwen3.5-coder"
echo "[*] Verwende HF_HOME = $HF_HOME"
if [ ! -f "$HF_HOME/$MODEL_REL_PATH" ]; then
echo "[!] Modell-Datei nicht gefunden: $HF_HOME/$MODEL_REL_PATH" >&2
exit 1
fi
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}\$"; then
echo "[*] Stoppe existierenden Container $CONTAINER_NAME ..."
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
echo "[*] Starte llama.cpp-Server für Coder ..."
docker run -d \
--gpus '"device=1,2"' \
--name "$CONTAINER_NAME" \
--restart unless-stopped \
-e HF_HOME="/hf_home" \
-v "$HF_HOME:/hf_home:ro" \
-p "${HOST_PORT}:${CONTAINER_PORT}" \
"$IMAGE" \
-m "/hf_home/${MODEL_REL_PATH}" \
--alias "${MODEL_ALIAS}" \
-c 262144 \
-n 16384 \
--jinja \
--chat-template-kwargs '{"enable_thinking":true}' \
--no-context-shift \
--temp 0.6 \
--top-p 0.80 \
--top-k 20 \
--min-p 0.01 \
--repeat-penalty 1.05 \
--main-gpu 0 \
--tensor-split 0.5,0.5 \
-ngl 999 \
-fa on \
--kv-unified \
--cache-type-k q4_0 \
--cache-type-v q4_0 \
--batch-size 1024 \
--ubatch-size 512 \
--parallel 1 \
--cont-batching \
--host 0.0.0.0 \
--port "$CONTAINER_PORT"
echo "[*] Warte auf Modell-Bereitschaft (Completion-Check, max. 180 s) ..."
MODEL_READY=0
for i in {1..90}; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 \
-X POST "http://localhost:${HOST_PORT}/v1/chat/completions" \
-H "Content-Type: application/json" \
-d "{\"model\":\"${MODEL_ALIAS}\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":1,\"temperature\":0.0,\"stream\":false}")
if [ "$HTTP_CODE" = "200" ]; then MODEL_READY=1; break; fi
echo " [${i}/90] HTTP ${HTTP_CODE:-000} — Modell lädt noch, warte 2s ..."
sleep 2
done
if [ "$MODEL_READY" -ne 1 ]; then
echo "[!] Modell wurde nicht rechtzeitig bereit (kein HTTP 200 auf Completion)." >&2
docker logs --tail 200 "$CONTAINER_NAME" || true
exit 1
fi
echo "[*] Modell bereit — erster Completion-Request erfolgreich (HTTP 200)."
echo "[*] Server läuft auf http://0.0.0.0:${HOST_PORT}"
echo "[*] Stoppen mit: docker rm -f ${CONTAINER_NAME}"

78
start-judge.sh Executable file
View file

@ -0,0 +1,78 @@
#!/usr/bin/env bash
set -euo pipefail
HF_HOME="${HF_HOME:-/home/dschlueter/nvme2n1p7_home/huggingface}"
MODEL_REL_PATH="models/qwen3/Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS.gguf"
IMAGE="ghcr.io/ggml-org/llama.cpp:server-cuda"
CONTAINER_NAME="qwen36-27b-judge"
HOST_PORT=8002
CONTAINER_PORT=8000
MODEL_ALIAS="qwen3.5-judge"
echo "[*] Verwende HF_HOME = $HF_HOME"
if [ ! -f "$HF_HOME/$MODEL_REL_PATH" ]; then
echo "[!] Modell-Datei nicht gefunden: $HF_HOME/$MODEL_REL_PATH" >&2
exit 1
fi
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}\$"; then
echo "[*] Stoppe existierenden Container $CONTAINER_NAME ..."
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
echo "[*] Starte llama.cpp-Server für Judge ..."
docker run -d \
--gpus '"device=1,2"' \
--name "$CONTAINER_NAME" \
--restart unless-stopped \
-e HF_HOME="/hf_home" \
-v "$HF_HOME:/hf_home:ro" \
-p "${HOST_PORT}:${CONTAINER_PORT}" \
"$IMAGE" \
-m "/hf_home/${MODEL_REL_PATH}" \
--alias "${MODEL_ALIAS}" \
-c 262144 \
-n 16384 \
--jinja \
--chat-template-kwargs '{"enable_thinking":true}' \
--no-context-shift \
--temp 0.7 \
--top-p 0.80 \
--top-k 20 \
--min-p 0.01 \
--repeat-penalty 1.05 \
--main-gpu 0 \
--tensor-split 0.5,0.5 \
-ngl 999 \
-fa on \
--kv-unified \
--cache-type-k q4_0 \
--cache-type-v q4_0 \
--batch-size 512 \
--ubatch-size 256 \
--parallel 1 \
--cont-batching \
--host 0.0.0.0 \
--port "$CONTAINER_PORT"
echo "[*] Warte auf Modell-Bereitschaft (Completion-Check, max. 180 s) ..."
MODEL_READY=0
for i in {1..90}; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 \
-X POST "http://localhost:${HOST_PORT}/v1/chat/completions" \
-H "Content-Type: application/json" \
-d "{\"model\":\"${MODEL_ALIAS}\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":1,\"temperature\":0.0,\"stream\":false}")
if [ "$HTTP_CODE" = "200" ]; then MODEL_READY=1; break; fi
echo " [${i}/90] HTTP ${HTTP_CODE:-000} — Modell lädt noch, warte 2s ..."
sleep 2
done
if [ "$MODEL_READY" -ne 1 ]; then
echo "[!] Modell wurde nicht rechtzeitig bereit (kein HTTP 200 auf Completion)." >&2
docker logs --tail 200 "$CONTAINER_NAME" || true
exit 1
fi
echo "[*] Modell bereit — erster Completion-Request erfolgreich (HTTP 200)."
echo "[*] Server läuft auf http://0.0.0.0:${HOST_PORT}"
echo "[*] Stoppen mit: docker rm -f ${CONTAINER_NAME}"

30
start-servers.sh Executable file
View file

@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LOG_CODER=$(mktemp /tmp/coder_XXXXXX.log)
LOG_JUDGE=$(mktemp /tmp/judge_XXXXXX.log)
echo "[*] Starte beide Server parallel ..."
bash "$SCRIPT_DIR/start-coder.sh" > "$LOG_CODER" 2>&1 &
PID_CODER=$!
bash "$SCRIPT_DIR/start-judge.sh" > "$LOG_JUDGE" 2>&1 &
PID_JUDGE=$!
wait_result() {
local PID="$1" NAME="$2" LOG="$3"
if wait "$PID"; then
echo "[✓] $NAME bereit"
else
echo "[✗] $NAME fehlgeschlagen — Log:"
cat "$LOG"
return 1
fi
}
RC=0
wait_result "$PID_CODER" "Coder (:8001)" "$LOG_CODER" || RC=1
wait_result "$PID_JUDGE" "Judge (:8002)" "$LOG_JUDGE" || RC=1
rm -f "$LOG_CODER" "$LOG_JUDGE"
exit $RC

34
status.sh Executable file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env bash
check_server() {
local NAME="$1"
local PORT="$2"
local ALIAS="$3"
printf "%-28s" "$NAME (Port $PORT):"
# Docker-Status
if docker ps --format '{{.Names}}' | grep -q "^${NAME}\$"; then
printf " Container=\033[32mRUNNING\033[0m"
elif docker ps -a --format '{{.Names}}' | grep -q "^${NAME}\$"; then
printf " Container=\033[33mSTOPPED\033[0m"
else
printf " Container=\033[31mNOT FOUND\033[0m"
echo
return
fi
# HTTP-Erreichbarkeit
if curl -s --max-time 3 "http://localhost:${PORT}/health" >/dev/null 2>&1 || \
curl -s --max-time 3 "http://localhost:${PORT}/v1/models" >/dev/null 2>&1; then
printf " HTTP=\033[32mOK\033[0m"
else
printf " HTTP=\033[31mNOT READY\033[0m"
fi
echo
}
echo "=== LLaMA-Server Status ==="
check_server "qwen36-27b-coder" 8001 "qwen3.5-coder"
check_server "qwen36-27b-judge" 8002 "qwen3.5-judge"

14
stop-servers.sh Executable file
View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
CODER="qwen36-27b-coder"
JUDGE="qwen36-27b-judge"
for NAME in "$CODER" "$JUDGE"; do
if docker ps -a --format '{{.Names}}' | grep -q "^${NAME}\$"; then
docker rm -f "$NAME" >/dev/null
echo "[*] Gestoppt: $NAME"
else
echo "[-] Nicht gefunden: $NAME"
fi
done

516
test-utils.ts Normal file
View file

@ -0,0 +1,516 @@
// Unit-Tests für reine Hilfsfunktionen aus pi-coder-judge-extension.ts
//
// Ausführung (TypeScript):
// npx ts-node test-utils.ts
//
// Ausführung ohne ts-node (schneller):
// node --input-type=module < <(sed 's/: string//g; s/: unknown//g; s/: void//g; s/: boolean//g' test-utils.ts)
//
// Oder: Funktionen aus dieser Datei kopieren und als .js ausführen.
// ── Funktionen (aus Extension kopiert, kein pi-API-Import nötig) ─────────────
function normalizeForComparison(s: string): string {
return s.trim().replace(/\s+/g, " ").replace(/[.,;:!?]+$/g, "").toLowerCase();
}
function parseVerdict(text: string): string {
const m = text.match(/Urteil:\s*(PASS WITH CONCERNS|PASS|FAIL)/i);
return m ? m[1].toUpperCase() : "UNREADABLE";
}
function parseBlockers(text: string): string {
const m = text.match(
/(?:\*\*Blocker\*\*|##\s*Blocker|[-*]\s*Blocker)[:\n]([\s\S]*?)(?:\n(?:\*\*Major\*\*|##\s*Major|[-*]\s*Major)|\n(?:\*\*Minor\*\*|##\s*Minor|[-*]\s*Minor)|$)/i
);
return m ? m[1].trim() : "";
}
// ── Test-Harness ──────────────────────────────────────────────────────────────
let passed = 0;
let failed = 0;
function expect(actual: unknown, expected: unknown, label: string): void {
if (actual === expected) {
console.log(`${label}`);
passed++;
} else {
console.error(`${label}`);
console.error(` erwartet: ${JSON.stringify(expected)}`);
console.error(` erhalten: ${JSON.stringify(actual)}`);
failed++;
}
}
// ── normalizeForComparison ────────────────────────────────────────────────────
console.log("\nnormalizeForComparison()");
expect(normalizeForComparison(" Foo Bar "), "foo bar",
"trimmt führende/nachfolgende Leerzeichen");
expect(normalizeForComparison("Foo.\n"), "foo",
"entfernt trailing Punkt + Newline");
expect(normalizeForComparison("A B"), "a b",
"kollabiert mehrfache Leerzeichen");
expect(normalizeForComparison("Foo:"), "foo",
"entfernt trailing Doppelpunkt");
expect(normalizeForComparison("Foo;"), "foo",
"entfernt trailing Semikolon");
expect(normalizeForComparison("Foo!"), "foo",
"entfernt trailing Ausrufezeichen");
expect(normalizeForComparison("Foo?"), "foo",
"entfernt trailing Fragezeichen");
expect(normalizeForComparison("UPPER CASE"), "upper case",
"konvertiert zu Kleinbuchstaben");
// Loop-Detection: gleiche Blocker nach Normalisierung erkannt
expect(
normalizeForComparison("missing error handling.") ===
normalizeForComparison("missing error handling"),
true,
"Loop-Detection: trailing Punkt macht keinen Unterschied"
);
expect(
normalizeForComparison("null check missing\n") ===
normalizeForComparison("null check missing"),
true,
"Loop-Detection: Newline am Ende macht keinen Unterschied"
);
expect(
normalizeForComparison("Fehler bei Import.") ===
normalizeForComparison("Fehler bei Import"),
true,
"Loop-Detection: mehrfache Leerzeichen + Punkt machen keinen Unterschied"
);
expect(
normalizeForComparison("Blocker A") === normalizeForComparison("Blocker B"),
false,
"Loop-Detection: verschiedene Blocker werden NICHT als gleich erkannt"
);
// ── parseVerdict ──────────────────────────────────────────────────────────────
console.log("\nparseVerdict()");
expect(parseVerdict("Urteil: PASS"), "PASS",
"erkennt PASS");
expect(parseVerdict("Urteil: PASS WITH CONCERNS"), "PASS WITH CONCERNS",
"erkennt PASS WITH CONCERNS (vor PASS gematcht)");
expect(parseVerdict("Urteil: FAIL"), "FAIL",
"erkennt FAIL");
expect(parseVerdict("kein Urteil hier"), "UNREADABLE",
"gibt UNREADABLE zurück wenn kein Urteil");
expect(parseVerdict("urteil: pass"), "PASS",
"case-insensitiv: 'urteil: pass'");
expect(parseVerdict("urteil: Pass With Concerns"), "PASS WITH CONCERNS",
"case-insensitiv: gemischte Groß-/Kleinschreibung");
expect(parseVerdict("Das ist mein Urteil: PASS — und mehr Text dahinter"), "PASS",
"ignoriert Text nach dem Urteil");
expect(parseVerdict("Urteil:PASS"), "PASS",
"toleriert fehlenden Leerzeichen nach Doppelpunkt");
expect(parseVerdict(""), "UNREADABLE",
"leerer String → UNREADABLE");
// ── parseBlockers ─────────────────────────────────────────────────────────────
console.log("\nparseBlockers()");
expect(
parseBlockers("**Blocker**:\n- fehlende Validierung\n**Major**:\n- anderes Problem"),
"- fehlende Validierung",
"erkennt **Blocker** mit Bold-Syntax"
);
expect(
parseBlockers("## Blocker\nNull-Check fehlt\n## Major\nanderes"),
"Null-Check fehlt",
"erkennt ## Blocker mit Heading-Syntax"
);
expect(
parseBlockers("- Blocker:\n- fehlender Import\n- Minor:\n- Stil"),
"- fehlender Import",
"erkennt - Blocker mit Bullet-Syntax"
);
expect(
parseBlockers(" Blocker\nKein Logging\n- Minor\nKleinigkeit"),
"Kein Logging",
"erkennt Blocker (Gedankenstrich)"
);
expect(
parseBlockers("Urteil: PASS\n\nAlles ok."),
"",
"gibt leeren String zurück wenn kein Blocker-Abschnitt"
);
expect(
parseBlockers("**Blocker**:\nkeine\n**Minor**:\n- Stil"),
"keine",
"extrahiert 'keine' als Blocker-Text"
);
// Mehrzeiliger Blocker
const multilineInput = `**Blocker**:
- Import fehlt
- Funktion nicht definiert
**Major**:
- weitere Sache`;
const multilineResult = parseBlockers(multilineInput);
expect(
multilineResult.includes("Import fehlt") && multilineResult.includes("Funktion nicht definiert"),
true,
"extrahiert mehrzeiligen Blocker vollständig"
);
// ── toolExecutionLabel ────────────────────────────────────────────────────────
function toolExecutionLabel(toolName, args) {
switch (toolName) {
case "edit": return `Editiere ${args.path ?? "Datei"}`;
case "write": return `Schreibe ${args.path ?? "Datei"} neu…`;
case "read": return `Lese ${args.path ?? "Datei"}`;
case "grep": return `Suche in ${args.path ?? args.pattern ?? "Dateien"}`;
case "find": return `Suche Dateien: ${args.pattern ?? ""}`;
case "ls": return `Verzeichnis: ${args.path ?? "."}`;
case "bash": {
const cmd = String(args.command ?? "").trim().replace(/\n[\s\S]*/s, "");
if (/git\s+commit/.test(cmd)) return "Git-Commit…";
if (/git\s+add/.test(cmd)) return "Stage Änderungen…";
if (/git\s+tag/.test(cmd)) return "Git-Tag setzen…";
if (/pytest|npm test|cargo test|go test|make test/.test(cmd)) return "Tests laufen…";
if (/git\s+(diff|log|show|tag -l)/.test(cmd)) return "Git-History lesen…";
if (/patch\s+-p1/.test(cmd)) return "Wende Patch an…";
if (/curl/.test(cmd)) return "HTTP-Request…";
return `Shell: ${cmd.slice(0, 55)}${cmd.length > 55 ? "…" : ""}`;
}
case "apply_patch": return "Wende Patch an…";
default: return "";
}
}
console.log("\ntoolExecutionLabel()");
expect(toolExecutionLabel("edit", { path: "src/main.ts" }), "Editiere src/main.ts…",
"edit: gibt Pfad zurück");
expect(toolExecutionLabel("edit", {}), "Editiere Datei…",
"edit: Fallback 'Datei' wenn kein Pfad");
expect(toolExecutionLabel("write", { path: "README.md" }), "Schreibe README.md neu…",
"write: gibt Pfad zurück");
expect(toolExecutionLabel("read", { path: "foo.py" }), "Lese foo.py…",
"read: gibt Pfad zurück");
expect(toolExecutionLabel("bash", { command: "git commit -m 'fix'" }), "Git-Commit…",
"bash: git commit → Git-Commit");
expect(toolExecutionLabel("bash", { command: "git add -A" }), "Stage Änderungen…",
"bash: git add → Stage Änderungen");
expect(toolExecutionLabel("bash", { command: "git tag v1.0.0" }), "Git-Tag setzen…",
"bash: git tag → Git-Tag setzen");
expect(toolExecutionLabel("bash", { command: "pytest tests/" }), "Tests laufen…",
"bash: pytest → Tests laufen");
expect(toolExecutionLabel("bash", { command: "cargo test" }), "Tests laufen…",
"bash: cargo test → Tests laufen");
expect(toolExecutionLabel("bash", { command: "git log --oneline" }), "Git-History lesen…",
"bash: git log → Git-History lesen");
expect(toolExecutionLabel("bash", { command: "patch -p1 < foo.patch" }), "Wende Patch an…",
"bash: patch -p1 → Wende Patch an");
expect(toolExecutionLabel("bash", { command: "curl https://api.example.com" }), "HTTP-Request…",
"bash: curl → HTTP-Request");
expect(toolExecutionLabel("bash", { command: "ls ." }), "Shell: ls .",
"bash: unbekannter Befehl kurz → kein abschließendes …");
expect(toolExecutionLabel("bash", { command: "a".repeat(60) }), `Shell: ${"a".repeat(55)}`,
"bash: Befehl > 55 Zeichen → abgeschnitten mit …");
expect(toolExecutionLabel("apply_patch", {}), "Wende Patch an…",
"apply_patch → Wende Patch an");
expect(toolExecutionLabel("unknown_tool", {}), "",
"unbekanntes Tool → leerer String");
// ── getLastAssistantText ──────────────────────────────────────────────────────
function getLastAssistantText(ctx) {
const entries = ctx.sessionManager.getBranch();
for (let i = entries.length - 1; i >= 0; i--) {
const entry = entries[i];
if (entry.type === "message") {
const msg = entry.message;
if (msg?.role === "assistant" && Array.isArray(msg.content)) {
return msg.content
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("\n");
}
}
}
return "";
}
function makeCtx(entries) {
return { sessionManager: { getBranch: () => entries } };
}
function makeMsg(role, content) {
return { type: "message", message: { role, content } };
}
console.log("\ngetLastAssistantText()");
expect(
getLastAssistantText(makeCtx([])),
"",
"leere Session → leerer String"
);
expect(
getLastAssistantText(makeCtx([
makeMsg("assistant", [{ type: "text", text: "Hallo" }]),
])),
"Hallo",
"eine assistant-Nachricht → deren Text"
);
expect(
getLastAssistantText(makeCtx([
makeMsg("user", [{ type: "text", text: "Frage" }]),
makeMsg("assistant", [{ type: "text", text: "Antwort" }]),
])),
"Antwort",
"user + assistant → gibt assistant-Text zurück"
);
expect(
getLastAssistantText(makeCtx([
makeMsg("assistant", [{ type: "text", text: "Erste" }]),
makeMsg("assistant", [{ type: "text", text: "Letzte" }]),
])),
"Letzte",
"mehrere assistant-Nachrichten → gibt die letzte zurück"
);
expect(
getLastAssistantText(makeCtx([
makeMsg("assistant", [{ type: "tool_use", id: "x", name: "bash", input: {} }]),
])),
"",
"assistant-Nachricht ohne text-Content → leerer String"
);
expect(
getLastAssistantText(makeCtx([
makeMsg("assistant", [
{ type: "text", text: "Teil 1" },
{ type: "tool_use", id: "x", name: "edit", input: {} },
{ type: "text", text: "Teil 2" },
]),
])),
"Teil 1\nTeil 2",
"gemischte text/tool_use-Inhalte → nur text-Teile mit \\n verbunden"
);
// ── detectBumpType ────────────────────────────────────────────────────────────
function detectBumpType(lines) {
if (lines.some(l => /^feat!:|BREAKING CHANGE/.test(l))) return "major";
if (lines.some(l => /^feat(\(.+\))?:/.test(l))) return "minor";
return "patch";
}
console.log("\ndetectBumpType()");
expect(detectBumpType(["feat!: neue API"]), "major",
"feat!: → major");
expect(detectBumpType(["BREAKING CHANGE: auth umgebaut"]), "major",
"BREAKING CHANGE → major");
expect(detectBumpType(["feat: CSV-Export"]), "minor",
"feat: → minor");
expect(detectBumpType(["feat(parser): neues Feature"]), "minor",
"feat(scope): → minor");
expect(detectBumpType(["fix: Crash behoben"]), "patch",
"fix: → patch");
expect(detectBumpType(["chore: cleanup"]), "patch",
"chore: → patch");
expect(detectBumpType(["feat: kleine Änderung", "feat!: breaking"]), "major",
"major hat Vorrang vor minor in gemischter Liste");
expect(detectBumpType([]), "patch",
"leere Commit-Liste → patch");
// ── parseSemVer ───────────────────────────────────────────────────────────────
function parseSemVer(tag) {
const m = tag.match(/^v?(\d+)\.(\d+)\.(\d+)$/);
return m ? [+m[1], +m[2], +m[3]] : null;
}
function expectDeep(actual, expected, label) {
expect(JSON.stringify(actual), JSON.stringify(expected), label);
}
console.log("\nparseSemVer()");
expectDeep(parseSemVer("v1.2.3"), [1, 2, 3], "v1.2.3 → [1, 2, 3]");
expectDeep(parseSemVer("1.2.3"), [1, 2, 3], "1.2.3 ohne v → [1, 2, 3]");
expectDeep(parseSemVer("v0.0.1"), [0, 0, 1], "v0.0.1 → [0, 0, 1]");
expectDeep(parseSemVer("v10.20.300"), [10, 20, 300], "v10.20.300 → dreistellige Zahlen");
expectDeep(parseSemVer("nicht-semver"), null, "ungültiger Tag → null");
expectDeep(parseSemVer("v1.2"), null, "unvollständig v1.2 → null");
expectDeep(parseSemVer(""), null, "leerer String → null");
// ── stripOptimizeFlags ────────────────────────────────────────────────────────
function stripOptimizeFlags(args) {
return (args || "")
.replace(/--rounds\s+\d+/, "")
.replace(/--test-timeout\s+\d+/, "")
.replace(/--with-doku/, "")
.replace(/--continue/, "")
.replace(/--interactive/, "")
.replace(/--no-tests/, "")
.replace(/--approve-concerns/, "")
.replace(/--test-cmd\s+"[^"]*"/, "")
.replace(/--test-cmd\s+\S+/, "")
.trim();
}
console.log("\nstripOptimizeFlags()");
expect(stripOptimizeFlags("mein Auftrag --rounds 3"), "mein Auftrag",
"--rounds N wird entfernt");
expect(stripOptimizeFlags("--with-doku Auftrag"), "Auftrag",
"--with-doku wird entfernt");
expect(stripOptimizeFlags("Auftrag --no-tests --approve-concerns"), "Auftrag",
"--no-tests und --approve-concerns werden entfernt");
expect(stripOptimizeFlags('Auftrag --test-cmd "pytest tests/"'), "Auftrag",
'--test-cmd "..." wird entfernt');
expect(stripOptimizeFlags("Auftrag --test-timeout 60"), "Auftrag",
"--test-timeout N wird entfernt");
expect(stripOptimizeFlags("--continue --interactive Auftrag"), "Auftrag",
"--continue und --interactive werden entfernt");
expect(stripOptimizeFlags("Nur ein Auftrag"), "Nur ein Auftrag",
"keine Flags → Auftrag unverändert");
// ── parseOptimizeOptions ─────────────────────────────────────────────────────
function parseOptimizeOptions(args) {
const roundsMatch = (args || "").match(/--rounds\s+(\d+)/);
const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 2;
const testCmdMatch = (args || "").match(/--test-cmd\s+"([^"]+)"|--test-cmd\s+'([^']+)'|--test-cmd\s+(\S+)/);
const testCmd = testCmdMatch ? (testCmdMatch[1] ?? testCmdMatch[2] ?? testCmdMatch[3]) : null;
const testTimeoutMatch = (args || "").match(/--test-timeout\s+(\d+)/);
const testTimeout = testTimeoutMatch ? Math.max(1, parseInt(testTimeoutMatch[1], 10)) : 120;
return { maxRounds, testCmd, testTimeout };
}
console.log("\nparseOptimizeOptions()");
expect(parseOptimizeOptions("--rounds 5").maxRounds, 5,
"--rounds 5 → maxRounds = 5");
expect(parseOptimizeOptions("--rounds 0").maxRounds, 1,
"--rounds 0 → maxRounds = 1 (Math.max-Clamp)");
expect(parseOptimizeOptions("--rounds abc").maxRounds, 2,
"--rounds abc → Regex matcht nicht → Default 2");
expect(parseOptimizeOptions("").maxRounds, 2,
"kein --rounds → Default 2");
expect(parseOptimizeOptions("--test-timeout 30").testTimeout, 30,
"--test-timeout 30 → testTimeout = 30");
expect(parseOptimizeOptions("--test-timeout 0").testTimeout, 1,
"--test-timeout 0 → testTimeout = 1 (Math.max-Clamp)");
expect(parseOptimizeOptions("").testTimeout, 120,
"kein --test-timeout → Default 120");
expect(parseOptimizeOptions('--test-cmd "pytest -v"').testCmd, "pytest -v",
'--test-cmd "..." → testCmd ohne Anführungszeichen');
expect(parseOptimizeOptions("--test-cmd pytest").testCmd, "pytest",
"--test-cmd ohne Anführungszeichen → testCmd = 'pytest'");
expect(parseOptimizeOptions("auftrag --rounds 3 --test-timeout 60").maxRounds, 3,
"mehrere Flags kombiniert: maxRounds korrekt");
// ── calcVersionStrings ────────────────────────────────────────────────────────
function calcVersionStrings(current, bump) {
const [maj, min, pat] = current ?? [0, 0, 0];
const initial = !current;
const versions = initial
? { patch: "v0.0.1", minor: "v0.1.0", major: "v1.0.0" }
: { patch: `v${maj}.${min}.${pat + 1}`, minor: `v${maj}.${min + 1}.0`, major: `v${maj + 1}.0.0` };
const recommended = initial ? "minor" : bump;
const labels = ["patch", "minor", "major"].map(
t => `${t}${versions[t]}${t === recommended ? " (empfohlen)" : ""}`
);
return { versions, recommended, labels };
}
console.log("\ncalcVersionStrings()");
expectDeep(
calcVersionStrings(null, "patch").versions,
{ patch: "v0.0.1", minor: "v0.1.0", major: "v1.0.0" },
"initial (null): alle drei Standard-Startwerte"
);
expect(calcVersionStrings(null, "patch").recommended, "minor",
"initial: recommended ist immer minor (unabhängig vom Bump)");
expectDeep(
calcVersionStrings([1, 2, 3], "patch").versions,
{ patch: "v1.2.4", minor: "v1.3.0", major: "v2.0.0" },
"[1,2,3]: patch/minor/major korrekt hochgezählt"
);
expect(calcVersionStrings([1, 2, 3], "patch").recommended, "patch",
"[1,2,3] patch-Bump → recommended = patch");
expect(calcVersionStrings([1, 2, 3], "minor").recommended, "minor",
"[1,2,3] minor-Bump → recommended = minor");
expect(calcVersionStrings([1, 2, 3], "major").recommended, "major",
"[1,2,3] major-Bump → recommended = major");
{
const labels = calcVersionStrings([1, 2, 3], "minor").labels;
expect(labels[1], "minor → v1.3.0 (empfohlen)",
"empfohlenes Label trägt Suffix '(empfohlen)'");
expect(labels[0], "patch → v1.2.4",
"nicht-empfohlenes Label hat keinen Suffix");
}
expectDeep(
calcVersionStrings([0, 9, 9], "major").versions,
{ patch: "v0.9.10", minor: "v0.10.0", major: "v1.0.0" },
"[0,9,9]: zweistellige Zahlen korrekt hochgezählt"
);
// ── parseShipVerdict ──────────────────────────────────────────────────────────
function parseShipVerdict(text) {
return text.match(/Urteil:\s*(SHIP|NO-SHIP)/i)?.[1]?.toUpperCase() ?? "";
}
console.log("\nparseShipVerdict()");
expect(parseShipVerdict("Urteil: SHIP"), "SHIP",
"erkennt SHIP");
expect(parseShipVerdict("Urteil: NO-SHIP"), "NO-SHIP",
"erkennt NO-SHIP");
expect(parseShipVerdict("urteil: ship"), "SHIP",
"case-insensitiv: 'urteil: ship'");
expect(parseShipVerdict("kein Urteil hier"), "",
"kein Urteil → leerer String");
expect(parseShipVerdict("Urteil: PASS"), "",
"PASS ist kein gültiges SHIP-Token → leerer String");
expect(parseShipVerdict(""), "",
"leerer String → leerer String");
// ── Ergebnis ──────────────────────────────────────────────────────────────────
console.log(`\n${"─".repeat(50)}`);
console.log(`Gesamt: ${passed + failed} Tests — ${passed} bestanden, ${failed} fehlgeschlagen`);
if (failed > 0) {
process.exit(1);
}