2026-05-19 18:21:34 +02:00
// 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 ;
}
2026-05-29 18:03:56 +02:00
// 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 ;
}
2026-05-20 21:10:25 +02:00
// 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 ;
}
2026-05-19 18:21:34 +02:00
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: <ein Satz>'" ,
"- 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" ) ;
}
2026-05-20 20:02:20 +02:00
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" ) ;
}
2026-05-19 18:21:34 +02:00
// ── Hilfsfunktionen ─────────────────────────────────────────────────────────
// Legt TASK.md neu an oder hängt einen Zusatzauftrag an.
async function writeTaskMd (
pi : ExtensionAPI ,
ctx : ExtensionCommandContext ,
task : string
) : Promise < void > {
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 < void > {
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
2026-05-22 23:49:53 +02:00
) : Promise < boolean > {
2026-05-29 18:03:56 +02:00
const key = ` ${ provider } / ${ modelId } ` ;
if ( key === currentModelKey ) return true ;
2026-05-19 18:21:34 +02:00
const model = ctx . modelRegistry . find ( provider , modelId ) ;
if ( ! model ) {
ctx . ui . notify ( ` Modell ${ provider } / ${ modelId } nicht gefunden ` , "error" ) ;
2026-05-22 23:49:53 +02:00
return false ;
2026-05-19 18:21:34 +02:00
}
const ok = await pi . setModel ( model ) ;
2026-05-29 18:03:56 +02:00
if ( ok !== false ) currentModelKey = key ;
2026-05-19 18:21:34 +02:00
if ( ! ok ) ctx . ui . notify ( ` Kein API-Key für ${ modelId } ` , "warning" ) ;
2026-05-22 23:49:53 +02:00
return ok !== false ;
2026-05-19 18:21:34 +02:00
}
// Sendet eine Nachricht und wartet bis der Agent fertig ist.
2026-05-20 21:44:37 +02:00
// Retry-Schleife fängt "Agent is already processing" ab — tritt auf wenn
// waitForIdle() zu früh zurückkehrt (Race Condition im pi-Agent).
2026-05-19 18:21:34 +02:00
async function sendAndWait (
pi : ExtensionAPI ,
ctx : ExtensionCommandContext ,
content : string
) : Promise < void > {
await ctx . waitForIdle ( ) ;
2026-05-20 21:44:37 +02:00
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 ( ) ;
}
}
2026-05-29 18:03:56 +02:00
await new Promise ( r = > setTimeout ( r , 150 ) ) ;
2026-05-19 18:21:34 +02:00
await ctx . waitForIdle ( ) ;
}
2026-05-22 23:49:53 +02:00
// 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 < boolean > {
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 } ) ;
}
}
2026-05-20 21:10:25 +02:00
// Führt einen Shell-Befehl aus und gibt stdout+stderr zurück (max. 6000 Zeichen).
2026-05-20 21:39:21 +02:00
// 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 < string [ ] > {
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 && " +
2026-05-22 23:49:53 +02:00
"grep -q '\"test\"' package.json && " +
"! grep -q 'no test' package.json"
2026-05-20 21:39:21 +02:00
] , { 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 (
2026-05-20 21:10:25 +02:00
pi : ExtensionAPI ,
ctx : ExtensionCommandContext ,
2026-05-20 23:47:06 +02:00
cmds : string [ ] ,
timeoutSecs : number = 120
2026-05-20 21:10:25 +02:00
) : Promise < string > {
2026-05-20 21:39:21 +02:00
const results = await Promise . all (
2026-05-20 23:47:06 +02:00
// 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 }
) )
2026-05-20 21:39:21 +02:00
) ;
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)" ;
2026-05-20 23:47:06 +02:00
const status = r . code === 0 ? "✓ OK"
: r . code === 124 ? ` ✗ Timeout (> ${ timeoutSecs } s) `
: ` ✗ Exit ${ r . code } ` ;
2026-05-20 21:39:21 +02:00
return ` === ${ cmds [ i ] } [ ${ status } ] === \ n ${ out } ` ;
} ) . join ( "\n\n" ) ;
2026-05-20 21:10:25 +02:00
}
2026-05-19 18:21:34 +02:00
// 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.
2026-05-22 23:49:53 +02:00
// "UNREADABLE" wenn kein Urteil erkennbar — unterscheidbar von einem expliziten FAIL.
2026-05-29 18:03:56 +02:00
// 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 ( ) ;
}
2026-05-19 18:21:34 +02:00
function parseVerdict ( text : string ) : string {
const m = text . match ( /Urteil:\s*(PASS WITH CONCERNS|PASS|FAIL)/i ) ;
2026-05-22 23:49:53 +02:00
return m ? m [ 1 ] . toUpperCase ( ) : "UNREADABLE" ;
2026-05-19 18:21:34 +02:00
}
// Extrahiert den Blocker-Abschnitt für die Loop-Erkennung.
2026-05-22 23:49:53 +02:00
// Erkennt Bullet-Listen (- / – / *), Bold (**Blocker**) und Headings (## Blocker).
2026-05-19 18:21:34 +02:00
function parseBlockers ( text : string ) : string {
2026-05-22 23:49:53 +02:00
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
) ;
2026-05-19 18:21:34 +02:00
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 < string [ ] | null > {
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 }
) ;
2026-05-22 23:49:53 +02:00
// Bei git-Fehler alles verarbeiten (sicherer als stilles Überspringen)
if ( diff . code !== 0 ) return null ;
2026-05-19 18:21:34 +02:00
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 < void > {
2026-05-22 23:49:53 +02:00
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.
2026-05-19 18:21:34 +02:00
// Phase 1: Code-Kommentare
2026-05-22 23:49:53 +02:00
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" ) ;
2026-05-19 18:21:34 +02:00
}
// Phase 2: README.md
2026-05-22 23:49:53 +02:00
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" ) ;
2026-05-19 18:21:34 +02:00
}
// Phase 3: BEDIENUNGSANLEITUNG.md
2026-05-22 23:49:53 +02:00
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" ) ;
2026-05-19 18:21:34 +02:00
}
2026-05-22 23:49:53 +02:00
// Abschließender Dokumentations-Commit (immer, auch bei Teilfehlern)
2026-05-19 18:21:34 +02:00
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" ) ;
}
2026-05-22 23:49:53 +02:00
// ── 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 < void > {
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 < void > {
// 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" ) ;
}
2026-05-23 01:36:02 +02:00
// Committed alle ungespeicherten Änderungen nach SHIP — Sicherheitsnetz falls der LLM es vergessen hat.
async function autoCommitIfDirty ( pi : ExtensionAPI , ctx : ExtensionCommandContext ) : Promise < void > {
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"
) ;
}
2026-05-20 02:08:09 +02:00
// 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" } ) ;
2026-05-22 23:49:53 +02:00
const level = verdict . startsWith ( "🚀" ) ? "info"
: verdict . includes ( "NO-SHIP" ) || verdict . startsWith ( "⛔" ) ? "error"
2026-05-20 02:08:09 +02:00
: verdict . includes ( "⚠" ) ? "warning"
: "info" ;
ctx . ui . notify ( ` ${ verdict } : ${ detail } ` , level ) ;
ctx . ui . setWidget ( "coder-judge" , [
` Letzter Lauf: ${ verdict } — ${ detail } ( ${ timestamp } ) ` ,
2026-05-22 23:49:53 +02:00
"/optimize <auftrag> [--rounds N] [--with-doku] [--continue] [--test-cmd \"cmd\"]" ,
"/fix · /judge · /shipit · /cancel · /continue · /help" ,
2026-05-20 02:08:09 +02:00
] ) ;
}
2026-05-19 18:21:34 +02:00
// ── Extension ────────────────────────────────────────────────────────────────
2026-05-20 20:02:20 +02:00
let cancelRequested = false ;
2026-05-29 18:03:56 +02:00
let currentModelKey = "" ; // Cache für switchModel() — verhindert redundante setModel()-Aufrufe
2026-05-29 17:51:54 +02:00
let interactivePauseActive = false ;
let interactiveContinueRequested = false ;
let interactivePauseTask = "" ;
2026-05-22 23:49:53 +02:00
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 , any > ) : 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 "" ;
}
}
2026-05-20 20:02:20 +02:00
2026-05-19 18:21:34 +02:00
export default function ( pi : ExtensionAPI ) {
pi . on ( "session_start" , async function ( _event , ctx ) {
ctx . ui . setWidget ( "coder-judge" , [
2026-05-22 23:49:53 +02:00
"/optimize <auftrag> [--rounds N] [--with-doku] [--continue] [--test-cmd \"cmd\"]" ,
"/fix · /judge · /shipit · /cancel · /continue · /help" ,
2026-05-19 18:21:34 +02:00
] ) ;
} ) ;
2026-05-22 23:49:53 +02:00
// ── 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 = "" ;
} ) ;
2026-05-19 18:21:34 +02:00
// ── 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" , {
2026-05-22 23:49:53 +02:00
description : "Implementiert <auftrag> ohne Review-Loop → qwen3.5-coder (:8001)." ,
2026-05-19 18:21:34 +02:00
handler : async function ( args : string , ctx : ExtensionCommandContext ) {
const task = ( args || "" ) . trim ( ) ;
if ( ! task ) {
ctx . ui . notify ( "Benutzung: /coder <auftrag>" , "error" ) ;
return ;
}
2026-05-22 23:49:53 +02:00
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 ;
}
2026-05-19 18:21:34 +02:00
await writeTaskMd ( pi , ctx , task ) ;
await switchModel ( pi , ctx , "llama-cpp-coder" , "qwen3.5-coder" ) ;
2026-05-22 23:49:53 +02:00
currentActivity = "Coder implementiert…" ;
await sendAndWait ( pi , ctx , coderKickoff ( task ) ) ;
2026-05-19 18:21:34 +02:00
}
} ) ;
pi . registerCommand ( "judge" , {
description : "Review gegen TASK.md + git show HEAD → qwen3.5-judge (:8002)." ,
handler : async function ( args : string , ctx : ExtensionCommandContext ) {
2026-05-22 23:49:53 +02:00
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 ;
}
2026-05-19 18:21:34 +02:00
await switchModel ( pi , ctx , "llama-cpp-judge" , "qwen3.5-judge" ) ;
2026-05-22 23:49:53 +02:00
currentActivity = "Judge reviewt…" ;
await sendAndWait ( pi , ctx , judgePrompt ( args || "" ) ) ;
2026-05-19 18:21:34 +02:00
}
} ) ;
pi . registerCommand ( "fix" , {
description : "Fixt Judge-Kritik, committet Ergebnis → qwen3.5-coder (:8001)." ,
handler : async function ( args : string , ctx : ExtensionCommandContext ) {
2026-05-22 23:49:53 +02:00
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 ;
}
2026-05-19 18:21:34 +02:00
await switchModel ( pi , ctx , "llama-cpp-coder" , "qwen3.5-coder" ) ;
2026-05-22 23:49:53 +02:00
currentActivity = "Coder fixt Judge-Kritik…" ;
await sendAndWait ( pi , ctx , fixPrompt ( args || "" ) ) ;
2026-05-19 18:21:34 +02:00
}
} ) ;
pi . registerCommand ( "shipit" , {
description : "Finale Freigabe gegen TASK.md + git log → qwen3.5-judge (:8002)." ,
handler : async function ( args : string , ctx : ExtensionCommandContext ) {
2026-05-22 23:49:53 +02:00
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 ;
}
2026-05-19 18:21:34 +02:00
await switchModel ( pi , ctx , "llama-cpp-judge" , "qwen3.5-judge" ) ;
2026-05-20 02:08:09 +02:00
ctx . ui . notify ( "Judge prüft finale Freigabe — Ergebnis erscheint im Chat (SHIP / NO-SHIP)" , "info" ) ;
2026-05-22 23:49:53 +02:00
currentActivity = "Judge: finale Freigabe…" ;
await sendAndWait ( pi , ctx , shipitPrompt ( args || "" ) ) ;
2026-05-23 01:36:02 +02:00
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" ) ;
}
2026-05-19 18:21:34 +02:00
}
} ) ;
// ── Automatische Optimierungsschleife ────────────────────────────────────
pi . registerCommand ( "optimize" , {
2026-05-29 18:03:56 +02:00
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 <auftrag> [--rounds N] [--with-doku] [--continue] [--interactive] [--no-tests] [--approve-concerns] [--test-cmd \"override\"] [--test-timeout N]" ,
2026-05-19 18:21:34 +02:00
handler : async function ( args : string , ctx : ExtensionCommandContext ) {
const roundsMatch = ( args || "" ) . match ( /--rounds\s+(\d+)/ ) ;
2026-05-29 17:51:54 +02:00
const maxRounds = roundsMatch ? Math . max ( 1 , parseInt ( roundsMatch [ 1 ] , 10 ) ) : 2 ;
2026-05-19 18:21:34 +02:00
const withDoku = /--with-doku/ . test ( args || "" ) ;
2026-05-20 01:42:26 +02:00
const continueMode = /--continue/ . test ( args || "" ) ;
2026-05-29 17:51:54 +02:00
const interactive = /--interactive/ . test ( args || "" ) ;
2026-05-29 18:03:56 +02:00
const noTests = /--no-tests/ . test ( args || "" ) ;
const approveConcerns = /--approve-concerns/ . test ( args || "" ) ;
2026-05-22 23:49:53 +02:00
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 ;
2026-05-20 23:47:06 +02:00
const testTimeoutMatch = ( args || "" ) . match ( /--test-timeout\s+(\d+)/ ) ;
2026-05-22 23:49:53 +02:00
const testTimeout = testTimeoutMatch ? Math . max ( 1 , parseInt ( testTimeoutMatch [ 1 ] , 10 ) ) : 120 ;
2026-05-19 18:21:34 +02:00
const task = ( args || "" )
. replace ( /--rounds\s+\d+/ , "" )
2026-05-20 23:47:06 +02:00
. replace ( /--test-timeout\s+\d+/ , "" )
2026-05-19 18:21:34 +02:00
. replace ( /--with-doku/ , "" )
2026-05-20 01:42:26 +02:00
. replace ( /--continue/ , "" )
2026-05-29 17:51:54 +02:00
. replace ( /--interactive/ , "" )
2026-05-29 18:03:56 +02:00
. replace ( /--no-tests/ , "" )
. replace ( /--approve-concerns/ , "" )
2026-05-20 21:10:25 +02:00
. replace ( /--test-cmd\s+"[^"]*"/ , "" )
. replace ( /--test-cmd\s+\S+/ , "" )
2026-05-19 18:21:34 +02:00
. trim ( ) ;
2026-05-20 01:42:26 +02:00
if ( ! continueMode && ! task ) {
2026-05-20 21:10:25 +02:00
ctx . ui . notify ( "Benutzung: /optimize <auftrag> [--rounds N] [--with-doku] [--continue] [--test-cmd \"befehl\"]" , "error" ) ;
2026-05-19 18:21:34 +02:00
return ;
}
2026-05-22 23:49:53 +02:00
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" ) ;
2026-05-29 18:03:56 +02:00
// 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 ) {
2026-05-22 23:49:53 +02:00
finalNotify ( ctx , "⛔ Coder nicht erreichbar" , "Port 8001 — kein HTTP 200 nach 3 min. start-coder.sh ausführen" ) ;
return ;
}
2026-05-29 18:03:56 +02:00
if ( ! judgeReady ) {
finalNotify ( ctx , "⛔ Judge nicht erreichbar" , "Port 8002 — kein HTTP 200 nach 3 min. start-judge.sh ausführen" ) ;
return ;
}
2026-05-22 23:49:53 +02:00
} else {
// TASK.md anlegen und Implementierung starten
await writeTaskMd ( pi , ctx , task ) ;
ctx . ui . setStatus ( "optimize" , ` Starte Optimierung (max ${ maxRounds } Runden)… ` ) ;
const taskPreview = task . length > 55 ? task . slice ( 0 , 52 ) + "…" : task ;
ctx . ui . setStatus ( "optimize" , ` ◉ Coder liest Anforderungen + implementiert: ${ taskPreview } ` ) ;
if ( ! await switchModel ( pi , ctx , "llama-cpp-coder" , "qwen3.5-coder" ) ) {
finalNotify ( ctx , "⛔ Modell-Fehler" , "Coder-Modell (llama-cpp-coder) nicht verfügbar" ) ;
return ;
}
currentActivity = "Coder implementiert…" ;
await sendAndWait ( pi , ctx , coderKickoff ( task ) ) ;
await tickTaskMdStatus ( pi , ctx , "Implementierung" ) ;
if ( cancelRequested ) { finalNotify ( ctx , "⛔ Abgebrochen" , "Nach Implementierung" ) ; return ; }
2026-05-19 18:21:34 +02:00
2026-05-29 18:03:56 +02:00
// 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 ;
}
2026-05-22 23:49:53 +02:00
}
2026-05-20 21:10:25 +02:00
2026-05-29 18:03:56 +02:00
// Test-Suiten ermitteln: --no-tests überspringt alles, --test-cmd überschreibt Auto-Erkennung.
2026-05-22 23:49:53 +02:00
// Läuft nach Coder, damit neu angelegte Test-Dateien bereits erkannt werden.
2026-05-29 18:03:56 +02:00
let autoTestCmds : string [ ] = [ ] ;
if ( noTests ) {
ctx . ui . notify ( "--no-tests: Test-Erkennung übersprungen." , "info" ) ;
2026-05-20 21:10:25 +02:00
} else {
2026-05-29 18:03:56 +02:00
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" ) ;
}
2026-05-20 21:10:25 +02:00
}
2026-05-19 18:21:34 +02:00
2026-05-22 23:49:53 +02:00
let lastBlockers = "" ;
let verdict = "" ;
2026-05-29 17:51:54 +02:00
let keepGoing = true ;
// Äußere Schleife für --interactive: nach PASS pausieren, Zusatzaufträge ermöglichen.
while ( keepGoing ) {
keepGoing = false ;
verdict = "" ;
lastBlockers = "" ;
// Schleife: Judge → (PASS? fertig : Fix → nächste Runde)
for ( let round = 1 ; round <= maxRounds ; round ++ ) {
const prog = "●" . repeat ( round - 1 ) + "◉" + "○" . repeat ( maxRounds - round ) ;
if ( ! await switchModel ( pi , ctx , "llama-cpp-judge" , "qwen3.5-judge" ) ) {
finalNotify ( ctx , "⛔ Modell-Fehler" , "Judge-Modell (llama-cpp-judge) nicht verfügbar" ) ;
return ;
}
2026-05-19 18:21:34 +02:00
2026-05-29 18:03:56 +02:00
// 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 ;
2026-05-29 17:51:54 +02:00
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 ) ;
2026-05-29 18:03:56 +02:00
const judgeLabel = useQuickJudge ? "Quick-Check" : "Judge analysiert" ;
ctx . ui . setStatus ( "optimize" , ` ${ prog } Runde ${ round } / ${ maxRounds } : ${ judgeLabel } Test-Ergebnis… ` ) ;
2026-05-29 17:51:54 +02:00
currentActivity = ` Judge reviewt (Runde ${ round } / ${ maxRounds } )… ` ;
2026-05-29 18:03:56 +02:00
await sendAndWait ( pi , ctx , useQuickJudge
? quickJudgeWithTestsPrompt ( testOutput , "" )
: judgeWithTestsPrompt ( testOutput , "" ) ) ;
2026-05-29 17:51:54 +02:00
} else {
2026-05-29 18:03:56 +02:00
const judgeLabel = useQuickJudge ? "Quick-Check" : "Judge — TASK.md + letzter Commit + Tests" ;
ctx . ui . setStatus ( "optimize" , ` ${ prog } Runde ${ round } / ${ maxRounds } : ${ judgeLabel } … ` ) ;
2026-05-29 17:51:54 +02:00
currentActivity = ` Judge reviewt (Runde ${ round } / ${ maxRounds } )… ` ;
2026-05-29 18:03:56 +02:00
await sendAndWait ( pi , ctx , useQuickJudge ? quickJudgePrompt ( "" ) : judgePrompt ( "" ) ) ;
2026-05-29 17:51:54 +02:00
}
if ( cancelRequested ) { finalNotify ( ctx , "⛔ Abgebrochen" , ` Nach Judge Runde ${ round } ` ) ; return ; }
2026-05-19 18:21:34 +02:00
2026-05-29 17:51:54 +02:00
const judgeText = getLastAssistantText ( ctx ) ;
verdict = parseVerdict ( judgeText ) ;
2026-05-19 18:21:34 +02:00
2026-05-29 17:51:54 +02:00
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 ;
}
2026-05-19 18:21:34 +02:00
2026-05-29 18:03:56 +02:00
// Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen.
// Normalisierung verhindert False-Negatives durch minimale Formulierungsunterschiede.
2026-05-29 17:51:54 +02:00
const currentBlockers = parseBlockers ( judgeText ) ;
2026-05-29 18:03:56 +02:00
if ( currentBlockers && normalizeForComparison ( currentBlockers ) === normalizeForComparison ( lastBlockers ) ) {
2026-05-29 17:51:54 +02:00
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 ;
}
2026-05-22 23:49:53 +02:00
2026-05-29 17:51:54 +02:00
// 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 ;
2026-05-22 23:49:53 +02:00
}
2026-05-29 17:51:54 +02:00
currentActivity = "Coder fixt Blocker…" ;
await sendAndWait ( pi , ctx , fixPrompt ( "" ) ) ;
if ( cancelRequested ) { finalNotify ( ctx , "⛔ Abgebrochen" , ` Nach Fix Runde ${ round } ` ) ; return ; }
2026-05-22 23:49:53 +02:00
}
2026-05-19 18:21:34 +02:00
2026-05-29 17:51:54 +02:00
// 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)
2026-05-22 23:49:53 +02:00
}
}
2026-05-19 18:21:34 +02:00
2026-05-29 17:51:54 +02:00
// Finaler SHIP-Schritt: klares PASS → direkt SHIP ohne zweiten Inference-Aufruf.
2026-05-29 18:03:56 +02:00
// "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 ) ) {
2026-05-29 17:51:54 +02:00
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?… ` ) ;
2026-05-22 23:49:53 +02:00
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" ) ;
2026-05-23 01:36:02 +02:00
await autoCommitIfDirty ( pi , ctx ) ;
notifyShipSuccess ( ctx ) ;
2026-05-22 23:49:53 +02:00
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" ) ;
2026-05-19 18:21:34 +02:00
} else {
2026-05-22 23:49:53 +02:00
ctx . ui . setStatus ( "optimize" , "ShipIt abgeschlossen" ) ;
finalNotify ( ctx , "ShipIt" , "Kein klares Urteil – Antwort im Chat prüfen" ) ;
2026-05-19 18:21:34 +02:00
}
}
2026-05-22 23:49:53 +02:00
} catch ( e : any ) {
finalNotify ( ctx , "⛔ Fehler" , String ( e ? . message ? ? e ) ) ;
} finally {
2026-05-29 17:51:54 +02:00
// Sicherstellen dass keine Zustandsvariable in späteren /optimize-Aufruf leckt
2026-05-22 23:49:53 +02:00
cancelRequested = false ;
2026-05-29 18:03:56 +02:00
currentModelKey = "" ;
2026-05-29 17:51:54 +02:00
interactivePauseActive = false ;
interactiveContinueRequested = false ;
interactivePauseTask = "" ;
2026-05-19 18:21:34 +02:00
}
}
} ) ;
// ── Schlanke Kommandos für kleine Änderungen ─────────────────────────────
pi . registerCommand ( "patch" , {
2026-05-22 23:49:53 +02:00
description : "Gezielte Minimaländerung ohne Refactoring, committet → qwen3.5-coder (:8001)." ,
2026-05-19 18:21:34 +02:00
handler : async function ( args : string , ctx : ExtensionCommandContext ) {
const change = ( args || "" ) . trim ( ) ;
if ( ! change ) {
ctx . ui . notify ( "Benutzung: /patch <beschreibung der änderung>" , "error" ) ;
return ;
}
2026-05-22 23:49:53 +02:00
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 ;
}
2026-05-19 18:21:34 +02:00
await switchModel ( pi , ctx , "llama-cpp-coder" , "qwen3.5-coder" ) ;
2026-05-22 23:49:53 +02:00
currentActivity = "Coder patcht…" ;
await sendAndWait ( pi , ctx , patchPrompt ( change ) ) ;
2026-05-19 18:21:34 +02:00
}
} ) ;
pi . registerCommand ( "quick_check" , {
2026-05-22 23:49:53 +02:00
description : "Schnelle OK/PROBLEM-Prüfung einer kleinen Codeänderung → qwen3.5-judge (:8002)." ,
2026-05-19 18:21:34 +02:00
handler : async function ( args : string , ctx : ExtensionCommandContext ) {
2026-05-22 23:49:53 +02:00
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 ;
}
2026-05-19 18:21:34 +02:00
await switchModel ( pi , ctx , "llama-cpp-judge" , "qwen3.5-judge" ) ;
2026-05-22 23:49:53 +02:00
currentActivity = "Judge: Schnellcheck…" ;
await sendAndWait ( pi , ctx , quickCheckPrompt ( args || "" ) ) ;
2026-05-19 18:21:34 +02:00
}
} ) ;
// ── Dokumentations-Phase ─────────────────────────────────────────────────
pi . registerCommand ( "update_doku" , {
2026-05-22 23:49:53 +02:00
description : "Inkrementelle Code-Kommentare + README.md + BEDIENUNGSANLEITUNG.md via Git-Tags." ,
2026-05-19 18:21:34 +02:00
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 ) {
2026-05-22 23:49:53 +02:00
const tmpFile = ` /tmp/pi_patch_ ${ Date . now ( ) } _ ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } .diff ` ;
2026-05-19 18:21:34 +02:00
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." } ] } ;
}
} ) ;
2026-05-20 20:02:20 +02:00
// ── Planungsmodus ────────────────────────────────────────────────────────
pi . registerCommand ( "plan" , {
2026-05-22 23:49:53 +02:00
description : "Erstellt Implementierungsplan in PLAN.md ohne Dateiänderungen → qwen3.5-coder." ,
2026-05-20 20:02:20 +02:00
handler : async function ( args : string , ctx : ExtensionCommandContext ) {
const task = ( args || "" ) . trim ( ) ;
if ( ! task ) {
ctx . ui . notify ( "Benutzung: /plan <auftrag>" , "error" ) ;
return ;
}
2026-05-22 23:49:53 +02:00
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 ;
}
2026-05-20 20:02:20 +02:00
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)…" ) ;
2026-05-22 23:49:53 +02:00
currentActivity = "Coder plant (kein Code)…" ;
await sendAndWait ( pi , ctx , planPrompt ( task ) ) ;
2026-05-20 20:02:20 +02:00
ctx . ui . setStatus ( "plan" , "" ) ;
finalNotify ( ctx , "📋 Plan" , "Analyse abgeschlossen — PLAN.md + Chat" ) ;
}
} ) ;
2026-05-22 23:49:53 +02:00
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 <auftrag> [--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 <auftrag> Nur Implementierung ohne Review-Loop → Coder" ,
"/patch <änderung> Gezielte Minimaländerung → Coder" ,
"/quick_check [was] Schnelle OK/PROBLEM-Prüfung → Judge" ,
"/plan <auftrag> 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 <pfad> Projektverzeichnis + git init + .gitignore" ,
] . join ( "\n" ) , "info" ) ;
}
} ) ;
2026-05-20 20:02:20 +02:00
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" , {
2026-05-22 23:49:53 +02:00
description : "Löscht PLAN.md und verwirft den aktuellen Plan." ,
2026-05-20 20:02:20 +02:00
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" , {
2026-05-29 17:51:54 +02:00
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
2026-05-22 23:49:53 +02:00
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 ;
}
2026-05-20 20:02:20 +02:00
await switchModel ( pi , ctx , "llama-cpp-coder" , "qwen3.5-coder" ) ;
ctx . ui . setStatus ( "continue" , "Analysiere unterbrochenen Prozess…" ) ;
2026-05-22 23:49:53 +02:00
currentActivity = "Coder analysiert Stand…" ;
await sendAndWait ( pi , ctx , [
2026-05-20 20:02:20 +02:00
"Ein Prozess wurde unterbrochen. Analysiere den aktuellen Stand und führe ihn sinnvoll fort:" ,
"1. Lies TASK.md für den Auftrag" ,
"2. Lies PLAN.md falls vorhanden (war ein Plan in Arbeit?)" ,
"3. Führe 'git log --oneline -5' aus um zu sehen was bereits committed wurde" ,
"4. Entscheide: Muss noch implementiert werden? Ist ein Review fällig? Müssen Fixes nachgezogen werden?" ,
"5. Fahre direkt mit dem nächsten sinnvollen Schritt fort — kein langer Bericht, einfach weitermachen." ,
] . join ( "\n" ) ) ;
ctx . ui . setStatus ( "continue" , "" ) ;
}
} ) ;
2026-05-19 18:21:34 +02:00
// ── Projekt-Scaffolding ──────────────────────────────────────────────────
pi . registerCommand ( "new_project" , {
2026-05-22 23:49:53 +02:00
description : "Legt Projektverzeichnis, git-Repo und .gitignore an." ,
2026-05-19 18:21:34 +02:00
handler : async function ( args : string , ctx : ExtensionCommandContext ) {
const rawPath = ( args || "" ) . trim ( ) ;
if ( ! rawPath ) {
ctx . ui . notify ( "Benutzung: /new_project <pfad>" , "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 ` ) ;
}
} ) ;
}