From 4074e10c1acf2932f1ad9f0d0acd6c90a1a22ed1 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Tue, 19 May 2026 18:21:34 +0200 Subject: [PATCH 01/32] chore: init pi_coder repository Pi agent extension, model config, and LLaMA server startup scripts for the coder/judge workflow (ports 8001/8002). --- .gitignore | 3 + install.sh | 19 + models.json | 111 ++++++ pi-coder-judge-extension.ts | 762 ++++++++++++++++++++++++++++++++++++ start-coder.sh | 90 +++++ start-judge.sh | 90 +++++ 6 files changed, 1075 insertions(+) create mode 100644 .gitignore create mode 100755 install.sh create mode 100644 models.json create mode 100644 pi-coder-judge-extension.ts create mode 100755 start-coder.sh create mode 100755 start-judge.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c99ffd2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.bak +node_modules/ +.DS_Store diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..7149c48 --- /dev/null +++ b/install.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail +REPO="$(cd "$(dirname "$0")" && pwd)" + +# Extension symlink +mkdir -p ~/.pi/agent/extensions +ln -sf "$REPO/pi-coder-judge-extension.ts" ~/.pi/agent/extensions/pi-coder-judge-extension.ts +echo "Symlink: ~/.pi/agent/extensions/pi-coder-judge-extension.ts -> $REPO/pi-coder-judge-extension.ts" + +# models.json symlink (Backup wenn reguläre Datei vorhanden) +if [ -f ~/.pi/agent/models.json ] && [ ! -L ~/.pi/agent/models.json ]; then + cp ~/.pi/agent/models.json ~/.pi/agent/models.json.bak + echo "Backup: ~/.pi/agent/models.json.bak" +fi +ln -sf "$REPO/models.json" ~/.pi/agent/models.json +echo "Symlink: ~/.pi/agent/models.json -> $REPO/models.json" + +echo "" +echo "Fertig. Bitte /reload in pi agent ausführen." diff --git a/models.json b/models.json new file mode 100644 index 0000000..aec20e6 --- /dev/null +++ b/models.json @@ -0,0 +1,111 @@ +{ + "providers": { + "ollama": { + "baseUrl": "http://localhost:11434/v1", + "api": "openai-completions", + "apiKey": "ollama", + "compat": { + "supportsDeveloperRole": false, + "supportsReasoningEffort": false + }, + "models": [ + { "id": "qwen2.5-coder:7b", "name": "Qwen2.5 Coder 7B (schnell)" }, + { "id": "qwen3-coder-30b-gpu:latest", "name": "Qwen3 Coder 30B GPU (Standard)" }, + { "id": "mistral-small3.2:24b", "name": "Mistral Small 3.2 24B" }, + { "id": "deepseek-r1:32b", "name": "DeepSeek R1 32B (Reasoning)" } + ] + }, + + "llama-cpp": { + "baseUrl": "http://127.0.0.1:8000/v1", + "api": "openai-completions", + "apiKey": "none", + "compat": { + "supportsDeveloperRole": false, + "supportsReasoningEffort": false, + "maxTokensField": "max_tokens", + "thinkingFormat": "qwen-chat-template" + }, + "models": [ + { + "id": "qwen35b-uncensored", + "name": "Qwen3.6 35B Uncensored (llama.cpp :8000)" + }, + { + "id": "qwen35b-moe-tools", + "name": "Qwen3.6 35B MoE Tools (llama.cpp :8000)" + } + ] + }, + + "llama-cpp-coder": { + "baseUrl": "http://127.0.0.1:8001/v1", + "api": "openai-completions", + "apiKey": "none", + "compat": { + "supportsDeveloperRole": false, + "supportsReasoningEffort": false, + "maxTokensField": "max_tokens", + "thinkingFormat": "qwen-chat-template" + }, + "models": [ + { + "id": "qwen3.5-coder", + "name": "Qwen3.6 27B Coder (llama.cpp :8001)", + "reasoning": true, + "input": ["text"], + "contextWindow": 131072, + "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": 65536, + "maxTokens": 8192, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + } + ] + }, + + "openrouter": { + "models": [ + { "id": "qwen/qwen3-235b-a22b:free", "name": "Qwen3 235B (Free)" }, + { "id": "deepseek/deepseek-r1:free", "name": "DeepSeek R1 (Free)" }, + { "id": "google/gemini-2.5-pro-exp-03-25:free", "name": "Gemini 2.5 Pro (Free)" }, + { "id": "meta-llama/llama-4-maverick:free", "name": "Llama 4 Maverick (Free)" }, + { "id": "microsoft/phi-4:free", "name": "Phi-4 (Free)" }, + { "id": "qwen/qwen-2.5-coder-32b-instruct", "name": "Qwen2.5 Coder 32B (günstig)" }, + { "id": "deepseek/deepseek-r1", "name": "DeepSeek R1 Full (Reasoning)" }, + { "id": "qwen/qwen3-235b-a22b", "name": "Qwen3 235B Full" } + ] + } + } +} + diff --git a/pi-coder-judge-extension.ts b/pi-coder-judge-extension.ts new file mode 100644 index 0000000..7e8dca6 --- /dev/null +++ b/pi-coder-judge-extension.ts @@ -0,0 +1,762 @@ +// pi-coder-judge-extension.ts +// Automatisierter Coder-Judge-Fix-Workflow für AI-Coding-Assistenten +// Modelle: qwen3.5-coder (Port 8001), qwen3.5-judge (Port 8002) + +import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; +import { Type } from "typebox"; + +// ── Prompt-Bausteine ──────────────────────────────────────────────────────── + +function coderKickoff(task: string): string { + return [ + "Du bist der Coding-Agent.", + "Lies TASK.md für die vollständige Aufgabenbeschreibung.", + "Halte dich strikt an die dort beschriebenen Anforderungen.", + "Arbeite sorgfältig, konkret und produktionsorientiert.", + "Lies jede Datei unmittelbar vor dem Editieren neu ein.", + "Wenn du mehrere Stellen in derselben Datei änderst: Erzeuge einen unified diff und nutze das apply_patch-Tool — nicht mehrere edit-Aufrufe.", + "Fallback: Datei komplett neu schreiben wenn apply_patch nicht möglich.", + "", + "Git-Pflichten:", + "- Falls noch kein git-Repository existiert, initialisiere es mit 'git init'.", + "- Führe nach der Implementierung einen Commit durch: git add -A && git commit -m 'feat: ...'", + "", + "Führe nach der Implementierung passende Tests oder Checks aus.", + "Melde knapp, was du geändert hast, welche Risiken bleiben und welche Tests du ausgeführt hast.", + "", + "Auftrag:", + task + ].join("\n"); +} + +function judgePrompt(extra: string): string { + const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : ""; + return [ + "Du bist ein pingeliger, skeptischer Senior-Reviewer und QA-Ingenieur.", + "Deine Aufgabe ist NICHT, nett zu sein, sondern Fehler, Risiken, Randfälle und Produktionsprobleme zu finden.", + "Arbeite reproduzierbar und konkret.", + "", + "Pflichten:", + "0. Lies TASK.md und prüfe, ob alle dort beschriebenen Anforderungen vollständig umgesetzt sind.", + "1. Sieh dir den letzten Commit an: 'git log -1 --stat' und 'git show HEAD'.", + "2. Führe relevante Tests, Linter oder Startchecks aus.", + "3. Versuche Fehler aktiv zu finden.", + "4. Bewerte Korrektheit, Robustheit, Fehlerbehandlung, Sicherheit, Logging, Wartbarkeit und Produktionsreife.", + "5. Wenn etwas fehlt, sage es klar und direkt.", + "", + "Ausgabeformat:", + "- Urteil: PASS | PASS WITH CONCERNS | FAIL", + "- Blocker", + "- Major", + "- Minor", + "- Fehlende Tests", + "- Produktionsrisiken", + "- Konkrete Fix-Aufträge an den Coder", + "", + "Wenn du etwas behauptest, nenne die Datei, den Befehl, den Test oder den Reproduktionshinweis." + ].join("\n") + suffix; +} + +function fixPrompt(extra: string): string { + const suffix = extra?.trim() ? "\n\nZusätzlicher User-Hinweis:\n" + extra.trim() : ""; + return [ + "Wechsle in den Reparaturmodus.", + "Lies TASK.md als Referenz — stelle sicher, dass nach den Fixes alle Anforderungen erfüllt bleiben.", + "Nutze den letzten Judge-Bericht als verbindliche Aufgabenliste.", + "Behebe zuerst Blocker, dann Major, dann Minor.", + "Lies jede betroffene Datei unmittelbar vor dem Editieren erneut ein.", + "Wenn du mehrere Stellen in derselben Datei änderst: Erzeuge einen unified diff und nutze das apply_patch-Tool — nicht mehrere edit-Aufrufe.", + "Fallback: Datei komplett neu schreiben wenn apply_patch nicht möglich.", + "Führe nach den Fixes passende Tests aus.", + "Führe danach einen Commit durch: git add -A && git commit -m 'fix: ...'", + "Wenn ein Punkt nicht sinnvoll umsetzbar ist, begründe das präzise.", + "Liefere am Ende nur:", + "- Was geändert wurde", + "- Welche Judge-Punkte geschlossen wurden", + "- Welche Punkte offen bleiben", + "- Welche Tests ausgeführt wurden" + ].join("\n") + suffix; +} + +function shipitPrompt(extra: string): string { + return [ + "Führe die finale Freigabeprüfung durch.", + "Lies TASK.md und prüfe, ob alle dort beschriebenen Anforderungen im finalen Stand enthalten sind.", + "Lies die relevanten geänderten Dateien und 'git log --oneline -10'.", + "Führe sinnvolle Tests, Linter und Startchecks aus.", + "Beurteile, ob der Stand produktionsreif ist.", + "", + "Ausgabeformat:", + "- Urteil: SHIP | NO-SHIP", + "- Letzte Blocker", + "- Restrisiken", + "- Empfohlene Sofortmaßnahmen vor Deployment" + ].join("\n") + (extra?.trim() ? "\n\nZusätzlicher Fokus:\n" + extra.trim() : ""); +} + +function patchPrompt(change: string): string { + return [ + "Du machst eine kleine, gezielte Codeänderung. Nichts weiter.", + "Ändere AUSSCHLIESSLICH das Folgende:", + change, + "", + "Regeln:", + "- Kein Refactoring, keine weiteren Verbesserungen, keine Umbenennungen", + "- Lies die Datei unmittelbar vor dem Editieren neu ein", + "- Wenn du mehrere Stellen in derselben Datei änderst: Erzeuge einen unified diff und nutze das apply_patch-Tool", + "- Fallback: Datei komplett neu schreiben", + "- Finde die betroffene Stelle direkt (grep oder gezielte Datei-Suche)", + "- Ändere nur die notwendigen Zeilen", + "- Prüfe danach nur: Kompiliert/startet es noch?", + "- Commit: git add -A && git commit -m 'fix: '", + "- Melde: Datei, Zeile(n), was geändert wurde. Fertig." + ].join("\n"); +} + +function quickCheckPrompt(what: string): string { + const focus = what?.trim() ? "\n\nZu prüfende Änderung:\n" + what.trim() : ""; + return [ + "Schnelle Prüfung einer kleinen Codeänderung.", + "Lies 'git show HEAD' um zu sehen was geändert wurde.", + "Prüfe NUR:", + "- Ist die Änderung korrekt umgesetzt?", + "- Gibt es offensichtliche Fehler oder Randfälle die übersehen wurden?", + "- Kompiliert/startet der Code?", + "", + "Ausgabeformat (kurz):", + "- Urteil: OK | PROBLEM", + "- Falls PROBLEM: konkret was falsch ist und wie zu fixen", + "", + "Kein vollständiger Review, keine Stilkritik, kein Refactoring-Vorschlag." + ].join("\n") + focus; +} + +function commentCodePrompt(): string { + return [ + "Lies TASK.md und alle Quelldateien des Projekts.", + "Füge wartungsfreundliche Kommentare ein, die Entwicklern ohne Vorkenntnis helfen, den Code zu verstehen und zu warten.", + "", + "Regeln:", + "- Kommentiere das WARUM, nicht das WAS (kein 'x += 1 // increment x')", + "- Erkläre nicht-offensichtliche Algorithmen, Randfälle und Design-Entscheidungen", + "- Füge Modul-/Datei-Level-Kommentare ein, die den Gesamtzweck der Datei erklären", + "- Keine trivialen Kommentare, die nur den Code wiederholen", + "- Sprache der Kommentare: Deutsch", + "", + "Wenn du mehrere Stellen in derselben Datei kommentierst: Erzeuge einen unified diff und nutze das apply_patch-Tool — nicht mehrere edit-Aufrufe.", + "Fallback: Datei komplett neu schreiben wenn apply_patch nicht möglich.", + "", + "Führe danach einen Build/Test aus um sicherzustellen, dass die Kommentare nichts kaputt gemacht haben.", + "Melde welche Dateien du kommentiert hast." + ].join("\n"); +} + +function readmeMdPrompt(): string { + return [ + "Lies TASK.md und alle Quelldateien des Projekts.", + "Schreibe oder aktualisiere README.md aus Entwicklerperspektive.", + "", + "Pflichtabschnitte:", + "- Projektbeschreibung (Was macht das Programm? Warum?)", + "- Voraussetzungen (Dependencies, Toolchain)", + "- Installation und Build", + "- Verwendung (alle Kommandozeilenoptionen und Flags)", + "- Beispiele mit konkreter Ausgabe", + "- Projektstruktur (Dateien und ihre Aufgabe)", + "", + "Halte es technisch präzise und korrekt. Sprache: Deutsch." + ].join("\n"); +} + +function bedienungsanleitungPrompt(): string { + return [ + "Lies TASK.md und README.md.", + "Schreibe oder aktualisiere BEDIENUNGSANLEITUNG.md aus Endnutzer-Perspektive.", + "Setze kein Entwicklerwissen voraus — die Zielgruppe sind normale Anwender.", + "", + "Pflichtabschnitte:", + "- Zweck des Programms (was kann der Nutzer damit tun?)", + "- Installation für Endnutzer (Schritt für Schritt)", + "- Erste Schritte / Schnellstart", + "- Alle Optionen mit verständlicher Erklärung und Beispielen", + "- Typische Anwendungsfälle", + "- Fehlermeldungen und ihre Lösung", + "", + "Sprache: Deutsch. Einfach, klar, ohne Jargon." + ].join("\n"); +} + +function commentCodePromptIncremental(files: string[]): string { + const fileList = files.map(f => ` - ${f}`).join("\n"); + return [ + "Kommentiere NUR die folgenden Quelldateien — sie haben sich seit dem letzten Kommentar-Update geändert:", + fileList, + "Alle anderen Dateien haben bereits aktuelle Kommentare — lass sie vollständig unberührt.", + "", + "Regeln:", + "- Kommentiere das WARUM, nicht das WAS (kein 'x += 1 // increment x')", + "- Erkläre nicht-offensichtliche Algorithmen, Randfälle und Design-Entscheidungen", + "- Füge Modul-/Datei-Level-Kommentare ein, die den Gesamtzweck der Datei erklären", + "- Keine trivialen Kommentare, die nur den Code wiederholen", + "- Sprache der Kommentare: Deutsch", + "", + "Wenn du mehrere Stellen in derselben Datei kommentierst: Erzeuge einen unified diff und nutze das apply_patch-Tool — nicht mehrere edit-Aufrufe.", + "Fallback: Datei komplett neu schreiben wenn apply_patch nicht möglich.", + "", + "Führe danach einen Build/Test aus um sicherzustellen, dass die Kommentare nichts kaputt gemacht haben.", + "Melde welche Dateien du kommentiert hast." + ].join("\n"); +} + +function readmeMdPromptIncremental(files: string[]): string { + const fileList = files.map(f => ` - ${f}`).join("\n"); + return [ + "Folgende Quelldateien haben sich seit dem letzten README-Update geändert:", + fileList, + "", + "Prüfe: Haben diese Änderungen Auswirkungen auf Installation, Verwendung, Optionen oder Projektstruktur?", + "- Falls JA: Lies README.md und aktualisiere NUR die betroffenen Abschnitte.", + "- Falls NEIN: Antworte nur mit dem Satz: 'README.md ist aktuell – keine Änderung nötig.'", + "", + "Wenn du mehrere Abschnitte in README.md aktualisierst: Erzeuge einen unified diff und nutze das apply_patch-Tool.", + "Halte es technisch präzise und korrekt. Sprache: Deutsch." + ].join("\n"); +} + +function bedienungsanleitungPromptIncremental(files: string[]): string { + const fileList = files.map(f => ` - ${f}`).join("\n"); + return [ + "Folgende Quelldateien haben sich seit dem letzten Bedienungsanleitung-Update geändert:", + fileList, + "", + "Prüfe: Haben diese Änderungen Auswirkungen auf die Benutzung des Programms durch Endnutzer?", + "(z.B. neue Optionen, geändertes Verhalten, neue Fehlermeldungen)", + "- Falls JA: Lies BEDIENUNGSANLEITUNG.md und aktualisiere NUR die betroffenen Abschnitte.", + "- Falls NEIN: Antworte nur mit dem Satz: 'BEDIENUNGSANLEITUNG.md ist aktuell – keine Änderung nötig.'", + "", + "Wenn du mehrere Abschnitte in BEDIENUNGSANLEITUNG.md aktualisierst: Erzeuge einen unified diff und nutze das apply_patch-Tool.", + "Sprache: Deutsch. Einfach, klar, ohne Jargon." + ].join("\n"); +} + +// ── Hilfsfunktionen ───────────────────────────────────────────────────────── + +// Legt TASK.md neu an oder hängt einen Zusatzauftrag an. +async function writeTaskMd( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + task: string +): Promise { + const check = await pi.exec("bash", ["-c", "test -f TASK.md && echo exists"], { cwd: ctx.cwd }); + const exists = check.stdout.trim() === "exists"; + + let content: string; + if (exists) { + content = [ + "", + "---", + "", + "## Zusatzauftrag", + "", + new Date().toISOString(), + "", + task, + "", + "## Status", + "- [ ] Implementierung", + "- [ ] Review bestanden (PASS)", + "- [ ] Produktionsreif (SHIP)", + ].join("\n") + "\n"; + } else { + content = [ + "# Aufgabe", + "", + task, + "", + "## Erstellt", + new Date().toISOString(), + "", + "## Status", + "- [ ] Implementierung", + "- [ ] Review bestanden (PASS)", + "- [ ] Produktionsreif (SHIP)", + ].join("\n") + "\n"; + } + + const redirect = exists ? ">>" : ">"; + await pi.exec("bash", ["-c", `printf "%s" "$1" ${redirect} TASK.md`, "_", content], { cwd: ctx.cwd }); + ctx.ui.notify(exists ? "TASK.md erweitert" : "TASK.md angelegt", "info"); +} + +// Hakt einen Status-Eintrag in TASK.md ab. +// label: exakt wie in der Checkbox, z.B. "Implementierung" +async function tickTaskMdStatus( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + label: string +): Promise { + const check = await pi.exec("bash", ["-c", "test -f TASK.md && echo exists"], { cwd: ctx.cwd }); + if (check.stdout.trim() !== "exists") return; + // Python übernimmt den String-Ersatz — kein Shell-Escaping-Problem + await pi.exec( + "python3", + ["-c", + "import sys; f=open('TASK.md','r'); c=f.read(); f.close(); " + + "c=c.replace('- [ ] '+sys.argv[1], '- [x] '+sys.argv[1]); " + + "f=open('TASK.md','w'); f.write(c); f.close()", + label + ], + { cwd: ctx.cwd } + ); +} + +async function switchModel( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + provider: string, + modelId: string +): Promise { + const model = ctx.modelRegistry.find(provider, modelId); + if (!model) { + ctx.ui.notify(`Modell ${provider}/${modelId} nicht gefunden`, "error"); + return; + } + const ok = await pi.setModel(model); + if (!ok) ctx.ui.notify(`Kein API-Key für ${modelId}`, "warning"); +} + +// Sendet eine Nachricht und wartet bis der Agent fertig ist. +// Erst idle abwarten, dann als followUp einstellen — verhindert "Agent is already processing". +async function sendAndWait( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + content: string +): Promise { + await ctx.waitForIdle(); + pi.sendUserMessage(content, { deliverAs: "followUp" }); + await new Promise(r => setTimeout(r, 400)); + await ctx.waitForIdle(); +} + +// Liest den Text der letzten Assistenten-Antwort aus dem Session-Branch. +function getLastAssistantText(ctx: ExtensionCommandContext): string { + const entries = ctx.sessionManager.getBranch(); + for (let i = entries.length - 1; i >= 0; i--) { + const entry = entries[i]; + if (entry.type === "message") { + const msg = (entry as any).message; + if (msg?.role === "assistant" && Array.isArray(msg.content)) { + return msg.content + .filter((c: any) => c.type === "text") + .map((c: any) => c.text as string) + .join("\n"); + } + } + } + return ""; +} + +// Extrahiert das Urteil aus einer Judge-Antwort. +function parseVerdict(text: string): string { + const m = text.match(/Urteil:\s*(PASS WITH CONCERNS|PASS|FAIL)/i); + return m ? m[1].toUpperCase() : ""; +} + +// Extrahiert den Blocker-Abschnitt für die Loop-Erkennung. +function parseBlockers(text: string): string { + const m = text.match(/[-–*]\s*Blocker[:\n]([\s\S]*?)(?:\n[-–*]\s*Major|\n[-–*]\s*Minor|$)/i); + return m ? m[1].trim() : ""; +} + +// Gibt geänderte Quelldateien seit einem Git-Tag zurück. +// null = Tag existiert nicht (erster Lauf) → alles verarbeiten +// [] = nichts geändert → Phase überspringen +// [...] = nur diese Dateien verarbeiten +async function getFilesSinceTag( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + tagName: string +): Promise { + const tagCheck = await pi.exec("bash", ["-c", `git tag -l "${tagName}"`], { cwd: ctx.cwd }); + if (!tagCheck.stdout.trim()) return null; + + const diff = await pi.exec( + "bash", + ["-c", `git diff "${tagName}" --name-only 2>/dev/null`], + { cwd: ctx.cwd } + ); + + return diff.stdout.trim() + .split("\n") + .filter(f => + f.length > 0 && + !f.endsWith(".md") && + !f.endsWith(".lock") && + !f.endsWith(".toml") && + !f.startsWith("target/") && + !f.endsWith(".gitignore") + ); +} + +// Dokumentations-Phase: inkrementell via Git-Tags, nur geänderte Dateien werden verarbeitet. +// Wird von /update_doku und /optimize --with-doku genutzt. +async function runUpdateDoku(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise { + await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); + + // Phase 1: Code-Kommentare + const commentFiles = await getFilesSinceTag(pi, ctx, "docs-last-commented"); + if (commentFiles === null) { + ctx.ui.setStatus("update_doku", "1/3: Code wird kommentiert (alle Dateien)…"); + await sendAndWait(pi, ctx, commentCodePrompt()); + } else if (commentFiles.length === 0) { + ctx.ui.notify("Code-Kommentare: keine Änderungen seit letztem Lauf – übersprungen.", "info"); + } else { + ctx.ui.setStatus("update_doku", `1/3: Code wird kommentiert (${commentFiles.length} Datei(en))…`); + await sendAndWait(pi, ctx, commentCodePromptIncremental(commentFiles)); + } + await pi.exec("bash", ["-c", "git tag -f docs-last-commented"], { cwd: ctx.cwd }); + + // Phase 2: README.md + const readmeFiles = await getFilesSinceTag(pi, ctx, "docs-last-readme"); + if (readmeFiles === null) { + ctx.ui.setStatus("update_doku", "2/3: README.md wird geschrieben…"); + await sendAndWait(pi, ctx, readmeMdPrompt()); + } else if (readmeFiles.length === 0) { + ctx.ui.notify("README.md: keine Änderungen seit letztem Lauf – übersprungen.", "info"); + } else { + ctx.ui.setStatus("update_doku", `2/3: README.md wird geprüft (${readmeFiles.length} Datei(en) geändert)…`); + await sendAndWait(pi, ctx, readmeMdPromptIncremental(readmeFiles)); + } + await pi.exec("bash", ["-c", "git tag -f docs-last-readme"], { cwd: ctx.cwd }); + + // Phase 3: BEDIENUNGSANLEITUNG.md + const bedFiles = await getFilesSinceTag(pi, ctx, "docs-last-bedienungsanleitung"); + if (bedFiles === null) { + ctx.ui.setStatus("update_doku", "3/3: BEDIENUNGSANLEITUNG.md wird geschrieben…"); + await sendAndWait(pi, ctx, bedienungsanleitungPrompt()); + } else if (bedFiles.length === 0) { + ctx.ui.notify("BEDIENUNGSANLEITUNG.md: keine Änderungen seit letztem Lauf – übersprungen.", "info"); + } else { + ctx.ui.setStatus("update_doku", `3/3: BEDIENUNGSANLEITUNG.md wird geprüft (${bedFiles.length} Datei(en) geändert)…`); + await sendAndWait(pi, ctx, bedienungsanleitungPromptIncremental(bedFiles)); + } + await pi.exec("bash", ["-c", "git tag -f docs-last-bedienungsanleitung"], { cwd: ctx.cwd }); + + // Abschließender Dokumentations-Commit + await pi.exec( + "bash", + ["-c", "git add -A && git commit -m 'docs: update comments, README, BEDIENUNGSANLEITUNG' || true"], + { cwd: ctx.cwd } + ); + + // TASK.md: Produktionsreif abhaken + await tickTaskMdStatus(pi, ctx, "Produktionsreif (SHIP)"); + + ctx.ui.setStatus("update_doku", "✓ Dokumentation abgeschlossen"); + ctx.ui.notify("Dokumentations-Phase abgeschlossen. Commit angelegt.", "info"); +} + +// ── Extension ──────────────────────────────────────────────────────────────── + +export default function (pi: ExtensionAPI) { + pi.on("session_start", async function (_event, ctx) { + ctx.ui.setWidget("coder-judge", [ + "Workflow: /coder | /judge | /fix | /shipit", + "Auto-Loop: /optimize [--rounds N] [--with-doku]", + "Kleine Änderung: /patch <änderung> → /quick_check [was]", + "Finale Doku: /update_doku (nach SHIP – Kommentare + README + Bedienungsanleitung)", + "Neues Projekt: /new_project ", + "Modell wird automatisch gewechselt (Coder→:8001, Judge→:8002)" + ]); + }); + + // ── Robustes edit: Bottom-up-Reordering via tool_call-Hook ───────────── + // Behebt "edits[n] doesn't match": Mehrere Edits auf dieselbe Datei werden + // von hinten nach vorne sortiert, damit frühere Edits spätere Positionen nicht verschieben. + + pi.on("tool_call", async function (event, ctx) { + if (event.toolName !== "edit") return; + + const input = event.input as { + path: string; + edits: Array<{ oldText: string; newText: string }>; + }; + + if (!input?.edits || input.edits.length <= 1) return; + + const readResult = await pi.exec( + "bash", + ["-c", `cat "$1"`, "_", input.path], + { cwd: ctx.cwd } + ); + if (readResult.code !== 0) return; + + const content = readResult.stdout; + + const positioned = input.edits.map(edit => ({ + edit, + idx: content.indexOf(edit.oldText) + })); + + // Nicht gefundene Einträge (idx === -1) ans Ende — sie schlagen sowieso fehl + positioned.sort((a, b) => b.idx - a.idx); + + input.edits.splice(0, input.edits.length, ...positioned.map(p => p.edit)); + }); + + // ── Manuelle Kommandos ─────────────────────────────────────────────────── + + pi.registerCommand("coder", { + description: "Legt TASK.md an, startet Implementierung → qwen3.5-coder (:8001).", + handler: async function (args: string, ctx: ExtensionCommandContext) { + const task = (args || "").trim(); + if (!task) { + ctx.ui.notify("Benutzung: /coder ", "error"); + return; + } + await writeTaskMd(pi, ctx, task); + await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); + pi.sendUserMessage(coderKickoff(task)); + } + }); + + pi.registerCommand("judge", { + description: "Review gegen TASK.md + git show HEAD → qwen3.5-judge (:8002).", + handler: async function (args: string, ctx: ExtensionCommandContext) { + await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge"); + pi.sendUserMessage(judgePrompt(args || "")); + } + }); + + pi.registerCommand("fix", { + description: "Fixt Judge-Kritik, committet Ergebnis → qwen3.5-coder (:8001).", + handler: async function (args: string, ctx: ExtensionCommandContext) { + await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); + pi.sendUserMessage(fixPrompt(args || "")); + } + }); + + pi.registerCommand("shipit", { + description: "Finale Freigabe gegen TASK.md + git log → qwen3.5-judge (:8002).", + handler: async function (args: string, ctx: ExtensionCommandContext) { + await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge"); + pi.sendUserMessage(shipitPrompt(args || "")); + } + }); + + // ── Automatische Optimierungsschleife ──────────────────────────────────── + + pi.registerCommand("optimize", { + description: "Coder→Judge→Fix-Schleife bis PASS + optional Doku. /optimize [--rounds N] [--with-doku]", + handler: async function (args: string, ctx: ExtensionCommandContext) { + const roundsMatch = (args || "").match(/--rounds\s+(\d+)/); + const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 3; + const withDoku = /--with-doku/.test(args || ""); + const task = (args || "") + .replace(/--rounds\s+\d+/, "") + .replace(/--with-doku/, "") + .trim(); + + if (!task) { + ctx.ui.notify("Benutzung: /optimize [--rounds N] [--with-doku]", "error"); + return; + } + + // TASK.md anlegen + await writeTaskMd(pi, ctx, task); + + ctx.ui.setStatus("optimize", `Starte Optimierung (max ${maxRounds} Runden)…`); + + // Phase 1: Initiale Implementierung + ctx.ui.setStatus("optimize", "Phase 1: Coder implementiert…"); + await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); + await sendAndWait(pi, ctx, coderKickoff(task)); + await tickTaskMdStatus(pi, ctx, "Implementierung"); + + let lastBlockers = ""; + let verdict = ""; + + // Schleife: Judge → (PASS? fertig : Fix → nächste Runde) + for (let round = 1; round <= maxRounds; round++) { + ctx.ui.setStatus("optimize", `Runde ${round}/${maxRounds}: Judge prüft…`); + await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge"); + await sendAndWait(pi, ctx, judgePrompt("")); + + const judgeText = getLastAssistantText(ctx); + verdict = parseVerdict(judgeText); + + if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") { + await tickTaskMdStatus(pi, ctx, "Review bestanden (PASS)"); + ctx.ui.setStatus("optimize", `✓ ${verdict} nach Runde ${round}`); + ctx.ui.notify(`Optimierung abgeschlossen: ${verdict} nach ${round} Runde(n)`, "info"); + break; + } + + // Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen + const currentBlockers = parseBlockers(judgeText); + if (currentBlockers && currentBlockers === lastBlockers) { + ctx.ui.setStatus("optimize", "⚠ Schleife: gleicher Blocker – manuelle Intervention nötig"); + ctx.ui.notify( + "Derselbe Blocker tritt erneut auf – Schleife abgebrochen. Bitte manuell prüfen.", + "warning" + ); + return; + } + lastBlockers = currentBlockers; + + if (round === maxRounds) { + ctx.ui.setStatus("optimize", `⚠ Max. ${maxRounds} Runden ohne PASS`); + ctx.ui.notify(`${maxRounds} Runden durchlaufen ohne PASS. Bitte manuell prüfen.`, "warning"); + return; + } + + // Fix-Phase + ctx.ui.setStatus("optimize", `Runde ${round}/${maxRounds}: Coder fixt…`); + await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); + await sendAndWait(pi, ctx, fixPrompt("")); + } + + // Finale ShipIt-Prüfung nur bei PASS + if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") { + ctx.ui.setStatus("optimize", "Finale ShipIt-Prüfung…"); + await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge"); + await sendAndWait(pi, ctx, shipitPrompt("")); + + const shipText = getLastAssistantText(ctx); + const shipVerdict = shipText.match(/Urteil:\s*(SHIP|NO-SHIP)/i)?.[1]?.toUpperCase() ?? ""; + + if (shipVerdict === "SHIP") { + ctx.ui.setStatus("optimize", "🚀 SHIP – produktionsreif"); + if (withDoku) { + await runUpdateDoku(pi, ctx); + } else { + ctx.ui.notify("Nächster Schritt: /update_doku für Code-Kommentare, README.md und BEDIENUNGSANLEITUNG.md", "info"); + } + } else if (shipVerdict === "NO-SHIP") { + ctx.ui.setStatus("optimize", "⛔ NO-SHIP – noch nicht bereit"); + } else { + ctx.ui.setStatus("optimize", "ShipIt abgeschlossen"); + } + } + } + }); + + // ── Schlanke Kommandos für kleine Änderungen ───────────────────────────── + + pi.registerCommand("patch", { + description: "Gezielte Minimaländerung ohne vollständigen Review → qwen3.5-coder (:8001).", + handler: async function (args: string, ctx: ExtensionCommandContext) { + const change = (args || "").trim(); + if (!change) { + ctx.ui.notify("Benutzung: /patch ", "error"); + return; + } + await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); + pi.sendUserMessage(patchPrompt(change)); + } + }); + + pi.registerCommand("quick_check", { + description: "Schnelle Prüfung der letzten Änderung (OK/PROBLEM) → qwen3.5-judge (:8002).", + handler: async function (args: string, ctx: ExtensionCommandContext) { + await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge"); + pi.sendUserMessage(quickCheckPrompt(args || "")); + } + }); + + // ── Dokumentations-Phase ───────────────────────────────────────────────── + + pi.registerCommand("update_doku", { + description: "Code kommentieren + README.md + BEDIENUNGSANLEITUNG.md + git commit → qwen3.5-coder (:8001).", + handler: async function (_args: string, ctx: ExtensionCommandContext) { + await runUpdateDoku(pi, ctx); + } + }); + + // ── Robustes Editieren via GNU patch ───────────────────────────────────── + + pi.registerTool({ + name: "apply_patch", + label: "Patch anwenden", + description: [ + "Wendet einen unified diff (git-Format) auf Dateien an.", + "Zuverlässiger als das edit-Tool bei mehrfachen Änderungen an derselben Datei.", + "Format: --- a/pfad/datei +++ b/pfad/datei @@ -n,m +n,m @@ ...", + "Verwende dieses Tool wenn du mehrere Stellen in einer Datei änderst." + ].join(" "), + parameters: Type.Object({ + patch: Type.String({ + description: "Unified diff im git-Format mit --- a/... und +++ b/... Headern." + }), + }), + async execute(_id, params, _signal, _onUpdate, ctx) { + const tmpFile = `/tmp/pi_patch_${Date.now()}.diff`; + await pi.exec( + "bash", + ["-c", `printf "%s" "$1" > "${tmpFile}"`, "_", params.patch], + { cwd: ctx.cwd } + ); + // -p1 entfernt führende a/ b/ Präfixe (git-Standard) + const result = await pi.exec( + "bash", + ["-c", `patch -p1 < "${tmpFile}"; rm -f "${tmpFile}"`], + { cwd: ctx.cwd } + ); + if (result.code !== 0) { + return { + content: [{ type: "text", text: `Patch fehlgeschlagen:\n${result.stderr}\n${result.stdout}` }], + isError: true + }; + } + return { content: [{ type: "text", text: result.stdout || "Patch erfolgreich angewendet." }] }; + } + }); + + // ── Projekt-Scaffolding ────────────────────────────────────────────────── + + pi.registerCommand("new_project", { + description: "Legt Projektverzeichnis an + git init + .gitignore. /new_project ", + handler: async function (args: string, ctx: ExtensionCommandContext) { + const rawPath = (args || "").trim(); + if (!rawPath) { + ctx.ui.notify("Benutzung: /new_project ", "error"); + return; + } + + // ~ expandieren + const projectPath = rawPath.startsWith("~/") + ? rawPath.replace("~/", (process.env.HOME || "") + "/") + : rawPath; + + // Verzeichnis anlegen + const mkResult = await pi.exec("bash", ["-c", 'mkdir -p "$1"', "_", projectPath], { cwd: ctx.cwd }); + if (mkResult.code !== 0) { + ctx.ui.notify(`Fehler: ${mkResult.stderr}`, "error"); + return; + } + + // git init (nur wenn noch kein Repo vorhanden) + const gitCheck = await pi.exec("bash", ["-c", "test -d .git && echo exists"], { cwd: projectPath }); + if (gitCheck.stdout.trim() !== "exists") { + await pi.exec("bash", ["-c", "git init"], { cwd: projectPath }); + } + + // .gitignore anlegen + const gitignore = "target/\n*.o\n*.d\n*.swp\n.env\n.DS_Store\n"; + await pi.exec("bash", ["-c", 'printf "%s" "$1" > .gitignore', "_", gitignore], { cwd: projectPath }); + await pi.exec( + "bash", + ["-c", "git add .gitignore && git commit -m 'chore: init project' || true"], + { cwd: projectPath } + ); + + ctx.ui.notify(`Projekt angelegt: ${projectPath}`, "info"); + ctx.ui.notify( + `⚠ Pi läuft noch in: ${ctx.cwd} — Session-Verzeichnis kann nicht gewechselt werden.\n` + + `Neues Projekt starten: cd ${projectPath} && pi`, + "warning" + ); + ctx.ui.setStatus("new_project", `Neues Projekt → cd ${projectPath} && pi`); + } + }); +} diff --git a/start-coder.sh b/start-coder.sh new file mode 100755 index 0000000..70fd59b --- /dev/null +++ b/start-coder.sh @@ -0,0 +1,90 @@ +#!/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 131072 \ + -n 16384 \ + --jinja \ + --no-context-shift \ + --temp 0.2 \ + --top-p 0.95 \ + --top-k 40 \ + --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 q8_0 \ + --cache-type-v q8_0 \ + --batch-size 1024 \ + --ubatch-size 512 \ + --parallel 2 \ + --cont-batching \ + --host 0.0.0.0 \ + --port "$CONTAINER_PORT" + +echo "[*] Warte auf HTTP ..." +HTTP_READY=0 +for i in {1..90}; do + if curl -s "http://localhost:${HOST_PORT}/health" >/dev/null 2>&1 || \ + curl -s "http://localhost:${HOST_PORT}/v1/models" >/dev/null 2>&1; then + HTTP_READY=1 + break + fi + sleep 2 +done + +if [ "$HTTP_READY" -ne 1 ]; then + echo "[!] HTTP-Server wurde nicht rechtzeitig erreichbar." >&2 + docker logs --tail 200 "$CONTAINER_NAME" || true + exit 1 +fi + +echo "[*] Teste Chat-Completion ..." +curl -s -X POST "http://localhost:${HOST_PORT}/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -d "{ + \"model\": \"${MODEL_ALIAS}\", + \"messages\": [ + { \"role\": \"system\", \"content\": \"Du bist ein präziser Coding-Assistent.\" }, + { \"role\": \"user\", \"content\": \"Antworte nur mit dem Wort: bereit\" } + ], + \"max_tokens\": 8, + \"temperature\": 0.0, + \"stream\": false + }" + +echo +echo "[*] Server bereit auf http://0.0.0.0:${HOST_PORT}" +echo "[*] Stoppen mit: docker rm -f ${CONTAINER_NAME}" diff --git a/start-judge.sh b/start-judge.sh new file mode 100755 index 0000000..4d78b15 --- /dev/null +++ b/start-judge.sh @@ -0,0 +1,90 @@ +#!/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 65536 \ + -n 8192 \ + --jinja \ + --no-context-shift \ + --temp 0.1 \ + --top-p 0.9 \ + --top-k 40 \ + --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 q8_0 \ + --cache-type-v q8_0 \ + --batch-size 512 \ + --ubatch-size 256 \ + --parallel 1 \ + --cont-batching \ + --host 0.0.0.0 \ + --port "$CONTAINER_PORT" + +echo "[*] Warte auf HTTP ..." +HTTP_READY=0 +for i in {1..90}; do + if curl -s "http://localhost:${HOST_PORT}/health" >/dev/null 2>&1 || \ + curl -s "http://localhost:${HOST_PORT}/v1/models" >/dev/null 2>&1; then + HTTP_READY=1 + break + fi + sleep 2 +done + +if [ "$HTTP_READY" -ne 1 ]; then + echo "[!] HTTP-Server wurde nicht rechtzeitig erreichbar." >&2 + docker logs --tail 200 "$CONTAINER_NAME" || true + exit 1 +fi + +echo "[*] Teste Judge-Endpoint ..." +curl -s -X POST "http://localhost:${HOST_PORT}/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -d "{ + \"model\": \"${MODEL_ALIAS}\", + \"messages\": [ + { \"role\": \"system\", \"content\": \"Du bist ein strenger Code-Reviewer.\" }, + { \"role\": \"user\", \"content\": \"Antworte nur mit dem Wort: bereit\" } + ], + \"max_tokens\": 8, + \"temperature\": 0.0, + \"stream\": false + }" + +echo +echo "[*] Server bereit auf http://0.0.0.0:${HOST_PORT}" +echo "[*] Stoppen mit: docker rm -f ${CONTAINER_NAME}" From 6c128f5cf6da07ceda6e47dae3bd66cc26510533 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Tue, 19 May 2026 18:51:07 +0200 Subject: [PATCH 02/32] fix: install.sh kopiert statt Symlinks zu setzen --- install.sh | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/install.sh b/install.sh index 7149c48..72a334f 100755 --- a/install.sh +++ b/install.sh @@ -1,19 +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)" -# Extension symlink mkdir -p ~/.pi/agent/extensions -ln -sf "$REPO/pi-coder-judge-extension.ts" ~/.pi/agent/extensions/pi-coder-judge-extension.ts -echo "Symlink: ~/.pi/agent/extensions/pi-coder-judge-extension.ts -> $REPO/pi-coder-judge-extension.ts" -# models.json symlink (Backup wenn reguläre Datei vorhanden) -if [ -f ~/.pi/agent/models.json ] && [ ! -L ~/.pi/agent/models.json ]; then - cp ~/.pi/agent/models.json ~/.pi/agent/models.json.bak - echo "Backup: ~/.pi/agent/models.json.bak" -fi -ln -sf "$REPO/models.json" ~/.pi/agent/models.json -echo "Symlink: ~/.pi/agent/models.json -> $REPO/models.json" +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." From 59b16059ccc21129703c5417c54171bbc79197ef Mon Sep 17 00:00:00 2001 From: dschlueter Date: Tue, 19 May 2026 18:57:31 +0200 Subject: [PATCH 03/32] feat: stop-servers.sh, status.sh, README.md --- README.md | 104 ++++++++++++++++++++++++++++++++++++++++++++++++ status.sh | 34 ++++++++++++++++ stop-servers.sh | 14 +++++++ 3 files changed, 152 insertions(+) create mode 100644 README.md create mode 100755 status.sh create mode 100755 stop-servers.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..907f656 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# pi_coder — Automatisierter Coder/Judge-Workflow für pi agent + +Dieses Repository enthält die Konfiguration und Skripte für einen automatisierten +Coding-Workflow mit zwei lokalen LLaMA-Modellen: ein Coder-Modell und ein Judge-Modell, +gesteuert über [pi agent](https://github.com/earendil-works/pi). + +## Überblick + +``` +Nutzer gibt Auftrag + │ + ▼ + /coder → qwen3.5-coder (:8001) → Implementierung + git commit + │ + ▼ + /judge → qwen3.5-judge (:8002) → Review: PASS / FAIL + Blocker + │ + FAIL? ▼ + /fix → qwen3.5-coder (:8001) → Fixes + git commit + │ + PASS? ▼ + /shipit → qwen3.5-judge (:8002) → Finale Freigabe: SHIP / NO-SHIP + + /optimize = Coder→Judge→Fix-Schleife automatisch (bis PASS oder max. N Runden) +``` + +## Modelle + +| Rolle | Modell | Port | Container | +|---------|-------------------------------------------------|------|---------------------| +| Coder | Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS | 8001 | qwen36-27b-coder | +| Judge | Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS | 8002 | qwen36-27b-judge | + +Beide Modelle laufen als separate llama.cpp-Docker-Container auf GPU 1 und 2 (tensor-split 0.5/0.5). + +## Voraussetzungen + +- Docker mit NVIDIA-GPU-Support (`nvidia-container-toolkit`) +- GPU 1 und GPU 2 verfügbar (`nvidia-smi`) +- GGUF-Modell unter: `$HF_HOME/models/qwen3/Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS.gguf` + - Standard: `HF_HOME=/home/dschlueter/nvme2n1p7_home/huggingface` +- [pi agent](https://github.com/earendil-works/pi) installiert (`~/.pi/`) + +## Setup + +```bash +# 1. Extension und Modell-Config nach ~/.pi/agent/ deployen +./install.sh + +# 2. /reload in pi agent ausführen +``` + +Nach Ä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 +# Coder-Server starten (Port 8001) +./start-coder.sh + +# Judge-Server starten (Port 8002) +./start-judge.sh + +# Beide stoppen +./stop-servers.sh + +# Status beider Server prüfen +./status.sh +``` + +Umgebungsvariable für alternativen Modellpfad: +```bash +HF_HOME=/anderer/pfad ./start-coder.sh +``` + +## pi-Kommandos + +| Kommando | Beschreibung | +|---|---| +| `/coder ` | TASK.md anlegen, Implementierung starten (Coder-Modell) | +| `/judge [fokus]` | Code-Review gegen TASK.md + letzten Commit (Judge-Modell) | +| `/fix [hinweis]` | Judge-Kritik beheben, committen (Coder-Modell) | +| `/shipit` | Finale Freigabeprüfung (Judge-Modell) | +| `/optimize [--rounds N] [--with-doku]` | Vollautomatische Schleife bis PASS | +| `/patch <änderung>` | Gezielte Minimaländerung ohne vollständigen Review | +| `/quick_check [was]` | Schnelle Prüfung der letzten Änderung (OK/PROBLEM) | +| `/update_doku` | Code kommentieren + README.md + BEDIENUNGSANLEITUNG.md | +| `/new_project ` | Neues Projektverzeichnis + git init anlegen | + +## 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-coder.sh` | Docker-Container für Coder-Modell (Port 8001) starten | +| `start-judge.sh` | Docker-Container für Judge-Modell (Port 8002) starten | +| `stop-servers.sh` | Beide Container stoppen | +| `status.sh` | Laufstatus beider Server anzeigen | +| `install.sh` | Extension + models.json nach `~/.pi/agent/` kopieren | diff --git a/status.sh b/status.sh new file mode 100755 index 0000000..9eacbf7 --- /dev/null +++ b/status.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +check_server() { + local NAME="$1" + local PORT="$2" + local ALIAS="$3" + + printf "%-28s" "$NAME (Port $PORT):" + + # Docker-Status + if docker ps --format '{{.Names}}' | grep -q "^${NAME}\$"; then + printf " Container=\033[32mRUNNING\033[0m" + elif docker ps -a --format '{{.Names}}' | grep -q "^${NAME}\$"; then + printf " Container=\033[33mSTOPPED\033[0m" + else + printf " Container=\033[31mNOT FOUND\033[0m" + echo + return + fi + + # HTTP-Erreichbarkeit + if curl -s --max-time 3 "http://localhost:${PORT}/health" >/dev/null 2>&1 || \ + curl -s --max-time 3 "http://localhost:${PORT}/v1/models" >/dev/null 2>&1; then + printf " HTTP=\033[32mOK\033[0m" + else + printf " HTTP=\033[31mNOT READY\033[0m" + fi + + echo +} + +echo "=== LLaMA-Server Status ===" +check_server "qwen36-27b-coder" 8001 "qwen3.5-coder" +check_server "qwen36-27b-judge" 8002 "qwen3.5-judge" diff --git a/stop-servers.sh b/stop-servers.sh new file mode 100755 index 0000000..1d25229 --- /dev/null +++ b/stop-servers.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +CODER="qwen36-27b-coder" +JUDGE="qwen36-27b-judge" + +for NAME in "$CODER" "$JUDGE"; do + if docker ps -a --format '{{.Names}}' | grep -q "^${NAME}\$"; then + docker rm -f "$NAME" >/dev/null + echo "[*] Gestoppt: $NAME" + else + echo "[-] Nicht gefunden: $NAME" + fi +done From da961e65f6a0be92813cf9ed7ba774f9cbeca7f4 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Tue, 19 May 2026 19:05:29 +0200 Subject: [PATCH 04/32] feat: start-servers.sh startet Coder und Judge parallel --- start-servers.sh | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100755 start-servers.sh diff --git a/start-servers.sh b/start-servers.sh new file mode 100755 index 0000000..c6f424f --- /dev/null +++ b/start-servers.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +LOG_CODER=$(mktemp /tmp/coder_XXXXXX.log) +LOG_JUDGE=$(mktemp /tmp/judge_XXXXXX.log) + +echo "[*] Starte beide Server parallel ..." +bash "$SCRIPT_DIR/start-coder.sh" > "$LOG_CODER" 2>&1 & +PID_CODER=$! +bash "$SCRIPT_DIR/start-judge.sh" > "$LOG_JUDGE" 2>&1 & +PID_JUDGE=$! + +wait_result() { + local PID="$1" NAME="$2" LOG="$3" + if wait "$PID"; then + echo "[✓] $NAME bereit" + else + echo "[✗] $NAME fehlgeschlagen — Log:" + cat "$LOG" + return 1 + fi +} + +RC=0 +wait_result "$PID_CODER" "Coder (:8001)" "$LOG_CODER" || RC=1 +wait_result "$PID_JUDGE" "Judge (:8002)" "$LOG_JUDGE" || RC=1 + +rm -f "$LOG_CODER" "$LOG_JUDGE" +exit $RC From 8dddd0eabd464ea25f570b4c53e0666d612b3674 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Tue, 19 May 2026 19:11:15 +0200 Subject: [PATCH 05/32] =?UTF-8?q?docs:=20start-servers.sh=20in=20README=20?= =?UTF-8?q?erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 907f656..774318d 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,12 @@ Nach Änderungen an `pi-coder-judge-extension.ts` oder `models.json`: ## Server starten / stoppen / status ```bash -# Coder-Server starten (Port 8001) -./start-coder.sh +# Beide Server parallel starten (empfohlen) +./start-servers.sh -# Judge-Server starten (Port 8002) -./start-judge.sh +# Einzeln starten (z.B. nach Absturz eines Servers) +./start-coder.sh # Port 8001 +./start-judge.sh # Port 8002 # Beide stoppen ./stop-servers.sh @@ -72,9 +73,13 @@ Nach Änderungen an `pi-coder-judge-extension.ts` oder `models.json`: ./status.sh ``` +`start-servers.sh` startet beide Container gleichzeitig und wartet bis beide +HTTP-ready sind — schneller als sequenziell. Logs werden getrennt gesammelt +und nur bei Fehler ausgegeben. + Umgebungsvariable für alternativen Modellpfad: ```bash -HF_HOME=/anderer/pfad ./start-coder.sh +HF_HOME=/anderer/pfad ./start-servers.sh ``` ## pi-Kommandos @@ -97,8 +102,9 @@ HF_HOME=/anderer/pfad ./start-coder.sh |---|---| | `pi-coder-judge-extension.ts` | pi agent Extension (Kommandos, Tools, Hooks) | | `models.json` | Provider- und Modell-Konfiguration für pi agent | -| `start-coder.sh` | Docker-Container für Coder-Modell (Port 8001) starten | -| `start-judge.sh` | Docker-Container für Judge-Modell (Port 8002) starten | +| `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 | From 120f223c9b866fe732983f3029b699aec4e9e6d0 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Tue, 19 May 2026 21:22:02 +0200 Subject: [PATCH 06/32] docs: umfassende README + BEDIENUNGSANLEITUNG mit llama.cpp-Parametern und Beispielen --- BEDIENUNGSANLEITUNG.md | 678 +++++++++++++++++++++++++++++++++++++++++ README.md | 241 +++++++++++++-- 2 files changed, 889 insertions(+), 30 deletions(-) create mode 100644 BEDIENUNGSANLEITUNG.md diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md new file mode 100644 index 0000000..baede8c --- /dev/null +++ b/BEDIENUNGSANLEITUNG.md @@ -0,0 +1,678 @@ +# 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) +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. [TASK.md verstehen und nutzen](#9-taskmd-verstehen-und-nutzen) +10. [Typische Anwendungsfälle](#10-typische-anwendungsfälle) +11. [Fehlermeldungen und Lösungen](#11-fehlermeldungen-und-lösungen) + +--- + +## 1. Konzept: Coder und Judge + +pi_coder verwendet zwei Rollen: + +**Coder** (Port 8001): Schreibt und repariert Code. Liest die Aufgabe aus `TASK.md`, +implementiert sie, führt Tests aus und erstellt Git-Commits. + +**Judge** (Port 8002): Überprüft den Code mit dem Blick eines skeptischen Senior-Entwicklers. +Prüft Korrektheit, Robustheit, Randfälle, Sicherheit und Produktionsreife. Gibt ein Urteil: +- `PASS` — Code ist in Ordnung +- `PASS WITH CONCERNS` — grundsätzlich akzeptabel, aber mit Anmerkungen +- `FAIL` — enthält Blocker, die behoben werden müssen + +Der Grundgedanke: Coder und Judge haben keine „Höflichkeitsschranke" zueinander — +der Judge kritisiert direkt und konkret, der Coder repariert ohne Widerspruch. + +--- + +## 2. Vorbereitung + +### Server starten + +```bash +cd ~/pi_coder +./start-servers.sh +``` + +Ausgabe bei Erfolg: +``` +[*] Starte beide Server parallel ... +[✓] Coder (:8001) bereit +[✓] Judge (:8002) bereit +``` + +Dauer: 1–3 Minuten (Modell wird in GPU-VRAM geladen). + +### Status prüfen + +```bash +./status.sh +``` + +``` +=== LLaMA-Server Status === +qwen36-27b-coder (Port 8001): Container=RUNNING HTTP=OK +qwen36-27b-judge (Port 8002): Container=RUNNING HTTP=OK +``` + +### pi agent öffnen + +pi agent im Projektverzeichnis starten — das ist das Verzeichnis, in dem dein Code liegt, +**nicht** `~/pi_coder`: + +```bash +cd ~/MeinProjekt +pi +``` + +--- + +## 3. Server starten und stoppen + +### Beide starten (empfohlen) + +```bash +./start-servers.sh +``` + +### Einzelnen Server neu starten + +Z.B. wenn nur der Coder-Server abgestürzt ist: + +```bash +./start-coder.sh +``` + +### Beide stoppen + +```bash +./stop-servers.sh +``` + +### Alternativer Modellpfad + +Falls die GGUF-Datei an einem anderen Ort liegt: + +```bash +HF_HOME=/mnt/daten/huggingface ./start-servers.sh +``` + +--- + +## 4. Neues Projekt anlegen + +### Kommando + +``` +/new_project +``` + +### Beispiel + +``` +/new_project ~/Python_Programs/mein_tool +``` + +Was passiert: +- Verzeichnis `~/Python_Programs/mein_tool` wird angelegt +- `git init` wird ausgeführt +- `.gitignore` wird mit Standardeinträgen angelegt und committed + +**Wichtig:** pi agent wechselt **nicht automatisch** in das neue Verzeichnis — +die Session bleibt im aktuellen Verzeichnis. Nach dem Anlegen: + +```bash +cd ~/Python_Programs/mein_tool +pi +``` + +Dann kannst du `/coder` oder `/optimize` mit dem neuen Projekt verwenden. + +--- + +## 5. Manueller Workflow + +Der manuelle Workflow gibt dir volle Kontrolle über jeden Schritt. + +### Schritt 1: /coder — Aufgabe übergeben + +``` +/coder +``` + +Der Coder: +1. Legt `TASK.md` im aktuellen Verzeichnis an (oder hängt an bestehende an) +2. Liest `TASK.md` und implementiert den Auftrag +3. Führt Tests oder Build-Checks aus +4. Erstellt einen Git-Commit + +**Beispiel:** + +``` +/coder Schreibe ein Python-Kommandozeilenprogramm 'textcount'. Es soll eine Textdatei als Argument nehmen und folgendes ausgeben: Anzahl Zeichen, Wörter, Zeilen und die 5 häufigsten Wörter (ohne Stoppwörter). +``` + +Typische Ausgabe des Coders: +``` +Implementierung abgeschlossen. +- src/textcount.py erstellt (Hauptprogramm) +- tests/test_textcount.py erstellt (Unit-Tests) +- requirements.txt angelegt (keine externen Abhängigkeiten) +- Alle 8 Tests bestanden +- Commit: feat: implement textcount CLI tool +Risiken: Stoppwortliste nur Deutsch/Englisch, keine Konfigurations-Option. +``` + +### Schritt 2: /judge — Code überprüfen lassen + +``` +/judge +``` + +Optionaler Fokus: +``` +/judge Besonderes Augenmerk auf Fehlerbehandlung und Edge Cases +``` + +Der Judge: +1. Liest `TASK.md` und prüft ob alle Anforderungen umgesetzt sind +2. Analysiert `git show HEAD` +3. Führt Tests aus +4. Gibt ein strukturiertes Urteil aus + +**Beispiel-Ausgabe PASS:** +``` +Urteil: PASS WITH CONCERNS + +Blocker: keine + +Major: +- Stoppwortliste ist hardcoded; große Projekte erwarten --stopwords-file Option + +Minor: +- Keine --version Flag +- Fehlermeldung bei nicht-existenter Datei gibt keinen Exit-Code 1 zurück + +Fehlende Tests: +- Test für leere Datei fehlt +- Test für Datei mit nur Leerzeichen fehlt + +Produktionsrisiken: +- Bei sehr großen Dateien (>1 GB) wird alles in den RAM geladen + +Konkrete Fix-Aufträge: +1. exit(1) bei FileNotFoundError +2. Test für leere Eingabedatei +``` + +**Beispiel-Ausgabe FAIL:** +``` +Urteil: FAIL + +Blocker: +- textcount.py importiert 'collections.Counter' aber das ist nicht installiert + (Counter ist stdlib, aber der Import-Fehler tritt bei Python < 3.9 auf) +- ./textcount.py existiert nicht — tests/test_textcount.py schlägt komplett fehl + +Major: ... +``` + +### Schritt 3: /fix — Kritik beheben + +``` +/fix +``` + +Optionaler Hinweis: +``` +/fix Den Major-Punkt mit der Stoppwortliste kannst du weglassen, das ist kein Produktionsprojekt +``` + +Der Coder arbeitet die Judge-Kritik ab (Blocker zuerst, dann Major, dann Minor) +und erstellt einen neuen Commit. + +### Schritt 4: /shipit — Finale Freigabe + +``` +/shipit +``` + +Der Judge gibt ein finales Urteil: +- `SHIP` — bereit für Produktion +- `NO-SHIP` — noch Probleme offen + +**Beispiel:** +``` +Urteil: SHIP + +Letzte Blocker: keine + +Restrisiken: +- Kein Streaming für sehr große Dateien (dokumentiert in README) + +Empfohlene Sofortmaßnahmen: keine +``` + +--- + +## 6. Automatischer Workflow: /optimize + +`/optimize` führt den gesamten Coder→Judge→Fix-Zyklus automatisch durch. + +### Syntax + +``` +/optimize [--rounds N] [--with-doku] +``` + +- `--rounds N` — maximale Anzahl Runden (Standard: 3) +- `--with-doku` — nach SHIP automatisch `/update_doku` ausführen + +### 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/3: Judge prüft... + → Urteil: FAIL (2 Blocker) +Phase 3: Runde 1/3: Coder fixt... +Phase 4: Runde 2/3: Judge prüft... + → Urteil: PASS WITH CONCERNS +✓ PASS WITH CONCERNS nach Runde 2 +Finale ShipIt-Prüfung... + → SHIP +``` + +### 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 + +### 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 + +``` +⚠ 3 Runden durchlaufen ohne PASS. Bitte manuell prüfen. +``` + +Dann: `/judge` und `/fix` manuell für gezielte Eingriffe. + +--- + +## 7. Kleine Änderungen: /patch und /quick_check + +Für minimale Korrekturen — kein voller Review-Zyklus, keine TASK.md-Änderungen. + +### /patch — kleine Änderung umsetzen + +``` +/patch +``` + +Der Coder ändert **ausschließlich** das Beschriebene, prüft ob es noch kompiliert/startet +und erstellt einen Commit. + +**Beispiele:** + +``` +/patch Mindestpasswortlänge von 4 auf 8 Zeichen erhöhen +``` + +``` +/patch Fehlermeldung bei ungültigem Argument von stderr auf stdout umleiten +``` + +``` +/patch Versionsnummer in Cargo.toml von 0.1.0 auf 0.2.0 erhöhen +``` + +``` +/patch Die Funktion parse_args() soll bei fehlendem --input-Argument eine sinnvolle Hilfsnachricht ausgeben statt zu paniken +``` + +### /quick_check — Änderung schnell prüfen lassen + +``` +/quick_check [was geprüft werden soll] +``` + +Der Judge schaut sich `git show HEAD` an und gibt nur `OK` oder `PROBLEM` zurück. + +**Beispiele:** + +``` +/quick_check +``` + +``` +/quick_check Prüfe ob die Mindestlängen-Änderung korrekt umgesetzt ist und keine Randfälle fehlen +``` + +**Typische Ausgaben:** + +``` +Urteil: OK + +Die Änderung in src/main.rs Zeile 47 ist korrekt. Mindestlänge wird jetzt +sowohl bei --length als auch im Standardfall geprüft. +``` + +``` +Urteil: PROBLEM + +src/lib.rs Zeile 23: Der neue Mindestwert von 8 wird nur bei --length geprüft, +nicht beim Standardwert (16). Wenn jemand --length 6 übergibt, schlägt die +Validierung korrekt fehl, aber der Standardfall ist nicht abgedeckt. +Fix: Validierung in die Funktion generate_password() verschieben statt in parse_args(). +``` + +### Typischer /patch + /quick_check Workflow + +``` +/patch Timeout bei HTTP-Requests von 30 auf 10 Sekunden setzen +``` +*(Coder ändert, committet)* + +``` +/quick_check Prüfe ob der Timeout auch bei Retry-Versuchen korrekt gilt +``` +*(Judge gibt OK oder zeigt konkretes Problem)* + +--- + +## 8. Dokumentation generieren: /update_doku + +Nach Abschluss der Entwicklung (nach `/shipit` oder `/optimize`) erstellt `/update_doku` +drei Dinge automatisch: + +1. **Code-Kommentare** — erklärt das WARUM in den Quelldateien (Deutsch) +2. **README.md** — Entwicklerperspektive: Installation, Build, Verwendung +3. **BEDIENUNGSANLEITUNG.md** — Endnutzerperspektive: einfach, ohne Jargon + +``` +/update_doku +``` + +### Inkrementelles Update + +`/update_doku` merkt sich via Git-Tags welche Dateien seit dem letzten Lauf geändert wurden. +Nur geänderte Quelldateien werden neu kommentiert — unveränderte bleiben unangetastet. + +``` +Code-Kommentare: keine Änderungen seit letztem Lauf – übersprungen. +README.md: 2 Datei(en) geändert – wird geprüft +BEDIENUNGSANLEITUNG.md: 2 Datei(en) geändert – wird geprüft +``` + +### Zusammen mit /optimize + +``` +/optimize Implementiere Feature X --with-doku +``` + +Führt nach SHIP automatisch `/update_doku` aus. + +--- + +## 9. 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` + +--- + +## 10. 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 +``` + +### 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 +``` + +--- + +## 11. 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. +``` + +### "Drei 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 + +### Server läuft, aber pi wechselt nicht das Modell + +**Ursache:** `models.json` wurde nach einer Änderung nicht neu deployt. + +**Lösung:** +```bash +cd ~/pi_coder +./install.sh +# Dann /reload in pi agent +``` + +### "Neues Projekt" wechselt nicht das Verzeichnis + +Das ist gewollt — pi-Sessions sind an ihr Startverzeichnis gebunden. +Nach `/new_project ` im Terminal: +```bash +cd +pi +``` diff --git a/README.md b/README.md index 774318d..a09553a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Dieses Repository enthält die Konfiguration und Skripte für einen automatisier 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 ``` @@ -24,45 +26,77 @@ Nutzer gibt Auftrag /optimize = Coder→Judge→Fix-Schleife automatisch (bis PASS oder max. N Runden) ``` +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 | -|---------|-------------------------------------------------|------|---------------------| -| Coder | Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS | 8001 | qwen36-27b-coder | -| Judge | Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS | 8002 | qwen36-27b-judge | +| 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 Modelle laufen als separate llama.cpp-Docker-Container auf GPU 1 und 2 (tensor-split 0.5/0.5). +Beide Container verwenden dasselbe GGUF-Datei, aber mit unterschiedlichen +Serverparametern (Kontext, Temperatur, Parallelität). + +--- ## Voraussetzungen -- Docker mit NVIDIA-GPU-Support (`nvidia-container-toolkit`) -- GPU 1 und GPU 2 verfügbar (`nvidia-smi`) -- GGUF-Modell unter: `$HF_HOME/models/qwen3/Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS.gguf` - - Standard: `HF_HOME=/home/dschlueter/nvme2n1p7_home/huggingface` +- 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/`) -## Setup +--- + +## Installation ```bash -# 1. Extension und Modell-Config nach ~/.pi/agent/ deployen +# 1. Repository klonen +git clone ~/pi_coder +cd ~/pi_coder + +# 2. Extension und Modell-Config nach ~/.pi/agent/ deployen ./install.sh -# 2. /reload in pi agent ausführen +# 3. pi agent neu laden (in der pi-Oberfläche) +# /reload + +# 4. Server starten +./start-servers.sh ``` -Nach Änderungen an `pi-coder-judge-extension.ts` oder `models.json`: +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) +# Beide Server parallel starten (empfohlen — dauert 1–3 Minuten) ./start-servers.sh -# Einzeln starten (z.B. nach Absturz eines Servers) +# Einzeln starten (z.B. nur einen neu starten) ./start-coder.sh # Port 8001 ./start-judge.sh # Port 8002 @@ -74,27 +108,156 @@ Nach Änderungen an `pi-coder-judge-extension.ts` oder `models.json`: ``` `start-servers.sh` startet beide Container gleichzeitig und wartet bis beide -HTTP-ready sind — schneller als sequenziell. Logs werden getrennt gesammelt -und nur bei Fehler ausgegeben. +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 q8_0` | — | KV-Cache Keys in 8-Bit quantisiert. Spart ~50 % VRAM gegenüber fp16, minimaler Qualitätsverlust. | +| `--cache-type-v q8_0` | — | KV-Cache Values ebenfalls 8-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 131072` | 128K Tokens | Großes Kontextfenster: gesamte Codebasis + Gesprächsverlauf passt rein. **Hoher VRAM-Bedarf.** Reduziere auf `65536` wenn VRAM knapp. | +| `-n 16384` | 16K Tokens | Maximale Ausgabelänge pro Anfrage. Für Kommentieraufgaben (`/update_doku`) nötig. | +| `--temp 0.2` | — | Niedrige Temperatur: deterministisch, konsistenter Code. Erhöhe auf `0.4–0.6` für kreativere Lösungsansätze. | +| `--top-p 0.95` | — | Nucleus Sampling: 95 % der Wahrscheinlichkeitsmasse. Passend zu temp 0.2. | +| `--batch-size 1024` | — | Prompt-Verarbeitungs-Batch. Größer = schnelleres Einlesen langer Dateien. | +| `--ubatch-size 512` | — | Micro-Batch für GPU-Kernel. Muss ≤ batch-size sein. | +| `--parallel 2` | — | 2 gleichzeitige Request-Slots. Nützlich wenn pi agent schnell Folgeanfragen schickt. | + +### Judge-Server (Port 8002) — optimiert für Reviews + +| Parameter | Wert | Erklärung / Wirkung | +|---|---|---| +| `-c 65536` | 64K Tokens | Mittleres Kontextfenster: reicht für Code-Review des letzten Commits + Konversationshistorie. | +| `-n 8192` | 8K Tokens | Reviews müssen nicht länger sein. Spart Inferenz-Zeit. | +| `--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`: -Umgebungsvariable für alternativen Modellpfad: ```bash -HF_HOME=/anderer/pfad ./start-servers.sh +# 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 ``` -## pi-Kommandos +### VRAM-Abschätzung für das 27B IQ4_XS-Modell -| Kommando | Beschreibung | +| Komponente | Größe (ca.) | |---|---| -| `/coder ` | TASK.md anlegen, Implementierung starten (Coder-Modell) | -| `/judge [fokus]` | Code-Review gegen TASK.md + letzten Commit (Judge-Modell) | -| `/fix [hinweis]` | Judge-Kritik beheben, committen (Coder-Modell) | -| `/shipit` | Finale Freigabeprüfung (Judge-Modell) | -| `/optimize [--rounds N] [--with-doku]` | Vollautomatische Schleife bis PASS | -| `/patch <änderung>` | Gezielte Minimaländerung ohne vollständigen Review | -| `/quick_check [was]` | Schnelle Prüfung der letzten Änderung (OK/PROBLEM) | -| `/update_doku` | Code kommentieren + README.md + BEDIENUNGSANLEITUNG.md | -| `/new_project ` | Neues Projektverzeichnis + git init anlegen | +| 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 65536 +``` + +Bei einer **16-GB-GPU** ist die Modellgröße allein schon grenzwertig. +Entweder ein kleineres Modell verwenden oder die Quantisierung weiter erhöhen (IQ3_XS, Q4_K_M). + +### Beide Server auf einer GPU betreiben + +Technisch möglich, aber beide Server laden das Modell gleichzeitig → doppelter VRAM-Bedarf. +Auf einer 24-GB-GPU daher **nicht empfohlen**. Alternativen: + +- Nur einen Server gleichzeitig starten (manuell umschalten) +- Kleinere Quantisierung wählen (IQ3_XS: ~11 GB) +- `ollama` als Alternative — lädt Modelle bei Bedarf und entlädt sie wieder + +--- + +## Parameter-Tuning-Guide + +### Temperatur (`--temp`) + +| Wert | Eignung | +|---|---| +| `0.0–0.1` | Maximale Reproduzierbarkeit. Gut für Judge/Review. | +| `0.1–0.3` | Guter Kompromiss für Coding. **Empfohlen für Coder.** | +| `0.4–0.6` | Kreativere Lösungen, mehr Varianz. Sinnvoll für Prototyping. | +| `0.7–1.0` | Kreativschreiben, Brainstorming. Für Coding meist zu viel Rauschen. | + +### Kontextgröße (`-c`) + +Je größer der Kontext, desto mehr VRAM braucht der KV-Cache. +Faustregel: KV-Cache ≈ `context_size × layers × head_dim × 2 × bytes_per_element`. +Bei q8_0 (1 Byte/Element) und Qwen3-27B (28 Schichten, 128 Head-Dim, 32 Heads): +KV-Cache ≈ `context_size × 28 × 128 × 32 × 2 × 1 Byte ≈ context_size × 0,23 MB` + +| Kontext | KV-Cache (q8_0) | Empfehlung | +|---|---|---| +| 32 768 | ~7,5 GB | 1 × 24-GB-GPU | +| 65 536 | ~15 GB | 2 × 16-GB-GPU | +| 131 072 | ~30 GB | 2 × 24-GB-GPU | + +### KV-Cache-Quantisierung + +| `--cache-type-k/v` | VRAM | Qualität | +|---|---|---| +| `f16` | 100 % (Basis) | Referenz | +| `q8_0` | ~50 % | Kaum merklich schlechter — **empfohlen** | +| `q4_0` | ~25 % | Sichtbarer Qualitätsverlust bei langen Kontexten | + +### 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 @@ -108,3 +271,21 @@ HF_HOME=/anderer/pfad ./start-servers.sh | `stop-servers.sh` | Beide Container stoppen | | `status.sh` | Laufstatus beider Server anzeigen | | `install.sh` | Extension + models.json nach `~/.pi/agent/` kopieren | + +--- + +## pi-Kommandos (Kurzübersicht) + +| Kommando | Modell | Beschreibung | +|---|---|---| +| `/coder ` | Coder | TASK.md anlegen, Implementierung starten | +| `/judge [fokus]` | Judge | Code-Review gegen TASK.md + letzten Commit | +| `/fix [hinweis]` | Coder | Judge-Kritik beheben, committen | +| `/shipit` | Judge | Finale Freigabeprüfung | +| `/optimize [--rounds N] [--with-doku]` | beide | Vollautomatische Schleife bis PASS | +| `/patch <änderung>` | Coder | Gezielte Minimaländerung ohne Review | +| `/quick_check [was]` | Judge | Schnelle Prüfung der letzten Änderung | +| `/update_doku` | Coder | Code kommentieren + README + Bedienungsanleitung | +| `/new_project ` | — | Neues Projektverzeichnis + git init | + +Ausführliche Beschreibung aller Kommandos mit Beispielen: siehe **BEDIENUNGSANLEITUNG.md**. From 1da712f0b8c94c3059498d1b01c43438ef051f2e Mon Sep 17 00:00:00 2001 From: dschlueter Date: Wed, 20 May 2026 01:20:07 +0200 Subject: [PATCH 07/32] =?UTF-8?q?fix:=20Judge-Kontext=20auf=20131072=20erh?= =?UTF-8?q?=C3=B6ht=20(war=2065536,=20zu=20klein=20bei=20langen=20Optimize?= =?UTF-8?q?-Runden)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- start-judge.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a09553a..f10fa14 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ und dann neu gestartet — ein laufender Inference-Request wird dabei abgebroche | Parameter | Wert | Erklärung / Wirkung | |---|---|---| -| `-c 65536` | 64K Tokens | Mittleres Kontextfenster: reicht für Code-Review des letzten Commits + Konversationshistorie. | +| `-c 131072` | 128K Tokens | Großes Kontextfenster: nötig bei langen /optimize-Runden, wo der Gesprächsverlauf stark anwächst. | | `-n 8192` | 8K Tokens | Reviews müssen nicht länger sein. Spart Inferenz-Zeit. | | `--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. | @@ -201,7 +201,7 @@ Bei einer **24-GB-GPU** ist nur ein Server gleichzeitig sinnvoll betreibbar: -n 8192 # statt 16384 # Judge — Kontext reduzieren --c 32768 # statt 65536 +-c 32768 # statt 131072 ``` Bei einer **16-GB-GPU** ist die Modellgröße allein schon grenzwertig. diff --git a/start-judge.sh b/start-judge.sh index 4d78b15..af20ed8 100755 --- a/start-judge.sh +++ b/start-judge.sh @@ -31,7 +31,7 @@ docker run -d \ "$IMAGE" \ -m "/hf_home/${MODEL_REL_PATH}" \ --alias "${MODEL_ALIAS}" \ - -c 65536 \ + -c 131072 \ -n 8192 \ --jinja \ --no-context-shift \ From aa00a8282e232bce114d6f3675637c572c2915a0 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Wed, 20 May 2026 01:27:52 +0200 Subject: [PATCH 08/32] fix: Judge contextWindow in models.json auf 131072 korrigiert --- models.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models.json b/models.json index aec20e6..35d6871 100644 --- a/models.json +++ b/models.json @@ -82,7 +82,7 @@ "name": "Qwen3.6 27B Judge (llama.cpp :8002)", "reasoning": true, "input": ["text"], - "contextWindow": 65536, + "contextWindow": 131072, "maxTokens": 8192, "cost": { "input": 0, From 7cb299ff66f5a0c11f3268cb4e449a26b1a60ac1 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Wed, 20 May 2026 01:42:26 +0200 Subject: [PATCH 09/32] =?UTF-8?q?feat:=20/optimize=20--continue=20=C3=BCbe?= =?UTF-8?q?rspringt=20Implementierungsphase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BEDIENUNGSANLEITUNG.md | 25 +++++++++++++- README.md | 1 + llama_cpp_parameter_uebersicht_2xRTX_3090.pdf | Bin 0 -> 58586 bytes llama_cpp_parameter_uebersicht_RTX_2080TI.pdf | Bin 0 -> 57746 bytes pi-coder-judge-extension.ts | 31 ++++++++++-------- 5 files changed, 43 insertions(+), 14 deletions(-) create mode 100644 llama_cpp_parameter_uebersicht_2xRTX_3090.pdf create mode 100644 llama_cpp_parameter_uebersicht_RTX_2080TI.pdf diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index baede8c..9279e55 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -277,11 +277,14 @@ Empfohlene Sofortmaßnahmen: keine ### Syntax ``` -/optimize [--rounds N] [--with-doku] +/optimize [--rounds N] [--with-doku] [--continue] ``` - `--rounds N` — maximale Anzahl Runden (Standard: 3) - `--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. ### Beispiel: einfacher Auftrag @@ -319,6 +322,26 @@ Nach SHIP werden automatisch ausgeführt: 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: diff --git a/README.md b/README.md index f10fa14..f3aa255 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,7 @@ wenn pi agent Folgeanfragen schnell hintereinander schickt. | `/fix [hinweis]` | Coder | Judge-Kritik beheben, committen | | `/shipit` | Judge | Finale Freigabeprüfung | | `/optimize [--rounds N] [--with-doku]` | beide | Vollautomatische Schleife bis PASS | +| `/optimize --continue [--rounds N] [--with-doku]` | beide | Judge→Fix-Schleife ab aktuellem Stand (überspringt Implementierung) | | `/patch <änderung>` | Coder | Gezielte Minimaländerung ohne Review | | `/quick_check [was]` | Judge | Schnelle Prüfung der letzten Änderung | | `/update_doku` | Coder | Code kommentieren + README + Bedienungsanleitung | diff --git a/llama_cpp_parameter_uebersicht_2xRTX_3090.pdf b/llama_cpp_parameter_uebersicht_2xRTX_3090.pdf new file mode 100644 index 0000000000000000000000000000000000000000..470e98649e61fc245397888ca6ec8735c3a96abb GIT binary patch literal 58586 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 EZ@_v?AOHXW literal 0 HcmV?d00001 diff --git a/llama_cpp_parameter_uebersicht_RTX_2080TI.pdf b/llama_cpp_parameter_uebersicht_RTX_2080TI.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6b4253b07450ff02608d8007a1dae2d0ea16a28f GIT binary patch literal 57746 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�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 [--rounds N] [--with-doku]", + description: "Coder→Judge→Fix-Schleife bis PASS + optional Doku. /optimize [--rounds N] [--with-doku] [--continue]", handler: async function (args: string, ctx: ExtensionCommandContext) { const roundsMatch = (args || "").match(/--rounds\s+(\d+)/); const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 3; const withDoku = /--with-doku/.test(args || ""); + const continueMode = /--continue/.test(args || ""); const task = (args || "") .replace(/--rounds\s+\d+/, "") .replace(/--with-doku/, "") + .replace(/--continue/, "") .trim(); - if (!task) { - ctx.ui.notify("Benutzung: /optimize [--rounds N] [--with-doku]", "error"); + if (!continueMode && !task) { + ctx.ui.notify("Benutzung: /optimize [--rounds N] [--with-doku] [--continue]", "error"); return; } - // TASK.md anlegen - await writeTaskMd(pi, ctx, task); - - ctx.ui.setStatus("optimize", `Starte Optimierung (max ${maxRounds} Runden)…`); - - // Phase 1: Initiale Implementierung - ctx.ui.setStatus("optimize", "Phase 1: Coder implementiert…"); - await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); - await sendAndWait(pi, ctx, coderKickoff(task)); - await tickTaskMdStatus(pi, ctx, "Implementierung"); + if (continueMode) { + // --continue: Implementierungsphase überspringen, direkt in Judge→Fix-Schleife + ctx.ui.setStatus("optimize", `Setze fort (max ${maxRounds} Runden Judge→Fix)…`); + ctx.ui.notify("--continue: Überspringe Implementierung, starte direkt mit Judge-Prüfung.", "info"); + } else { + // TASK.md anlegen und Implementierung starten + await writeTaskMd(pi, ctx, task); + ctx.ui.setStatus("optimize", `Starte Optimierung (max ${maxRounds} Runden)…`); + ctx.ui.setStatus("optimize", "Phase 1: Coder implementiert…"); + await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); + await sendAndWait(pi, ctx, coderKickoff(task)); + await tickTaskMdStatus(pi, ctx, "Implementierung"); + } let lastBlockers = ""; let verdict = ""; From b19c189e2e4b1a2851b6122b4de99eeb3b32c328 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Wed, 20 May 2026 02:08:09 +0200 Subject: [PATCH 10/32] =?UTF-8?q?feat:=20prominente=20Abschluss-Notificati?= =?UTF-8?q?ons=20+=20Widget-Update=20f=C3=BCr=20/optimize=20und=20/shipit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pi-coder-judge-extension.ts | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/pi-coder-judge-extension.ts b/pi-coder-judge-extension.ts index 7323e49..6c91bb5 100644 --- a/pi-coder-judge-extension.ts +++ b/pi-coder-judge-extension.ts @@ -456,6 +456,28 @@ async function runUpdateDoku(pi: ExtensionAPI, ctx: ExtensionCommandContext): Pr ctx.ui.notify("Dokumentations-Phase abgeschlossen. Commit angelegt.", "info"); } +// Prominente Abschluss-Notification + Widget-Update mit Uhrzeit und Ergebnis. +function finalNotify( + ctx: ExtensionCommandContext, + verdict: string, + detail: string +): void { + const timestamp = new Date().toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }); + const level = verdict.includes("SHIP") && !verdict.includes("NO-SHIP") ? "warning" + : verdict.includes("NO-SHIP") ? "error" + : verdict.includes("⚠") ? "warning" + : "info"; + ctx.ui.notify(`${verdict}: ${detail}`, level); + ctx.ui.setWidget("coder-judge", [ + `Letzter Lauf: ${verdict} — ${detail} (${timestamp})`, + "─────────────────────────────────────────", + "Workflow: /coder | /judge | /fix | /shipit", + "Auto-Loop: /optimize [--rounds N] [--with-doku] [--continue]", + "Kleine Änderung: /patch <änderung> → /quick_check [was]", + "Finale Doku: /update_doku | Neues Projekt: /new_project ", + ]); +} + // ── Extension ──────────────────────────────────────────────────────────────── export default function (pi: ExtensionAPI) { @@ -540,6 +562,7 @@ export default function (pi: ExtensionAPI) { description: "Finale Freigabe gegen TASK.md + git log → qwen3.5-judge (:8002).", handler: async function (args: string, ctx: ExtensionCommandContext) { await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge"); + ctx.ui.notify("Judge prüft finale Freigabe — Ergebnis erscheint im Chat (SHIP / NO-SHIP)", "info"); pi.sendUserMessage(shipitPrompt(args || "")); } }); @@ -593,7 +616,6 @@ export default function (pi: ExtensionAPI) { if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") { await tickTaskMdStatus(pi, ctx, "Review bestanden (PASS)"); ctx.ui.setStatus("optimize", `✓ ${verdict} nach Runde ${round}`); - ctx.ui.notify(`Optimierung abgeschlossen: ${verdict} nach ${round} Runde(n)`, "info"); break; } @@ -601,17 +623,14 @@ export default function (pi: ExtensionAPI) { const currentBlockers = parseBlockers(judgeText); if (currentBlockers && currentBlockers === lastBlockers) { ctx.ui.setStatus("optimize", "⚠ Schleife: gleicher Blocker – manuelle Intervention nötig"); - ctx.ui.notify( - "Derselbe Blocker tritt erneut auf – Schleife abgebrochen. Bitte manuell prüfen.", - "warning" - ); + finalNotify(ctx, "⚠ Schleife", "Gleicher Blocker zweimal – manuelle Intervention nötig"); return; } lastBlockers = currentBlockers; if (round === maxRounds) { ctx.ui.setStatus("optimize", `⚠ Max. ${maxRounds} Runden ohne PASS`); - ctx.ui.notify(`${maxRounds} Runden durchlaufen ohne PASS. Bitte manuell prüfen.`, "warning"); + finalNotify(ctx, "⚠ Kein PASS", `${maxRounds} Runden ohne PASS – bitte /judge und /fix manuell`); return; } @@ -632,6 +651,7 @@ export default function (pi: ExtensionAPI) { if (shipVerdict === "SHIP") { ctx.ui.setStatus("optimize", "🚀 SHIP – produktionsreif"); + finalNotify(ctx, "🚀 SHIP", "Programm ist produktionsreif"); if (withDoku) { await runUpdateDoku(pi, ctx); } else { @@ -639,8 +659,10 @@ export default function (pi: ExtensionAPI) { } } else if (shipVerdict === "NO-SHIP") { ctx.ui.setStatus("optimize", "⛔ NO-SHIP – noch nicht bereit"); + finalNotify(ctx, "⛔ NO-SHIP", "Noch Blocker offen – bitte /judge und /fix manuell"); } else { ctx.ui.setStatus("optimize", "ShipIt abgeschlossen"); + finalNotify(ctx, "ShipIt", "Kein klares Urteil – Antwort im Chat prüfen"); } } } From 4a31535b76b77dfd10c06ca86aafc518f974916c Mon Sep 17 00:00:00 2001 From: dschlueter Date: Wed, 20 May 2026 20:02:20 +0200 Subject: [PATCH 11/32] feat: /plan, /cancel, /continue, /discard + Context 262144 + KV-Cache q4_0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- models.json | 4 +- pi-coder-judge-extension.ts | 134 +++++++++++++++++++++++++++++++----- start-coder.sh | 8 +-- start-judge.sh | 6 +- 4 files changed, 126 insertions(+), 26 deletions(-) diff --git a/models.json b/models.json index 35d6871..0e9052a 100644 --- a/models.json +++ b/models.json @@ -54,7 +54,7 @@ "name": "Qwen3.6 27B Coder (llama.cpp :8001)", "reasoning": true, "input": ["text"], - "contextWindow": 131072, + "contextWindow": 262144, "maxTokens": 16384, "cost": { "input": 0, @@ -82,7 +82,7 @@ "name": "Qwen3.6 27B Judge (llama.cpp :8002)", "reasoning": true, "input": ["text"], - "contextWindow": 131072, + "contextWindow": 262144, "maxTokens": 8192, "cost": { "input": 0, diff --git a/pi-coder-judge-extension.ts b/pi-coder-judge-extension.ts index 6c91bb5..597fc53 100644 --- a/pi-coder-judge-extension.ts +++ b/pi-coder-judge-extension.ts @@ -239,6 +239,37 @@ function bedienungsanleitungPromptIncremental(files: string[]): string { ].join("\n"); } +function planPrompt(task: string): string { + return [ + "Du bist ein erfahrener Software-Architekt im PLANUNGSMODUS.", + "", + "ABSOLUTE VERBOTE — du darfst NICHT:", + "- Dateien editieren, schreiben oder löschen (kein edit, write, apply_patch)", + "- Git-Commits durchführen", + "- Tests oder Skripte ausführen die Seiteneffekte haben", + "", + "ERLAUBT:", + "- Dateien lesen (read, cat, grep, find)", + "- Git-History lesen (git log, git show, git diff)", + "- PLAN.md anlegen oder überschreiben (das ist dein Ausgabe-Dokument)", + "", + "Analysiere den Auftrag gründlich und erstelle einen konkreten Implementierungsplan.", + "", + "Auftrag:", + task, + "", + "Struktur deiner Ausgabe:", + "1. IST-Analyse (relevante Dateien, Architektur, Abhängigkeiten)", + "2. Implementierungsplan (nummerierte Schritte, konkret und umsetzbar)", + "3. Kritische Entscheidungen (Alternativen + Empfehlung)", + "4. Risiken und offene Fragen", + "5. Geschätzte Komplexität: einfach / mittel / komplex", + "", + "Schreibe den vollständigen Plan in PLAN.md.", + "Schließe ab mit: 'Plan bereit. Starte Umsetzung mit /coder oder /optimize --continue'", + ].join("\n"); +} + // ── Hilfsfunktionen ───────────────────────────────────────────────────────── // Legt TASK.md neu an oder hängt einen Zusatzauftrag an. @@ -471,24 +502,30 @@ function finalNotify( ctx.ui.setWidget("coder-judge", [ `Letzter Lauf: ${verdict} — ${detail} (${timestamp})`, "─────────────────────────────────────────", - "Workflow: /coder | /judge | /fix | /shipit", + "Workflow: /coder | /judge | /fix | /shipit", "Auto-Loop: /optimize [--rounds N] [--with-doku] [--continue]", - "Kleine Änderung: /patch <änderung> → /quick_check [was]", - "Finale Doku: /update_doku | Neues Projekt: /new_project ", + "Planung: /plan → /coder | /optimize --continue | /discard", + "Patch: /patch <änderung> → /quick_check [was]", + "Doku: /update_doku | Neues Projekt: /new_project ", + "Abbruch: Escape (Generation laufend) | /cancel (Loop nach aktuellem Schritt)", + "Resume: /continue | Modell: auto (Coder→:8001, Judge→:8002)", ]); } // ── Extension ──────────────────────────────────────────────────────────────── +let cancelRequested = false; + export default function (pi: ExtensionAPI) { pi.on("session_start", async function (_event, ctx) { ctx.ui.setWidget("coder-judge", [ - "Workflow: /coder | /judge | /fix | /shipit", - "Auto-Loop: /optimize [--rounds N] [--with-doku]", - "Kleine Änderung: /patch <änderung> → /quick_check [was]", - "Finale Doku: /update_doku (nach SHIP – Kommentare + README + Bedienungsanleitung)", - "Neues Projekt: /new_project ", - "Modell wird automatisch gewechselt (Coder→:8001, Judge→:8002)" + "Workflow: /coder | /judge | /fix | /shipit", + "Auto-Loop: /optimize [--rounds N] [--with-doku] [--continue]", + "Planung: /plan → /coder | /optimize --continue | /discard", + "Patch: /patch <änderung> → /quick_check [was]", + "Doku: /update_doku | Neues Projekt: /new_project ", + "Abbruch: Escape (Generation laufend) | /cancel (Loop nach aktuellem Schritt)", + "Resume: /continue | Modell: auto (Coder→:8001, Judge→:8002)", ]); }); @@ -595,10 +632,12 @@ export default function (pi: ExtensionAPI) { // TASK.md anlegen und Implementierung starten await writeTaskMd(pi, ctx, task); ctx.ui.setStatus("optimize", `Starte Optimierung (max ${maxRounds} Runden)…`); - ctx.ui.setStatus("optimize", "Phase 1: Coder implementiert…"); + const taskPreview = task.length > 55 ? task.slice(0, 52) + "…" : task; + ctx.ui.setStatus("optimize", `◉ Coder liest Anforderungen + implementiert: ${taskPreview}`); await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); await sendAndWait(pi, ctx, coderKickoff(task)); await tickTaskMdStatus(pi, ctx, "Implementierung"); + if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", "Nach Implementierung"); return; } } let lastBlockers = ""; @@ -606,43 +645,49 @@ export default function (pi: ExtensionAPI) { // Schleife: Judge → (PASS? fertig : Fix → nächste Runde) for (let round = 1; round <= maxRounds; round++) { - ctx.ui.setStatus("optimize", `Runde ${round}/${maxRounds}: Judge prüft…`); + const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round); + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge — TASK.md + letzter Commit + Tests…`); await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge"); await sendAndWait(pi, ctx, judgePrompt("")); + if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", `Nach Judge Runde ${round}`); return; } const judgeText = getLastAssistantText(ctx); verdict = parseVerdict(judgeText); if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") { await tickTaskMdStatus(pi, ctx, "Review bestanden (PASS)"); - ctx.ui.setStatus("optimize", `✓ ${verdict} nach Runde ${round}`); + ctx.ui.setStatus("optimize", `${"●".repeat(round)} ✓ ${verdict} nach Runde ${round}/${maxRounds} — ShipIt…`); break; } // Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen const currentBlockers = parseBlockers(judgeText); if (currentBlockers && currentBlockers === lastBlockers) { - ctx.ui.setStatus("optimize", "⚠ Schleife: gleicher Blocker – manuelle Intervention nötig"); + ctx.ui.setStatus("optimize", `${prog} ⚠ Gleicher Blocker in Runde ${round} – manuelle Intervention nötig`); finalNotify(ctx, "⚠ Schleife", "Gleicher Blocker zweimal – manuelle Intervention nötig"); return; } lastBlockers = currentBlockers; if (round === maxRounds) { - ctx.ui.setStatus("optimize", `⚠ Max. ${maxRounds} Runden ohne PASS`); + ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)} ⚠ Max. ${maxRounds} Runden ohne PASS`); finalNotify(ctx, "⚠ Kein PASS", `${maxRounds} Runden ohne PASS – bitte /judge und /fix manuell`); return; } - // Fix-Phase - ctx.ui.setStatus("optimize", `Runde ${round}/${maxRounds}: Coder fixt…`); + // Fix-Phase: Blocker-Preview aus Judge-Bericht anzeigen + const blockerHint = currentBlockers + ? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers) + : "Kritikpunkte aus Judge-Bericht"; + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Coder fixt — ${blockerHint}`); await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); await sendAndWait(pi, ctx, fixPrompt("")); + if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", `Nach Fix Runde ${round}`); return; } } // Finale ShipIt-Prüfung nur bei PASS if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") { - ctx.ui.setStatus("optimize", "Finale ShipIt-Prüfung…"); + ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)}◉ ShipIt — SHIP oder NO-SHIP?…`); await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge"); await sendAndWait(pi, ctx, shipitPrompt("")); @@ -739,6 +784,61 @@ export default function (pi: ExtensionAPI) { } }); + // ── Planungsmodus ──────────────────────────────────────────────────────── + + pi.registerCommand("plan", { + description: "Analysiert Auftrag, schmiedet Implementierungsplan in PLAN.md — macht keine Dateiänderungen. → qwen3.5-coder (:8001)", + handler: async function (args: string, ctx: ExtensionCommandContext) { + const task = (args || "").trim(); + if (!task) { + ctx.ui.notify("Benutzung: /plan ", "error"); + return; + } + await writeTaskMd(pi, ctx, task); + await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); + ctx.ui.setStatus("plan", "Analysiere und plane (keine Dateiänderungen)…"); + pi.sendUserMessage(planPrompt(task)); + await ctx.waitForIdle(); + ctx.ui.setStatus("plan", ""); + finalNotify(ctx, "📋 Plan", "Analyse abgeschlossen — PLAN.md + Chat"); + } + }); + + pi.registerCommand("cancel", { + description: "Bricht laufenden Optimize-Loop nach dem aktuellen Schritt ab.", + handler: async function (_args: string, ctx: ExtensionCommandContext) { + cancelRequested = true; + ctx.ui.notify("Abbruch angefordert — wird nach aktuellem Schritt gestoppt", "warning"); + } + }); + + pi.registerCommand("discard", { + description: "Verwirft PLAN.md und setzt den Planungsstatus zurück.", + handler: async function (_args: string, ctx: ExtensionCommandContext) { + await pi.exec("bash", ["-c", "rm -f PLAN.md"], { cwd: ctx.cwd }); + ctx.ui.notify("PLAN.md gelöscht — Plan verworfen", "info"); + finalNotify(ctx, "🗑 Plan verworfen", "Neu starten mit /plan oder /coder"); + } + }); + + pi.registerCommand("continue", { + description: "Nimmt unterbrochenen Prozess wieder auf — liest TASK.md, PLAN.md, git log und entscheidet den nächsten Schritt.", + handler: async function (_args: string, ctx: ExtensionCommandContext) { + await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); + ctx.ui.setStatus("continue", "Analysiere unterbrochenen Prozess…"); + pi.sendUserMessage([ + "Ein Prozess wurde unterbrochen. Analysiere den aktuellen Stand und führe ihn sinnvoll fort:", + "1. Lies TASK.md für den Auftrag", + "2. Lies PLAN.md falls vorhanden (war ein Plan in Arbeit?)", + "3. Führe 'git log --oneline -5' aus um zu sehen was bereits committed wurde", + "4. Entscheide: Muss noch implementiert werden? Ist ein Review fällig? Müssen Fixes nachgezogen werden?", + "5. Fahre direkt mit dem nächsten sinnvollen Schritt fort — kein langer Bericht, einfach weitermachen.", + ].join("\n")); + await ctx.waitForIdle(); + ctx.ui.setStatus("continue", ""); + } + }); + // ── Projekt-Scaffolding ────────────────────────────────────────────────── pi.registerCommand("new_project", { diff --git a/start-coder.sh b/start-coder.sh index 70fd59b..21ef768 100755 --- a/start-coder.sh +++ b/start-coder.sh @@ -31,7 +31,7 @@ docker run -d \ "$IMAGE" \ -m "/hf_home/${MODEL_REL_PATH}" \ --alias "${MODEL_ALIAS}" \ - -c 131072 \ + -c 262144 \ -n 16384 \ --jinja \ --no-context-shift \ @@ -45,11 +45,11 @@ docker run -d \ -ngl 999 \ -fa on \ --kv-unified \ - --cache-type-k q8_0 \ - --cache-type-v q8_0 \ + --cache-type-k q4_0 \ + --cache-type-v q4_0 \ --batch-size 1024 \ --ubatch-size 512 \ - --parallel 2 \ + --parallel 1 \ --cont-batching \ --host 0.0.0.0 \ --port "$CONTAINER_PORT" diff --git a/start-judge.sh b/start-judge.sh index af20ed8..8076cbb 100755 --- a/start-judge.sh +++ b/start-judge.sh @@ -31,7 +31,7 @@ docker run -d \ "$IMAGE" \ -m "/hf_home/${MODEL_REL_PATH}" \ --alias "${MODEL_ALIAS}" \ - -c 131072 \ + -c 262144 \ -n 8192 \ --jinja \ --no-context-shift \ @@ -45,8 +45,8 @@ docker run -d \ -ngl 999 \ -fa on \ --kv-unified \ - --cache-type-k q8_0 \ - --cache-type-v q8_0 \ + --cache-type-k q4_0 \ + --cache-type-v q4_0 \ --batch-size 512 \ --ubatch-size 256 \ --parallel 1 \ From 8bdf73b4ce054d8e0469ebfd886f2f2a6ffc6f4f Mon Sep 17 00:00:00 2001 From: dschlueter Date: Wed, 20 May 2026 20:03:34 +0200 Subject: [PATCH 12/32] =?UTF-8?q?docs:=20CLAUDE.md=20=E2=80=94=20Architekt?= =?UTF-8?q?ur,=20Deploy-Workflow,=20GPU-Setup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fa0cd13 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,69 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Was ist dieses Repo? + +Eine **TypeScript-Extension** für den [`pi`-Coding-Agent](https://github.com/earendil-works/pi) (`@earendil-works/pi-coding-agent`). Sie implementiert einen automatisierten **Coder → Judge → Fix**-Loop mit zwei lokalen llama.cpp-Servern. + +Nach jeder Änderung an `pi-coder-judge-extension.ts` oder `models.json` muss deployed werden: + +```bash +./install.sh # kopiert nach ~/.pi/agent/extensions/ und ~/.pi/agent/ +# dann in pi agent: /reload +``` + +## Server-Lifecycle + +```bash +./start-servers.sh # beide Container parallel starten (empfohlen, ~1–3 min) +./start-coder.sh # nur Coder :8001 +./start-judge.sh # nur Judge :8002 +./stop-servers.sh # beide stoppen +./status.sh # Container- und HTTP-Status beider Server +``` + +Die Start-Skripte stoppen existierende Container automatisch vor dem Neustart. + +## Architektur der Extension + +`pi-coder-judge-extension.ts` ist die einzige Logikdatei. Sie registriert alle Commands, das `apply_patch`-Custom-Tool und zwei Event-Hooks beim pi-Agent. + +**Zwei LLM-Rollen:** + +| Rolle | Port | Container | Alias | +|-------|------|-----------|-------| +| Coder (Implementierung, Fixes, Doku) | 8001 | `qwen36-27b-coder` | `qwen3.5-coder` | +| Judge (Review, ShipIt, QuickCheck) | 8002 | `qwen36-27b-judge` | `qwen3.5-judge` | + +Beide nutzen dasselbe GGUF (`Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS.gguf`), aber unterschiedliche Serverparameter. + +**Zentraler Ablauf in `/optimize`:** +1. `writeTaskMd()` → TASK.md anlegen +2. Coder: `coderKickoff()` → implementiert + committet +3. Loop (max. N Runden): Judge → `parseVerdict()` → PASS? → ShipIt. FAIL? → `parseBlockers()` → Fix → nächste Runde +4. Loop-Erkennung: gleicher Blocker zweimal → Abbruch (`finalNotify()`) +5. 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. +- **`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". From 0fb0ec9e5dbf165b0e2d1ca0bd55a2c03f707187 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Wed, 20 May 2026 21:07:16 +0200 Subject: [PATCH 13/32] fix: /optimize --continue schreibt Zusatzauftrag in TASK.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pi-coder-judge-extension.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pi-coder-judge-extension.ts b/pi-coder-judge-extension.ts index 597fc53..1211c1b 100644 --- a/pi-coder-judge-extension.ts +++ b/pi-coder-judge-extension.ts @@ -626,8 +626,13 @@ export default function (pi: ExtensionAPI) { if (continueMode) { // --continue: Implementierungsphase überspringen, direkt in Judge→Fix-Schleife + // Erweiterter Auftrag wird als Zusatzauftrag in TASK.md eingetragen (falls angegeben) + if (task) await writeTaskMd(pi, ctx, task); ctx.ui.setStatus("optimize", `Setze fort (max ${maxRounds} Runden Judge→Fix)…`); - ctx.ui.notify("--continue: Überspringe Implementierung, starte direkt mit Judge-Prüfung.", "info"); + const continueMsg = task + ? `--continue: Zusatzauftrag in TASK.md eingetragen, überspringe Implementierung.` + : `--continue: Überspringe Implementierung, starte direkt mit Judge-Prüfung.`; + ctx.ui.notify(continueMsg, "info"); } else { // TASK.md anlegen und Implementierung starten await writeTaskMd(pi, ctx, task); From 6afcd6a2716acbf84c78bb82897df9912feb41e4 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Wed, 20 May 2026 21:10:25 +0200 Subject: [PATCH 14/32] =?UTF-8?q?feat:=20--test-cmd=20f=C3=BCr=20/optimize?= =?UTF-8?q?=20=E2=80=94=20Tests=20laufen=20in=20Extension,=20nicht=20im=20?= =?UTF-8?q?Judge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neues Flag: /optimize --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 --- pi-coder-judge-extension.ts | 72 ++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/pi-coder-judge-extension.ts b/pi-coder-judge-extension.ts index 1211c1b..98a00e7 100644 --- a/pi-coder-judge-extension.ts +++ b/pi-coder-judge-extension.ts @@ -57,6 +57,38 @@ function judgePrompt(extra: string): string { ].join("\n") + suffix; } +// Wie judgePrompt, aber Tests werden NICHT vom Judge ausgeführt — +// die Extension hat sie bereits extern gestartet und übergibt den Output. +function judgeWithTestsPrompt(testOutput: string, extra: string): string { + const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : ""; + return [ + "Du bist ein pingeliger, skeptischer Senior-Reviewer und QA-Ingenieur.", + "Deine Aufgabe ist NICHT, nett zu sein, sondern Fehler, Risiken, Randfälle und Produktionsprobleme zu finden.", + "", + "Die Test-Suite wurde bereits extern ausgeführt. Das Ergebnis steht unten.", + "Führe KEINE weiteren Tests aus — weder dieselben noch andere.", + "", + "Pflichten:", + "0. Lies TASK.md und prüfe, ob alle dort beschriebenen Anforderungen vollständig umgesetzt sind.", + "1. Sieh dir den letzten Commit an: 'git log -1 --stat' und 'git show HEAD'.", + "2. Analysiere das folgende Test-Ergebnis und leite daraus Blocker/Major/Minor ab:", + "```", + testOutput, + "```", + "3. Versuche weitere Fehler im Code aktiv zu finden (Randfälle, Sicherheit, Robustheit).", + "4. Wenn du etwas behauptest, nenne die Datei, die Zeile oder den Reproduktionshinweis.", + "", + "Ausgabeformat:", + "- Urteil: PASS | PASS WITH CONCERNS | FAIL", + "- Blocker", + "- Major", + "- Minor", + "- Fehlende Tests", + "- Produktionsrisiken", + "- Konkrete Fix-Aufträge an den Coder", + ].join("\n") + suffix; +} + function fixPrompt(extra: string): string { const suffix = extra?.trim() ? "\n\nZusätzlicher User-Hinweis:\n" + extra.trim() : ""; return [ @@ -369,6 +401,22 @@ async function sendAndWait( await ctx.waitForIdle(); } +// Führt einen Shell-Befehl aus und gibt stdout+stderr zurück (max. 6000 Zeichen). +// Wird von /optimize --test-cmd genutzt, damit Judge keine Tests selbst starten muss. +async function runTests( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + cmd: string +): Promise { + const result = await pi.exec("bash", ["-c", cmd], { cwd: ctx.cwd }); + const output = (result.stdout + (result.stderr ? "\n" + result.stderr : "")).trim(); + const MAX = 6000; + if (output.length > MAX) { + return output.slice(0, MAX) + `\n\n[… Ausgabe gekürzt, ${output.length} Zeichen gesamt]`; + } + return output || "(kein Output)"; +} + // Liest den Text der letzten Assistenten-Antwort aus dem Session-Branch. function getLastAssistantText(ctx: ExtensionCommandContext): string { const entries = ctx.sessionManager.getBranch(); @@ -520,7 +568,7 @@ export default function (pi: ExtensionAPI) { pi.on("session_start", async function (_event, ctx) { ctx.ui.setWidget("coder-judge", [ "Workflow: /coder | /judge | /fix | /shipit", - "Auto-Loop: /optimize [--rounds N] [--with-doku] [--continue]", + "Auto-Loop: /optimize [--rounds N] [--with-doku] [--continue] [--test-cmd \"cmd\"]", "Planung: /plan → /coder | /optimize --continue | /discard", "Patch: /patch <änderung> → /quick_check [was]", "Doku: /update_doku | Neues Projekt: /new_project ", @@ -607,20 +655,25 @@ export default function (pi: ExtensionAPI) { // ── Automatische Optimierungsschleife ──────────────────────────────────── pi.registerCommand("optimize", { - description: "Coder→Judge→Fix-Schleife bis PASS + optional Doku. /optimize [--rounds N] [--with-doku] [--continue]", + description: "Coder→Judge→Fix-Schleife bis PASS + optional Doku. /optimize [--rounds N] [--with-doku] [--continue] [--test-cmd \"befehl\"]", handler: async function (args: string, ctx: ExtensionCommandContext) { const roundsMatch = (args || "").match(/--rounds\s+(\d+)/); const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 3; const withDoku = /--with-doku/.test(args || ""); const continueMode = /--continue/.test(args || ""); + // --test-cmd "befehl" oder --test-cmd befehl (ohne Leerzeichen im Befehl) + const testCmdMatch = (args || "").match(/--test-cmd\s+"([^"]+)"|--test-cmd\s+(\S+)/); + const testCmd: string | null = testCmdMatch ? (testCmdMatch[1] ?? testCmdMatch[2]) : null; const task = (args || "") .replace(/--rounds\s+\d+/, "") .replace(/--with-doku/, "") .replace(/--continue/, "") + .replace(/--test-cmd\s+"[^"]*"/, "") + .replace(/--test-cmd\s+\S+/, "") .trim(); if (!continueMode && !task) { - ctx.ui.notify("Benutzung: /optimize [--rounds N] [--with-doku] [--continue]", "error"); + ctx.ui.notify("Benutzung: /optimize [--rounds N] [--with-doku] [--continue] [--test-cmd \"befehl\"]", "error"); return; } @@ -651,9 +704,18 @@ export default function (pi: ExtensionAPI) { // Schleife: Judge → (PASS? fertig : Fix → nächste Runde) for (let round = 1; round <= maxRounds; round++) { const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round); - ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge — TASK.md + letzter Commit + Tests…`); await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge"); - await sendAndWait(pi, ctx, judgePrompt("")); + + if (testCmd) { + // Tests laufen in der Extension — Judge bekommt den Output fertig geliefert + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${testCmd})…`); + const testOutput = await runTests(pi, ctx, testCmd); + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge analysiert Test-Ergebnis…`); + await sendAndWait(pi, ctx, judgeWithTestsPrompt(testOutput, "")); + } else { + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge — TASK.md + letzter Commit + Tests…`); + await sendAndWait(pi, ctx, judgePrompt("")); + } if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", `Nach Judge Runde ${round}`); return; } const judgeText = getLastAssistantText(ctx); From d5a2c10fa6ffe895c4947dd6c0bd3aff0562d6f4 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Wed, 20 May 2026 21:14:01 +0200 Subject: [PATCH 15/32] =?UTF-8?q?fix:=20--test-cmd=20in=20finalNotify-Widg?= =?UTF-8?q?et=20erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- pi-coder-judge-extension.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pi-coder-judge-extension.ts b/pi-coder-judge-extension.ts index 98a00e7..bcb25a6 100644 --- a/pi-coder-judge-extension.ts +++ b/pi-coder-judge-extension.ts @@ -551,7 +551,7 @@ function finalNotify( `Letzter Lauf: ${verdict} — ${detail} (${timestamp})`, "─────────────────────────────────────────", "Workflow: /coder | /judge | /fix | /shipit", - "Auto-Loop: /optimize [--rounds N] [--with-doku] [--continue]", + "Auto-Loop: /optimize [--rounds N] [--with-doku] [--continue] [--test-cmd \"cmd\"]", "Planung: /plan → /coder | /optimize --continue | /discard", "Patch: /patch <änderung> → /quick_check [was]", "Doku: /update_doku | Neues Projekt: /new_project ", From 2c07fb9d1cc47547525fe0caeadd52c0c2abd0e2 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Wed, 20 May 2026 21:39:21 +0200 Subject: [PATCH 16/32] =?UTF-8?q?feat:=20automatische=20Test-Erkennung=20+?= =?UTF-8?q?=20parallele=20Ausf=C3=BChrung=20in=20/optimize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pi-coder-judge-extension.ts | 88 ++++++++++++++++++++++++++++++------- 1 file changed, 73 insertions(+), 15 deletions(-) diff --git a/pi-coder-judge-extension.ts b/pi-coder-judge-extension.ts index bcb25a6..aa10824 100644 --- a/pi-coder-judge-extension.ts +++ b/pi-coder-judge-extension.ts @@ -402,19 +402,59 @@ async function sendAndWait( } // Führt einen Shell-Befehl aus und gibt stdout+stderr zurück (max. 6000 Zeichen). -// Wird von /optimize --test-cmd genutzt, damit Judge keine Tests selbst starten muss. -async function runTests( +// Erkennt Test-Suiten im Projektverzeichnis anhand von Framework-Markern. +// Alle Checks laufen parallel — konservativ, keine False Positives. +async function detectTestCommands( + pi: ExtensionAPI, + ctx: ExtensionCommandContext +): Promise { + const [hasPytest, hasNpm, hasCargo, hasGo, hasMake] = await Promise.all([ + pi.exec("bash", ["-c", + "test -f pytest.ini || test -f conftest.py || " + + "(test -f pyproject.toml && grep -q 'pytest' pyproject.toml) || " + + "find . -maxdepth 4 \\( -name 'test_*.py' -o -name '*_test.py' \\) 2>/dev/null | grep -q ." + ], { cwd: ctx.cwd }), + pi.exec("bash", ["-c", + "test -f package.json && " + + "node -e \"const p=require('./package.json');process.exit(" + + "p.scripts&&p.scripts.test&&!p.scripts.test.includes('no test')?0:1)\" 2>/dev/null" + ], { cwd: ctx.cwd }), + pi.exec("bash", ["-c", "test -f Cargo.toml"], { cwd: ctx.cwd }), + pi.exec("bash", ["-c", + "test -f go.mod && find . -maxdepth 4 -name '*_test.go' 2>/dev/null | grep -q ." + ], { cwd: ctx.cwd }), + pi.exec("bash", ["-c", + "test -f Makefile && grep -qE '^test[[:space:]]*:' Makefile" + ], { cwd: ctx.cwd }), + ]); + return ([ + hasPytest.code === 0 ? "pytest -x -q 2>&1" : null, + hasNpm.code === 0 ? "npm test 2>&1" : null, + hasCargo.code === 0 ? "cargo test 2>&1" : null, + hasGo.code === 0 ? "go test ./... 2>&1" : null, + hasMake.code === 0 ? "make test 2>&1" : null, + ] as (string | null)[]).filter((c): c is string => c !== null); +} + +// Führt mehrere Test-Befehle parallel als CPU-Prozesse aus und liefert einen +// kombinierten Output-Block für judgeWithTestsPrompt(). +async function runTestsParallel( pi: ExtensionAPI, ctx: ExtensionCommandContext, - cmd: string + cmds: string[] ): Promise { - const result = await pi.exec("bash", ["-c", cmd], { cwd: ctx.cwd }); - const output = (result.stdout + (result.stderr ? "\n" + result.stderr : "")).trim(); - const MAX = 6000; - if (output.length > MAX) { - return output.slice(0, MAX) + `\n\n[… Ausgabe gekürzt, ${output.length} Zeichen gesamt]`; - } - return output || "(kein Output)"; + const results = await Promise.all( + cmds.map(cmd => pi.exec("bash", ["-c", cmd], { cwd: ctx.cwd })) + ); + const MAX_PER = Math.max(1000, Math.floor(6000 / cmds.length)); + return results.map((r, i) => { + const raw = (r.stdout + (r.stderr ? "\n" + r.stderr : "")).trim(); + const out = raw.length > MAX_PER + ? raw.slice(0, MAX_PER) + `\n[… gekürzt, ${raw.length} Zeichen]` + : raw || "(kein Output)"; + const status = r.code === 0 ? "✓ OK" : `✗ Exit ${r.code}`; + return `=== ${cmds[i]} [${status}] ===\n${out}`; + }).join("\n\n"); } // Liest den Text der letzten Assistenten-Antwort aus dem Session-Branch. @@ -655,7 +695,7 @@ export default function (pi: ExtensionAPI) { // ── Automatische Optimierungsschleife ──────────────────────────────────── pi.registerCommand("optimize", { - description: "Coder→Judge→Fix-Schleife bis PASS + optional Doku. /optimize [--rounds N] [--with-doku] [--continue] [--test-cmd \"befehl\"]", + description: "Coder→Judge→Fix-Schleife bis PASS. Tests werden automatisch erkannt und parallel ausgeführt. /optimize [--rounds N] [--with-doku] [--continue] [--test-cmd \"override\"]", handler: async function (args: string, ctx: ExtensionCommandContext) { const roundsMatch = (args || "").match(/--rounds\s+(\d+)/); const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 3; @@ -698,6 +738,22 @@ export default function (pi: ExtensionAPI) { if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", "Nach Implementierung"); return; } } + // Test-Suiten einmalig ermitteln: --test-cmd überschreibt Auto-Erkennung. + // Läuft nach Coder, damit neu angelegte Test-Dateien bereits erkannt werden. + ctx.ui.setStatus("optimize", "Test-Suiten werden erkannt…"); + const autoTestCmds: string[] = testCmd + ? [testCmd] + : await detectTestCommands(pi, ctx); + if (autoTestCmds.length > 0) { + const label = autoTestCmds.map(c => c.split(" ")[0]).join(", "); + ctx.ui.notify( + `${autoTestCmds.length} Test-Suite${autoTestCmds.length > 1 ? "n" : ""} erkannt: ${label}`, + "info" + ); + } else { + ctx.ui.notify("Keine Test-Suiten erkannt — Judge führt Tests selbst aus.", "info"); + } + let lastBlockers = ""; let verdict = ""; @@ -706,10 +762,12 @@ export default function (pi: ExtensionAPI) { const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round); await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge"); - if (testCmd) { - // Tests laufen in der Extension — Judge bekommt den Output fertig geliefert - ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${testCmd})…`); - const testOutput = await runTests(pi, ctx, testCmd); + if (autoTestCmds.length > 0) { + const label = autoTestCmds.length === 1 + ? autoTestCmds[0].split(" ")[0] + : `${autoTestCmds.length} Suiten parallel`; + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${label})…`); + const testOutput = await runTestsParallel(pi, ctx, autoTestCmds); ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge analysiert Test-Ergebnis…`); await sendAndWait(pi, ctx, judgeWithTestsPrompt(testOutput, "")); } else { From 14c47ecd0187a35d50d7ec8d022459a597e00ab0 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Wed, 20 May 2026 21:44:37 +0200 Subject: [PATCH 17/32] fix: sendAndWait-Retry + Judge-Server-Bereitschaftscheck vor Loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- pi-coder-judge-extension.ts | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/pi-coder-judge-extension.ts b/pi-coder-judge-extension.ts index aa10824..f79cd24 100644 --- a/pi-coder-judge-extension.ts +++ b/pi-coder-judge-extension.ts @@ -389,14 +389,25 @@ async function switchModel( } // Sendet eine Nachricht und wartet bis der Agent fertig ist. -// Erst idle abwarten, dann als followUp einstellen — verhindert "Agent is already processing". +// Retry-Schleife fängt "Agent is already processing" ab — tritt auf wenn +// waitForIdle() zu früh zurückkehrt (Race Condition im pi-Agent). async function sendAndWait( pi: ExtensionAPI, ctx: ExtensionCommandContext, content: string ): Promise { await ctx.waitForIdle(); - pi.sendUserMessage(content, { deliverAs: "followUp" }); + for (let attempt = 1; attempt <= 5; attempt++) { + try { + pi.sendUserMessage(content, { deliverAs: "followUp" }); + break; + } catch (e: any) { + if (attempt === 5) throw e; + // Exponentieller Backoff: 500ms, 1s, 2s, 4s + await new Promise(r => setTimeout(r, 500 * Math.pow(2, attempt - 1))); + await ctx.waitForIdle(); + } + } await new Promise(r => setTimeout(r, 400)); await ctx.waitForIdle(); } @@ -738,6 +749,22 @@ export default function (pi: ExtensionAPI) { if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", "Nach Implementierung"); return; } } + // Judge-Server-Bereitschaft prüfen — bei 503 (Modell lädt noch) bis zu 60s warten. + ctx.ui.setStatus("optimize", "Judge-Server wird geprüft…"); + let serverReady = false; + for (let i = 0; i < 20; i++) { + const hc = await pi.exec("bash", ["-c", + "curl -sf --max-time 3 http://localhost:8002/health || " + + "curl -sf --max-time 3 http://localhost:8002/v1/models" + ], { cwd: ctx.cwd }); + if (hc.code === 0) { serverReady = true; break; } + await new Promise(r => setTimeout(r, 3000)); + } + if (!serverReady) { + finalNotify(ctx, "⛔ Judge nicht erreichbar", "Port 8002 antwortet nicht — start-judge.sh ausführen"); + return; + } + // Test-Suiten einmalig ermitteln: --test-cmd überschreibt Auto-Erkennung. // Läuft nach Coder, damit neu angelegte Test-Dateien bereits erkannt werden. ctx.ui.setStatus("optimize", "Test-Suiten werden erkannt…"); From a6f7f968b55e39a53b7b610459f6df8b8c9ca7bd Mon Sep 17 00:00:00 2001 From: dschlueter Date: Wed, 20 May 2026 23:47:06 +0200 Subject: [PATCH 18/32] =?UTF-8?q?fix:=20Test-Timeout=20verhindert=20h?= =?UTF-8?q?=C3=A4ngenden=20Loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pi-coder-judge-extension.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/pi-coder-judge-extension.ts b/pi-coder-judge-extension.ts index f79cd24..a3d5c41 100644 --- a/pi-coder-judge-extension.ts +++ b/pi-coder-judge-extension.ts @@ -452,10 +452,16 @@ async function detectTestCommands( async function runTestsParallel( pi: ExtensionAPI, ctx: ExtensionCommandContext, - cmds: string[] + cmds: string[], + timeoutSecs: number = 120 ): Promise { const results = await Promise.all( - cmds.map(cmd => pi.exec("bash", ["-c", cmd], { cwd: ctx.cwd })) + // timeout-Wrapper: verhindert hängende Tests (Exit 124 = Timeout) + cmds.map(cmd => pi.exec( + "bash", + ["-c", `timeout ${timeoutSecs} bash -c ${JSON.stringify(cmd)}`], + { cwd: ctx.cwd } + )) ); const MAX_PER = Math.max(1000, Math.floor(6000 / cmds.length)); return results.map((r, i) => { @@ -463,7 +469,9 @@ async function runTestsParallel( const out = raw.length > MAX_PER ? raw.slice(0, MAX_PER) + `\n[… gekürzt, ${raw.length} Zeichen]` : raw || "(kein Output)"; - const status = r.code === 0 ? "✓ OK" : `✗ Exit ${r.code}`; + const status = r.code === 0 ? "✓ OK" + : r.code === 124 ? `✗ Timeout (>${timeoutSecs}s)` + : `✗ Exit ${r.code}`; return `=== ${cmds[i]} [${status}] ===\n${out}`; }).join("\n\n"); } @@ -706,17 +714,19 @@ export default function (pi: ExtensionAPI) { // ── Automatische Optimierungsschleife ──────────────────────────────────── pi.registerCommand("optimize", { - description: "Coder→Judge→Fix-Schleife bis PASS. Tests werden automatisch erkannt und parallel ausgeführt. /optimize [--rounds N] [--with-doku] [--continue] [--test-cmd \"override\"]", + description: "Coder→Judge→Fix-Schleife bis PASS. Tests werden automatisch erkannt und parallel ausgeführt. /optimize [--rounds N] [--with-doku] [--continue] [--test-cmd \"override\"] [--test-timeout N]", handler: async function (args: string, ctx: ExtensionCommandContext) { const roundsMatch = (args || "").match(/--rounds\s+(\d+)/); const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 3; const withDoku = /--with-doku/.test(args || ""); const continueMode = /--continue/.test(args || ""); - // --test-cmd "befehl" oder --test-cmd befehl (ohne Leerzeichen im Befehl) const testCmdMatch = (args || "").match(/--test-cmd\s+"([^"]+)"|--test-cmd\s+(\S+)/); const testCmd: string | null = testCmdMatch ? (testCmdMatch[1] ?? testCmdMatch[2]) : null; + const testTimeoutMatch = (args || "").match(/--test-timeout\s+(\d+)/); + const testTimeout = testTimeoutMatch ? parseInt(testTimeoutMatch[1], 10) : 120; const task = (args || "") .replace(/--rounds\s+\d+/, "") + .replace(/--test-timeout\s+\d+/, "") .replace(/--with-doku/, "") .replace(/--continue/, "") .replace(/--test-cmd\s+"[^"]*"/, "") @@ -793,8 +803,8 @@ export default function (pi: ExtensionAPI) { const label = autoTestCmds.length === 1 ? autoTestCmds[0].split(" ")[0] : `${autoTestCmds.length} Suiten parallel`; - ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${label})…`); - const testOutput = await runTestsParallel(pi, ctx, autoTestCmds); + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${label}, max. ${testTimeout}s)…`); + const testOutput = await runTestsParallel(pi, ctx, autoTestCmds, testTimeout); ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge analysiert Test-Ergebnis…`); await sendAndWait(pi, ctx, judgeWithTestsPrompt(testOutput, "")); } else { From e13e9382ff0ffb1f9f984c2a3046bc2854274f9a Mon Sep 17 00:00:00 2001 From: dschlueter Date: Fri, 22 May 2026 23:49:53 +0200 Subject: [PATCH 19/32] feat: automatische SemVer-Versionierung nach SHIP + /version-Command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- models.json | 2 +- pi-coder-judge-extension.ts | 751 ++++++++++++++++++++++++++---------- start-coder.sh | 44 +-- start-judge.sh | 46 +-- 4 files changed, 580 insertions(+), 263 deletions(-) diff --git a/models.json b/models.json index 0e9052a..3ffe010 100644 --- a/models.json +++ b/models.json @@ -83,7 +83,7 @@ "reasoning": true, "input": ["text"], "contextWindow": 262144, - "maxTokens": 8192, + "maxTokens": 16384, "cost": { "input": 0, "output": 0, diff --git a/pi-coder-judge-extension.ts b/pi-coder-judge-extension.ts index a3d5c41..1f76ae0 100644 --- a/pi-coder-judge-extension.ts +++ b/pi-coder-judge-extension.ts @@ -378,14 +378,15 @@ async function switchModel( ctx: ExtensionCommandContext, provider: string, modelId: string -): Promise { +): Promise { const model = ctx.modelRegistry.find(provider, modelId); if (!model) { ctx.ui.notify(`Modell ${provider}/${modelId} nicht gefunden`, "error"); - return; + return false; } const ok = await pi.setModel(model); if (!ok) ctx.ui.notify(`Kein API-Key für ${modelId}`, "warning"); + return ok !== false; } // Sendet eine Nachricht und wartet bis der Agent fertig ist. @@ -412,6 +413,47 @@ async function sendAndWait( await ctx.waitForIdle(); } +// Prüft via POST /v1/chat/completions ob das Modell im VRAM bereit ist. +// /health und /v1/models antworten bereits während des GPU-Ladevorgangs — nur +// ein echter Completion-Request liefert zuverlässig HTTP 200 wenn das Modell ready ist. +async function waitUntilModelReady( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + port: number, + modelAlias: string, + maxWaitMs = 180_000 +): Promise { + const deadline = Date.now() + maxWaitMs; + const body = JSON.stringify({ + model: modelAlias, + messages: [{ role: "user", content: "ping" }], + max_tokens: 1, temperature: 0.0, stream: false, + }); + // Body als Datei — verhindert Shell-Injection wenn modelAlias Sonderzeichen enthält + const tmpBody = `/tmp/pi_ready_${Date.now()}_${Math.random().toString(36).slice(2)}.json`; + await pi.exec("bash", ["-c", `printf "%s" "$1" > "${tmpBody}"`, "_", body], { cwd: ctx.cwd }); + let notified = false; + try { + while (Date.now() < deadline) { + const r = await pi.exec("bash", ["-c", + `curl -s -o /dev/null -w "%{http_code}" --max-time 5 ` + + `-X POST http://localhost:${port}/v1/chat/completions ` + + `-H "Content-Type: application/json" ` + + `-d "@${tmpBody}"` + ], { cwd: ctx.cwd }); + if (r.stdout?.trim() === "200") return true; + if (!notified) { + ctx.ui.notify(`Modell-Server (Port ${port}) lädt noch — warte bis zu 3 min…`, "info"); + notified = true; + } + await new Promise(res => setTimeout(res, 3000)); + } + return false; + } finally { + await pi.exec("bash", ["-c", `rm -f "${tmpBody}"`], { cwd: ctx.cwd }); + } +} + // Führt einen Shell-Befehl aus und gibt stdout+stderr zurück (max. 6000 Zeichen). // Erkennt Test-Suiten im Projektverzeichnis anhand von Framework-Markern. // Alle Checks laufen parallel — konservativ, keine False Positives. @@ -427,8 +469,8 @@ async function detectTestCommands( ], { cwd: ctx.cwd }), pi.exec("bash", ["-c", "test -f package.json && " + - "node -e \"const p=require('./package.json');process.exit(" + - "p.scripts&&p.scripts.test&&!p.scripts.test.includes('no test')?0:1)\" 2>/dev/null" + "grep -q '\"test\"' package.json && " + + "! grep -q 'no test' package.json" ], { cwd: ctx.cwd }), pi.exec("bash", ["-c", "test -f Cargo.toml"], { cwd: ctx.cwd }), pi.exec("bash", ["-c", @@ -495,14 +537,18 @@ function getLastAssistantText(ctx: ExtensionCommandContext): string { } // Extrahiert das Urteil aus einer Judge-Antwort. +// "UNREADABLE" wenn kein Urteil erkennbar — unterscheidbar von einem expliziten FAIL. function parseVerdict(text: string): string { const m = text.match(/Urteil:\s*(PASS WITH CONCERNS|PASS|FAIL)/i); - return m ? m[1].toUpperCase() : ""; + return m ? m[1].toUpperCase() : "UNREADABLE"; } // Extrahiert den Blocker-Abschnitt für die Loop-Erkennung. +// Erkennt Bullet-Listen (- / – / *), Bold (**Blocker**) und Headings (## Blocker). function parseBlockers(text: string): string { - const m = text.match(/[-–*]\s*Blocker[:\n]([\s\S]*?)(?:\n[-–*]\s*Major|\n[-–*]\s*Minor|$)/i); + 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() : ""; } @@ -524,6 +570,9 @@ async function getFilesSinceTag( { cwd: ctx.cwd } ); + // Bei git-Fehler alles verarbeiten (sicherer als stilles Überspringen) + if (diff.code !== 0) return null; + return diff.stdout.trim() .split("\n") .filter(f => @@ -539,48 +588,75 @@ async function getFilesSinceTag( // Dokumentations-Phase: inkrementell via Git-Tags, nur geänderte Dateien werden verarbeitet. // Wird von /update_doku und /optimize --with-doku genutzt. async function runUpdateDoku(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise { - await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); + if (!await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder")) { + ctx.ui.notify("Coder-Modell nicht verfügbar — Dokumentations-Phase abgebrochen", "error"); + return; + } + + // Jede Phase läuft unabhängig — Fehler in Phase 1 blockieren nicht Phase 2/3. + // Tag wird nur NACH erfolgreichem sendAndWait gesetzt. // Phase 1: Code-Kommentare - const commentFiles = await getFilesSinceTag(pi, ctx, "docs-last-commented"); - if (commentFiles === null) { - ctx.ui.setStatus("update_doku", "1/3: Code wird kommentiert (alle Dateien)…"); - await sendAndWait(pi, ctx, commentCodePrompt()); - } else if (commentFiles.length === 0) { - ctx.ui.notify("Code-Kommentare: keine Änderungen seit letztem Lauf – übersprungen.", "info"); - } else { - ctx.ui.setStatus("update_doku", `1/3: Code wird kommentiert (${commentFiles.length} Datei(en))…`); - await sendAndWait(pi, ctx, commentCodePromptIncremental(commentFiles)); + try { + const commentFiles = await getFilesSinceTag(pi, ctx, "docs-last-commented"); + if (commentFiles === null) { + ctx.ui.setStatus("update_doku", "1/3: Code wird kommentiert (alle Dateien)…"); + currentActivity = "Coder kommentiert Code…"; + await sendAndWait(pi, ctx, commentCodePrompt()); + await pi.exec("bash", ["-c", "git tag -f docs-last-commented"], { cwd: ctx.cwd }); + } else if (commentFiles.length === 0) { + ctx.ui.notify("Code-Kommentare: keine Änderungen seit letztem Lauf – übersprungen.", "info"); + } else { + ctx.ui.setStatus("update_doku", `1/3: Code wird kommentiert (${commentFiles.length} Datei(en))…`); + currentActivity = "Coder kommentiert Code…"; + await sendAndWait(pi, ctx, commentCodePromptIncremental(commentFiles)); + await pi.exec("bash", ["-c", "git tag -f docs-last-commented"], { cwd: ctx.cwd }); + } + } catch (e: any) { + ctx.ui.notify(`1/3 Code-Kommentare fehlgeschlagen: ${String(e?.message ?? e)}`, "error"); } - await pi.exec("bash", ["-c", "git tag -f docs-last-commented"], { cwd: ctx.cwd }); // Phase 2: README.md - const readmeFiles = await getFilesSinceTag(pi, ctx, "docs-last-readme"); - if (readmeFiles === null) { - ctx.ui.setStatus("update_doku", "2/3: README.md wird geschrieben…"); - await sendAndWait(pi, ctx, readmeMdPrompt()); - } else if (readmeFiles.length === 0) { - ctx.ui.notify("README.md: keine Änderungen seit letztem Lauf – übersprungen.", "info"); - } else { - ctx.ui.setStatus("update_doku", `2/3: README.md wird geprüft (${readmeFiles.length} Datei(en) geändert)…`); - await sendAndWait(pi, ctx, readmeMdPromptIncremental(readmeFiles)); + try { + const readmeFiles = await getFilesSinceTag(pi, ctx, "docs-last-readme"); + if (readmeFiles === null) { + ctx.ui.setStatus("update_doku", "2/3: README.md wird geschrieben…"); + currentActivity = "Coder schreibt README…"; + await sendAndWait(pi, ctx, readmeMdPrompt()); + await pi.exec("bash", ["-c", "git tag -f docs-last-readme"], { cwd: ctx.cwd }); + } else if (readmeFiles.length === 0) { + ctx.ui.notify("README.md: keine Änderungen seit letztem Lauf – übersprungen.", "info"); + } else { + ctx.ui.setStatus("update_doku", `2/3: README.md wird geprüft (${readmeFiles.length} Datei(en) geändert)…`); + currentActivity = "Coder schreibt README…"; + await sendAndWait(pi, ctx, readmeMdPromptIncremental(readmeFiles)); + await pi.exec("bash", ["-c", "git tag -f docs-last-readme"], { cwd: ctx.cwd }); + } + } catch (e: any) { + ctx.ui.notify(`2/3 README.md fehlgeschlagen: ${String(e?.message ?? e)}`, "error"); } - await pi.exec("bash", ["-c", "git tag -f docs-last-readme"], { cwd: ctx.cwd }); // Phase 3: BEDIENUNGSANLEITUNG.md - const bedFiles = await getFilesSinceTag(pi, ctx, "docs-last-bedienungsanleitung"); - if (bedFiles === null) { - ctx.ui.setStatus("update_doku", "3/3: BEDIENUNGSANLEITUNG.md wird geschrieben…"); - await sendAndWait(pi, ctx, bedienungsanleitungPrompt()); - } else if (bedFiles.length === 0) { - ctx.ui.notify("BEDIENUNGSANLEITUNG.md: keine Änderungen seit letztem Lauf – übersprungen.", "info"); - } else { - ctx.ui.setStatus("update_doku", `3/3: BEDIENUNGSANLEITUNG.md wird geprüft (${bedFiles.length} Datei(en) geändert)…`); - await sendAndWait(pi, ctx, bedienungsanleitungPromptIncremental(bedFiles)); + try { + const bedFiles = await getFilesSinceTag(pi, ctx, "docs-last-bedienungsanleitung"); + if (bedFiles === null) { + ctx.ui.setStatus("update_doku", "3/3: BEDIENUNGSANLEITUNG.md wird geschrieben…"); + currentActivity = "Coder schreibt Bedienungsanleitung…"; + await sendAndWait(pi, ctx, bedienungsanleitungPrompt()); + await pi.exec("bash", ["-c", "git tag -f docs-last-bedienungsanleitung"], { cwd: ctx.cwd }); + } else if (bedFiles.length === 0) { + ctx.ui.notify("BEDIENUNGSANLEITUNG.md: keine Änderungen seit letztem Lauf – übersprungen.", "info"); + } else { + ctx.ui.setStatus("update_doku", `3/3: BEDIENUNGSANLEITUNG.md wird geprüft (${bedFiles.length} Datei(en) geändert)…`); + currentActivity = "Coder schreibt Bedienungsanleitung…"; + await sendAndWait(pi, ctx, bedienungsanleitungPromptIncremental(bedFiles)); + await pi.exec("bash", ["-c", "git tag -f docs-last-bedienungsanleitung"], { cwd: ctx.cwd }); + } + } catch (e: any) { + ctx.ui.notify(`3/3 BEDIENUNGSANLEITUNG.md fehlgeschlagen: ${String(e?.message ?? e)}`, "error"); } - await pi.exec("bash", ["-c", "git tag -f docs-last-bedienungsanleitung"], { cwd: ctx.cwd }); - // Abschließender Dokumentations-Commit + // Abschließender Dokumentations-Commit (immer, auch bei Teilfehlern) await pi.exec( "bash", ["-c", "git add -A && git commit -m 'docs: update comments, README, BEDIENUNGSANLEITUNG' || true"], @@ -594,6 +670,120 @@ async function runUpdateDoku(pi: ExtensionAPI, ctx: ExtensionCommandContext): Pr ctx.ui.notify("Dokumentations-Phase abgeschlossen. Commit angelegt.", "info"); } +// ── Versions-Verwaltung (SemVer + Git-Tags) ────────────────────────────────── + +// Liest den höchsten vX.Y.Z-Tag via `git tag -l`. Gibt null zurück wenn kein Tag existiert. +async function getCurrentVersion( + pi: ExtensionAPI, + ctx: ExtensionCommandContext +): Promise<[number, number, number] | null> { + const res = await pi.exec("bash", ["-c", "git tag -l 'v*' | sort -V | tail -1"], { cwd: ctx.cwd }); + const raw = (res.stdout ?? "").trim(); + const m = raw.match(/^v?(\d+)\.(\d+)\.(\d+)$/); + return m ? [+m[1], +m[2], +m[3]] : null; +} + +// Analysiert Commit-Subjects seit dem letzten Tag nach Conventional Commits. +// feat! / BREAKING CHANGE → major, feat: → minor, alles andere → patch. +async function analyzeBumpType( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + since?: string +): Promise<"major" | "minor" | "patch"> { + const range = since ? `${since}..HEAD` : "HEAD"; + const res = await pi.exec("bash", ["-c", `git log ${range} --format="%s" 2>/dev/null`], { cwd: ctx.cwd }); + const lines = (res.stdout ?? "").split("\n"); + if (lines.some(l => /^feat!:|BREAKING CHANGE/.test(l))) return "major"; + if (lines.some(l => /^feat(\(.+\))?:/.test(l))) return "minor"; + return "patch"; +} + +// Findet die erste vorhandene Versions-Manifest-Datei im Arbeitsverzeichnis. +async function detectVersionFile( + pi: ExtensionAPI, + ctx: ExtensionCommandContext +): Promise<"package.json" | "Cargo.toml" | "pyproject.toml" | "VERSION" | null> { + for (const f of ["package.json", "Cargo.toml", "pyproject.toml"]) { + const r = await pi.exec("bash", ["-c", `test -f ${f}`], { cwd: ctx.cwd }); + if (r.exitCode === 0) return f as "package.json" | "Cargo.toml" | "pyproject.toml"; + } + const r = await pi.exec("bash", ["-c", "test -f VERSION"], { cwd: ctx.cwd }); + return r.exitCode === 0 ? "VERSION" : null; +} + +// Schreibt die neue Version in die Manifest-Datei und erstellt einen chore-Commit. +async function applyVersionBump( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + manifest: string, + version: string +): Promise { + let cmd: string; + if (manifest === "package.json") { + cmd = `npm version --no-git-tag-version ${version}`; + } else if (manifest === "Cargo.toml") { + cmd = `sed -i 's/^version = ".*"/version = "${version}"/' Cargo.toml`; + } else if (manifest === "pyproject.toml") { + cmd = `sed -i 's/^version = ".*"/version = "${version}"/' pyproject.toml`; + } else { + cmd = `printf 'v%s\\n' '${version}' > VERSION`; + } + await pi.exec("bash", ["-c", cmd], { cwd: ctx.cwd }); + await pi.exec( + "bash", + ["-c", `git add ${manifest} && git commit -m "chore: bump version to v${version}"`], + { cwd: ctx.cwd } + ); +} + +// Hauptfunktion: ermittelt aktuelle Version, analysiert Commits, zeigt Dialog, setzt Tag. +async function runVersionBump(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise { + // Early exit wenn kein git-Repo vorhanden + const gitCheck = await pi.exec("bash", ["-c", "git rev-parse --is-inside-work-tree 2>/dev/null"], { cwd: ctx.cwd }); + if (gitCheck.exitCode !== 0) return; + + const current = await getCurrentVersion(pi, ctx); + const tag = current ? `v${current[0]}.${current[1]}.${current[2]}` : undefined; + const bump = await analyzeBumpType(pi, ctx, tag); + + const [maj, min, pat] = current ?? [0, 0, 0]; + const initial = !current; + const versions: Record<"patch" | "minor" | "major", string> = initial + ? { patch: "v0.0.1", minor: "v0.1.0", major: "v1.0.0" } + : { patch: `v${maj}.${min}.${pat + 1}`, minor: `v${maj}.${min + 1}.0`, major: `v${maj + 1}.0.0` }; + + const recommended: "patch" | "minor" | "major" = initial ? "minor" : bump; + const labels = (["patch", "minor", "major"] as const).map( + t => `${t} → ${versions[t]}${t === recommended ? " (empfohlen)" : ""}` + ); + + const choice = await ctx.ui.select({ + title: "Version", + message: current + ? `Aktuelle Version: ${tag}. Commits seit letztem Tag: ${bump}-Bump erkannt.` + : "Noch kein Versions-Tag vorhanden.", + options: [...labels, "Überspringen"], + }); + + if (!choice || choice.startsWith("Überspringen")) return; + + const chosen = (["patch", "minor", "major"] as const).find(t => choice.startsWith(t))!; + const newVersion = versions[chosen].replace(/^v/, ""); + const newTag = `v${newVersion}`; + + const manifest = await detectVersionFile(pi, ctx); + if (manifest) { + await applyVersionBump(pi, ctx, manifest, newVersion); + } + + const tagResult = await pi.exec("bash", ["-c", `git tag ${newTag}`], { cwd: ctx.cwd }); + if (tagResult.exitCode !== 0) { + ctx.ui.notify(`Tag ${newTag} existiert bereits — manuell löschen mit: git tag -d ${newTag}`, "error"); + return; + } + ctx.ui.notify(`Version ${newTag} getaggt.`, "info"); +} + // Prominente Abschluss-Notification + Widget-Update mit Uhrzeit und Ergebnis. function finalNotify( ctx: ExtensionCommandContext, @@ -601,41 +791,88 @@ function finalNotify( detail: string ): void { const timestamp = new Date().toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }); - const level = verdict.includes("SHIP") && !verdict.includes("NO-SHIP") ? "warning" - : verdict.includes("NO-SHIP") ? "error" + const level = verdict.startsWith("🚀") ? "info" + : verdict.includes("NO-SHIP") || verdict.startsWith("⛔") ? "error" : verdict.includes("⚠") ? "warning" : "info"; ctx.ui.notify(`${verdict}: ${detail}`, level); ctx.ui.setWidget("coder-judge", [ `Letzter Lauf: ${verdict} — ${detail} (${timestamp})`, - "─────────────────────────────────────────", - "Workflow: /coder | /judge | /fix | /shipit", - "Auto-Loop: /optimize [--rounds N] [--with-doku] [--continue] [--test-cmd \"cmd\"]", - "Planung: /plan → /coder | /optimize --continue | /discard", - "Patch: /patch <änderung> → /quick_check [was]", - "Doku: /update_doku | Neues Projekt: /new_project ", - "Abbruch: Escape (Generation laufend) | /cancel (Loop nach aktuellem Schritt)", - "Resume: /continue | Modell: auto (Coder→:8001, Judge→:8002)", + "/optimize [--rounds N] [--with-doku] [--continue] [--test-cmd \"cmd\"]", + "/fix · /judge · /shipit · /cancel · /continue · /help", ]); } // ── Extension ──────────────────────────────────────────────────────────────── let cancelRequested = false; +let currentActivity = ""; // Working-Message für den aktuellen Command-Kontext + +// Erzeugt eine knappe Statuszeile aus Tool-Name und Argumenten. +function toolExecutionLabel(toolName: string, args: Record): string { + switch (toolName) { + case "edit": + return `Editiere ${args.path ?? "Datei"}…`; + case "write": + return `Schreibe ${args.path ?? "Datei"} neu…`; + case "read": + return `Lese ${args.path ?? "Datei"}…`; + case "grep": + return `Suche in ${args.path ?? args.pattern ?? "Dateien"}…`; + case "find": + return `Suche Dateien: ${args.pattern ?? ""}…`; + case "ls": + return `Verzeichnis: ${args.path ?? "."}…`; + case "bash": { + const cmd = String(args.command ?? "").trim().replace(/\n[\s\S]*/s, ""); + if (/git\s+commit/.test(cmd)) return "Git-Commit…"; + if (/git\s+add/.test(cmd)) return "Stage Änderungen…"; + if (/git\s+tag/.test(cmd)) return "Git-Tag setzen…"; + if (/pytest|npm test|cargo test|go test|make test/.test(cmd)) return "Tests laufen…"; + if (/git\s+(diff|log|show|tag -l)/.test(cmd)) return "Git-History lesen…"; + if (/patch\s+-p1/.test(cmd)) return "Wende Patch an…"; + if (/curl/.test(cmd)) return "HTTP-Request…"; + return `Shell: ${cmd.slice(0, 55)}${cmd.length > 55 ? "…" : ""}`; + } + case "apply_patch": + return "Wende Patch an…"; + default: + return ""; + } +} export default function (pi: ExtensionAPI) { pi.on("session_start", async function (_event, ctx) { ctx.ui.setWidget("coder-judge", [ - "Workflow: /coder | /judge | /fix | /shipit", - "Auto-Loop: /optimize [--rounds N] [--with-doku] [--continue] [--test-cmd \"cmd\"]", - "Planung: /plan → /coder | /optimize --continue | /discard", - "Patch: /patch <änderung> → /quick_check [was]", - "Doku: /update_doku | Neues Projekt: /new_project ", - "Abbruch: Escape (Generation laufend) | /cancel (Loop nach aktuellem Schritt)", - "Resume: /continue | Modell: auto (Coder→:8001, Judge→:8002)", + "/optimize [--rounds N] [--with-doku] [--continue] [--test-cmd \"cmd\"]", + "/fix · /judge · /shipit · /cancel · /continue · /help", ]); }); + // ── Live-Aktivitätsstatus ──────────────────────────────────────────────── + // turn_start: Working-Text auf aktuellen Command-Kontext setzen + pi.on("turn_start", function (_event, ctx) { + if (currentActivity) ctx.ui.setWorkingMessage(currentActivity); + }); + + // tool_execution_start: präzise Statuszeile während Tool-Ausführung + pi.on("tool_execution_start", function (event, ctx) { + const label = toolExecutionLabel(event.toolName, (event as any).args ?? {}); + if (label) ctx.ui.setStatus("agent", label); + }); + + // tool_execution_end: Statuszeile löschen + pi.on("tool_execution_end", function (_event, ctx) { + ctx.ui.setStatus("agent", undefined); + }); + + // agent_end: Working-Text und Statuszeile zurücksetzen + pi.on("agent_end", function (_event, ctx) { + ctx.ui.setWorkingMessage(); + ctx.ui.setStatus("agent", undefined); + currentActivity = ""; + }); + // ── Robustes edit: Bottom-up-Reordering via tool_call-Hook ───────────── // Behebt "edits[n] doesn't match": Mehrere Edits auf dieselbe Datei werden // von hinten nach vorne sortiert, damit frühere Edits spätere Positionen nicht verschieben. @@ -673,41 +910,61 @@ export default function (pi: ExtensionAPI) { // ── Manuelle Kommandos ─────────────────────────────────────────────────── pi.registerCommand("coder", { - description: "Legt TASK.md an, startet Implementierung → qwen3.5-coder (:8001).", + description: "Implementiert ohne Review-Loop → qwen3.5-coder (:8001).", handler: async function (args: string, ctx: ExtensionCommandContext) { const task = (args || "").trim(); if (!task) { ctx.ui.notify("Benutzung: /coder ", "error"); return; } + if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder")) { + ctx.ui.notify("Coder-Server nicht bereit (Port 8001) — start-coder.sh ausführen", "error"); + return; + } await writeTaskMd(pi, ctx, task); await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); - pi.sendUserMessage(coderKickoff(task)); + currentActivity = "Coder implementiert…"; + await sendAndWait(pi, ctx, coderKickoff(task)); } }); pi.registerCommand("judge", { description: "Review gegen TASK.md + git show HEAD → qwen3.5-judge (:8002).", handler: async function (args: string, ctx: ExtensionCommandContext) { + if (!await waitUntilModelReady(pi, ctx, 8002, "qwen3.5-judge")) { + ctx.ui.notify("Judge-Server nicht bereit (Port 8002) — start-judge.sh ausführen", "error"); + return; + } await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge"); - pi.sendUserMessage(judgePrompt(args || "")); + currentActivity = "Judge reviewt…"; + await sendAndWait(pi, ctx, judgePrompt(args || "")); } }); pi.registerCommand("fix", { description: "Fixt Judge-Kritik, committet Ergebnis → qwen3.5-coder (:8001).", handler: async function (args: string, ctx: ExtensionCommandContext) { + if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder")) { + ctx.ui.notify("Coder-Server nicht bereit (Port 8001) — start-coder.sh ausführen", "error"); + return; + } await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); - pi.sendUserMessage(fixPrompt(args || "")); + currentActivity = "Coder fixt Judge-Kritik…"; + await sendAndWait(pi, ctx, fixPrompt(args || "")); } }); pi.registerCommand("shipit", { description: "Finale Freigabe gegen TASK.md + git log → qwen3.5-judge (:8002).", handler: async function (args: string, ctx: ExtensionCommandContext) { + if (!await waitUntilModelReady(pi, ctx, 8002, "qwen3.5-judge")) { + ctx.ui.notify("Judge-Server nicht bereit (Port 8002) — start-judge.sh ausführen", "error"); + return; + } await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge"); ctx.ui.notify("Judge prüft finale Freigabe — Ergebnis erscheint im Chat (SHIP / NO-SHIP)", "info"); - pi.sendUserMessage(shipitPrompt(args || "")); + currentActivity = "Judge: finale Freigabe…"; + await sendAndWait(pi, ctx, shipitPrompt(args || "")); } }); @@ -720,10 +977,10 @@ export default function (pi: ExtensionAPI) { const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 3; const withDoku = /--with-doku/.test(args || ""); const continueMode = /--continue/.test(args || ""); - const testCmdMatch = (args || "").match(/--test-cmd\s+"([^"]+)"|--test-cmd\s+(\S+)/); - const testCmd: string | null = testCmdMatch ? (testCmdMatch[1] ?? testCmdMatch[2]) : null; + const testCmdMatch = (args || "").match(/--test-cmd\s+"([^"]+)"|--test-cmd\s+'([^']+)'|--test-cmd\s+(\S+)/); + const testCmd: string | null = testCmdMatch ? (testCmdMatch[1] ?? testCmdMatch[2] ?? testCmdMatch[3]) : null; const testTimeoutMatch = (args || "").match(/--test-timeout\s+(\d+)/); - const testTimeout = testTimeoutMatch ? parseInt(testTimeoutMatch[1], 10) : 120; + const testTimeout = testTimeoutMatch ? Math.max(1, parseInt(testTimeoutMatch[1], 10)) : 120; const task = (args || "") .replace(/--rounds\s+\d+/, "") .replace(/--test-timeout\s+\d+/, "") @@ -738,139 +995,169 @@ export default function (pi: ExtensionAPI) { return; } - if (continueMode) { - // --continue: Implementierungsphase überspringen, direkt in Judge→Fix-Schleife - // Erweiterter Auftrag wird als Zusatzauftrag in TASK.md eingetragen (falls angegeben) - if (task) await writeTaskMd(pi, ctx, task); - ctx.ui.setStatus("optimize", `Setze fort (max ${maxRounds} Runden Judge→Fix)…`); - const continueMsg = task - ? `--continue: Zusatzauftrag in TASK.md eingetragen, überspringe Implementierung.` - : `--continue: Überspringe Implementierung, starte direkt mit Judge-Prüfung.`; - ctx.ui.notify(continueMsg, "info"); - } else { - // TASK.md anlegen und Implementierung starten - await writeTaskMd(pi, ctx, task); - ctx.ui.setStatus("optimize", `Starte Optimierung (max ${maxRounds} Runden)…`); - const taskPreview = task.length > 55 ? task.slice(0, 52) + "…" : task; - ctx.ui.setStatus("optimize", `◉ Coder liest Anforderungen + implementiert: ${taskPreview}`); - await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); - await sendAndWait(pi, ctx, coderKickoff(task)); - await tickTaskMdStatus(pi, ctx, "Implementierung"); - if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", "Nach Implementierung"); return; } - } + try { + if (continueMode) { + // --continue: Implementierungsphase überspringen, direkt in Judge→Fix-Schleife + // Erweiterter Auftrag wird als Zusatzauftrag in TASK.md eingetragen (falls angegeben) + if (task) await writeTaskMd(pi, ctx, task); + ctx.ui.setStatus("optimize", `Setze fort (max ${maxRounds} Runden Judge→Fix)…`); + const continueMsg = task + ? `--continue: Zusatzauftrag in TASK.md eingetragen, überspringe Implementierung.` + : `--continue: Überspringe Implementierung, starte direkt mit Judge-Prüfung.`; + ctx.ui.notify(continueMsg, "info"); - // Judge-Server-Bereitschaft prüfen — bei 503 (Modell lädt noch) bis zu 60s warten. - ctx.ui.setStatus("optimize", "Judge-Server wird geprüft…"); - let serverReady = false; - for (let i = 0; i < 20; i++) { - const hc = await pi.exec("bash", ["-c", - "curl -sf --max-time 3 http://localhost:8002/health || " + - "curl -sf --max-time 3 http://localhost:8002/v1/models" - ], { cwd: ctx.cwd }); - if (hc.code === 0) { serverReady = true; break; } - await new Promise(r => setTimeout(r, 3000)); - } - if (!serverReady) { - finalNotify(ctx, "⛔ Judge nicht erreichbar", "Port 8002 antwortet nicht — start-judge.sh ausführen"); - return; - } - - // Test-Suiten einmalig ermitteln: --test-cmd überschreibt Auto-Erkennung. - // Läuft nach Coder, damit neu angelegte Test-Dateien bereits erkannt werden. - ctx.ui.setStatus("optimize", "Test-Suiten werden erkannt…"); - const autoTestCmds: string[] = testCmd - ? [testCmd] - : await detectTestCommands(pi, ctx); - if (autoTestCmds.length > 0) { - const label = autoTestCmds.map(c => c.split(" ")[0]).join(", "); - ctx.ui.notify( - `${autoTestCmds.length} Test-Suite${autoTestCmds.length > 1 ? "n" : ""} erkannt: ${label}`, - "info" - ); - } else { - ctx.ui.notify("Keine Test-Suiten erkannt — Judge führt Tests selbst aus.", "info"); - } - - let lastBlockers = ""; - let verdict = ""; - - // Schleife: Judge → (PASS? fertig : Fix → nächste Runde) - for (let round = 1; round <= maxRounds; round++) { - const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round); - await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge"); - - if (autoTestCmds.length > 0) { - const label = autoTestCmds.length === 1 - ? autoTestCmds[0].split(" ")[0] - : `${autoTestCmds.length} Suiten parallel`; - ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${label}, max. ${testTimeout}s)…`); - const testOutput = await runTestsParallel(pi, ctx, autoTestCmds, testTimeout); - ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge analysiert Test-Ergebnis…`); - await sendAndWait(pi, ctx, judgeWithTestsPrompt(testOutput, "")); - } else { - ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge — TASK.md + letzter Commit + Tests…`); - await sendAndWait(pi, ctx, judgePrompt("")); - } - if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", `Nach Judge Runde ${round}`); return; } - - const judgeText = getLastAssistantText(ctx); - verdict = parseVerdict(judgeText); - - if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") { - await tickTaskMdStatus(pi, ctx, "Review bestanden (PASS)"); - ctx.ui.setStatus("optimize", `${"●".repeat(round)} ✓ ${verdict} nach Runde ${round}/${maxRounds} — ShipIt…`); - break; - } - - // Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen - const currentBlockers = parseBlockers(judgeText); - if (currentBlockers && currentBlockers === lastBlockers) { - ctx.ui.setStatus("optimize", `${prog} ⚠ Gleicher Blocker in Runde ${round} – manuelle Intervention nötig`); - finalNotify(ctx, "⚠ Schleife", "Gleicher Blocker zweimal – manuelle Intervention nötig"); - return; - } - lastBlockers = currentBlockers; - - if (round === maxRounds) { - ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)} ⚠ Max. ${maxRounds} Runden ohne PASS`); - finalNotify(ctx, "⚠ Kein PASS", `${maxRounds} Runden ohne PASS – bitte /judge und /fix manuell`); - return; - } - - // Fix-Phase: Blocker-Preview aus Judge-Bericht anzeigen - const blockerHint = currentBlockers - ? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers) - : "Kritikpunkte aus Judge-Bericht"; - ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Coder fixt — ${blockerHint}`); - await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); - await sendAndWait(pi, ctx, fixPrompt("")); - if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", `Nach Fix Runde ${round}`); return; } - } - - // Finale ShipIt-Prüfung nur bei PASS - if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") { - ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)}◉ ShipIt — SHIP oder NO-SHIP?…`); - await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge"); - await sendAndWait(pi, ctx, shipitPrompt("")); - - const shipText = getLastAssistantText(ctx); - const shipVerdict = shipText.match(/Urteil:\s*(SHIP|NO-SHIP)/i)?.[1]?.toUpperCase() ?? ""; - - if (shipVerdict === "SHIP") { - ctx.ui.setStatus("optimize", "🚀 SHIP – produktionsreif"); - finalNotify(ctx, "🚀 SHIP", "Programm ist produktionsreif"); - if (withDoku) { - await runUpdateDoku(pi, ctx); - } else { - ctx.ui.notify("Nächster Schritt: /update_doku für Code-Kommentare, README.md und BEDIENUNGSANLEITUNG.md", "info"); + // Im --continue-Modus: Coder-Server jetzt prüfen, da er für die Fix-Phase gebraucht wird + // (in normalem Modus wird er beim coderKickoff implizit geprüft) + ctx.ui.setStatus("optimize", "Coder-Server wird geprüft…"); + if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder")) { + finalNotify(ctx, "⛔ Coder nicht erreichbar", "Port 8001 — kein HTTP 200 nach 3 min. start-coder.sh ausführen"); + return; } - } else if (shipVerdict === "NO-SHIP") { - ctx.ui.setStatus("optimize", "⛔ NO-SHIP – noch nicht bereit"); - finalNotify(ctx, "⛔ NO-SHIP", "Noch Blocker offen – bitte /judge und /fix manuell"); } else { - ctx.ui.setStatus("optimize", "ShipIt abgeschlossen"); - finalNotify(ctx, "ShipIt", "Kein klares Urteil – Antwort im Chat prüfen"); + // TASK.md anlegen und Implementierung starten + await writeTaskMd(pi, ctx, task); + ctx.ui.setStatus("optimize", `Starte Optimierung (max ${maxRounds} Runden)…`); + const taskPreview = task.length > 55 ? task.slice(0, 52) + "…" : task; + ctx.ui.setStatus("optimize", `◉ Coder liest Anforderungen + implementiert: ${taskPreview}`); + if (!await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder")) { + finalNotify(ctx, "⛔ Modell-Fehler", "Coder-Modell (llama-cpp-coder) nicht verfügbar"); + return; + } + currentActivity = "Coder implementiert…"; + await sendAndWait(pi, ctx, coderKickoff(task)); + await tickTaskMdStatus(pi, ctx, "Implementierung"); + if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", "Nach Implementierung"); return; } } + + // Judge-Bereitschaft via Completion-Check — /health antwortet bereits während des + // GPU-Ladevorgangs und ist kein verlässliches Signal. Nur HTTP 200 auf einen + // echten Completion-Request bedeutet: Modell ist im VRAM und bereit. + ctx.ui.setStatus("optimize", "Judge-Server wird geprüft…"); + if (!await waitUntilModelReady(pi, ctx, 8002, "qwen3.5-judge")) { + finalNotify(ctx, "⛔ Judge nicht erreichbar", "Port 8002 — kein HTTP 200 nach 3 min. start-judge.sh ausführen"); + return; + } + + // Test-Suiten einmalig ermitteln: --test-cmd überschreibt Auto-Erkennung. + // Läuft nach Coder, damit neu angelegte Test-Dateien bereits erkannt werden. + ctx.ui.setStatus("optimize", "Test-Suiten werden erkannt…"); + const autoTestCmds: string[] = testCmd + ? [testCmd] + : await detectTestCommands(pi, ctx); + if (autoTestCmds.length > 0) { + const label = autoTestCmds.map(c => c.split(" ")[0]).join(", "); + ctx.ui.notify( + `${autoTestCmds.length} Test-Suite${autoTestCmds.length > 1 ? "n" : ""} erkannt: ${label}`, + "info" + ); + } else { + ctx.ui.notify("Keine Test-Suiten erkannt — Judge führt Tests selbst aus.", "info"); + } + + let lastBlockers = ""; + let verdict = ""; + + // Schleife: Judge → (PASS? fertig : Fix → nächste Runde) + for (let round = 1; round <= maxRounds; round++) { + const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round); + if (!await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge")) { + finalNotify(ctx, "⛔ Modell-Fehler", "Judge-Modell (llama-cpp-judge) nicht verfügbar"); + return; + } + + if (autoTestCmds.length > 0) { + const label = autoTestCmds.length === 1 + ? autoTestCmds[0].split(" ")[0] + : `${autoTestCmds.length} Suiten parallel`; + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${label}, max. ${testTimeout}s)…`); + const testOutput = await runTestsParallel(pi, ctx, autoTestCmds, testTimeout); + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge analysiert Test-Ergebnis…`); + currentActivity = `Judge reviewt (Runde ${round}/${maxRounds})…`; + await sendAndWait(pi, ctx, judgeWithTestsPrompt(testOutput, "")); + } else { + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge — TASK.md + letzter Commit + Tests…`); + currentActivity = `Judge reviewt (Runde ${round}/${maxRounds})…`; + await sendAndWait(pi, ctx, judgePrompt("")); + } + if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Judge Runde ${round}`); return; } + + const judgeText = getLastAssistantText(ctx); + verdict = parseVerdict(judgeText); + + if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") { + await tickTaskMdStatus(pi, ctx, "Review bestanden (PASS)"); + ctx.ui.setStatus("optimize", `${"●".repeat(round)} ✓ ${verdict} nach Runde ${round}/${maxRounds} — ShipIt…`); + break; + } + + // Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen + const currentBlockers = parseBlockers(judgeText); + if (currentBlockers && currentBlockers === lastBlockers) { + ctx.ui.setStatus("optimize", `${prog} ⚠ Gleicher Blocker in Runde ${round} – manuelle Intervention nötig`); + finalNotify(ctx, "⚠ Schleife", "Gleicher Blocker zweimal – manuelle Intervention nötig"); + return; + } + lastBlockers = currentBlockers; + + if (round === maxRounds) { + ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)} ⚠ Max. ${maxRounds} Runden ohne PASS`); + if (verdict === "UNREADABLE") { + finalNotify(ctx, "⚠ Urteil unklar", `${maxRounds} Runden – Judge-Urteil nicht erkennbar, Antwort im Chat prüfen`); + } else { + finalNotify(ctx, "⚠ Kein PASS", `${maxRounds} Runden ohne PASS – bitte /judge und /fix manuell`); + } + return; + } + + // Fix-Phase: Blocker-Preview aus Judge-Bericht anzeigen + const blockerHint = currentBlockers + ? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers) + : "Kritikpunkte aus Judge-Bericht"; + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Coder fixt — ${blockerHint}`); + if (!await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder")) { + finalNotify(ctx, "⛔ Modell-Fehler", "Coder-Modell (llama-cpp-coder) nicht verfügbar"); + return; + } + currentActivity = "Coder fixt Blocker…"; + await sendAndWait(pi, ctx, fixPrompt("")); + if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Fix Runde ${round}`); return; } + } + + // Finale ShipIt-Prüfung nur bei PASS + if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") { + ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)}◉ ShipIt — SHIP oder NO-SHIP?…`); + if (!await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge")) { + finalNotify(ctx, "⛔ Modell-Fehler", "Judge-Modell (llama-cpp-judge) nicht verfügbar"); + return; + } + currentActivity = "Judge: finale Freigabe…"; + await sendAndWait(pi, ctx, shipitPrompt("")); + + const shipText = getLastAssistantText(ctx); + const shipVerdict = shipText.match(/Urteil:\s*(SHIP|NO-SHIP)/i)?.[1]?.toUpperCase() ?? ""; + + if (shipVerdict === "SHIP") { + ctx.ui.setStatus("optimize", "🚀 SHIP – produktionsreif"); + finalNotify(ctx, "🚀 SHIP", "Programm ist produktionsreif"); + await runVersionBump(pi, ctx); + if (withDoku) { + await runUpdateDoku(pi, ctx); + } else { + ctx.ui.notify("Nächster Schritt: /update_doku für Code-Kommentare, README.md und BEDIENUNGSANLEITUNG.md", "info"); + } + } else if (shipVerdict === "NO-SHIP") { + ctx.ui.setStatus("optimize", "⛔ NO-SHIP – noch nicht bereit"); + finalNotify(ctx, "⛔ NO-SHIP", "Noch Blocker offen – bitte /judge und /fix manuell"); + } else { + ctx.ui.setStatus("optimize", "ShipIt abgeschlossen"); + finalNotify(ctx, "ShipIt", "Kein klares Urteil – Antwort im Chat prüfen"); + } + } + } catch (e: any) { + finalNotify(ctx, "⛔ Fehler", String(e?.message ?? e)); + } finally { + // Sicherstellen dass cancelRequested nie in einen späteren /optimize-Aufruf leckt + cancelRequested = false; } } }); @@ -878,30 +1165,40 @@ export default function (pi: ExtensionAPI) { // ── Schlanke Kommandos für kleine Änderungen ───────────────────────────── pi.registerCommand("patch", { - description: "Gezielte Minimaländerung ohne vollständigen Review → qwen3.5-coder (:8001).", + description: "Gezielte Minimaländerung ohne Refactoring, committet → qwen3.5-coder (:8001).", handler: async function (args: string, ctx: ExtensionCommandContext) { const change = (args || "").trim(); if (!change) { ctx.ui.notify("Benutzung: /patch ", "error"); return; } + if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder")) { + ctx.ui.notify("Coder-Server nicht bereit (Port 8001) — start-coder.sh ausführen", "error"); + return; + } await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); - pi.sendUserMessage(patchPrompt(change)); + currentActivity = "Coder patcht…"; + await sendAndWait(pi, ctx, patchPrompt(change)); } }); pi.registerCommand("quick_check", { - description: "Schnelle Prüfung der letzten Änderung (OK/PROBLEM) → qwen3.5-judge (:8002).", + description: "Schnelle OK/PROBLEM-Prüfung einer kleinen Codeänderung → qwen3.5-judge (:8002).", handler: async function (args: string, ctx: ExtensionCommandContext) { + if (!await waitUntilModelReady(pi, ctx, 8002, "qwen3.5-judge")) { + ctx.ui.notify("Judge-Server nicht bereit (Port 8002) — start-judge.sh ausführen", "error"); + return; + } await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge"); - pi.sendUserMessage(quickCheckPrompt(args || "")); + currentActivity = "Judge: Schnellcheck…"; + await sendAndWait(pi, ctx, quickCheckPrompt(args || "")); } }); // ── Dokumentations-Phase ───────────────────────────────────────────────── pi.registerCommand("update_doku", { - description: "Code kommentieren + README.md + BEDIENUNGSANLEITUNG.md + git commit → qwen3.5-coder (:8001).", + description: "Inkrementelle Code-Kommentare + README.md + BEDIENUNGSANLEITUNG.md via Git-Tags.", handler: async function (_args: string, ctx: ExtensionCommandContext) { await runUpdateDoku(pi, ctx); } @@ -924,7 +1221,7 @@ export default function (pi: ExtensionAPI) { }), }), async execute(_id, params, _signal, _onUpdate, ctx) { - const tmpFile = `/tmp/pi_patch_${Date.now()}.diff`; + const tmpFile = `/tmp/pi_patch_${Date.now()}_${Math.random().toString(36).slice(2)}.diff`; await pi.exec( "bash", ["-c", `printf "%s" "$1" > "${tmpFile}"`, "_", params.patch], @@ -949,23 +1246,63 @@ export default function (pi: ExtensionAPI) { // ── Planungsmodus ──────────────────────────────────────────────────────── pi.registerCommand("plan", { - description: "Analysiert Auftrag, schmiedet Implementierungsplan in PLAN.md — macht keine Dateiänderungen. → qwen3.5-coder (:8001)", + description: "Erstellt Implementierungsplan in PLAN.md ohne Dateiänderungen → qwen3.5-coder.", handler: async function (args: string, ctx: ExtensionCommandContext) { const task = (args || "").trim(); if (!task) { ctx.ui.notify("Benutzung: /plan ", "error"); return; } + if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder")) { + ctx.ui.notify("Coder-Server nicht bereit (Port 8001) — start-coder.sh ausführen", "error"); + return; + } await writeTaskMd(pi, ctx, task); await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); ctx.ui.setStatus("plan", "Analysiere und plane (keine Dateiänderungen)…"); - pi.sendUserMessage(planPrompt(task)); - await ctx.waitForIdle(); + currentActivity = "Coder plant (kein Code)…"; + await sendAndWait(pi, ctx, planPrompt(task)); ctx.ui.setStatus("plan", ""); finalNotify(ctx, "📋 Plan", "Analyse abgeschlossen — PLAN.md + Chat"); } }); + pi.registerCommand("version", { + description: "Versionsnummer des Projekts erhöhen (SemVer + Git-Tag). Analysiert Commits seit letztem Tag.", + handler: async function (_args: string, ctx: ExtensionCommandContext) { + await runVersionBump(pi, ctx); + } + }); + + pi.registerCommand("help", { + description: "Zeigt alle Kommandos der pi-coder-judge-Extension.", + handler: async function (_args: string, ctx: ExtensionCommandContext) { + ctx.ui.notify([ + "── Kern-Workflow ─────────────────────────────────────────", + "/optimize [--rounds N] [--with-doku] [--continue]", + " [--test-cmd \"cmd\"] [--test-timeout N]", + " Coder→Judge→Fix-Schleife bis PASS (empfohlener Einstieg)", + "/fix [kommentar] Fixt Judge-Kritik, committet → Coder", + "/judge [kommentar] Review gegen TASK.md + HEAD → Judge", + "/shipit [kommentar] Finale Freigabe (SHIP/NO-SHIP) → Judge", + "", + "── Steuerung ─────────────────────────────────────────────", + "/continue Unterbrochenen Prozess fortsetzen", + "/cancel Laufenden Loop nach aktuellem Schritt abbrechen", + "", + "── Erweiterte Kommandos (immer tippbar, nicht im Menü) ───", + "/coder Nur Implementierung ohne Review-Loop → Coder", + "/patch <änderung> Gezielte Minimaländerung → Coder", + "/quick_check [was] Schnelle OK/PROBLEM-Prüfung → Judge", + "/plan Implementierungsplan in PLAN.md → Coder", + "/update_doku Code-Kommentare + README.md + BEDIENUNGSANLEITUNG.md", + "/version Versionsnummer erhöhen (SemVer + Git-Tag)", + "/discard Verwirft PLAN.md", + "/new_project Projektverzeichnis + git init + .gitignore", + ].join("\n"), "info"); + } + }); + pi.registerCommand("cancel", { description: "Bricht laufenden Optimize-Loop nach dem aktuellen Schritt ab.", handler: async function (_args: string, ctx: ExtensionCommandContext) { @@ -975,7 +1312,7 @@ export default function (pi: ExtensionAPI) { }); pi.registerCommand("discard", { - description: "Verwirft PLAN.md und setzt den Planungsstatus zurück.", + description: "Löscht PLAN.md und verwirft den aktuellen Plan.", handler: async function (_args: string, ctx: ExtensionCommandContext) { await pi.exec("bash", ["-c", "rm -f PLAN.md"], { cwd: ctx.cwd }); ctx.ui.notify("PLAN.md gelöscht — Plan verworfen", "info"); @@ -986,9 +1323,14 @@ export default function (pi: ExtensionAPI) { pi.registerCommand("continue", { description: "Nimmt unterbrochenen Prozess wieder auf — liest TASK.md, PLAN.md, git log und entscheidet den nächsten Schritt.", handler: async function (_args: string, ctx: ExtensionCommandContext) { + if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder")) { + ctx.ui.notify("Coder-Server nicht bereit (Port 8001) — start-coder.sh ausführen", "error"); + return; + } await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder"); ctx.ui.setStatus("continue", "Analysiere unterbrochenen Prozess…"); - pi.sendUserMessage([ + currentActivity = "Coder analysiert Stand…"; + await sendAndWait(pi, ctx, [ "Ein Prozess wurde unterbrochen. Analysiere den aktuellen Stand und führe ihn sinnvoll fort:", "1. Lies TASK.md für den Auftrag", "2. Lies PLAN.md falls vorhanden (war ein Plan in Arbeit?)", @@ -996,7 +1338,6 @@ export default function (pi: ExtensionAPI) { "4. Entscheide: Muss noch implementiert werden? Ist ein Review fällig? Müssen Fixes nachgezogen werden?", "5. Fahre direkt mit dem nächsten sinnvollen Schritt fort — kein langer Bericht, einfach weitermachen.", ].join("\n")); - await ctx.waitForIdle(); ctx.ui.setStatus("continue", ""); } }); @@ -1004,7 +1345,7 @@ export default function (pi: ExtensionAPI) { // ── Projekt-Scaffolding ────────────────────────────────────────────────── pi.registerCommand("new_project", { - description: "Legt Projektverzeichnis an + git init + .gitignore. /new_project ", + description: "Legt Projektverzeichnis, git-Repo und .gitignore an.", handler: async function (args: string, ctx: ExtensionCommandContext) { const rawPath = (args || "").trim(); if (!rawPath) { diff --git a/start-coder.sh b/start-coder.sh index 21ef768..0032fc4 100755 --- a/start-coder.sh +++ b/start-coder.sh @@ -34,10 +34,11 @@ docker run -d \ -c 262144 \ -n 16384 \ --jinja \ + --chat-template-kwargs '{"enable_thinking":true}' \ --no-context-shift \ - --temp 0.2 \ - --top-p 0.95 \ - --top-k 40 \ + --temp 0.6 \ + --top-p 0.80 \ + --top-k 20 \ --min-p 0.01 \ --repeat-penalty 1.05 \ --main-gpu 0 \ @@ -54,37 +55,24 @@ docker run -d \ --host 0.0.0.0 \ --port "$CONTAINER_PORT" -echo "[*] Warte auf HTTP ..." -HTTP_READY=0 +echo "[*] Warte auf Modell-Bereitschaft (Completion-Check, max. 180 s) ..." +MODEL_READY=0 for i in {1..90}; do - if curl -s "http://localhost:${HOST_PORT}/health" >/dev/null 2>&1 || \ - curl -s "http://localhost:${HOST_PORT}/v1/models" >/dev/null 2>&1; then - HTTP_READY=1 - break - fi + 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 [ "$HTTP_READY" -ne 1 ]; then - echo "[!] HTTP-Server wurde nicht rechtzeitig erreichbar." >&2 +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 "[*] Teste Chat-Completion ..." -curl -s -X POST "http://localhost:${HOST_PORT}/v1/chat/completions" \ - -H "Content-Type: application/json" \ - -d "{ - \"model\": \"${MODEL_ALIAS}\", - \"messages\": [ - { \"role\": \"system\", \"content\": \"Du bist ein präziser Coding-Assistent.\" }, - { \"role\": \"user\", \"content\": \"Antworte nur mit dem Wort: bereit\" } - ], - \"max_tokens\": 8, - \"temperature\": 0.0, - \"stream\": false - }" - -echo -echo "[*] Server bereit auf http://0.0.0.0:${HOST_PORT}" +echo "[*] Modell bereit — erster Completion-Request erfolgreich (HTTP 200)." +echo "[*] Server läuft auf http://0.0.0.0:${HOST_PORT}" echo "[*] Stoppen mit: docker rm -f ${CONTAINER_NAME}" diff --git a/start-judge.sh b/start-judge.sh index 8076cbb..40c138e 100755 --- a/start-judge.sh +++ b/start-judge.sh @@ -32,12 +32,13 @@ docker run -d \ -m "/hf_home/${MODEL_REL_PATH}" \ --alias "${MODEL_ALIAS}" \ -c 262144 \ - -n 8192 \ + -n 16384 \ --jinja \ + --chat-template-kwargs '{"enable_thinking":true}' \ --no-context-shift \ - --temp 0.1 \ - --top-p 0.9 \ - --top-k 40 \ + --temp 0.7 \ + --top-p 0.80 \ + --top-k 20 \ --min-p 0.01 \ --repeat-penalty 1.05 \ --main-gpu 0 \ @@ -54,37 +55,24 @@ docker run -d \ --host 0.0.0.0 \ --port "$CONTAINER_PORT" -echo "[*] Warte auf HTTP ..." -HTTP_READY=0 +echo "[*] Warte auf Modell-Bereitschaft (Completion-Check, max. 180 s) ..." +MODEL_READY=0 for i in {1..90}; do - if curl -s "http://localhost:${HOST_PORT}/health" >/dev/null 2>&1 || \ - curl -s "http://localhost:${HOST_PORT}/v1/models" >/dev/null 2>&1; then - HTTP_READY=1 - break - fi + 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 [ "$HTTP_READY" -ne 1 ]; then - echo "[!] HTTP-Server wurde nicht rechtzeitig erreichbar." >&2 +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 "[*] Teste Judge-Endpoint ..." -curl -s -X POST "http://localhost:${HOST_PORT}/v1/chat/completions" \ - -H "Content-Type: application/json" \ - -d "{ - \"model\": \"${MODEL_ALIAS}\", - \"messages\": [ - { \"role\": \"system\", \"content\": \"Du bist ein strenger Code-Reviewer.\" }, - { \"role\": \"user\", \"content\": \"Antworte nur mit dem Wort: bereit\" } - ], - \"max_tokens\": 8, - \"temperature\": 0.0, - \"stream\": false - }" - -echo -echo "[*] Server bereit auf http://0.0.0.0:${HOST_PORT}" +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}" From 8aadb317e59c999995394c65210abf5573f6e9a4 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Sat, 23 May 2026 00:23:23 +0200 Subject: [PATCH 20/32] docs: /version, Live-Status, korrekte Serverparameter in Doku MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- BEDIENUNGSANLEITUNG.md | 100 ++++++++++++++++++++++++++++++++++++++--- README.md | 48 ++++++++++++++------ 2 files changed, 129 insertions(+), 19 deletions(-) diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 9279e55..08e202a 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -16,9 +16,10 @@ einfache Slash-Kommandos in der pi-Agent-Oberfläche. 6. [Automatischer Workflow: /optimize](#6-automatischer-workflow-optimize) 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. [TASK.md verstehen und nutzen](#9-taskmd-verstehen-und-nutzen) -10. [Typische Anwendungsfälle](#10-typische-anwendungsfälle) -11. [Fehlermeldungen und Lösungen](#11-fehlermeldungen-und-lösungen) +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) --- @@ -303,8 +304,12 @@ Phase 4: Runde 2/3: Judge prüft... ✓ PASS WITH CONCERNS nach Runde 2 Finale ShipIt-Prüfung... → SHIP +[Dialog: Version → v0.1.0 (empfohlen)] ``` +Während des Ablaufs zeigt die Statuszeile immer die aktuelle Aktivität: +`Coder implementiert…` → `Editiere src/main.rs…` → `Git-Commit…` → `Judge reviewt (Runde 1/3)…` + ### Beispiel: mehr Runden ``` @@ -476,7 +481,79 @@ Führt nach SHIP automatisch `/update_doku` aus. --- -## 9. TASK.md verstehen und nutzen +## 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. @@ -534,7 +611,7 @@ Die Checkboxen werden automatisch abgehakt: --- -## 10. Typische Anwendungsfälle +## 11. Typische Anwendungsfälle ### Neues Rust-Programm von Null @@ -566,6 +643,17 @@ Die Checkboxen werden automatisch abgehakt: /quick_check ``` +### 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 @@ -592,7 +680,7 @@ Die Checkboxen werden automatisch abgehakt: --- -## 11. Fehlermeldungen und Lösungen +## 12. Fehlermeldungen und Lösungen ### "Modell-Datei nicht gefunden" diff --git a/README.md b/README.md index f3aa255..7e88eed 100644 --- a/README.md +++ b/README.md @@ -130,8 +130,8 @@ und dann neu gestartet — ein laufender Inference-Request wird dabei abgebroche | `-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 q8_0` | — | KV-Cache Keys in 8-Bit quantisiert. Spart ~50 % VRAM gegenüber fp16, minimaler Qualitätsverlust. | -| `--cache-type-v q8_0` | — | KV-Cache Values ebenfalls 8-Bit quantisiert. | +| `--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. | @@ -141,7 +141,7 @@ und dann neu gestartet — ein laufender Inference-Request wird dabei abgebroche | Parameter | Wert | Erklärung / Wirkung | |---|---|---| -| `-c 131072` | 128K Tokens | Großes Kontextfenster: gesamte Codebasis + Gesprächsverlauf passt rein. **Hoher VRAM-Bedarf.** Reduziere auf `65536` wenn VRAM knapp. | +| `-c 262144` | 256K Tokens | Sehr großes Kontextfenster: gesamte Codebasis + langer Gesprächsverlauf passt rein. **Sehr hoher VRAM-Bedarf.** Reduziere auf `65536` wenn VRAM knapp. | | `-n 16384` | 16K Tokens | Maximale Ausgabelänge pro Anfrage. Für Kommentieraufgaben (`/update_doku`) nötig. | | `--temp 0.2` | — | Niedrige Temperatur: deterministisch, konsistenter Code. Erhöhe auf `0.4–0.6` für kreativere Lösungsansätze. | | `--top-p 0.95` | — | Nucleus Sampling: 95 % der Wahrscheinlichkeitsmasse. Passend zu temp 0.2. | @@ -153,8 +153,8 @@ und dann neu gestartet — ein laufender Inference-Request wird dabei abgebroche | Parameter | Wert | Erklärung / Wirkung | |---|---|---| -| `-c 131072` | 128K Tokens | Großes Kontextfenster: nötig bei langen /optimize-Runden, wo der Gesprächsverlauf stark anwächst. | -| `-n 8192` | 8K Tokens | Reviews müssen nicht länger sein. Spart Inferenz-Zeit. | +| `-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. | @@ -236,19 +236,20 @@ Faustregel: KV-Cache ≈ `context_size × layers × head_dim × 2 × bytes_per_e 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 (q8_0) | Empfehlung | +| Kontext | KV-Cache (q4_0) | Empfehlung | |---|---|---| -| 32 768 | ~7,5 GB | 1 × 24-GB-GPU | -| 65 536 | ~15 GB | 2 × 16-GB-GPU | -| 131 072 | ~30 GB | 2 × 24-GB-GPU | +| 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 — **empfohlen** | -| `q4_0` | ~25 % | Sichtbarer Qualitätsverlust bei langen Kontexten | +| `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`) @@ -282,11 +283,32 @@ wenn pi agent Folgeanfragen schnell hintereinander schickt. | `/judge [fokus]` | Judge | Code-Review gegen TASK.md + letzten Commit | | `/fix [hinweis]` | Coder | Judge-Kritik beheben, committen | | `/shipit` | Judge | Finale Freigabeprüfung | -| `/optimize [--rounds N] [--with-doku]` | beide | Vollautomatische Schleife bis PASS | -| `/optimize --continue [--rounds N] [--with-doku]` | beide | Judge→Fix-Schleife ab aktuellem Stand (überspringt Implementierung) | +| `/optimize [--rounds N] [--with-doku] [--continue]` | beide | Vollautomatische Schleife bis PASS | +| `/optimize ... [--test-cmd "cmd"] [--test-timeout N]` | beide | Externe Test-Suite im Loop ausführen | | `/patch <änderung>` | Coder | Gezielte Minimaländerung ohne Review | | `/quick_check [was]` | Judge | Schnelle Prüfung der letzten Änderung | +| `/version` | — | Versionsnummer erhöhen (SemVer + Git-Tag) | | `/update_doku` | Coder | Code kommentieren + README + Bedienungsanleitung | +| `/plan ` | Coder | Implementierungsplan in PLAN.md (kein Code) | +| `/continue` | Coder | Unterbrochenen Prozess fortsetzen | +| `/cancel` | — | Laufenden Loop nach aktuellem Schritt abbrechen | | `/new_project ` | — | Neues Projektverzeichnis + git init | Ausführliche Beschreibung aller Kommandos mit Beispielen: siehe **BEDIENUNGSANLEITUNG.md**. + +--- + +## Live-Aktivitätsstatus + +Während der Ausführung zeigt pi_coder in der Statuszeile, was gerade passiert: + +| Situation | Anzeige | +|---|---| +| Coder implementiert | `Coder implementiert…` | +| edit-Tool aktiv | `Editiere src/main.py…` | +| git commit | `Git-Commit…` | +| Judge reviewt (Runde 2/3) | `Judge reviewt (Runde 2/3)…` | +| Tests laufen | `Tests laufen…` | +| Fix-Phase | `Coder fixt Blocker…` | + +So ist jederzeit erkennbar, in welcher Phase sich der automatische Loop befindet. From 5dee5f25e400e805d14627ac3dd6bd922954a1a1 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Sat, 23 May 2026 01:36:02 +0200 Subject: [PATCH 21/32] feat: Erfolgsmeldung + Auto-Commit nach SHIP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pi-coder-judge-extension.ts | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pi-coder-judge-extension.ts b/pi-coder-judge-extension.ts index 1f76ae0..bc0a698 100644 --- a/pi-coder-judge-extension.ts +++ b/pi-coder-judge-extension.ts @@ -784,6 +784,27 @@ async function runVersionBump(pi: ExtensionAPI, ctx: ExtensionCommandContext): P ctx.ui.notify(`Version ${newTag} getaggt.`, "info"); } +// Committed alle ungespeicherten Änderungen nach SHIP — Sicherheitsnetz falls der LLM es vergessen hat. +async function autoCommitIfDirty(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise { + const status = await pi.exec("bash", ["-c", "git status --porcelain"], { cwd: ctx.cwd }); + if ((status.stdout ?? "").trim()) { + await pi.exec( + "bash", + ["-c", "git add -A && git commit -m 'chore: Abschluss-Commit (produktionsreif)'"], + { cwd: ctx.cwd } + ); + } +} + +// Zeigt die abschließende Erfolgsmeldung nach SHIP. +// "info" ist der einzige verfügbare positive Notification-Level in der pi-API. +function notifyShipSuccess(ctx: ExtensionCommandContext): void { + ctx.ui.notify( + "✅ Fertig! Das Programm ist jetzt produktionsreif und committed.", + "info" + ); +} + // Prominente Abschluss-Notification + Widget-Update mit Uhrzeit und Ergebnis. function finalNotify( ctx: ExtensionCommandContext, @@ -965,6 +986,14 @@ export default function (pi: ExtensionAPI) { ctx.ui.notify("Judge prüft finale Freigabe — Ergebnis erscheint im Chat (SHIP / NO-SHIP)", "info"); currentActivity = "Judge: finale Freigabe…"; await sendAndWait(pi, ctx, shipitPrompt(args || "")); + const shipText = getLastAssistantText(ctx); + const shipVerdict = shipText.match(/Urteil:\s*(SHIP|NO-SHIP)/i)?.[1]?.toUpperCase() ?? ""; + if (shipVerdict === "SHIP") { + await autoCommitIfDirty(pi, ctx); + notifyShipSuccess(ctx); + } else if (shipVerdict === "NO-SHIP") { + ctx.ui.notify("NO-SHIP — noch Blocker offen. Bitte /fix aufrufen.", "error"); + } } }); @@ -1138,6 +1167,8 @@ export default function (pi: ExtensionAPI) { if (shipVerdict === "SHIP") { ctx.ui.setStatus("optimize", "🚀 SHIP – produktionsreif"); + await autoCommitIfDirty(pi, ctx); + notifyShipSuccess(ctx); finalNotify(ctx, "🚀 SHIP", "Programm ist produktionsreif"); await runVersionBump(pi, ctx); if (withDoku) { From 11ac46e56526af7ee5147c307c37d8b2bfbd27b3 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Fri, 29 May 2026 17:51:54 +0200 Subject: [PATCH 22/32] feat: --interactive-Checkpoint, direktes SHIP bei PASS, default rounds 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /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 --- BEDIENUNGSANLEITUNG.md | 94 +++++++++++++-- CLAUDE.md | 10 +- README.md | 7 +- pi-coder-judge-extension.ts | 222 +++++++++++++++++++++++++----------- 4 files changed, 251 insertions(+), 82 deletions(-) diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 08e202a..20c52ab 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -13,7 +13,7 @@ einfache Slash-Kommandos in der pi-Agent-Oberfläche. 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) +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) @@ -278,14 +278,16 @@ Empfohlene Sofortmaßnahmen: keine ### Syntax ``` -/optimize [--rounds N] [--with-doku] [--continue] +/optimize [--rounds N] [--with-doku] [--continue] [--interactive] ``` -- `--rounds N` — maximale Anzahl Runden (Standard: 3) +- `--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. +- `--interactive` — pausiert nach erstem PASS für einen menschlichen Checkpoint. + Details: siehe [Interactive-Modus](#interactive-modus) weiter unten. ### Beispiel: einfacher Auftrag @@ -296,19 +298,21 @@ Empfohlene Sofortmaßnahmen: keine Was im Hintergrund passiert: ``` Phase 1: Coder implementiert... -Phase 2: Runde 1/3: Judge prüft... +Phase 2: Runde 1/2: Judge prüft... → Urteil: FAIL (2 Blocker) -Phase 3: Runde 1/3: Coder fixt... -Phase 4: Runde 2/3: Judge prüft... +Phase 3: Runde 1/2: Coder fixt... +Phase 4: Runde 2/2: Judge prüft... → Urteil: PASS WITH CONCERNS ✓ PASS WITH CONCERNS nach Runde 2 -Finale ShipIt-Prüfung... +Finale ShipIt-Prüfung... (nur bei PASS WITH CONCERNS) → SHIP [Dialog: Version → v0.1.0 (empfohlen)] ``` +Bei klarem `PASS` entfällt die ShipIt-Runde — es wird direkt SHIP ausgelöst. + Während des Ablaufs zeigt die Statuszeile immer die aktuelle Aktivität: -`Coder implementiert…` → `Editiere src/main.rs…` → `Git-Commit…` → `Judge reviewt (Runde 1/3)…` +`Coder implementiert…` → `Editiere src/main.rs…` → `Git-Commit…` → `Judge reviewt (Runde 1/2)…` ### Beispiel: mehr Runden @@ -359,10 +363,57 @@ In diesem Fall: `/judge` manuell ausführen, Blocker lesen, mit `/fix` manuell e ### Max. Runden ohne PASS ``` -⚠ 3 Runden durchlaufen ohne PASS. Bitte manuell prüfen. +⚠ 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 --- @@ -678,6 +729,28 @@ Die Checkboxen werden automatisch abgehakt: /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 @@ -750,7 +823,7 @@ bereit, das GNU `patch -p1` mit Fuzzy-Matching nutzt. Lies die Datei neu ein und wende die Änderungen als unified diff mit apply_patch an. ``` -### "Drei Runden ohne PASS" / Loop-Erkennung schlägt an +### "N Runden ohne PASS" / Loop-Erkennung schlägt an ``` ⚠ Derselbe Blocker tritt erneut auf – Schleife abgebrochen. @@ -767,6 +840,7 @@ 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 diff --git a/CLAUDE.md b/CLAUDE.md index fa0cd13..d685c82 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,9 +41,12 @@ Beide nutzen dasselbe GGUF (`Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS.g **Zentraler Ablauf in `/optimize`:** 1. `writeTaskMd()` → TASK.md anlegen 2. Coder: `coderKickoff()` → implementiert + committet -3. Loop (max. N Runden): Judge → `parseVerdict()` → PASS? → ShipIt. FAIL? → `parseBlockers()` → Fix → nächste Runde -4. Loop-Erkennung: gleicher Blocker zweimal → Abbruch (`finalNotify()`) -5. Optional: `runUpdateDoku()` bei `--with-doku` +3. Äußere `while(keepGoing)`-Schleife (für `--interactive`-Zusatzaufträge) +4. Loop (max. N Runden, Standard 2): Judge → `parseVerdict()` → PASS? → break. FAIL? → `parseBlockers()` → Fix → nächste Runde +5. Bei PASS + `--interactive`: Polling auf `interactiveContinueRequested`. Kein Zusatzauftrag → ShipIt. Zusatzauftrag → `coderKickoff()` → `keepGoing = true` +6. SHIP-Schritt: klares `PASS` → direkt SHIP (kein ShipIt-Call). `PASS WITH CONCERNS` → `shipitPrompt()` → SHIP/NO-SHIP +7. Loop-Erkennung: gleicher Blocker zweimal → Abbruch (`finalNotify()`) +8. 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. @@ -60,6 +63,7 @@ Kritische Felder bei llama-cpp-Providern: `contextWindow` muss mit dem `-c`-Para ## Wichtige Invarianten - **`cancelRequested`** ist eine modulare Variable — sie wird von `/cancel` gesetzt und nach jedem Loop-Schritt in `/optimize` geprüft und zurückgesetzt. +- **`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. diff --git a/README.md b/README.md index 7e88eed..5f5aed4 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,10 @@ Nutzer gibt Auftrag │ 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 @@ -283,7 +285,7 @@ wenn pi agent Folgeanfragen schnell hintereinander schickt. | `/judge [fokus]` | Judge | Code-Review gegen TASK.md + letzten Commit | | `/fix [hinweis]` | Coder | Judge-Kritik beheben, committen | | `/shipit` | Judge | Finale Freigabeprüfung | -| `/optimize [--rounds N] [--with-doku] [--continue]` | beide | Vollautomatische Schleife bis PASS | +| `/optimize [--rounds N] [--with-doku] [--continue] [--interactive]` | beide | Vollautomatische Schleife bis PASS (Standard: 2 Runden) | | `/optimize ... [--test-cmd "cmd"] [--test-timeout N]` | beide | Externe Test-Suite im Loop ausführen | | `/patch <änderung>` | Coder | Gezielte Minimaländerung ohne Review | | `/quick_check [was]` | Judge | Schnelle Prüfung der letzten Änderung | @@ -307,8 +309,9 @@ Während der Ausführung zeigt pi_coder in der Statuszeile, was gerade passiert: | Coder implementiert | `Coder implementiert…` | | edit-Tool aktiv | `Editiere src/main.py…` | | git commit | `Git-Commit…` | -| Judge reviewt (Runde 2/3) | `Judge reviewt (Runde 2/3)…` | +| Judge reviewt (Runde 2/2) | `Judge reviewt (Runde 2/2)…` | | Tests laufen | `Tests laufen…` | | Fix-Phase | `Coder fixt Blocker…` | +| Interactive-Pause (--interactive) | `⏸ PASS – warte auf /continue…` | So ist jederzeit erkennbar, in welcher Phase sich der automatische Loop befindet. diff --git a/pi-coder-judge-extension.ts b/pi-coder-judge-extension.ts index bc0a698..f40ce23 100644 --- a/pi-coder-judge-extension.ts +++ b/pi-coder-judge-extension.ts @@ -827,6 +827,9 @@ function finalNotify( // ── Extension ──────────────────────────────────────────────────────────────── let cancelRequested = false; +let interactivePauseActive = false; +let interactiveContinueRequested = false; +let interactivePauseTask = ""; let currentActivity = ""; // Working-Message für den aktuellen Command-Kontext // Erzeugt eine knappe Statuszeile aus Tool-Name und Argumenten. @@ -1000,12 +1003,13 @@ export default function (pi: ExtensionAPI) { // ── Automatische Optimierungsschleife ──────────────────────────────────── pi.registerCommand("optimize", { - description: "Coder→Judge→Fix-Schleife bis PASS. Tests werden automatisch erkannt und parallel ausgeführt. /optimize [--rounds N] [--with-doku] [--continue] [--test-cmd \"override\"] [--test-timeout N]", + description: "Coder→Judge→Fix-Schleife bis PASS (default 2 Runden). Klares PASS → direkt SHIP; PASS WITH CONCERNS → ShipIt-Runde. --interactive pausiert nach PASS für Zusatzaufträge via /continue. /optimize [--rounds N] [--with-doku] [--continue] [--interactive] [--test-cmd \"override\"] [--test-timeout N]", handler: async function (args: string, ctx: ExtensionCommandContext) { const roundsMatch = (args || "").match(/--rounds\s+(\d+)/); - const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 3; + const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 2; const withDoku = /--with-doku/.test(args || ""); const continueMode = /--continue/.test(args || ""); + const interactive = /--interactive/.test(args || ""); const testCmdMatch = (args || "").match(/--test-cmd\s+"([^"]+)"|--test-cmd\s+'([^']+)'|--test-cmd\s+(\S+)/); const testCmd: string | null = testCmdMatch ? (testCmdMatch[1] ?? testCmdMatch[2] ?? testCmdMatch[3]) : null; const testTimeoutMatch = (args || "").match(/--test-timeout\s+(\d+)/); @@ -1015,6 +1019,7 @@ export default function (pi: ExtensionAPI) { .replace(/--test-timeout\s+\d+/, "") .replace(/--with-doku/, "") .replace(/--continue/, "") + .replace(/--interactive/, "") .replace(/--test-cmd\s+"[^"]*"/, "") .replace(/--test-cmd\s+\S+/, "") .trim(); @@ -1085,76 +1090,144 @@ export default function (pi: ExtensionAPI) { let lastBlockers = ""; let verdict = ""; + let keepGoing = true; - // Schleife: Judge → (PASS? fertig : Fix → nächste Runde) - for (let round = 1; round <= maxRounds; round++) { - const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round); - if (!await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge")) { - finalNotify(ctx, "⛔ Modell-Fehler", "Judge-Modell (llama-cpp-judge) nicht verfügbar"); - return; - } + // Äußere Schleife für --interactive: nach PASS pausieren, Zusatzaufträge ermöglichen. + while (keepGoing) { + keepGoing = false; + verdict = ""; + lastBlockers = ""; - if (autoTestCmds.length > 0) { - const label = autoTestCmds.length === 1 - ? autoTestCmds[0].split(" ")[0] - : `${autoTestCmds.length} Suiten parallel`; - ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${label}, max. ${testTimeout}s)…`); - const testOutput = await runTestsParallel(pi, ctx, autoTestCmds, testTimeout); - ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge analysiert Test-Ergebnis…`); - currentActivity = `Judge reviewt (Runde ${round}/${maxRounds})…`; - await sendAndWait(pi, ctx, judgeWithTestsPrompt(testOutput, "")); - } else { - ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge — TASK.md + letzter Commit + Tests…`); - currentActivity = `Judge reviewt (Runde ${round}/${maxRounds})…`; - await sendAndWait(pi, ctx, judgePrompt("")); - } - if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Judge Runde ${round}`); return; } - - const judgeText = getLastAssistantText(ctx); - verdict = parseVerdict(judgeText); - - if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") { - await tickTaskMdStatus(pi, ctx, "Review bestanden (PASS)"); - ctx.ui.setStatus("optimize", `${"●".repeat(round)} ✓ ${verdict} nach Runde ${round}/${maxRounds} — ShipIt…`); - break; - } - - // Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen - const currentBlockers = parseBlockers(judgeText); - if (currentBlockers && currentBlockers === lastBlockers) { - ctx.ui.setStatus("optimize", `${prog} ⚠ Gleicher Blocker in Runde ${round} – manuelle Intervention nötig`); - finalNotify(ctx, "⚠ Schleife", "Gleicher Blocker zweimal – manuelle Intervention nötig"); - return; - } - lastBlockers = currentBlockers; - - if (round === maxRounds) { - ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)} ⚠ Max. ${maxRounds} Runden ohne PASS`); - if (verdict === "UNREADABLE") { - finalNotify(ctx, "⚠ Urteil unklar", `${maxRounds} Runden – Judge-Urteil nicht erkennbar, Antwort im Chat prüfen`); - } else { - finalNotify(ctx, "⚠ Kein PASS", `${maxRounds} Runden ohne PASS – bitte /judge und /fix manuell`); + // Schleife: Judge → (PASS? fertig : Fix → nächste Runde) + for (let round = 1; round <= maxRounds; round++) { + const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round); + if (!await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge")) { + finalNotify(ctx, "⛔ Modell-Fehler", "Judge-Modell (llama-cpp-judge) nicht verfügbar"); + return; } - return; + + if (autoTestCmds.length > 0) { + const label = autoTestCmds.length === 1 + ? autoTestCmds[0].split(" ")[0] + : `${autoTestCmds.length} Suiten parallel`; + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${label}, max. ${testTimeout}s)…`); + const testOutput = await runTestsParallel(pi, ctx, autoTestCmds, testTimeout); + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge analysiert Test-Ergebnis…`); + currentActivity = `Judge reviewt (Runde ${round}/${maxRounds})…`; + await sendAndWait(pi, ctx, judgeWithTestsPrompt(testOutput, "")); + } else { + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge — TASK.md + letzter Commit + Tests…`); + currentActivity = `Judge reviewt (Runde ${round}/${maxRounds})…`; + await sendAndWait(pi, ctx, judgePrompt("")); + } + if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Judge Runde ${round}`); return; } + + const judgeText = getLastAssistantText(ctx); + verdict = parseVerdict(judgeText); + + if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") { + await tickTaskMdStatus(pi, ctx, "Review bestanden (PASS)"); + const nextStep = interactive ? "warte auf /continue…" : "ShipIt…"; + ctx.ui.setStatus("optimize", `${"●".repeat(round)} ✓ ${verdict} nach Runde ${round}/${maxRounds} — ${nextStep}`); + break; + } + + // Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen + const currentBlockers = parseBlockers(judgeText); + if (currentBlockers && currentBlockers === lastBlockers) { + ctx.ui.setStatus("optimize", `${prog} ⚠ Gleicher Blocker in Runde ${round} – manuelle Intervention nötig`); + finalNotify(ctx, "⚠ Schleife", "Gleicher Blocker zweimal – manuelle Intervention nötig"); + return; + } + lastBlockers = currentBlockers; + + if (round === maxRounds) { + ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)} ⚠ Max. ${maxRounds} Runden ohne PASS`); + if (verdict === "UNREADABLE") { + finalNotify(ctx, "⚠ Urteil unklar", `${maxRounds} Runden – Judge-Urteil nicht erkennbar, Antwort im Chat prüfen`); + } else { + finalNotify(ctx, "⚠ Kein PASS", `${maxRounds} Runden ohne PASS – bitte /judge und /fix manuell`); + } + return; + } + + // Fix-Phase: Blocker-Preview aus Judge-Bericht anzeigen + const blockerHint = currentBlockers + ? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers) + : "Kritikpunkte aus Judge-Bericht"; + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Coder fixt — ${blockerHint}`); + if (!await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder")) { + finalNotify(ctx, "⛔ Modell-Fehler", "Coder-Modell (llama-cpp-coder) nicht verfügbar"); + return; + } + currentActivity = "Coder fixt Blocker…"; + await sendAndWait(pi, ctx, fixPrompt("")); + if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Fix Runde ${round}`); return; } } - // Fix-Phase: Blocker-Preview aus Judge-Bericht anzeigen - const blockerHint = currentBlockers - ? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers) - : "Kritikpunkte aus Judge-Bericht"; - ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Coder fixt — ${blockerHint}`); - if (!await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder")) { - finalNotify(ctx, "⛔ Modell-Fehler", "Coder-Modell (llama-cpp-coder) nicht verfügbar"); - return; + // Interactive-Modus: nach PASS pausieren und auf /continue warten (max 30 min). + // /continue ohne Args → direkt ShipIt. /continue "Zusatz" → Coder implementiert, Loop nochmal. + if (interactive && (verdict === "PASS" || verdict === "PASS WITH CONCERNS")) { + interactivePauseActive = true; + interactiveContinueRequested = false; + interactivePauseTask = ""; + + ctx.ui.setStatus("optimize", `⏸ ${verdict} – warte auf /continue…`); + ctx.ui.notify( + `✅ ${verdict} erreicht. Weitere Features? /continue "Zusatzauftrag" — oder /continue zum Shippern.`, + "info" + ); + + const waitStart = Date.now(); + while (!interactiveContinueRequested && !cancelRequested) { + if (Date.now() - waitStart > 30 * 60 * 1000) { + interactivePauseActive = false; + finalNotify(ctx, "⚠ Timeout", "30 min ohne /continue — abgebrochen"); + return; + } + await new Promise(r => setTimeout(r, 500)); + } + interactivePauseActive = false; + + if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", "Im Interactive-Modus"); return; } + + if (interactivePauseTask) { + // Zusatzauftrag: Coder implementiert, dann Judge-Loop erneut + const addPreview = interactivePauseTask.length > 50 + ? interactivePauseTask.slice(0, 47) + "…" + : interactivePauseTask; + ctx.ui.setStatus("optimize", `◉ Coder implementiert Zusatzauftrag: ${addPreview}`); + await writeTaskMd(pi, ctx, interactivePauseTask); + if (!await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder")) { + finalNotify(ctx, "⛔ Modell-Fehler", "Coder-Modell nicht verfügbar"); + return; + } + currentActivity = "Coder implementiert Zusatzauftrag…"; + await sendAndWait(pi, ctx, coderKickoff(interactivePauseTask)); + await tickTaskMdStatus(pi, ctx, "Implementierung"); + if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", "Nach Zusatz-Implementierung"); return; } + keepGoing = true; + continue; + } + // /continue ohne Args → direkt zu ShipIt (verdict bleibt PASS) } - currentActivity = "Coder fixt Blocker…"; - await sendAndWait(pi, ctx, fixPrompt("")); - if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Fix Runde ${round}`); return; } } - // Finale ShipIt-Prüfung nur bei PASS - if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") { - ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)}◉ ShipIt — SHIP oder NO-SHIP?…`); + // Finaler SHIP-Schritt: klares PASS → direkt SHIP ohne zweiten Inference-Aufruf. + // "PASS WITH CONCERNS" → ShipIt-Runde als finale Abwägung. + if (verdict === "PASS") { + ctx.ui.setStatus("optimize", "🚀 SHIP – produktionsreif"); + await autoCommitIfDirty(pi, ctx); + notifyShipSuccess(ctx); + finalNotify(ctx, "🚀 SHIP", "Programm ist produktionsreif"); + await runVersionBump(pi, ctx); + if (withDoku) { + await runUpdateDoku(pi, ctx); + } else { + ctx.ui.notify("Nächster Schritt: /update_doku für Code-Kommentare, README.md und BEDIENUNGSANLEITUNG.md", "info"); + } + } else if (verdict === "PASS WITH CONCERNS") { + ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)}◉ ShipIt — PASS WITH CONCERNS, finale Freigabe?…`); if (!await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge")) { finalNotify(ctx, "⛔ Modell-Fehler", "Judge-Modell (llama-cpp-judge) nicht verfügbar"); return; @@ -1187,8 +1260,11 @@ export default function (pi: ExtensionAPI) { } catch (e: any) { finalNotify(ctx, "⛔ Fehler", String(e?.message ?? e)); } finally { - // Sicherstellen dass cancelRequested nie in einen späteren /optimize-Aufruf leckt + // Sicherstellen dass keine Zustandsvariable in späteren /optimize-Aufruf leckt cancelRequested = false; + interactivePauseActive = false; + interactiveContinueRequested = false; + interactivePauseTask = ""; } } }); @@ -1352,8 +1428,20 @@ export default function (pi: ExtensionAPI) { }); pi.registerCommand("continue", { - description: "Nimmt unterbrochenen Prozess wieder auf — liest TASK.md, PLAN.md, git log und entscheidet den nächsten Schritt.", - handler: async function (_args: string, ctx: ExtensionCommandContext) { + description: "Im --interactive-Modus: bestätigt PASS und geht zu ShipIt — oder /continue \"Zusatz\" für weiteren Auftrag. Sonst: nimmt unterbrochenen Prozess wieder auf.", + handler: async function (args: string, ctx: ExtensionCommandContext) { + // Interactive-Pause-Handler: Signal an laufenden /optimize-Loop + if (interactivePauseActive) { + interactivePauseTask = (args || "").trim(); + interactiveContinueRequested = true; + const msg = interactivePauseTask + ? `Zusatzauftrag eingetragen: "${interactivePauseTask}" — Coder startet` + : "Fortfahren — ShipIt wird gestartet"; + ctx.ui.notify(msg, "info"); + return; + } + + // Standard-Verhalten: unterbrochenen Prozess wieder aufnehmen if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder")) { ctx.ui.notify("Coder-Server nicht bereit (Port 8001) — start-coder.sh ausführen", "error"); return; From 482d98fb63ff61dbd51ebdeaa45fde4f1731973e Mon Sep 17 00:00:00 2001 From: dschlueter Date: Fri, 29 May 2026 18:03:56 +0200 Subject: [PATCH 23/32] perf: Quick-Judge Runde 1, switchModel-Cache, Blocker-Normalisierung + weitere TAT-Optimierungen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pi-coder-judge-extension.ts | 151 +++++++++++++++++++++++++++--------- 1 file changed, 116 insertions(+), 35 deletions(-) diff --git a/pi-coder-judge-extension.ts b/pi-coder-judge-extension.ts index f40ce23..950949c 100644 --- a/pi-coder-judge-extension.ts +++ b/pi-coder-judge-extension.ts @@ -57,6 +57,53 @@ function judgePrompt(extra: string): string { ].join("\n") + suffix; } +// Kompakter Ersteindruck-Prompt für Runde 1: kein TASK.md, nur Diff-Review. +// Reduziert Inference-Zeit wenn der Code offensichtlich gut ist. +// Bei FAIL → Runde 2 mit vollem judgePrompt() für detaillierte Analyse. +function quickJudgePrompt(extra: string): string { + const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : ""; + return [ + "Schneller Code-Review — erster Eindruck.", + "Du bist ein skeptischer Senior-Reviewer. Sei direkt und knapp.", + "", + "1. Sieh dir 'git show HEAD' an.", + "2. Führe relevante Tests aus, falls vorhanden.", + "3. Gibt es offensichtliche Blocker? (Bugs, fehlende Fehlerbehandlung, Sicherheitslücken, kaputte Imports)", + "4. Wenn alles offensichtlich in Ordnung ist: PASS.", + "5. Bei Zweifeln oder Lücken: FAIL — konkrete Blocker benennen.", + "", + "Ausgabeformat (kompakt):", + "- Urteil: PASS | PASS WITH CONCERNS | FAIL", + "- Blocker (falls vorhanden)", + "- Konkrete Fix-Aufträge (falls FAIL)" + ].join("\n") + suffix; +} + +// Quick-Variante für Runde 1 mit bereits vorliegendem Test-Output. +function quickJudgeWithTestsPrompt(testOutput: string, extra: string): string { + const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : ""; + return [ + "Schneller Code-Review — erster Eindruck.", + "Du bist ein skeptischer Senior-Reviewer. Sei direkt und knapp.", + "", + "Die Test-Suite wurde bereits extern ausgeführt. Führe KEINE weiteren Tests aus.", + "", + "1. Sieh dir 'git show HEAD' an.", + "2. Analysiere das folgende Test-Ergebnis:", + "```", + testOutput, + "```", + "3. Gibt es offensichtliche Blocker? (Test-Failures, Bugs, Sicherheitslücken)", + "4. Wenn alles offensichtlich in Ordnung ist: PASS.", + "5. Bei Zweifeln: FAIL — konkrete Blocker benennen.", + "", + "Ausgabeformat (kompakt):", + "- Urteil: PASS | PASS WITH CONCERNS | FAIL", + "- Blocker (falls vorhanden)", + "- Konkrete Fix-Aufträge (falls FAIL)" + ].join("\n") + suffix; +} + // Wie judgePrompt, aber Tests werden NICHT vom Judge ausgeführt — // die Extension hat sie bereits extern gestartet und übergibt den Output. function judgeWithTestsPrompt(testOutput: string, extra: string): string { @@ -379,12 +426,15 @@ async function switchModel( provider: string, modelId: string ): Promise { + const key = `${provider}/${modelId}`; + if (key === currentModelKey) return true; const model = ctx.modelRegistry.find(provider, modelId); if (!model) { ctx.ui.notify(`Modell ${provider}/${modelId} nicht gefunden`, "error"); return false; } const ok = await pi.setModel(model); + if (ok !== false) currentModelKey = key; if (!ok) ctx.ui.notify(`Kein API-Key für ${modelId}`, "warning"); return ok !== false; } @@ -409,7 +459,7 @@ async function sendAndWait( await ctx.waitForIdle(); } } - await new Promise(r => setTimeout(r, 400)); + await new Promise(r => setTimeout(r, 150)); await ctx.waitForIdle(); } @@ -538,6 +588,12 @@ function getLastAssistantText(ctx: ExtensionCommandContext): string { // Extrahiert das Urteil aus einer Judge-Antwort. // "UNREADABLE" wenn kein Urteil erkennbar — unterscheidbar von einem expliziten FAIL. +// Normalisiert Blocker-Text für die Loop-Erkennung — verhindert False-Negatives +// durch minimale Formulierungsunterschiede im Judge-Output (Whitespace, Satzzeichen). +function normalizeForComparison(s: string): string { + return s.trim().replace(/\s+/g, " ").replace(/[.,;:!?]+$/g, "").toLowerCase(); +} + function parseVerdict(text: string): string { const m = text.match(/Urteil:\s*(PASS WITH CONCERNS|PASS|FAIL)/i); return m ? m[1].toUpperCase() : "UNREADABLE"; @@ -827,6 +883,7 @@ function finalNotify( // ── Extension ──────────────────────────────────────────────────────────────── let cancelRequested = false; +let currentModelKey = ""; // Cache für switchModel() — verhindert redundante setModel()-Aufrufe let interactivePauseActive = false; let interactiveContinueRequested = false; let interactivePauseTask = ""; @@ -1003,13 +1060,15 @@ export default function (pi: ExtensionAPI) { // ── Automatische Optimierungsschleife ──────────────────────────────────── pi.registerCommand("optimize", { - description: "Coder→Judge→Fix-Schleife bis PASS (default 2 Runden). Klares PASS → direkt SHIP; PASS WITH CONCERNS → ShipIt-Runde. --interactive pausiert nach PASS für Zusatzaufträge via /continue. /optimize [--rounds N] [--with-doku] [--continue] [--interactive] [--test-cmd \"override\"] [--test-timeout N]", + description: "Coder→Judge→Fix-Schleife bis PASS (default 2 Runden, Runde 1: Quick-Judge). Klares PASS → direkt SHIP; PASS WITH CONCERNS → ShipIt-Runde (oder --approve-concerns zum Überspringen). --interactive: Checkpoint nach PASS. --no-tests: Test-Erkennung überspringen. /optimize [--rounds N] [--with-doku] [--continue] [--interactive] [--no-tests] [--approve-concerns] [--test-cmd \"override\"] [--test-timeout N]", handler: async function (args: string, ctx: ExtensionCommandContext) { const roundsMatch = (args || "").match(/--rounds\s+(\d+)/); const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 2; const withDoku = /--with-doku/.test(args || ""); const continueMode = /--continue/.test(args || ""); const interactive = /--interactive/.test(args || ""); + const noTests = /--no-tests/.test(args || ""); + const approveConcerns = /--approve-concerns/.test(args || ""); const testCmdMatch = (args || "").match(/--test-cmd\s+"([^"]+)"|--test-cmd\s+'([^']+)'|--test-cmd\s+(\S+)/); const testCmd: string | null = testCmdMatch ? (testCmdMatch[1] ?? testCmdMatch[2] ?? testCmdMatch[3]) : null; const testTimeoutMatch = (args || "").match(/--test-timeout\s+(\d+)/); @@ -1020,6 +1079,8 @@ export default function (pi: ExtensionAPI) { .replace(/--with-doku/, "") .replace(/--continue/, "") .replace(/--interactive/, "") + .replace(/--no-tests/, "") + .replace(/--approve-concerns/, "") .replace(/--test-cmd\s+"[^"]*"/, "") .replace(/--test-cmd\s+\S+/, "") .trim(); @@ -1040,13 +1101,20 @@ export default function (pi: ExtensionAPI) { : `--continue: Überspringe Implementierung, starte direkt mit Judge-Prüfung.`; ctx.ui.notify(continueMsg, "info"); - // Im --continue-Modus: Coder-Server jetzt prüfen, da er für die Fix-Phase gebraucht wird - // (in normalem Modus wird er beim coderKickoff implizit geprüft) - ctx.ui.setStatus("optimize", "Coder-Server wird geprüft…"); - if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder")) { + // Im --continue-Modus: beide Server parallel prüfen — spart bis zu 3 min bei Kaltstart. + ctx.ui.setStatus("optimize", "Coder- und Judge-Server werden geprüft (parallel)…"); + const [coderReady, judgeReady] = await Promise.all([ + waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder"), + waitUntilModelReady(pi, ctx, 8002, "qwen3.5-judge"), + ]); + if (!coderReady) { finalNotify(ctx, "⛔ Coder nicht erreichbar", "Port 8001 — kein HTTP 200 nach 3 min. start-coder.sh ausführen"); return; } + if (!judgeReady) { + finalNotify(ctx, "⛔ Judge nicht erreichbar", "Port 8002 — kein HTTP 200 nach 3 min. start-judge.sh ausführen"); + return; + } } else { // TASK.md anlegen und Implementierung starten await writeTaskMd(pi, ctx, task); @@ -1061,31 +1129,34 @@ export default function (pi: ExtensionAPI) { await sendAndWait(pi, ctx, coderKickoff(task)); await tickTaskMdStatus(pi, ctx, "Implementierung"); if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", "Nach Implementierung"); return; } + + // Judge-Bereitschaft via Completion-Check — /health antwortet bereits während des + // GPU-Ladevorgangs und ist kein verlässliches Signal. Nur HTTP 200 auf einen + // echten Completion-Request bedeutet: Modell ist im VRAM und bereit. + ctx.ui.setStatus("optimize", "Judge-Server wird geprüft…"); + if (!await waitUntilModelReady(pi, ctx, 8002, "qwen3.5-judge")) { + finalNotify(ctx, "⛔ Judge nicht erreichbar", "Port 8002 — kein HTTP 200 nach 3 min. start-judge.sh ausführen"); + return; + } } - // Judge-Bereitschaft via Completion-Check — /health antwortet bereits während des - // GPU-Ladevorgangs und ist kein verlässliches Signal. Nur HTTP 200 auf einen - // echten Completion-Request bedeutet: Modell ist im VRAM und bereit. - ctx.ui.setStatus("optimize", "Judge-Server wird geprüft…"); - if (!await waitUntilModelReady(pi, ctx, 8002, "qwen3.5-judge")) { - finalNotify(ctx, "⛔ Judge nicht erreichbar", "Port 8002 — kein HTTP 200 nach 3 min. start-judge.sh ausführen"); - return; - } - - // Test-Suiten einmalig ermitteln: --test-cmd überschreibt Auto-Erkennung. + // Test-Suiten ermitteln: --no-tests überspringt alles, --test-cmd überschreibt Auto-Erkennung. // Läuft nach Coder, damit neu angelegte Test-Dateien bereits erkannt werden. - ctx.ui.setStatus("optimize", "Test-Suiten werden erkannt…"); - const autoTestCmds: string[] = testCmd - ? [testCmd] - : await detectTestCommands(pi, ctx); - if (autoTestCmds.length > 0) { - const label = autoTestCmds.map(c => c.split(" ")[0]).join(", "); - ctx.ui.notify( - `${autoTestCmds.length} Test-Suite${autoTestCmds.length > 1 ? "n" : ""} erkannt: ${label}`, - "info" - ); + let autoTestCmds: string[] = []; + if (noTests) { + ctx.ui.notify("--no-tests: Test-Erkennung übersprungen.", "info"); } else { - ctx.ui.notify("Keine Test-Suiten erkannt — Judge führt Tests selbst aus.", "info"); + ctx.ui.setStatus("optimize", "Test-Suiten werden erkannt…"); + autoTestCmds = testCmd ? [testCmd] : await detectTestCommands(pi, ctx); + if (autoTestCmds.length > 0) { + const label = autoTestCmds.map(c => c.split(" ")[0]).join(", "); + ctx.ui.notify( + `${autoTestCmds.length} Test-Suite${autoTestCmds.length > 1 ? "n" : ""} erkannt: ${label}`, + "info" + ); + } else { + ctx.ui.notify("Keine Test-Suiten erkannt — Judge führt Tests selbst aus.", "info"); + } } let lastBlockers = ""; @@ -1106,19 +1177,26 @@ export default function (pi: ExtensionAPI) { return; } + // Runde 1 ohne --continue: Quick-Judge (kein TASK.md, kürzerer Prompt). + // Bei FAIL folgt Runde 2 mit vollem judgePrompt für detaillierte Analyse. + const useQuickJudge = round === 1 && !continueMode; if (autoTestCmds.length > 0) { const label = autoTestCmds.length === 1 ? autoTestCmds[0].split(" ")[0] : `${autoTestCmds.length} Suiten parallel`; ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${label}, max. ${testTimeout}s)…`); const testOutput = await runTestsParallel(pi, ctx, autoTestCmds, testTimeout); - ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge analysiert Test-Ergebnis…`); + const judgeLabel = useQuickJudge ? "Quick-Check" : "Judge analysiert"; + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: ${judgeLabel} Test-Ergebnis…`); currentActivity = `Judge reviewt (Runde ${round}/${maxRounds})…`; - await sendAndWait(pi, ctx, judgeWithTestsPrompt(testOutput, "")); + await sendAndWait(pi, ctx, useQuickJudge + ? quickJudgeWithTestsPrompt(testOutput, "") + : judgeWithTestsPrompt(testOutput, "")); } else { - ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge — TASK.md + letzter Commit + Tests…`); + const judgeLabel = useQuickJudge ? "Quick-Check" : "Judge — TASK.md + letzter Commit + Tests"; + ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: ${judgeLabel}…`); currentActivity = `Judge reviewt (Runde ${round}/${maxRounds})…`; - await sendAndWait(pi, ctx, judgePrompt("")); + await sendAndWait(pi, ctx, useQuickJudge ? quickJudgePrompt("") : judgePrompt("")); } if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Judge Runde ${round}`); return; } @@ -1132,9 +1210,10 @@ export default function (pi: ExtensionAPI) { break; } - // Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen + // Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen. + // Normalisierung verhindert False-Negatives durch minimale Formulierungsunterschiede. const currentBlockers = parseBlockers(judgeText); - if (currentBlockers && currentBlockers === lastBlockers) { + if (currentBlockers && normalizeForComparison(currentBlockers) === normalizeForComparison(lastBlockers)) { ctx.ui.setStatus("optimize", `${prog} ⚠ Gleicher Blocker in Runde ${round} – manuelle Intervention nötig`); finalNotify(ctx, "⚠ Schleife", "Gleicher Blocker zweimal – manuelle Intervention nötig"); return; @@ -1214,8 +1293,9 @@ export default function (pi: ExtensionAPI) { } // Finaler SHIP-Schritt: klares PASS → direkt SHIP ohne zweiten Inference-Aufruf. - // "PASS WITH CONCERNS" → ShipIt-Runde als finale Abwägung. - if (verdict === "PASS") { + // "PASS WITH CONCERNS" + --approve-concerns → direkt SHIP (ShipIt-Runde überspringen). + // "PASS WITH CONCERNS" ohne Flag → ShipIt-Runde als finale Abwägung. + if (verdict === "PASS" || (verdict === "PASS WITH CONCERNS" && approveConcerns)) { ctx.ui.setStatus("optimize", "🚀 SHIP – produktionsreif"); await autoCommitIfDirty(pi, ctx); notifyShipSuccess(ctx); @@ -1262,6 +1342,7 @@ export default function (pi: ExtensionAPI) { } finally { // Sicherstellen dass keine Zustandsvariable in späteren /optimize-Aufruf leckt cancelRequested = false; + currentModelKey = ""; interactivePauseActive = false; interactiveContinueRequested = false; interactivePauseTask = ""; From cba70b67a1eb7d2b4c1b95a2a91d2da7f29cf6ea Mon Sep 17 00:00:00 2001 From: dschlueter Date: Fri, 29 May 2026 18:08:00 +0200 Subject: [PATCH 24/32] docs: neue Flags, Quick-Judge und parallele Server-Checks dokumentiert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - --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 --- BEDIENUNGSANLEITUNG.md | 41 +++++++++++++++++++++++++++++++++++++---- CLAUDE.md | 21 ++++++++++++++------- README.md | 4 ++-- 3 files changed, 53 insertions(+), 13 deletions(-) diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 20c52ab..9adb373 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -279,6 +279,7 @@ Empfohlene Sofortmaßnahmen: keine ``` /optimize [--rounds N] [--with-doku] [--continue] [--interactive] + [--no-tests] [--approve-concerns] [--test-cmd "cmd"] [--test-timeout N] ``` - `--rounds N` — maximale Anzahl Runden (Standard: 2) @@ -286,8 +287,16 @@ Empfohlene Sofortmaßnahmen: keine - `--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 @@ -298,21 +307,25 @@ Empfohlene Sofortmaßnahmen: keine Was im Hintergrund passiert: ``` Phase 1: Coder implementiert... -Phase 2: Runde 1/2: Judge prüft... +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 prüft... +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) +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…` → `Editiere src/main.rs…` → `Git-Commit…` → `Judge reviewt (Runde 1/2)…` +`Coder implementiert…` → `Quick-Check…` → `Coder fixt Blocker…` → `Judge reviewt (Runde 2/2)…` ### Beispiel: mehr Runden @@ -694,6 +707,26 @@ Die Checkboxen werden automatisch abgehakt: /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 diff --git a/CLAUDE.md b/CLAUDE.md index d685c82..cde1fd7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,13 +40,17 @@ Beide nutzen dasselbe GGUF (`Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS.g **Zentraler Ablauf in `/optimize`:** 1. `writeTaskMd()` → TASK.md anlegen -2. Coder: `coderKickoff()` → implementiert + committet -3. Äußere `while(keepGoing)`-Schleife (für `--interactive`-Zusatzaufträge) -4. Loop (max. N Runden, Standard 2): Judge → `parseVerdict()` → PASS? → break. FAIL? → `parseBlockers()` → Fix → nächste Runde -5. Bei PASS + `--interactive`: Polling auf `interactiveContinueRequested`. Kein Zusatzauftrag → ShipIt. Zusatzauftrag → `coderKickoff()` → `keepGoing = true` -6. SHIP-Schritt: klares `PASS` → direkt SHIP (kein ShipIt-Call). `PASS WITH CONCERNS` → `shipitPrompt()` → SHIP/NO-SHIP -7. Loop-Erkennung: gleicher Blocker zweimal → Abbruch (`finalNotify()`) -8. Optional: `runUpdateDoku()` bei `--with-doku` +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. @@ -63,6 +67,9 @@ Kritische Felder bei llama-cpp-Providern: `contextWindow` muss mit dem `-c`-Para ## 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). diff --git a/README.md b/README.md index 5f5aed4..17e3c52 100644 --- a/README.md +++ b/README.md @@ -285,8 +285,8 @@ wenn pi agent Folgeanfragen schnell hintereinander schickt. | `/judge [fokus]` | Judge | Code-Review gegen TASK.md + letzten Commit | | `/fix [hinweis]` | Coder | Judge-Kritik beheben, committen | | `/shipit` | Judge | Finale Freigabeprüfung | -| `/optimize [--rounds N] [--with-doku] [--continue] [--interactive]` | beide | Vollautomatische Schleife bis PASS (Standard: 2 Runden) | -| `/optimize ... [--test-cmd "cmd"] [--test-timeout N]` | beide | Externe Test-Suite im Loop ausführen | +| `/optimize [--rounds N] [--with-doku] [--continue] [--interactive]` | beide | Vollautomatische Schleife bis PASS (Standard: 2 Runden, Runde 1: Quick-Judge) | +| `/optimize ... [--no-tests] [--approve-concerns] [--test-cmd "cmd"] [--test-timeout N]` | beide | Test-Erkennung überspringen / PASS WITH CONCERNS direkt shippern | | `/patch <änderung>` | Coder | Gezielte Minimaländerung ohne Review | | `/quick_check [was]` | Judge | Schnelle Prüfung der letzten Änderung | | `/version` | — | Versionsnummer erhöhen (SemVer + Git-Tag) | From 7b13c4996d9b2de7c00f8c7e2d50ed7e30fe47ae Mon Sep 17 00:00:00 2001 From: dschlueter Date: Fri, 29 May 2026 18:14:03 +0200 Subject: [PATCH 25/32] =?UTF-8?q?test:=20Unit-Tests=20f=C3=BCr=20normalize?= =?UTF-8?q?ForComparison,=20parseVerdict,=20parseBlockers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- run-tests.sh | 27 +++++++ test-utils.ts | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100755 run-tests.sh create mode 100644 test-utils.ts diff --git a/run-tests.sh b/run-tests.sh new file mode 100755 index 0000000..344b718 --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Unit-Tests für pi-coder-judge-extension.ts +# Keine Abhängigkeiten außer node — entfernt TypeScript-Annotationen on-the-fly. + +set -euo pipefail + +TS_FILE="$(dirname "$0")/test-utils.ts" + +if ! command -v node &>/dev/null; then + echo "❌ node nicht gefunden" >&2 + exit 1 +fi + +# TypeScript-Annotationen entfernen: `: string`, `: unknown`, `: void`, `: boolean` +# und `function f(a: T, b: T)` → `function f(a, b)` (einfache Parameterlisten) +node --input-type=module < <( + sed \ + -e 's/: string\b//g' \ + -e 's/: unknown\b//g' \ + -e 's/: void\b//g' \ + -e 's/: boolean\b//g' \ + -e 's/: number\b//g' \ + -e 's/(s: )/(s)/g' \ + -e 's/(text: )/(text)/g' \ + -e 's/(actual, expected: unknown, label: string)/( actual, expected, label)/g' \ + "$TS_FILE" +) diff --git a/test-utils.ts b/test-utils.ts new file mode 100644 index 0000000..cf3f369 --- /dev/null +++ b/test-utils.ts @@ -0,0 +1,193 @@ +// 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" +); + +// ── Ergebnis ────────────────────────────────────────────────────────────────── + +console.log(`\n${"─".repeat(50)}`); +console.log(`Gesamt: ${passed + failed} Tests — ${passed} bestanden, ${failed} fehlgeschlagen`); + +if (failed > 0) { + process.exit(1); +} From fb4e96919af6172753901286c02e6d08277cc6e5 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Fri, 29 May 2026 19:06:29 +0200 Subject: [PATCH 26/32] test: Unit-Tests auf 97 erweitern (toolExecutionLabel, getLastAssistantText, SemVer, Flags, ShipVerdict) Co-Authored-By: Claude Sonnet 4.6 --- test-utils.ts | 323 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 323 insertions(+) diff --git a/test-utils.ts b/test-utils.ts index cf3f369..3199b52 100644 --- a/test-utils.ts +++ b/test-utils.ts @@ -183,6 +183,329 @@ expect( "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)}`); From 64c2b7f0fd328644bae51922a4a0a02302349722 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Fri, 29 May 2026 19:06:36 +0200 Subject: [PATCH 27/32] feat: Demo-Examples (Python/Rust/Go/C) mit Protokoll-Templates und Restore-Skript Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 7 ++ examples/README.md | 74 +++++++++++++++++++ examples/c-linkedlist/PROTOKOLL.md | 35 +++++++++ examples/c-linkedlist/README.md | 48 ++++++++++++ examples/c-linkedlist/linked_list.c | 43 +++++++++++ examples/c-linkedlist/linked_list.h | 16 ++++ examples/c-linkedlist/main.c | 16 ++++ examples/go-fibonacci/PROTOKOLL.md | 37 ++++++++++ examples/go-fibonacci/README.md | 48 ++++++++++++ examples/go-fibonacci/go.mod | 3 + examples/go-fibonacci/main.go | 18 +++++ examples/go-fibonacci/main_test.go | 20 +++++ examples/python-calculator/PROTOKOLL.md | 19 +++++ examples/python-calculator/README.md | 51 +++++++++++++ examples/python-calculator/calculator.py | 11 +++ examples/python-calculator/test_calculator.py | 18 +++++ examples/restore-all.sh | 31 ++++++++ examples/rust-wordcount/Cargo.toml | 4 + examples/rust-wordcount/PROTOKOLL.md | 29 ++++++++ examples/rust-wordcount/README.md | 50 +++++++++++++ examples/rust-wordcount/src/main.rs | 36 +++++++++ 21 files changed, 614 insertions(+) create mode 100644 examples/README.md create mode 100644 examples/c-linkedlist/PROTOKOLL.md create mode 100644 examples/c-linkedlist/README.md create mode 100644 examples/c-linkedlist/linked_list.c create mode 100644 examples/c-linkedlist/linked_list.h create mode 100644 examples/c-linkedlist/main.c create mode 100644 examples/go-fibonacci/PROTOKOLL.md create mode 100644 examples/go-fibonacci/README.md create mode 100644 examples/go-fibonacci/go.mod create mode 100644 examples/go-fibonacci/main.go create mode 100644 examples/go-fibonacci/main_test.go create mode 100644 examples/python-calculator/PROTOKOLL.md create mode 100644 examples/python-calculator/README.md create mode 100644 examples/python-calculator/calculator.py create mode 100644 examples/python-calculator/test_calculator.py create mode 100755 examples/restore-all.sh create mode 100644 examples/rust-wordcount/Cargo.toml create mode 100644 examples/rust-wordcount/PROTOKOLL.md create mode 100644 examples/rust-wordcount/README.md create mode 100644 examples/rust-wordcount/src/main.rs diff --git a/.gitignore b/.gitignore index c99ffd2..59fc5e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ *.bak node_modules/ .DS_Store + +# Build-Artefakte (Examples) +examples/rust-wordcount/target/ +examples/**/__pycache__/ +examples/**/.pytest_cache/ +examples/**/Cargo.lock +examples/**/ll_demo diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..93f647c --- /dev/null +++ b/examples/README.md @@ -0,0 +1,74 @@ +# pi-coder Beispielprojekte + +Vier kleine, eigenständige Projekte als Demonstrationsgrundlage für die pi-coder-Features. +Jedes Projekt startet bewusst unvollständig — genau der Ausgangspunkt, für den pi-coder gebaut ist. + +## Übersicht + +| Verzeichnis | Sprache | Demonstriert | +|---|---|---| +| `python-calculator/` | Python | `/optimize` mit `--test-cmd pytest` | +| `rust-wordcount/` | Rust | `/optimize` mit `--test-cmd "cargo test"` + `/version` | +| `go-fibonacci/` | Go | `/optimize --interactive` + `/continue` + `/shipit` | +| `c-linkedlist/` | C | `/quick_check` + `/fix` + `/patch` | + +## Demo-Workflow + +### Schritt 1 — Vorbereitung: Sub-Repos anlegen + +Jedes Example braucht ein eigenes git-Repo, damit pi-coder commit-basierte +Features nutzen kann (Loop-Erkennung, Diff-Anzeige, `/version`): + +```bash +for dir in python-calculator rust-wordcount go-fibonacci c-linkedlist; do + cd examples/$dir + git init && git add -A && git commit -m "feat: initial $dir" + cd ../.. +done +``` + +Für `/version` im rust-wordcount-Beispiel zusätzlich: + +```bash +cd examples/rust-wordcount && git tag v0.1.0 +``` + +### Schritt 2 — Demo ausführen + +In pi das jeweilige Unterverzeichnis als Arbeitsverzeichnis öffnen. +Die genauen Befehle stehen im README.md des jeweiligen Examples. +Zeitmessung: Systemuhr notieren oder Terminal-Kommando `time` nutzen. + +### Schritt 3 — Protokoll ausfüllen + +Jedes Example enthält eine `PROTOKOLL.md`. +Startzeit, Endzeit, Rundenanzahl und Endergebnis eintragen. + +### Schritt 4 — Ausgangszustand wiederherstellen + +```bash +bash examples/restore-all.sh +``` + +Das Skript löscht Sub-Repos, restauriert alle Quelldateien aus dem Haupt-Repo +und bereinigt Build-Artefakte (`target/`, `__pycache__` etc.). + +--- + +## Empfohlene Demo-Reihenfolge + +| # | Beispiel | Geschätzte Dauer | Highlights | +|---|---|---|---| +| 1 | `python-calculator` | ~5–10 min | Einstieg, Test-Loop | +| 2 | `c-linkedlist` | ~5 min | `/quick_check` + `/fix`, kein Loop | +| 3 | `rust-wordcount` | ~10–15 min | Loop + `/version` | +| 4 | `go-fibonacci` | ~15–20 min | `--interactive` + `/shipit` | + +--- + +## Weitere Details + +[python-calculator](python-calculator/README.md) · +[rust-wordcount](rust-wordcount/README.md) · +[go-fibonacci](go-fibonacci/README.md) · +[c-linkedlist](c-linkedlist/README.md) diff --git a/examples/c-linkedlist/PROTOKOLL.md b/examples/c-linkedlist/PROTOKOLL.md new file mode 100644 index 0000000..939ddd9 --- /dev/null +++ b/examples/c-linkedlist/PROTOKOLL.md @@ -0,0 +1,35 @@ +# Demo-Protokoll: c-linkedlist + +## Lauf 1 + +**Datum:** + +**Befehl /quick_check:** +``` +/quick_check "Gibt es Speicherlecks oder sonstige Probleme in diesem C-Projekt?" +``` +**Startzeit:** +**Endzeit:** +**Dauer (min):** +**Ergebnis:** OK / PROBLEM (Kurzbeschreibung): + +**Befehl /fix:** +``` +/fix "Implementiere list_free() korrekt, sodass valgrind --leak-check=full sauber ist." +``` +**Startzeit:** +**Endzeit:** +**Dauer (min):** +**Ergebnis:** erledigt / fehlgeschlagen + +**Befehl /patch (optional):** +``` +/patch "Ergänze list_search(head, value) in Header und Implementierung. + Gibt den ersten Node* mit dem gesuchten Wert zurück, oder NULL." +``` +**Startzeit:** +**Endzeit:** +**Dauer (min):** +**Besonderheiten / Beobachtungen:** + +--- diff --git a/examples/c-linkedlist/README.md b/examples/c-linkedlist/README.md new file mode 100644 index 0000000..7861652 --- /dev/null +++ b/examples/c-linkedlist/README.md @@ -0,0 +1,48 @@ +# C Linked List + +Vollständige einfach-verkettete Liste — bis auf `list_free()`, das als leerer Stub vorliegt. +Jeder Programmlauf leckt den gesamten Listen-Speicher. + +## Aktueller Stand + +``` +linked_list.h Interface: node_new, list_prepend, list_append, list_print, list_free, list_length +linked_list.c Alles implementiert — außer list_free() (Stub, tut nichts) +main.c Baut Liste 1–5, gibt sie aus, ruft list_free() auf (ohne Wirkung) +``` + +## Demo 1: `/quick_check` als Diagnose + +``` +/quick_check "Gibt es Speicherlecks oder sonstige Probleme in diesem C-Projekt?" +``` + +Der Judge analysiert den Code und identifiziert das leere `list_free()` als Speicherleck-Quelle. + +## Demo 2: `/fix` für gezieltes Nacharbeiten + +``` +/fix "Implementiere list_free() korrekt, sodass valgrind --leak-check=full sauber ist." +``` + +Coder implementiert die Funktion, committet. Kein vollständiger Judge-Loop — +ideal für kleine, klar abgegrenzte Fixes. + +## Demo 3: `/patch` für Minimal-Erweiterungen + +``` +/patch "Ergänze list_search(head, value) in Header und Implementierung. + Gibt den ersten Node* mit dem gesuchten Wert zurück, oder NULL." +``` + +pi-coder wendet einen unified diff an (`apply_patch`-Tool), ohne den vollständigen Loop. + +## Manueller Build + +```bash +gcc -Wall -Wextra -o ll_demo linked_list.c main.c +./ll_demo + +# Mit Leak-Check: +valgrind --leak-check=full ./ll_demo +``` diff --git a/examples/c-linkedlist/linked_list.c b/examples/c-linkedlist/linked_list.c new file mode 100644 index 0000000..e8d62ca --- /dev/null +++ b/examples/c-linkedlist/linked_list.c @@ -0,0 +1,43 @@ +#include +#include +#include "linked_list.h" + +Node *node_new(int value) { + Node *n = malloc(sizeof(Node)); + n->value = value; + n->next = NULL; + return n; +} + +Node *list_prepend(Node *head, int value) { + Node *n = node_new(value); + n->next = head; + return n; +} + +Node *list_append(Node *head, int value) { + Node *n = node_new(value); + if (!head) return n; + Node *cur = head; + while (cur->next) cur = cur->next; + cur->next = n; + return head; +} + +void list_print(const Node *head) { + for (const Node *cur = head; cur; cur = cur->next) + printf("%d ", cur->value); + printf("\n"); +} + +/* BUG: Speicher wird nicht freigegeben — valgrind meldet Leaks. */ +void list_free(Node *head) { + (void)head; /* TODO: implementieren */ +} + +int list_length(const Node *head) { + int len = 0; + for (const Node *cur = head; cur; cur = cur->next) + len++; + return len; +} diff --git a/examples/c-linkedlist/linked_list.h b/examples/c-linkedlist/linked_list.h new file mode 100644 index 0000000..9dc46ac --- /dev/null +++ b/examples/c-linkedlist/linked_list.h @@ -0,0 +1,16 @@ +#ifndef LINKED_LIST_H +#define LINKED_LIST_H + +typedef struct Node { + int value; + struct Node *next; +} Node; + +Node *node_new(int value); +Node *list_prepend(Node *head, int value); +Node *list_append(Node *head, int value); +void list_print(const Node *head); +void list_free(Node *head); +int list_length(const Node *head); + +#endif diff --git a/examples/c-linkedlist/main.c b/examples/c-linkedlist/main.c new file mode 100644 index 0000000..1f2f3ad --- /dev/null +++ b/examples/c-linkedlist/main.c @@ -0,0 +1,16 @@ +#include +#include "linked_list.h" + +int main(void) { + Node *list = NULL; + + for (int i = 1; i <= 5; i++) + list = list_append(list, i); + + printf("Liste: "); + list_print(list); + printf("Länge: %d\n", list_length(list)); + + list_free(list); /* leckt wegen unvollständigem TODO */ + return 0; +} diff --git a/examples/go-fibonacci/PROTOKOLL.md b/examples/go-fibonacci/PROTOKOLL.md new file mode 100644 index 0000000..ef9a63b --- /dev/null +++ b/examples/go-fibonacci/PROTOKOLL.md @@ -0,0 +1,37 @@ +# Demo-Protokoll: go-fibonacci + +## Lauf 1 + +**Datum:** +**Befehl:** +``` +/optimize "Ersetze die naive Rekursion durch Memoization. + fib(50) soll in unter 1ms abgeschlossen sein. + Bestehende Tests müssen weiterhin grün bleiben." \ + --test-cmd "go test ./..." --interactive +``` +**Startzeit:** +**Ende Loop (PASS):** +**Dauer Loop (min):** +**Runden:** +**Endergebnis Loop:** PASS / PASS WITH CONCERNS + +**Befehl im --interactive-Checkpoint:** +``` +/continue "Gib zusätzlich die Berechnungszeit in Mikrosekunden aus." +``` +*(oder: `/continue` ohne Zusatzauftrag)* + +**Startzeit /continue:** +**Ende /continue:** + +**Befehl /shipit:** +``` +/shipit +``` +**Startzeit /shipit:** +**Endzeit /shipit:** +**Endergebnis /shipit:** SHIP / NO-SHIP +**Besonderheiten / Beobachtungen:** + +--- diff --git a/examples/go-fibonacci/README.md b/examples/go-fibonacci/README.md new file mode 100644 index 0000000..7d1cd94 --- /dev/null +++ b/examples/go-fibonacci/README.md @@ -0,0 +1,48 @@ +# 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 Memoization. + fib(50) soll in unter 1ms abgeschlossen sein. + Bestehende Tests müssen weiterhin grün bleiben." \ + --test-cmd "go test ./..." --interactive +``` + +Nach dem ersten PASS hält pi-coder im **interaktiven Checkpoint** an. +Hier kann ein Zusatzauftrag erteilt werden: + +``` +/continue "Gib zusätzlich die Berechnungszeit in Mikrosekunden aus." +``` + +Oder einfach bestätigen: + +``` +/continue +``` + +## Demo 2: Abschluss mit `/shipit` + +``` +/shipit +``` + +Der Judge prüft nochmals explizit auf Produktionsreife und gibt SHIP oder NO-SHIP zurück. + +## Manueller Test + +```bash +go test ./... +go run main.go +``` diff --git a/examples/go-fibonacci/go.mod b/examples/go-fibonacci/go.mod new file mode 100644 index 0000000..18dcbe5 --- /dev/null +++ b/examples/go-fibonacci/go.mod @@ -0,0 +1,3 @@ +module fibonacci + +go 1.21 diff --git a/examples/go-fibonacci/main.go b/examples/go-fibonacci/main.go new file mode 100644 index 0000000..4e08244 --- /dev/null +++ b/examples/go-fibonacci/main.go @@ -0,0 +1,18 @@ +package main + +import "fmt" + +// fib berechnet die n-te Fibonacci-Zahl rekursiv. +// Korrekt, aber für n > 40 sehr langsam (exponentiell). +func fib(n int) int { + if n <= 1 { + return n + } + return fib(n-1) + fib(n-2) +} + +func main() { + for i := 0; i <= 10; i++ { + fmt.Printf("fib(%2d) = %d\n", i, fib(i)) + } +} diff --git a/examples/go-fibonacci/main_test.go b/examples/go-fibonacci/main_test.go new file mode 100644 index 0000000..621cd25 --- /dev/null +++ b/examples/go-fibonacci/main_test.go @@ -0,0 +1,20 @@ +package main + +import "testing" + +func TestFib(t *testing.T) { + cases := []struct { + n, want int + }{ + {0, 0}, + {1, 1}, + {2, 1}, + {5, 5}, + {10, 55}, + } + for _, c := range cases { + if got := fib(c.n); got != c.want { + t.Errorf("fib(%d) = %d, want %d", c.n, got, c.want) + } + } +} diff --git a/examples/python-calculator/PROTOKOLL.md b/examples/python-calculator/PROTOKOLL.md new file mode 100644 index 0000000..b7e8b9f --- /dev/null +++ b/examples/python-calculator/PROTOKOLL.md @@ -0,0 +1,19 @@ +# Demo-Protokoll: python-calculator + +## Lauf 1 + +**Datum:** +**Befehl:** +``` +/optimize "Ergänze multiply, divide (wirft ZeroDivisionError bei 0) und power. + Schreibe pytest-Tests für alle neuen Funktionen." \ + --test-cmd "pytest test_calculator.py -v" +``` +**Startzeit:** +**Endzeit:** +**Dauer (min):** +**Runden:** +**Endergebnis:** PASS / PASS WITH CONCERNS / SHIP / NO-SHIP +**Besonderheiten / Beobachtungen:** + +--- diff --git a/examples/python-calculator/README.md b/examples/python-calculator/README.md new file mode 100644 index 0000000..23853d0 --- /dev/null +++ b/examples/python-calculator/README.md @@ -0,0 +1,51 @@ +# Python Calculator + +Einfacher Taschenrechner mit `add()` und `subtract()`. +Multiply, divide, power und Fehlerbehandlung fehlen noch. + +## Aktueller Stand + +``` +calculator.py add(), subtract() +test_calculator.py 5 pytest-Tests (alle grün) +``` + +## Demo: `/optimize` mit Test-Integration + +``` +/optimize "Ergänze multiply, divide (wirft ZeroDivisionError bei 0) und power. + Schreibe pytest-Tests für alle neuen Funktionen." \ + --test-cmd "pytest test_calculator.py -v" +``` + +**Was pi-coder hier zeigt:** +- Coder implementiert, committet +- Extension führt `pytest` aus und übergibt das Ergebnis an den Judge +- Judge bewertet Korrektheit anhand der Testergebnisse +- Bei FAIL: Coder fixt, nächste Runde + +## Voraussetzungen + +```bash +pip install pytest +``` + +## Weitere Demo-Befehle nach dem `/optimize`-Lauf + +``` +/quick_check "Sind alle Randfälle (negative Zahlen, floats) korrekt behandelt?" +``` +Schnelle Einzel-Beurteilung ohne neuen Fix-Loop. + +``` +/update_doku +``` +Lässt den Coder Code-Kommentare ergänzen, README aktualisieren und eine +Bedienungsanleitung erzeugen. + +## Manueller Test + +```bash +pytest test_calculator.py -v +python calculator.py +``` diff --git a/examples/python-calculator/calculator.py b/examples/python-calculator/calculator.py new file mode 100644 index 0000000..34fc2f0 --- /dev/null +++ b/examples/python-calculator/calculator.py @@ -0,0 +1,11 @@ +def add(a, b): + return a + b + + +def subtract(a, b): + return a - b + + +if __name__ == "__main__": + print(add(3, 4)) # 7 + print(subtract(10, 3)) # 7 diff --git a/examples/python-calculator/test_calculator.py b/examples/python-calculator/test_calculator.py new file mode 100644 index 0000000..20b9369 --- /dev/null +++ b/examples/python-calculator/test_calculator.py @@ -0,0 +1,18 @@ +import pytest +from calculator import add, subtract + + +def test_add_positive(): + assert add(2, 3) == 5 + +def test_add_negative(): + assert add(-1, 1) == 0 + +def test_add_zero(): + assert add(0, 0) == 0 + +def test_subtract_basic(): + assert subtract(5, 3) == 2 + +def test_subtract_negative_result(): + assert subtract(3, 5) == -2 diff --git a/examples/restore-all.sh b/examples/restore-all.sh new file mode 100755 index 0000000..e28c3ff --- /dev/null +++ b/examples/restore-all.sh @@ -0,0 +1,31 @@ +#!/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. + +set -euo pipefail + +ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)" +EXAMPLES="$ROOT/examples" + +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 + git -C "$ROOT" checkout -- "examples/$dir/" + echo " ✓ Dateien restauriert: $dir" +done + +# Build-Artefakte 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 + +echo "" +echo "Fertig. Alle Examples sind im Ausgangszustand." +echo "git status zeigt examples/ als clean (wenn kein PROTOKOLL.md verändert wurde)." diff --git a/examples/rust-wordcount/Cargo.toml b/examples/rust-wordcount/Cargo.toml new file mode 100644 index 0000000..2845fab --- /dev/null +++ b/examples/rust-wordcount/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "wordcount" +version = "0.1.0" +edition = "2021" diff --git a/examples/rust-wordcount/PROTOKOLL.md b/examples/rust-wordcount/PROTOKOLL.md new file mode 100644 index 0000000..762f940 --- /dev/null +++ b/examples/rust-wordcount/PROTOKOLL.md @@ -0,0 +1,29 @@ +# Demo-Protokoll: rust-wordcount + +## Lauf 1 + +**Datum:** +**Befehl:** +``` +/optimize "Ergänze --lines (Zeilenzählung) und --chars (Zeichenzählung) als CLI-Flags. + Ohne Flag: Standardausgabe wie bisher (Wörter). + Schreibe Tests für alle drei Modi." \ + --test-cmd "cargo test" +``` +**Startzeit:** +**Endzeit:** +**Dauer /optimize (min):** +**Runden:** +**Endergebnis /optimize:** PASS / PASS WITH CONCERNS / SHIP / NO-SHIP + +**Befehl /version:** +``` +/version +``` +**Startzeit /version:** +**Endzeit /version:** +**Gewählter Bump:** patch / minor / major +**Gesetzter Tag:** +**Besonderheiten / Beobachtungen:** + +--- diff --git a/examples/rust-wordcount/README.md b/examples/rust-wordcount/README.md new file mode 100644 index 0000000..69a6c10 --- /dev/null +++ b/examples/rust-wordcount/README.md @@ -0,0 +1,50 @@ +# Rust Word Counter + +Liest stdin und gibt die Anzahl der Wörter aus. +Zeilen- und Zeichenzählung sowie CLI-Flags fehlen noch. + +## Aktueller Stand + +``` +src/main.rs count_words() — nur Wortzählung, kein Argument-Parsing +Cargo.toml Version 0.1.0 +``` + +## Demo 1: `/optimize` mit Cargo-Test-Integration + +``` +/optimize "Ergänze --lines (Zeilenzählung) und --chars (Zeichenzählung) als CLI-Flags. + Ohne Flag: Standardausgabe wie bisher (Wörter). + Schreibe Tests für alle drei Modi." \ + --test-cmd "cargo test" +``` + +**Was pi-coder hier zeigt:** +- Rust-Toolchain wird automatisch erkannt +- `cargo test`-Output geht an den Judge +- Mehrere Compile-Test-Fix-Zyklen möglich + +## Demo 2: `/version` nach dem Feature + +**Voraussetzung:** Das Verzeichnis muss ein git-Repo mit mindestens einem Commit sein. +Falls noch kein Repo existiert, vorher einmalig: + +```bash +git init && git add -A && git commit -m "feat: initial wordcount" +git tag v0.1.0 +``` + +``` +/version +``` + +Analysiert die Commits seit `v0.1.0`, erkennt `feat:`-Commits → schlägt `minor`-Bump vor +und setzt den Git-Tag `v0.2.0`. + +## Manueller Test + +```bash +cargo test +echo "Hallo Welt" | cargo run +echo -e "Zeile 1\nZeile 2" | cargo run -- --lines +``` diff --git a/examples/rust-wordcount/src/main.rs b/examples/rust-wordcount/src/main.rs new file mode 100644 index 0000000..257cc19 --- /dev/null +++ b/examples/rust-wordcount/src/main.rs @@ -0,0 +1,36 @@ +use std::io::{self, Read}; + +fn count_words(text: &str) -> usize { + text.split_whitespace().count() +} + +fn main() { + let mut input = String::new(); + io::stdin().read_to_string(&mut input).unwrap(); + println!("{} words", count_words(&input)); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_empty_input() { + assert_eq!(count_words(""), 0); + } + + #[test] + fn test_single_word() { + assert_eq!(count_words("hallo"), 1); + } + + #[test] + fn test_multiple_words() { + assert_eq!(count_words("eins zwei drei"), 3); + } + + #[test] + fn test_extra_whitespace() { + assert_eq!(count_words(" a b "), 2); + } +} From f5d2a5d66ef0d51db01feee988e82526456cced8 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Fri, 29 May 2026 20:24:00 +0200 Subject: [PATCH 28/32] =?UTF-8?q?fix:=20go-fibonacci=20./...=20=E2=86=92?= =?UTF-8?q?=20.=20und=20Memoization-Aufgabe=20auf=20Single-Threaded=20eins?= =?UTF-8?q?chr=C3=A4nken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ./... schlägt bei Single-Package-Modulen ohne Unterverzeichnisse fehl. Mutex-Hinweis verhindert Deadlock durch rekursive Lock-Acquisition. Co-Authored-By: Claude Sonnet 4.6 --- examples/go-fibonacci/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/go-fibonacci/README.md b/examples/go-fibonacci/README.md index 7d1cd94..b3fd085 100644 --- a/examples/go-fibonacci/README.md +++ b/examples/go-fibonacci/README.md @@ -13,10 +13,11 @@ main_test.go TestFib mit 5 Tabellen-Tests (alle grün) ## Demo 1: `/optimize --interactive` ``` -/optimize "Ersetze die naive Rekursion durch Memoization. +/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 + --test-cmd "go test ." --interactive ``` Nach dem ersten PASS hält pi-coder im **interaktiven Checkpoint** an. @@ -43,6 +44,6 @@ Der Judge prüft nochmals explizit auf Produktionsreife und gibt SHIP oder NO-SH ## Manueller Test ```bash -go test ./... +go test . go run main.go ``` From 0956f3f569fcbbd5d017a0c017bbc733380247ba Mon Sep 17 00:00:00 2001 From: dschlueter Date: Fri, 29 May 2026 20:26:44 +0200 Subject: [PATCH 29/32] =?UTF-8?q?chore:=20Examples=20finalisieren=20?= =?UTF-8?q?=E2=80=94=20Benchmark,=20.gitignore,=20TASK.md-Cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .gitignore | 4 ++++ examples/go-fibonacci/fib_bench_test.go | 9 +++++++++ examples/python-calculator/.gitignore | 2 ++ examples/restore-all.sh | 3 ++- examples/rust-wordcount/.gitignore | 3 +++ 5 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 examples/go-fibonacci/fib_bench_test.go create mode 100644 examples/python-calculator/.gitignore create mode 100644 examples/rust-wordcount/.gitignore diff --git a/.gitignore b/.gitignore index 59fc5e2..c1bfaae 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,10 @@ node_modules/ .DS_Store +# pi-coder Laufzeitartefakte +TASK.md +examples/**/TASK.md + # Build-Artefakte (Examples) examples/rust-wordcount/target/ examples/**/__pycache__/ diff --git a/examples/go-fibonacci/fib_bench_test.go b/examples/go-fibonacci/fib_bench_test.go new file mode 100644 index 0000000..0c91c44 --- /dev/null +++ b/examples/go-fibonacci/fib_bench_test.go @@ -0,0 +1,9 @@ +package main + +import "testing" + +func BenchmarkFib50(b *testing.B) { + for i := 0; i < b.N; i++ { + fib(50) + } +} diff --git a/examples/python-calculator/.gitignore b/examples/python-calculator/.gitignore new file mode 100644 index 0000000..43ae0e2 --- /dev/null +++ b/examples/python-calculator/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.py[cod] diff --git a/examples/restore-all.sh b/examples/restore-all.sh index e28c3ff..f7ca3a8 100755 --- a/examples/restore-all.sh +++ b/examples/restore-all.sh @@ -20,11 +20,12 @@ for dir in python-calculator rust-wordcount go-fibonacci c-linkedlist; do echo " ✓ Dateien restauriert: $dir" done -# Build-Artefakte bereinigen +# 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." diff --git a/examples/rust-wordcount/.gitignore b/examples/rust-wordcount/.gitignore new file mode 100644 index 0000000..d408a53 --- /dev/null +++ b/examples/rust-wordcount/.gitignore @@ -0,0 +1,3 @@ +target/ +*.rlib +*.pdb From c6f2f1f8e064c8796a2f37c11218275dbaaae1cb Mon Sep 17 00:00:00 2001 From: dschlueter Date: Fri, 29 May 2026 20:48:25 +0200 Subject: [PATCH 30/32] =?UTF-8?q?docs:=20Demo-Protokolle=20ausgef=C3=BCllt?= =?UTF-8?q?=20(automatisierter=20Lauf=20Fr=2029.=20Mai=202026)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- examples/c-linkedlist/PROTOKOLL.md | 28 +++++++++++--------- examples/go-fibonacci/PROTOKOLL.md | 35 ++++++++++++------------- examples/python-calculator/PROTOKOLL.md | 15 ++++++----- examples/rust-wordcount/PROTOKOLL.md | 26 +++++++++++------- 4 files changed, 58 insertions(+), 46 deletions(-) diff --git a/examples/c-linkedlist/PROTOKOLL.md b/examples/c-linkedlist/PROTOKOLL.md index 939ddd9..2e071ea 100644 --- a/examples/c-linkedlist/PROTOKOLL.md +++ b/examples/c-linkedlist/PROTOKOLL.md @@ -2,34 +2,38 @@ ## Lauf 1 -**Datum:** +**Datum:** Fr 29. Mai 2026 **Befehl /quick_check:** ``` /quick_check "Gibt es Speicherlecks oder sonstige Probleme in diesem C-Projekt?" ``` -**Startzeit:** -**Endzeit:** -**Dauer (min):** -**Ergebnis:** OK / PROBLEM (Kurzbeschreibung): +**Startzeit:** Fr 29. Mai 20:33:53 CEST 2026 +**Endzeit:** Fr 29. Mai 20:34:14 CEST 2026 +**Dauer (min):** 0:21 +**Ergebnis:** PROBLEM — list_free() ist leerer Stub → Speicherleck aller 5 Nodes. +Zusätzlich: malloc()-Rückgabe wird in node_new() nicht auf NULL geprüft. **Befehl /fix:** ``` /fix "Implementiere list_free() korrekt, sodass valgrind --leak-check=full sauber ist." ``` -**Startzeit:** -**Endzeit:** -**Dauer (min):** -**Ergebnis:** erledigt / fehlgeschlagen +**Startzeit:** Fr 29. Mai 20:34:21 CEST 2026 +**Endzeit:** Fr 29. Mai 20:35:11 CEST 2026 +**Dauer (min):** 0:50 +**Ergebnis:** erledigt — valgrind: 0 errors, no leaks possible ✅ **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):** +**Startzeit:** Fr 29. Mai 20:35:19 CEST 2026 +**Endzeit:** Fr 29. Mai 20:35:50 CEST 2026 +**Dauer (min):** 0:31 **Besonderheiten / Beobachtungen:** +Alle drei Befehle liefen ohne Fehler durch. /quick_check identifizierte das +Speicherleck präzise. /fix verifizierte mit valgrind. /patch ergänzte +list_search() in Header und Implementierung ohne Full-Loop. --- diff --git a/examples/go-fibonacci/PROTOKOLL.md b/examples/go-fibonacci/PROTOKOLL.md index ef9a63b..3659db5 100644 --- a/examples/go-fibonacci/PROTOKOLL.md +++ b/examples/go-fibonacci/PROTOKOLL.md @@ -2,36 +2,35 @@ ## Lauf 1 -**Datum:** +**Datum:** Fr 29. Mai 2026 **Befehl:** ``` -/optimize "Ersetze die naive Rekursion durch Memoization. +/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 + --test-cmd "go test ." ``` -**Startzeit:** -**Ende Loop (PASS):** -**Dauer Loop (min):** -**Runden:** -**Endergebnis Loop:** PASS / PASS WITH CONCERNS +**Startzeit:** Fr 29. Mai 20:38:39 CEST 2026 +**Ende Loop (PASS):** ca. 20:40 CEST 2026 +**Dauer Loop (min):** ca. 1–2 +**Runden:** 1 (PASS) +**Endergebnis Loop:** PASS — fib(50) in 11 ns (Ziel: <1ms, Faktor 87.000 darunter) **Befehl im --interactive-Checkpoint:** -``` -/continue "Gib zusätzlich die Berechnungszeit in Mikrosekunden aus." -``` -*(oder: `/continue` ohne Zusatzauftrag)* - -**Startzeit /continue:** -**Ende /continue:** +entfällt — --interactive nicht mit --print-Modus kombinierbar **Befehl /shipit:** ``` /shipit ``` -**Startzeit /shipit:** -**Endzeit /shipit:** -**Endergebnis /shipit:** SHIP / NO-SHIP +**Startzeit /shipit:** Fr 29. Mai 20:46:01 CEST 2026 +**Endzeit /shipit:** Fr 29. Mai 20:46:52 CEST 2026 +**Endergebnis /shipit:** SHIP **Besonderheiten / Beobachtungen:** +init()-vorbesetzte map[int]int, kein Mutex, go vet + gofmt sauber. +Judge empfiehlt README-Update (Einleitung beschreibt noch O(2^n)). +Hinweis: --interactive erfordert interaktiven pi-Agent (wartet auf +/continue, bricht in --print-Modus nicht automatisch ab). --- diff --git a/examples/python-calculator/PROTOKOLL.md b/examples/python-calculator/PROTOKOLL.md index b7e8b9f..5d62983 100644 --- a/examples/python-calculator/PROTOKOLL.md +++ b/examples/python-calculator/PROTOKOLL.md @@ -2,18 +2,21 @@ ## Lauf 1 -**Datum:** +**Datum:** Fr 29. Mai 2026 **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 +**Startzeit:** Fr 29. Mai 20:32:00 CEST 2026 +**Endzeit:** Fr 29. Mai 20:33:15 CEST 2026 +**Dauer (min):** 1:15 +**Runden:** 1 (Quick-Judge, direkt PASS) +**Endergebnis:** PASS **Besonderheiten / Beobachtungen:** +Alle 3 Funktionen (multiply, divide, power) korrekt implementiert. +17 Tests, alle grün. ZeroDivisionError und pytest.approx für Float-Vergleiche +korrekt eingesetzt. Kein Fix-Durchlauf nötig. --- diff --git a/examples/rust-wordcount/PROTOKOLL.md b/examples/rust-wordcount/PROTOKOLL.md index 762f940..276c890 100644 --- a/examples/rust-wordcount/PROTOKOLL.md +++ b/examples/rust-wordcount/PROTOKOLL.md @@ -2,7 +2,7 @@ ## Lauf 1 -**Datum:** +**Datum:** Fr 29. Mai 2026 **Befehl:** ``` /optimize "Ergänze --lines (Zeilenzählung) und --chars (Zeichenzählung) als CLI-Flags. @@ -10,20 +10,26 @@ 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 +**Startzeit:** Fr 29. Mai 20:36:14 CEST 2026 +**Endzeit:** Fr 29. Mai 20:37:54 CEST 2026 +**Dauer /optimize (min):** 1:40 +**Runden:** 1 (Quick-Judge, direkt PASS) +**Endergebnis /optimize:** PASS WITH CONCERNS +Concerns: wc -l-Semantik-Abweichung, stilles Ignorieren unbekannter Flags — +kein Blocker. **Befehl /version:** ``` /version ``` -**Startzeit /version:** -**Endzeit /version:** -**Gewählter Bump:** patch / minor / major -**Gesetzter Tag:** +**Startzeit /version:** Fr 29. Mai 20:38:02 CEST 2026 +**Endzeit /version:** Fr 29. Mai 20:38:03 CEST 2026 +**Gewählter Bump:** minor (feat: add --lines and --chars CLI flags with tests) +**Gesetzter Tag:** v0.2.0 **Besonderheiten / Beobachtungen:** +18 Tests, alle grün. Mode-Enum und parse_mode() sauber implementiert. +/version erkennt feat:-Commit und empfiehlt minor-Bump korrekt. +Hinweis: /version benötigt interaktiven UI-Dialog — Tag wurde im +automatisierten Lauf manuell gesetzt. --- From a86d8b39ad7fa21bb35ebddeee6922465a8a65d4 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Fri, 29 May 2026 20:53:01 +0200 Subject: [PATCH 31/32] =?UTF-8?q?fix:=20restore-all.sh=20bewahrt=20PROTOKO?= =?UTF-8?q?LL.md=20standardm=C3=A4=C3=9Fig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- examples/restore-all.sh | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/examples/restore-all.sh b/examples/restore-all.sh index f7ca3a8..ce7fde2 100755 --- a/examples/restore-all.sh +++ b/examples/restore-all.sh @@ -2,11 +2,20 @@ # 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..." @@ -16,8 +25,17 @@ for dir in python-calculator rust-wordcount go-fibonacci c-linkedlist; do rm -rf "$path/.git" echo " ✓ Sub-Repo entfernt: $dir" fi - git -C "$ROOT" checkout -- "examples/$dir/" - echo " ✓ Dateien restauriert: $dir" + # 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 @@ -29,4 +47,9 @@ find "$EXAMPLES" -name "TASK.md" -delete 2>/dev/null || true echo "" echo "Fertig. Alle Examples sind im Ausgangszustand." -echo "git status zeigt examples/ als clean (wenn kein PROTOKOLL.md verändert wurde)." +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 From 433d3970fb6b20a9fcc129061910ff68120fcb9f Mon Sep 17 00:00:00 2001 From: dschlueter Date: Fri, 29 May 2026 21:06:07 +0200 Subject: [PATCH 32/32] =?UTF-8?q?chore:=20PROTOKOLL.md-Dateien=20auf=20lee?= =?UTF-8?q?re=20Templates=20zur=C3=BCcksetzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- examples/c-linkedlist/PROTOKOLL.md | 28 +++++++++----------- examples/go-fibonacci/PROTOKOLL.md | 35 +++++++++++++------------ examples/python-calculator/PROTOKOLL.md | 15 +++++------ examples/rust-wordcount/PROTOKOLL.md | 26 +++++++----------- 4 files changed, 46 insertions(+), 58 deletions(-) diff --git a/examples/c-linkedlist/PROTOKOLL.md b/examples/c-linkedlist/PROTOKOLL.md index 2e071ea..939ddd9 100644 --- a/examples/c-linkedlist/PROTOKOLL.md +++ b/examples/c-linkedlist/PROTOKOLL.md @@ -2,38 +2,34 @@ ## Lauf 1 -**Datum:** Fr 29. Mai 2026 +**Datum:** **Befehl /quick_check:** ``` /quick_check "Gibt es Speicherlecks oder sonstige Probleme in diesem C-Projekt?" ``` -**Startzeit:** Fr 29. Mai 20:33:53 CEST 2026 -**Endzeit:** Fr 29. Mai 20:34:14 CEST 2026 -**Dauer (min):** 0:21 -**Ergebnis:** PROBLEM — list_free() ist leerer Stub → Speicherleck aller 5 Nodes. -Zusätzlich: malloc()-Rückgabe wird in node_new() nicht auf NULL geprüft. +**Startzeit:** +**Endzeit:** +**Dauer (min):** +**Ergebnis:** OK / PROBLEM (Kurzbeschreibung): **Befehl /fix:** ``` /fix "Implementiere list_free() korrekt, sodass valgrind --leak-check=full sauber ist." ``` -**Startzeit:** Fr 29. Mai 20:34:21 CEST 2026 -**Endzeit:** Fr 29. Mai 20:35:11 CEST 2026 -**Dauer (min):** 0:50 -**Ergebnis:** erledigt — valgrind: 0 errors, no leaks possible ✅ +**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:** Fr 29. Mai 20:35:19 CEST 2026 -**Endzeit:** Fr 29. Mai 20:35:50 CEST 2026 -**Dauer (min):** 0:31 +**Startzeit:** +**Endzeit:** +**Dauer (min):** **Besonderheiten / Beobachtungen:** -Alle drei Befehle liefen ohne Fehler durch. /quick_check identifizierte das -Speicherleck präzise. /fix verifizierte mit valgrind. /patch ergänzte -list_search() in Header und Implementierung ohne Full-Loop. --- diff --git a/examples/go-fibonacci/PROTOKOLL.md b/examples/go-fibonacci/PROTOKOLL.md index 3659db5..ef9a63b 100644 --- a/examples/go-fibonacci/PROTOKOLL.md +++ b/examples/go-fibonacci/PROTOKOLL.md @@ -2,35 +2,36 @@ ## Lauf 1 -**Datum:** Fr 29. Mai 2026 +**Datum:** **Befehl:** ``` -/optimize "Ersetze die naive Rekursion durch einfache Memoization mit einer map[int]int. - Kein Mutex, kein Goroutine-Overhead — Single-Threaded reicht. +/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 ." + --test-cmd "go test ./..." --interactive ``` -**Startzeit:** Fr 29. Mai 20:38:39 CEST 2026 -**Ende Loop (PASS):** ca. 20:40 CEST 2026 -**Dauer Loop (min):** ca. 1–2 -**Runden:** 1 (PASS) -**Endergebnis Loop:** PASS — fib(50) in 11 ns (Ziel: <1ms, Faktor 87.000 darunter) +**Startzeit:** +**Ende Loop (PASS):** +**Dauer Loop (min):** +**Runden:** +**Endergebnis Loop:** PASS / PASS WITH CONCERNS **Befehl im --interactive-Checkpoint:** -entfällt — --interactive nicht mit --print-Modus kombinierbar +``` +/continue "Gib zusätzlich die Berechnungszeit in Mikrosekunden aus." +``` +*(oder: `/continue` ohne Zusatzauftrag)* + +**Startzeit /continue:** +**Ende /continue:** **Befehl /shipit:** ``` /shipit ``` -**Startzeit /shipit:** Fr 29. Mai 20:46:01 CEST 2026 -**Endzeit /shipit:** Fr 29. Mai 20:46:52 CEST 2026 -**Endergebnis /shipit:** SHIP +**Startzeit /shipit:** +**Endzeit /shipit:** +**Endergebnis /shipit:** SHIP / NO-SHIP **Besonderheiten / Beobachtungen:** -init()-vorbesetzte map[int]int, kein Mutex, go vet + gofmt sauber. -Judge empfiehlt README-Update (Einleitung beschreibt noch O(2^n)). -Hinweis: --interactive erfordert interaktiven pi-Agent (wartet auf -/continue, bricht in --print-Modus nicht automatisch ab). --- diff --git a/examples/python-calculator/PROTOKOLL.md b/examples/python-calculator/PROTOKOLL.md index 5d62983..b7e8b9f 100644 --- a/examples/python-calculator/PROTOKOLL.md +++ b/examples/python-calculator/PROTOKOLL.md @@ -2,21 +2,18 @@ ## Lauf 1 -**Datum:** Fr 29. Mai 2026 +**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:** Fr 29. Mai 20:32:00 CEST 2026 -**Endzeit:** Fr 29. Mai 20:33:15 CEST 2026 -**Dauer (min):** 1:15 -**Runden:** 1 (Quick-Judge, direkt PASS) -**Endergebnis:** PASS +**Startzeit:** +**Endzeit:** +**Dauer (min):** +**Runden:** +**Endergebnis:** PASS / PASS WITH CONCERNS / SHIP / NO-SHIP **Besonderheiten / Beobachtungen:** -Alle 3 Funktionen (multiply, divide, power) korrekt implementiert. -17 Tests, alle grün. ZeroDivisionError und pytest.approx für Float-Vergleiche -korrekt eingesetzt. Kein Fix-Durchlauf nötig. --- diff --git a/examples/rust-wordcount/PROTOKOLL.md b/examples/rust-wordcount/PROTOKOLL.md index 276c890..762f940 100644 --- a/examples/rust-wordcount/PROTOKOLL.md +++ b/examples/rust-wordcount/PROTOKOLL.md @@ -2,7 +2,7 @@ ## Lauf 1 -**Datum:** Fr 29. Mai 2026 +**Datum:** **Befehl:** ``` /optimize "Ergänze --lines (Zeilenzählung) und --chars (Zeichenzählung) als CLI-Flags. @@ -10,26 +10,20 @@ Schreibe Tests für alle drei Modi." \ --test-cmd "cargo test" ``` -**Startzeit:** Fr 29. Mai 20:36:14 CEST 2026 -**Endzeit:** Fr 29. Mai 20:37:54 CEST 2026 -**Dauer /optimize (min):** 1:40 -**Runden:** 1 (Quick-Judge, direkt PASS) -**Endergebnis /optimize:** PASS WITH CONCERNS -Concerns: wc -l-Semantik-Abweichung, stilles Ignorieren unbekannter Flags — -kein Blocker. +**Startzeit:** +**Endzeit:** +**Dauer /optimize (min):** +**Runden:** +**Endergebnis /optimize:** PASS / PASS WITH CONCERNS / SHIP / NO-SHIP **Befehl /version:** ``` /version ``` -**Startzeit /version:** Fr 29. Mai 20:38:02 CEST 2026 -**Endzeit /version:** Fr 29. Mai 20:38:03 CEST 2026 -**Gewählter Bump:** minor (feat: add --lines and --chars CLI flags with tests) -**Gesetzter Tag:** v0.2.0 +**Startzeit /version:** +**Endzeit /version:** +**Gewählter Bump:** patch / minor / major +**Gesetzter Tag:** **Besonderheiten / Beobachtungen:** -18 Tests, alle grün. Mode-Enum und parse_mode() sauber implementiert. -/version erkennt feat:-Commit und empfiehlt minor-Bump korrekt. -Hinweis: /version benötigt interaktiven UI-Dialog — Tag wurde im -automatisierten Lauf manuell gesetzt. ---