pi_coder/pi-coder-judge-extension.ts
dschlueter 4a31535b76 feat: /plan, /cancel, /continue, /discard + Context 262144 + KV-Cache q4_0
- Neue Befehle: /plan (Planungsmodus, nur PLAN.md), /cancel (Loop-Abbruch),
  /continue (Resume nach Unterbrechung), /discard (PLAN.md verwerfen)
- contextWindow in models.json und llama.cpp-Servern: 131072 → 262144
- KV-Cache: q8_0 → q4_0 (weniger VRAM, passt zu 262k-Kontext auf 2× 3090)
- parallel: 2 → 1 beim Coder (stabiler bei großem Kontext)
- Optimize-Status mit ASCII-Fortschrittsbalken + Blocker-Preview
- cancelRequested-Flag prüft nach jedem Loop-Schritt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 20:02:20 +02:00

889 lines
38 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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");
}
function planPrompt(task: string): string {
return [
"Du bist ein erfahrener Software-Architekt im PLANUNGSMODUS.",
"",
"ABSOLUTE VERBOTE — du darfst NICHT:",
"- Dateien editieren, schreiben oder löschen (kein edit, write, apply_patch)",
"- Git-Commits durchführen",
"- Tests oder Skripte ausführen die Seiteneffekte haben",
"",
"ERLAUBT:",
"- Dateien lesen (read, cat, grep, find)",
"- Git-History lesen (git log, git show, git diff)",
"- PLAN.md anlegen oder überschreiben (das ist dein Ausgabe-Dokument)",
"",
"Analysiere den Auftrag gründlich und erstelle einen konkreten Implementierungsplan.",
"",
"Auftrag:",
task,
"",
"Struktur deiner Ausgabe:",
"1. IST-Analyse (relevante Dateien, Architektur, Abhängigkeiten)",
"2. Implementierungsplan (nummerierte Schritte, konkret und umsetzbar)",
"3. Kritische Entscheidungen (Alternativen + Empfehlung)",
"4. Risiken und offene Fragen",
"5. Geschätzte Komplexität: einfach / mittel / komplex",
"",
"Schreibe den vollständigen Plan in PLAN.md.",
"Schließe ab mit: 'Plan bereit. Starte Umsetzung mit /coder oder /optimize --continue'",
].join("\n");
}
// ── Hilfsfunktionen ─────────────────────────────────────────────────────────
// Legt TASK.md neu an oder hängt einen Zusatzauftrag an.
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");
}
// Prominente Abschluss-Notification + Widget-Update mit Uhrzeit und Ergebnis.
function finalNotify(
ctx: ExtensionCommandContext,
verdict: string,
detail: string
): void {
const timestamp = new Date().toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
const level = verdict.includes("SHIP") && !verdict.includes("NO-SHIP") ? "warning"
: verdict.includes("NO-SHIP") ? "error"
: verdict.includes("⚠") ? "warning"
: "info";
ctx.ui.notify(`${verdict}: ${detail}`, level);
ctx.ui.setWidget("coder-judge", [
`Letzter Lauf: ${verdict}${detail} (${timestamp})`,
"─────────────────────────────────────────",
"Workflow: /coder <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)",
]);
}
// ── Extension ────────────────────────────────────────────────────────────────
let cancelRequested = false;
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] [--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)",
]);
});
// ── 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");
ctx.ui.notify("Judge prüft finale Freigabe — Ergebnis erscheint im Chat (SHIP / NO-SHIP)", "info");
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)…`);
const taskPreview = task.length > 55 ? task.slice(0, 52) + "…" : task;
ctx.ui.setStatus("optimize", `◉ Coder liest Anforderungen + implementiert: ${taskPreview}`);
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
await sendAndWait(pi, ctx, coderKickoff(task));
await tickTaskMdStatus(pi, ctx, "Implementierung");
if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", "Nach Implementierung"); return; }
}
let lastBlockers = "";
let verdict = "";
// Schleife: Judge → (PASS? fertig : Fix → nächste Runde)
for (let round = 1; round <= maxRounds; round++) {
const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round);
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge — TASK.md + letzter Commit + Tests…`);
await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge");
await sendAndWait(pi, ctx, judgePrompt(""));
if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", `Nach Judge Runde ${round}`); return; }
const judgeText = getLastAssistantText(ctx);
verdict = parseVerdict(judgeText);
if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") {
await tickTaskMdStatus(pi, ctx, "Review bestanden (PASS)");
ctx.ui.setStatus("optimize", `${"●".repeat(round)}${verdict} nach Runde ${round}/${maxRounds} — ShipIt…`);
break;
}
// Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen
const currentBlockers = parseBlockers(judgeText);
if (currentBlockers && currentBlockers === lastBlockers) {
ctx.ui.setStatus("optimize", `${prog} ⚠ Gleicher Blocker in Runde ${round} manuelle Intervention nötig`);
finalNotify(ctx, "⚠ Schleife", "Gleicher Blocker zweimal manuelle Intervention nötig");
return;
}
lastBlockers = currentBlockers;
if (round === maxRounds) {
ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)} ⚠ Max. ${maxRounds} Runden ohne PASS`);
finalNotify(ctx, "⚠ Kein PASS", `${maxRounds} Runden ohne PASS bitte /judge und /fix manuell`);
return;
}
// Fix-Phase: Blocker-Preview aus Judge-Bericht anzeigen
const blockerHint = currentBlockers
? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers)
: "Kritikpunkte aus Judge-Bericht";
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Coder fixt — ${blockerHint}`);
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
await sendAndWait(pi, ctx, fixPrompt(""));
if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", `Nach Fix Runde ${round}`); return; }
}
// Finale ShipIt-Prüfung nur bei PASS
if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") {
ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)}◉ ShipIt — SHIP oder NO-SHIP?…`);
await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge");
await sendAndWait(pi, ctx, shipitPrompt(""));
const shipText = getLastAssistantText(ctx);
const shipVerdict = shipText.match(/Urteil:\s*(SHIP|NO-SHIP)/i)?.[1]?.toUpperCase() ?? "";
if (shipVerdict === "SHIP") {
ctx.ui.setStatus("optimize", "🚀 SHIP produktionsreif");
finalNotify(ctx, "🚀 SHIP", "Programm ist produktionsreif");
if (withDoku) {
await runUpdateDoku(pi, ctx);
} else {
ctx.ui.notify("Nächster Schritt: /update_doku für Code-Kommentare, README.md und BEDIENUNGSANLEITUNG.md", "info");
}
} else if (shipVerdict === "NO-SHIP") {
ctx.ui.setStatus("optimize", "⛔ NO-SHIP noch nicht bereit");
finalNotify(ctx, "⛔ NO-SHIP", "Noch Blocker offen bitte /judge und /fix manuell");
} else {
ctx.ui.setStatus("optimize", "ShipIt abgeschlossen");
finalNotify(ctx, "ShipIt", "Kein klares Urteil Antwort im Chat prüfen");
}
}
}
});
// ── 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." }] };
}
});
// ── 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", "");
}
});
// ── 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`);
}
});
}