From 4074e10c1acf2932f1ad9f0d0acd6c90a1a22ed1 Mon Sep 17 00:00:00 2001 From: dschlueter Date: Tue, 19 May 2026 18:21:34 +0200 Subject: [PATCH] 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}"