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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
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<void> {
|
|
|
|
|
|
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<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 }
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
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> {
|
|
|
|
|
|
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");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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" });
|
|
|
|
|
|
const level = verdict.includes("SHIP") && !verdict.includes("NO-SHIP") ? "warning"
|
|
|
|
|
|
: verdict.includes("NO-SHIP") ? "error"
|
|
|
|
|
|
: verdict.includes("⚠") ? "warning"
|
|
|
|
|
|
: "info";
|
|
|
|
|
|
ctx.ui.notify(`${verdict}: ${detail}`, level);
|
|
|
|
|
|
ctx.ui.setWidget("coder-judge", [
|
|
|
|
|
|
`Letzter Lauf: ${verdict} — ${detail} (${timestamp})`,
|
|
|
|
|
|
"─────────────────────────────────────────",
|
2026-05-20 20:02:20 +02:00
|
|
|
|
"Workflow: /coder <auftrag> | /judge | /fix | /shipit",
|
2026-05-20 02:08:09 +02:00
|
|
|
|
"Auto-Loop: /optimize <auftrag> [--rounds N] [--with-doku] [--continue]",
|
2026-05-20 20:02:20 +02:00
|
|
|
|
"Planung: /plan <auftrag> → /coder | /optimize --continue | /discard",
|
|
|
|
|
|
"Patch: /patch <änderung> → /quick_check [was]",
|
|
|
|
|
|
"Doku: /update_doku | Neues Projekt: /new_project <pfad>",
|
|
|
|
|
|
"Abbruch: Escape (Generation laufend) | /cancel (Loop nach aktuellem Schritt)",
|
|
|
|
|
|
"Resume: /continue | Modell: auto (Coder→:8001, Judge→:8002)",
|
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-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-20 20:02:20 +02:00
|
|
|
|
"Workflow: /coder <auftrag> | /judge | /fix | /shipit",
|
|
|
|
|
|
"Auto-Loop: /optimize <auftrag> [--rounds N] [--with-doku] [--continue]",
|
|
|
|
|
|
"Planung: /plan <auftrag> → /coder | /optimize --continue | /discard",
|
|
|
|
|
|
"Patch: /patch <änderung> → /quick_check [was]",
|
|
|
|
|
|
"Doku: /update_doku | Neues Projekt: /new_project <pfad>",
|
|
|
|
|
|
"Abbruch: Escape (Generation laufend) | /cancel (Loop nach aktuellem Schritt)",
|
|
|
|
|
|
"Resume: /continue | Modell: auto (Coder→:8001, Judge→:8002)",
|
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", {
|
|
|
|
|
|
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 <auftrag>", "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");
|
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-19 18:21:34 +02:00
|
|
|
|
pi.sendUserMessage(shipitPrompt(args || ""));
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── Automatische Optimierungsschleife ────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
pi.registerCommand("optimize", {
|
2026-05-20 01:42:26 +02:00
|
|
|
|
description: "Coder→Judge→Fix-Schleife bis PASS + optional Doku. /optimize <auftrag> [--rounds N] [--with-doku] [--continue]",
|
2026-05-19 18:21:34 +02:00
|
|
|
|
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 || "");
|
2026-05-20 01:42:26 +02:00
|
|
|
|
const continueMode = /--continue/.test(args || "");
|
2026-05-19 18:21:34 +02:00
|
|
|
|
const task = (args || "")
|
|
|
|
|
|
.replace(/--rounds\s+\d+/, "")
|
|
|
|
|
|
.replace(/--with-doku/, "")
|
2026-05-20 01:42:26 +02:00
|
|
|
|
.replace(/--continue/, "")
|
2026-05-19 18:21:34 +02:00
|
|
|
|
.trim();
|
|
|
|
|
|
|
2026-05-20 01:42:26 +02:00
|
|
|
|
if (!continueMode && !task) {
|
|
|
|
|
|
ctx.ui.notify("Benutzung: /optimize <auftrag> [--rounds N] [--with-doku] [--continue]", "error");
|
2026-05-19 18:21:34 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 01:42:26 +02:00
|
|
|
|
if (continueMode) {
|
|
|
|
|
|
// --continue: Implementierungsphase überspringen, direkt in Judge→Fix-Schleife
|
2026-05-20 21:07:16 +02:00
|
|
|
|
// Erweiterter Auftrag wird als Zusatzauftrag in TASK.md eingetragen (falls angegeben)
|
|
|
|
|
|
if (task) await writeTaskMd(pi, ctx, task);
|
2026-05-20 01:42:26 +02:00
|
|
|
|
ctx.ui.setStatus("optimize", `Setze fort (max ${maxRounds} Runden Judge→Fix)…`);
|
2026-05-20 21:07:16 +02:00
|
|
|
|
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-20 01:42:26 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
// TASK.md anlegen und Implementierung starten
|
|
|
|
|
|
await writeTaskMd(pi, ctx, task);
|
|
|
|
|
|
ctx.ui.setStatus("optimize", `Starte Optimierung (max ${maxRounds} Runden)…`);
|
2026-05-20 20:02:20 +02:00
|
|
|
|
const taskPreview = task.length > 55 ? task.slice(0, 52) + "…" : task;
|
|
|
|
|
|
ctx.ui.setStatus("optimize", `◉ Coder liest Anforderungen + implementiert: ${taskPreview}`);
|
2026-05-20 01:42:26 +02:00
|
|
|
|
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
|
|
|
|
|
await sendAndWait(pi, ctx, coderKickoff(task));
|
|
|
|
|
|
await tickTaskMdStatus(pi, ctx, "Implementierung");
|
2026-05-20 20:02:20 +02:00
|
|
|
|
if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", "Nach Implementierung"); return; }
|
2026-05-20 01:42:26 +02:00
|
|
|
|
}
|
2026-05-19 18:21:34 +02:00
|
|
|
|
|
|
|
|
|
|
let lastBlockers = "";
|
|
|
|
|
|
let verdict = "";
|
|
|
|
|
|
|
|
|
|
|
|
// Schleife: Judge → (PASS? fertig : Fix → nächste Runde)
|
|
|
|
|
|
for (let round = 1; round <= maxRounds; round++) {
|
2026-05-20 20:02:20 +02:00
|
|
|
|
const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round);
|
|
|
|
|
|
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge — TASK.md + letzter Commit + Tests…`);
|
2026-05-19 18:21:34 +02:00
|
|
|
|
await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge");
|
|
|
|
|
|
await sendAndWait(pi, ctx, judgePrompt(""));
|
2026-05-20 20:02:20 +02:00
|
|
|
|
if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", `Nach Judge Runde ${round}`); return; }
|
2026-05-19 18:21:34 +02:00
|
|
|
|
|
|
|
|
|
|
const judgeText = getLastAssistantText(ctx);
|
|
|
|
|
|
verdict = parseVerdict(judgeText);
|
|
|
|
|
|
|
|
|
|
|
|
if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") {
|
|
|
|
|
|
await tickTaskMdStatus(pi, ctx, "Review bestanden (PASS)");
|
2026-05-20 20:02:20 +02:00
|
|
|
|
ctx.ui.setStatus("optimize", `${"●".repeat(round)} ✓ ${verdict} nach Runde ${round}/${maxRounds} — ShipIt…`);
|
2026-05-19 18:21:34 +02:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen
|
|
|
|
|
|
const currentBlockers = parseBlockers(judgeText);
|
|
|
|
|
|
if (currentBlockers && currentBlockers === lastBlockers) {
|
2026-05-20 20:02:20 +02:00
|
|
|
|
ctx.ui.setStatus("optimize", `${prog} ⚠ Gleicher Blocker in Runde ${round} – manuelle Intervention nötig`);
|
2026-05-20 02:08:09 +02:00
|
|
|
|
finalNotify(ctx, "⚠ Schleife", "Gleicher Blocker zweimal – manuelle Intervention nötig");
|
2026-05-19 18:21:34 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
lastBlockers = currentBlockers;
|
|
|
|
|
|
|
|
|
|
|
|
if (round === maxRounds) {
|
2026-05-20 20:02:20 +02:00
|
|
|
|
ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)} ⚠ Max. ${maxRounds} Runden ohne PASS`);
|
2026-05-20 02:08:09 +02:00
|
|
|
|
finalNotify(ctx, "⚠ Kein PASS", `${maxRounds} Runden ohne PASS – bitte /judge und /fix manuell`);
|
2026-05-19 18:21:34 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 20:02:20 +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}`);
|
2026-05-19 18:21:34 +02:00
|
|
|
|
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
|
|
|
|
|
await sendAndWait(pi, ctx, fixPrompt(""));
|
2026-05-20 20:02:20 +02:00
|
|
|
|
if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", `Nach Fix Runde ${round}`); return; }
|
2026-05-19 18:21:34 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Finale ShipIt-Prüfung nur bei PASS
|
|
|
|
|
|
if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") {
|
2026-05-20 20:02:20 +02:00
|
|
|
|
ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)}◉ ShipIt — SHIP oder NO-SHIP?…`);
|
2026-05-19 18:21:34 +02:00
|
|
|
|
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");
|
2026-05-20 02:08:09 +02:00
|
|
|
|
finalNotify(ctx, "🚀 SHIP", "Programm ist produktionsreif");
|
2026-05-19 18:21:34 +02:00
|
|
|
|
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");
|
2026-05-20 02:08:09 +02:00
|
|
|
|
finalNotify(ctx, "⛔ NO-SHIP", "Noch Blocker offen – bitte /judge und /fix manuell");
|
2026-05-19 18:21:34 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
ctx.ui.setStatus("optimize", "ShipIt abgeschlossen");
|
2026-05-20 02:08:09 +02:00
|
|
|
|
finalNotify(ctx, "ShipIt", "Kein klares Urteil – Antwort im Chat prüfen");
|
2026-05-19 18:21:34 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── 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 <beschreibung der änderung>", "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." }] };
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-20 20:02:20 +02:00
|
|
|
|
// ── Planungsmodus ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
pi.registerCommand("plan", {
|
|
|
|
|
|
description: "Analysiert Auftrag, schmiedet Implementierungsplan in PLAN.md — macht keine Dateiänderungen. → qwen3.5-coder (:8001)",
|
|
|
|
|
|
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
|
|
|
|
|
const task = (args || "").trim();
|
|
|
|
|
|
if (!task) {
|
|
|
|
|
|
ctx.ui.notify("Benutzung: /plan <auftrag>", "error");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
await writeTaskMd(pi, ctx, task);
|
|
|
|
|
|
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
|
|
|
|
|
ctx.ui.setStatus("plan", "Analysiere und plane (keine Dateiänderungen)…");
|
|
|
|
|
|
pi.sendUserMessage(planPrompt(task));
|
|
|
|
|
|
await ctx.waitForIdle();
|
|
|
|
|
|
ctx.ui.setStatus("plan", "");
|
|
|
|
|
|
finalNotify(ctx, "📋 Plan", "Analyse abgeschlossen — PLAN.md + Chat");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
pi.registerCommand("cancel", {
|
|
|
|
|
|
description: "Bricht laufenden Optimize-Loop nach dem aktuellen Schritt ab.",
|
|
|
|
|
|
handler: async function (_args: string, ctx: ExtensionCommandContext) {
|
|
|
|
|
|
cancelRequested = true;
|
|
|
|
|
|
ctx.ui.notify("Abbruch angefordert — wird nach aktuellem Schritt gestoppt", "warning");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
pi.registerCommand("discard", {
|
|
|
|
|
|
description: "Verwirft PLAN.md und setzt den Planungsstatus zurück.",
|
|
|
|
|
|
handler: async function (_args: string, ctx: ExtensionCommandContext) {
|
|
|
|
|
|
await pi.exec("bash", ["-c", "rm -f PLAN.md"], { cwd: ctx.cwd });
|
|
|
|
|
|
ctx.ui.notify("PLAN.md gelöscht — Plan verworfen", "info");
|
|
|
|
|
|
finalNotify(ctx, "🗑 Plan verworfen", "Neu starten mit /plan oder /coder");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
pi.registerCommand("continue", {
|
|
|
|
|
|
description: "Nimmt unterbrochenen Prozess wieder auf — liest TASK.md, PLAN.md, git log und entscheidet den nächsten Schritt.",
|
|
|
|
|
|
handler: async function (_args: string, ctx: ExtensionCommandContext) {
|
|
|
|
|
|
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
|
|
|
|
|
ctx.ui.setStatus("continue", "Analysiere unterbrochenen Prozess…");
|
|
|
|
|
|
pi.sendUserMessage([
|
|
|
|
|
|
"Ein Prozess wurde unterbrochen. Analysiere den aktuellen Stand und führe ihn sinnvoll fort:",
|
|
|
|
|
|
"1. Lies TASK.md für den Auftrag",
|
|
|
|
|
|
"2. Lies PLAN.md falls vorhanden (war ein Plan in Arbeit?)",
|
|
|
|
|
|
"3. Führe 'git log --oneline -5' aus um zu sehen was bereits committed wurde",
|
|
|
|
|
|
"4. Entscheide: Muss noch implementiert werden? Ist ein Review fällig? Müssen Fixes nachgezogen werden?",
|
|
|
|
|
|
"5. Fahre direkt mit dem nächsten sinnvollen Schritt fort — kein langer Bericht, einfach weitermachen.",
|
|
|
|
|
|
].join("\n"));
|
|
|
|
|
|
await ctx.waitForIdle();
|
|
|
|
|
|
ctx.ui.setStatus("continue", "");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-19 18:21:34 +02:00
|
|
|
|
// ── Projekt-Scaffolding ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
pi.registerCommand("new_project", {
|
|
|
|
|
|
description: "Legt Projektverzeichnis an + git init + .gitignore. /new_project <pfad>",
|
|
|
|
|
|
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`);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|