767 lines
31 KiB
TypeScript
767 lines
31 KiB
TypeScript
// 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");
|
||
}
|
||
|
||
// ── 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");
|
||
}
|
||
|
||
// ── Extension ────────────────────────────────────────────────────────────────
|
||
|
||
export default function (pi: ExtensionAPI) {
|
||
pi.on("session_start", async function (_event, ctx) {
|
||
ctx.ui.setWidget("coder-judge", [
|
||
"Workflow: /coder <auftrag> | /judge | /fix | /shipit",
|
||
"Auto-Loop: /optimize <auftrag> [--rounds N] [--with-doku]",
|
||
"Kleine Änderung: /patch <änderung> → /quick_check [was]",
|
||
"Finale Doku: /update_doku (nach SHIP – Kommentare + README + Bedienungsanleitung)",
|
||
"Neues Projekt: /new_project <pfad>",
|
||
"Modell wird automatisch gewechselt (Coder→:8001, Judge→:8002)"
|
||
]);
|
||
});
|
||
|
||
// ── Robustes edit: Bottom-up-Reordering via tool_call-Hook ─────────────
|
||
// Behebt "edits[n] doesn't match": Mehrere Edits auf dieselbe Datei werden
|
||
// von hinten nach vorne sortiert, damit frühere Edits spätere Positionen nicht verschieben.
|
||
|
||
pi.on("tool_call", async function (event, ctx) {
|
||
if (event.toolName !== "edit") return;
|
||
|
||
const input = event.input as {
|
||
path: string;
|
||
edits: Array<{ oldText: string; newText: string }>;
|
||
};
|
||
|
||
if (!input?.edits || input.edits.length <= 1) return;
|
||
|
||
const readResult = await pi.exec(
|
||
"bash",
|
||
["-c", `cat "$1"`, "_", input.path],
|
||
{ cwd: ctx.cwd }
|
||
);
|
||
if (readResult.code !== 0) return;
|
||
|
||
const content = readResult.stdout;
|
||
|
||
const positioned = input.edits.map(edit => ({
|
||
edit,
|
||
idx: content.indexOf(edit.oldText)
|
||
}));
|
||
|
||
// Nicht gefundene Einträge (idx === -1) ans Ende — sie schlagen sowieso fehl
|
||
positioned.sort((a, b) => b.idx - a.idx);
|
||
|
||
input.edits.splice(0, input.edits.length, ...positioned.map(p => p.edit));
|
||
});
|
||
|
||
// ── Manuelle Kommandos ───────────────────────────────────────────────────
|
||
|
||
pi.registerCommand("coder", {
|
||
description: "Legt TASK.md an, startet Implementierung → qwen3.5-coder (:8001).",
|
||
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||
const task = (args || "").trim();
|
||
if (!task) {
|
||
ctx.ui.notify("Benutzung: /coder <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");
|
||
pi.sendUserMessage(shipitPrompt(args || ""));
|
||
}
|
||
});
|
||
|
||
// ── Automatische Optimierungsschleife ────────────────────────────────────
|
||
|
||
pi.registerCommand("optimize", {
|
||
description: "Coder→Judge→Fix-Schleife bis PASS + optional Doku. /optimize <auftrag> [--rounds N] [--with-doku] [--continue]",
|
||
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||
const roundsMatch = (args || "").match(/--rounds\s+(\d+)/);
|
||
const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 3;
|
||
const withDoku = /--with-doku/.test(args || "");
|
||
const continueMode = /--continue/.test(args || "");
|
||
const task = (args || "")
|
||
.replace(/--rounds\s+\d+/, "")
|
||
.replace(/--with-doku/, "")
|
||
.replace(/--continue/, "")
|
||
.trim();
|
||
|
||
if (!continueMode && !task) {
|
||
ctx.ui.notify("Benutzung: /optimize <auftrag> [--rounds N] [--with-doku] [--continue]", "error");
|
||
return;
|
||
}
|
||
|
||
if (continueMode) {
|
||
// --continue: Implementierungsphase überspringen, direkt in Judge→Fix-Schleife
|
||
ctx.ui.setStatus("optimize", `Setze fort (max ${maxRounds} Runden Judge→Fix)…`);
|
||
ctx.ui.notify("--continue: Überspringe Implementierung, starte direkt mit Judge-Prüfung.", "info");
|
||
} else {
|
||
// TASK.md anlegen und Implementierung starten
|
||
await writeTaskMd(pi, ctx, task);
|
||
ctx.ui.setStatus("optimize", `Starte Optimierung (max ${maxRounds} Runden)…`);
|
||
ctx.ui.setStatus("optimize", "Phase 1: Coder implementiert…");
|
||
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
||
await sendAndWait(pi, ctx, coderKickoff(task));
|
||
await tickTaskMdStatus(pi, ctx, "Implementierung");
|
||
}
|
||
|
||
let lastBlockers = "";
|
||
let verdict = "";
|
||
|
||
// Schleife: Judge → (PASS? fertig : Fix → nächste Runde)
|
||
for (let round = 1; round <= maxRounds; round++) {
|
||
ctx.ui.setStatus("optimize", `Runde ${round}/${maxRounds}: Judge prüft…`);
|
||
await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge");
|
||
await sendAndWait(pi, ctx, judgePrompt(""));
|
||
|
||
const judgeText = getLastAssistantText(ctx);
|
||
verdict = parseVerdict(judgeText);
|
||
|
||
if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") {
|
||
await tickTaskMdStatus(pi, ctx, "Review bestanden (PASS)");
|
||
ctx.ui.setStatus("optimize", `✓ ${verdict} nach Runde ${round}`);
|
||
ctx.ui.notify(`Optimierung abgeschlossen: ${verdict} nach ${round} Runde(n)`, "info");
|
||
break;
|
||
}
|
||
|
||
// Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen
|
||
const currentBlockers = parseBlockers(judgeText);
|
||
if (currentBlockers && currentBlockers === lastBlockers) {
|
||
ctx.ui.setStatus("optimize", "⚠ Schleife: gleicher Blocker – manuelle Intervention nötig");
|
||
ctx.ui.notify(
|
||
"Derselbe Blocker tritt erneut auf – Schleife abgebrochen. Bitte manuell prüfen.",
|
||
"warning"
|
||
);
|
||
return;
|
||
}
|
||
lastBlockers = currentBlockers;
|
||
|
||
if (round === maxRounds) {
|
||
ctx.ui.setStatus("optimize", `⚠ Max. ${maxRounds} Runden ohne PASS`);
|
||
ctx.ui.notify(`${maxRounds} Runden durchlaufen ohne PASS. Bitte manuell prüfen.`, "warning");
|
||
return;
|
||
}
|
||
|
||
// Fix-Phase
|
||
ctx.ui.setStatus("optimize", `Runde ${round}/${maxRounds}: Coder fixt…`);
|
||
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
||
await sendAndWait(pi, ctx, fixPrompt(""));
|
||
}
|
||
|
||
// Finale ShipIt-Prüfung nur bei PASS
|
||
if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") {
|
||
ctx.ui.setStatus("optimize", "Finale ShipIt-Prüfung…");
|
||
await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge");
|
||
await sendAndWait(pi, ctx, shipitPrompt(""));
|
||
|
||
const shipText = getLastAssistantText(ctx);
|
||
const shipVerdict = shipText.match(/Urteil:\s*(SHIP|NO-SHIP)/i)?.[1]?.toUpperCase() ?? "";
|
||
|
||
if (shipVerdict === "SHIP") {
|
||
ctx.ui.setStatus("optimize", "🚀 SHIP – produktionsreif");
|
||
if (withDoku) {
|
||
await runUpdateDoku(pi, ctx);
|
||
} else {
|
||
ctx.ui.notify("Nächster Schritt: /update_doku für Code-Kommentare, README.md und BEDIENUNGSANLEITUNG.md", "info");
|
||
}
|
||
} else if (shipVerdict === "NO-SHIP") {
|
||
ctx.ui.setStatus("optimize", "⛔ NO-SHIP – noch nicht bereit");
|
||
} else {
|
||
ctx.ui.setStatus("optimize", "ShipIt abgeschlossen");
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// ── Schlanke Kommandos für kleine Änderungen ─────────────────────────────
|
||
|
||
pi.registerCommand("patch", {
|
||
description: "Gezielte Minimaländerung ohne vollständigen Review → qwen3.5-coder (:8001).",
|
||
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||
const change = (args || "").trim();
|
||
if (!change) {
|
||
ctx.ui.notify("Benutzung: /patch <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." }] };
|
||
}
|
||
});
|
||
|
||
// ── 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`);
|
||
}
|
||
});
|
||
}
|