refactor: Single-GPU-Betrieb + System-Prompt-Rollen + /optimize-Bugfixes

Single-GPU-Umstellung:
- models.json auf ein Modell qwen3.5-single kollabiert; tote Provider
  (llama-cpp, llama-cpp-coder, llama-cpp-judge) entfernt
- Coder/Judge-Rolle via before_agent_start-Hook (Persona als System-Prompt)
  statt Modellwechsel; switchModel -> ensureSingleModel + selectRole
- Dual-GPU-Skripte entfernt (start-coder/judge/servers.sh); stop/status auf
  Single-Container reduziert

/optimize-Bugfixes:
- Crash behoben: ctx.ui.select positional aufrufen (verursachte
  "Cannot read properties of undefined (reading 'length')" bei SHIP/version)
- PASS WITH CONCERNS loest jetzt eine Coder-Fix-Runde aus; nur sauberes PASS
  gilt als bestanden (--approve-concerns bleibt Opt-out)
- Quick-Judge abgeschafft: immer voller Judge ab Runde 1
- Loop-Erkennung greift auch ohne Blocker-Abschnitt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-15 02:28:42 +02:00
commit ca85c45a39
5 changed files with 129 additions and 273 deletions

View file

@ -16,28 +16,6 @@
] ]
}, },
"llama-cpp": {
"baseUrl": "http://127.0.0.1:8000/v1",
"api": "openai-completions",
"apiKey": "none",
"compat": {
"supportsDeveloperRole": false,
"supportsReasoningEffort": false,
"maxTokensField": "max_tokens",
"thinkingFormat": "qwen-chat-template"
},
"models": [
{
"id": "qwen35b-uncensored",
"name": "Qwen3.6 35B Uncensored (llama.cpp :8000)"
},
{
"id": "qwen35b-moe-tools",
"name": "Qwen3.6 35B MoE Tools (llama.cpp :8000)"
}
]
},
"llama-cpp-single": { "llama-cpp-single": {
"baseUrl": "http://127.0.0.1:8001/v1", "baseUrl": "http://127.0.0.1:8001/v1",
"api": "openai-completions", "api": "openai-completions",
@ -50,78 +28,13 @@
}, },
"models": [ "models": [
{ {
"id": "qwen3.5-coder", "id": "qwen3.5-single",
"name": "Qwen3.6 27B Single-Server Coder (llama.cpp :8001)", "name": "Qwen3.6 27B Single-Server (llama.cpp :8001)",
"reasoning": true, "reasoning": true,
"input": ["text"], "input": ["text"],
"contextWindow": 262144, "contextWindow": 262144,
"maxTokens": 16384, "maxTokens": 16384,
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
},
{
"id": "qwen3.5-judge",
"name": "Qwen3.6 27B Single-Server Judge (llama.cpp :8001)",
"reasoning": true,
"input": ["text"],
"contextWindow": 262144,
"maxTokens": 16384,
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
}
]
},
"llama-cpp-coder": {
"baseUrl": "http://127.0.0.1:8001/v1",
"api": "openai-completions",
"apiKey": "none",
"compat": {
"supportsDeveloperRole": false,
"supportsReasoningEffort": false,
"maxTokensField": "max_tokens",
"thinkingFormat": "qwen-chat-template"
},
"models": [
{
"id": "qwen3.5-coder",
"name": "Qwen3.6 27B Coder (llama.cpp :8001)",
"reasoning": true,
"input": ["text"],
"contextWindow": 262144,
"maxTokens": 16384,
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
}
}
]
},
"llama-cpp-judge": {
"baseUrl": "http://127.0.0.1:8002/v1",
"api": "openai-completions",
"apiKey": "none",
"compat": {
"supportsDeveloperRole": false,
"supportsReasoningEffort": false,
"maxTokensField": "max_tokens",
"thinkingFormat": "qwen-chat-template"
},
"models": [
{
"id": "qwen3.5-judge",
"name": "Qwen3.6 27B Judge (llama.cpp :8002)",
"reasoning": true,
"input": ["text"],
"contextWindow": 262144,
"maxTokens": 16384,
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
}
} }
] ]
}, },

View file

@ -1,15 +1,37 @@
// pi-coder-judge-extension.ts // pi-coder-judge-extension.ts
// Automatisierter Coder-Judge-Fix-Workflow für AI-Coding-Assistenten // Automatisierter Coder-Judge-Fix-Workflow für AI-Coding-Assistenten
// Modelle: qwen3.5-coder + qwen3.5-judge auf Single-Server (Port 8001) // Ein Modell (qwen3.5-single) auf Single-Server (Port 8001); Rollen Coder/Judge via System-Prompt
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox"; import { Type } from "typebox";
// ── Persona-System-Prompts (Rollenumschaltung Coder/Judge) ───────────────────
// Diese beschreiben die STABILE Rolle und werden im before_agent_start-Hook an den
// Basis-System-Prompt angehängt — abhängig von currentRole. Bewusst neutral gehalten
// (keine Git-Commit-/Edit-Pflicht), damit sie auch für /plan (Nur-Lesen) gültig bleiben.
// Task-spezifische Anweisungen (committen, Ausgabeformat, Auftrag) stehen weiter in den
// jeweiligen User-Prompts unten.
function coderPersona(): string {
return [
"Du bist der Coding-Agent in einem Coder-Judge-Workflow.",
"Arbeite sorgfältig, konkret und produktionsorientiert; bevorzuge klare, einfache Lösungen.",
"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 statt mehrerer edit-Aufrufe. Fallback: Datei komplett neu schreiben.",
].join("\n");
}
function judgePersona(): string {
return [
"Du bist ein pingeliger, skeptischer Senior-Reviewer und QA-Ingenieur in einem Coder-Judge-Workflow.",
"Deine Aufgabe ist NICHT, nett zu sein, sondern Fehler, Risiken, Randfälle und Produktionsprobleme aktiv zu finden.",
"Arbeite reproduzierbar und konkret: Wenn du etwas behauptest, nenne die Datei, den Befehl, den Test oder den Reproduktionshinweis.",
].join("\n");
}
// ── Prompt-Bausteine ──────────────────────────────────────────────────────── // ── Prompt-Bausteine ────────────────────────────────────────────────────────
function coderKickoff(task: string): string { function coderKickoff(task: string): string {
return [ return [
"Du bist der Coding-Agent.",
"Lies TASK.md für die vollständige Aufgabenbeschreibung.", "Lies TASK.md für die vollständige Aufgabenbeschreibung.",
"Halte dich strikt an die dort beschriebenen Anforderungen.", "Halte dich strikt an die dort beschriebenen Anforderungen.",
"Arbeite sorgfältig, konkret und produktionsorientiert.", "Arbeite sorgfältig, konkret und produktionsorientiert.",
@ -32,7 +54,6 @@ function coderKickoff(task: string): string {
function judgePrompt(extra: string): string { function judgePrompt(extra: string): string {
const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : ""; const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : "";
return [ 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.", "Deine Aufgabe ist NICHT, nett zu sein, sondern Fehler, Risiken, Randfälle und Produktionsprobleme zu finden.",
"Arbeite reproduzierbar und konkret.", "Arbeite reproduzierbar und konkret.",
"", "",
@ -57,59 +78,11 @@ function judgePrompt(extra: string): string {
].join("\n") + suffix; ].join("\n") + suffix;
} }
// Kompakter Ersteindruck-Prompt für Runde 1: kein TASK.md, nur Diff-Review.
// Reduziert Inference-Zeit wenn der Code offensichtlich gut ist.
// Bei FAIL → Runde 2 mit vollem judgePrompt() für detaillierte Analyse.
function quickJudgePrompt(extra: string): string {
const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : "";
return [
"Schneller Code-Review — erster Eindruck.",
"Du bist ein skeptischer Senior-Reviewer. Sei direkt und knapp.",
"",
"1. Sieh dir 'git show HEAD' an.",
"2. Führe relevante Tests aus, falls vorhanden.",
"3. Gibt es offensichtliche Blocker? (Bugs, fehlende Fehlerbehandlung, Sicherheitslücken, kaputte Imports)",
"4. Wenn alles offensichtlich in Ordnung ist: PASS.",
"5. Bei Zweifeln oder Lücken: FAIL — konkrete Blocker benennen.",
"",
"Ausgabeformat (kompakt):",
"- Urteil: PASS | PASS WITH CONCERNS | FAIL",
"- Blocker (falls vorhanden)",
"- Konkrete Fix-Aufträge (falls FAIL)"
].join("\n") + suffix;
}
// Quick-Variante für Runde 1 mit bereits vorliegendem Test-Output.
function quickJudgeWithTestsPrompt(testOutput: string, extra: string): string {
const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : "";
return [
"Schneller Code-Review — erster Eindruck.",
"Du bist ein skeptischer Senior-Reviewer. Sei direkt und knapp.",
"",
"Die Test-Suite wurde bereits extern ausgeführt. Führe KEINE weiteren Tests aus.",
"",
"1. Sieh dir 'git show HEAD' an.",
"2. Analysiere das folgende Test-Ergebnis:",
"```",
testOutput,
"```",
"3. Gibt es offensichtliche Blocker? (Test-Failures, Bugs, Sicherheitslücken)",
"4. Wenn alles offensichtlich in Ordnung ist: PASS.",
"5. Bei Zweifeln: FAIL — konkrete Blocker benennen.",
"",
"Ausgabeformat (kompakt):",
"- Urteil: PASS | PASS WITH CONCERNS | FAIL",
"- Blocker (falls vorhanden)",
"- Konkrete Fix-Aufträge (falls FAIL)"
].join("\n") + suffix;
}
// Wie judgePrompt, aber Tests werden NICHT vom Judge ausgeführt — // Wie judgePrompt, aber Tests werden NICHT vom Judge ausgeführt —
// die Extension hat sie bereits extern gestartet und übergibt den Output. // die Extension hat sie bereits extern gestartet und übergibt den Output.
function judgeWithTestsPrompt(testOutput: string, extra: string): string { function judgeWithTestsPrompt(testOutput: string, extra: string): string {
const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : ""; const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : "";
return [ 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.", "Deine Aufgabe ist NICHT, nett zu sein, sondern Fehler, Risiken, Randfälle und Produktionsprobleme zu finden.",
"", "",
"Die Test-Suite wurde bereits extern ausgeführt. Das Ergebnis steht unten.", "Die Test-Suite wurde bereits extern ausgeführt. Das Ergebnis steht unten.",
@ -420,25 +393,43 @@ async function tickTaskMdStatus(
); );
} }
async function switchModel( // Single-Server: genau ein Modell (qwen3.5-single auf :8001) für beide Rollen.
// Der Rollenwechsel Coder/Judge erfolgt NICHT mehr über den Modellwechsel, sondern
// über den System-Prompt (siehe before_agent_start-Hook + currentRole). Diese Funktion
// stellt nur sicher, dass das eine Single-Modell aktiv ist; nach dem ersten Aufruf
// ist der Wechsel via currentModelKey gecacht und damit ein No-op.
const SINGLE_PROVIDER = "llama-cpp-single";
const SINGLE_MODEL_ID = "qwen3.5-single";
async function ensureSingleModel(
pi: ExtensionAPI, pi: ExtensionAPI,
ctx: ExtensionCommandContext, ctx: ExtensionCommandContext
provider: string,
modelId: string
): Promise<boolean> { ): Promise<boolean> {
const key = `${provider}/${modelId}`; const key = `${SINGLE_PROVIDER}/${SINGLE_MODEL_ID}`;
if (key === currentModelKey) return true; if (key === currentModelKey) return true;
const model = ctx.modelRegistry.find(provider, modelId); const model = ctx.modelRegistry.find(SINGLE_PROVIDER, SINGLE_MODEL_ID);
if (!model) { if (!model) {
ctx.ui.notify(`Modell ${provider}/${modelId} nicht gefunden`, "error"); ctx.ui.notify(`Modell ${SINGLE_PROVIDER}/${SINGLE_MODEL_ID} nicht gefunden`, "error");
return false; return false;
} }
const ok = await pi.setModel(model); const ok = await pi.setModel(model);
if (ok !== false) currentModelKey = key; if (ok !== false) currentModelKey = key;
if (!ok) ctx.ui.notify(`Kein API-Key für ${modelId}`, "warning"); if (!ok) ctx.ui.notify(`Kein API-Key für ${SINGLE_MODEL_ID}`, "warning");
return ok !== false; return ok !== false;
} }
// Setzt die aktive Rolle (steuert den per before_agent_start injizierten System-Prompt)
// und stellt das Single-Modell sicher. Ersetzt das frühere switchModel(): da beide Rollen
// dasselbe Modell nutzen, ist der einzige echte Effekt das Setzen von currentRole.
async function selectRole(
pi: ExtensionAPI,
ctx: ExtensionCommandContext,
role: "coder" | "judge"
): Promise<boolean> {
currentRole = role;
return ensureSingleModel(pi, ctx);
}
// Sendet eine Nachricht und wartet bis der Agent fertig ist. // Sendet eine Nachricht und wartet bis der Agent fertig ist.
// Retry-Schleife fängt "Agent is already processing" ab — tritt auf wenn // Retry-Schleife fängt "Agent is already processing" ab — tritt auf wenn
// waitForIdle() zu früh zurückkehrt (Race Condition im pi-Agent). // waitForIdle() zu früh zurückkehrt (Race Condition im pi-Agent).
@ -644,7 +635,7 @@ async function getFilesSinceTag(
// Dokumentations-Phase: inkrementell via Git-Tags, nur geänderte Dateien werden verarbeitet. // Dokumentations-Phase: inkrementell via Git-Tags, nur geänderte Dateien werden verarbeitet.
// Wird von /update_doku und /optimize --with-doku genutzt. // Wird von /update_doku und /optimize --with-doku genutzt.
async function runUpdateDoku(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> { async function runUpdateDoku(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> {
if (!await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder")) { if (!await selectRole(pi, ctx, "coder")) {
ctx.ui.notify("Coder-Modell nicht verfügbar — Dokumentations-Phase abgebrochen", "error"); ctx.ui.notify("Coder-Modell nicht verfügbar — Dokumentations-Phase abgebrochen", "error");
return; return;
} }
@ -761,10 +752,10 @@ async function detectVersionFile(
): Promise<"package.json" | "Cargo.toml" | "pyproject.toml" | "VERSION" | null> { ): Promise<"package.json" | "Cargo.toml" | "pyproject.toml" | "VERSION" | null> {
for (const f of ["package.json", "Cargo.toml", "pyproject.toml"]) { for (const f of ["package.json", "Cargo.toml", "pyproject.toml"]) {
const r = await pi.exec("bash", ["-c", `test -f ${f}`], { cwd: ctx.cwd }); const r = await pi.exec("bash", ["-c", `test -f ${f}`], { cwd: ctx.cwd });
if (r.exitCode === 0) return f as "package.json" | "Cargo.toml" | "pyproject.toml"; if (r.code === 0) return f as "package.json" | "Cargo.toml" | "pyproject.toml";
} }
const r = await pi.exec("bash", ["-c", "test -f VERSION"], { cwd: ctx.cwd }); const r = await pi.exec("bash", ["-c", "test -f VERSION"], { cwd: ctx.cwd });
return r.exitCode === 0 ? "VERSION" : null; return r.code === 0 ? "VERSION" : null;
} }
// Schreibt die neue Version in die Manifest-Datei und erstellt einen chore-Commit. // Schreibt die neue Version in die Manifest-Datei und erstellt einen chore-Commit.
@ -796,7 +787,7 @@ async function applyVersionBump(
async function runVersionBump(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> { async function runVersionBump(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> {
// Early exit wenn kein git-Repo vorhanden // Early exit wenn kein git-Repo vorhanden
const gitCheck = await pi.exec("bash", ["-c", "git rev-parse --is-inside-work-tree 2>/dev/null"], { cwd: ctx.cwd }); const gitCheck = await pi.exec("bash", ["-c", "git rev-parse --is-inside-work-tree 2>/dev/null"], { cwd: ctx.cwd });
if (gitCheck.exitCode !== 0) return; if (gitCheck.code !== 0) return;
const current = await getCurrentVersion(pi, ctx); const current = await getCurrentVersion(pi, ctx);
const tag = current ? `v${current[0]}.${current[1]}.${current[2]}` : undefined; const tag = current ? `v${current[0]}.${current[1]}.${current[2]}` : undefined;
@ -813,13 +804,12 @@ async function runVersionBump(pi: ExtensionAPI, ctx: ExtensionCommandContext): P
t => `${t}${versions[t]}${t === recommended ? " (empfohlen)" : ""}` t => `${t}${versions[t]}${t === recommended ? " (empfohlen)" : ""}`
); );
const choice = await ctx.ui.select({ // ctx.ui.select ist positional: select(title, options[], opts?). Die Erklärung muss in den
title: "Version", // Titel — ExtensionUIDialogOptions kennt kein message-Feld.
message: current const titleLine = current
? `Aktuelle Version: ${tag}. Commits seit letztem Tag: ${bump}-Bump erkannt.` ? `Version — aktuell ${tag}, ${bump}-Bump erkannt`
: "Noch kein Versions-Tag vorhanden.", : "Version — noch kein Versions-Tag vorhanden";
options: [...labels, "Überspringen"], const choice = await ctx.ui.select(titleLine, [...labels, "Überspringen"]);
});
if (!choice || choice.startsWith("Überspringen")) return; if (!choice || choice.startsWith("Überspringen")) return;
@ -833,7 +823,7 @@ async function runVersionBump(pi: ExtensionAPI, ctx: ExtensionCommandContext): P
} }
const tagResult = await pi.exec("bash", ["-c", `git tag ${newTag}`], { cwd: ctx.cwd }); const tagResult = await pi.exec("bash", ["-c", `git tag ${newTag}`], { cwd: ctx.cwd });
if (tagResult.exitCode !== 0) { if (tagResult.code !== 0) {
ctx.ui.notify(`Tag ${newTag} existiert bereits — manuell löschen mit: git tag -d ${newTag}`, "error"); ctx.ui.notify(`Tag ${newTag} existiert bereits — manuell löschen mit: git tag -d ${newTag}`, "error");
return; return;
} }
@ -883,7 +873,10 @@ function finalNotify(
// ── Extension ──────────────────────────────────────────────────────────────── // ── Extension ────────────────────────────────────────────────────────────────
let cancelRequested = false; let cancelRequested = false;
let currentModelKey = ""; // Cache für switchModel() — verhindert redundante setModel()-Aufrufe let currentModelKey = ""; // Cache für ensureSingleModel() — verhindert redundante setModel()-Aufrufe
// Aktive Rolle für die Single-Server-Architektur: steuert, welcher Persona-System-Prompt
// im before_agent_start-Hook injiziert wird. Wird ausschließlich über selectRole() gesetzt.
let currentRole: "coder" | "judge" = "coder";
let interactivePauseActive = false; let interactivePauseActive = false;
let interactiveContinueRequested = false; let interactiveContinueRequested = false;
let interactivePauseTask = ""; let interactivePauseTask = "";
@ -952,6 +945,17 @@ export default function (pi: ExtensionAPI) {
]); ]);
}); });
// ── Rollenumschaltung via System-Prompt ──────────────────────────────────
// Single-Server: ein Modell, zwei Rollen. Vor jedem Agent-Lauf wird die passende
// Persona (Coder/Judge, gesteuert über currentRole) an den fertig zusammengebauten
// Basis-System-Prompt ANGEHÄNGT — nicht ersetzt, damit Pi-Basis (Tools, cwd, Umgebung)
// erhalten bleibt. So unterscheiden sich Coder- und Judge-Turns auf System-Prompt-Ebene,
// ohne Server- oder Modellwechsel.
pi.on("before_agent_start", function (event) {
const persona = currentRole === "judge" ? judgePersona() : coderPersona();
return { systemPrompt: event.systemPrompt + "\n\n" + persona };
});
// ── Live-Aktivitätsstatus ──────────────────────────────────────────────── // ── Live-Aktivitätsstatus ────────────────────────────────────────────────
// turn_start: Working-Text auf aktuellen Command-Kontext setzen // turn_start: Working-Text auf aktuellen Command-Kontext setzen
pi.on("turn_start", function (_event, ctx) { pi.on("turn_start", function (_event, ctx) {
@ -1013,7 +1017,7 @@ export default function (pi: ExtensionAPI) {
// ── Manuelle Kommandos ─────────────────────────────────────────────────── // ── Manuelle Kommandos ───────────────────────────────────────────────────
pi.registerCommand("coder", { pi.registerCommand("coder", {
description: "Implementiert <auftrag> ohne Review-Loop → qwen3.5-coder (:8001).", description: "Implementiert <auftrag> ohne Review-Loop → Coder-Rolle (:8001).",
handler: async function (args: string, ctx: ExtensionCommandContext) { handler: async function (args: string, ctx: ExtensionCommandContext) {
const task = (args || "").trim(); const task = (args || "").trim();
if (!task) { if (!task) {
@ -1025,46 +1029,46 @@ export default function (pi: ExtensionAPI) {
return; return;
} }
await writeTaskMd(pi, ctx, task); await writeTaskMd(pi, ctx, task);
await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder"); await selectRole(pi, ctx, "coder");
currentActivity = "Coder implementiert…"; currentActivity = "Coder implementiert…";
await sendAndWait(pi, ctx, coderKickoff(task)); await sendAndWait(pi, ctx, coderKickoff(task));
} }
}); });
pi.registerCommand("judge", { pi.registerCommand("judge", {
description: "Review gegen TASK.md + git show HEAD → qwen3.5-judge (:8001).", description: "Review gegen TASK.md + git show HEAD → Judge-Rolle (:8001).",
handler: async function (args: string, ctx: ExtensionCommandContext) { handler: async function (args: string, ctx: ExtensionCommandContext) {
if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) { if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error"); ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error");
return; return;
} }
await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-judge"); await selectRole(pi, ctx, "judge");
currentActivity = "Judge reviewt…"; currentActivity = "Judge reviewt…";
await sendAndWait(pi, ctx, judgePrompt(args || "")); await sendAndWait(pi, ctx, judgePrompt(args || ""));
} }
}); });
pi.registerCommand("fix", { pi.registerCommand("fix", {
description: "Fixt Judge-Kritik, committet Ergebnis → qwen3.5-coder (:8001).", description: "Fixt Judge-Kritik, committet Ergebnis → Coder-Rolle (:8001).",
handler: async function (args: string, ctx: ExtensionCommandContext) { handler: async function (args: string, ctx: ExtensionCommandContext) {
if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) { if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error"); ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error");
return; return;
} }
await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder"); await selectRole(pi, ctx, "coder");
currentActivity = "Coder fixt Judge-Kritik…"; currentActivity = "Coder fixt Judge-Kritik…";
await sendAndWait(pi, ctx, fixPrompt(args || "")); await sendAndWait(pi, ctx, fixPrompt(args || ""));
} }
}); });
pi.registerCommand("shipit", { pi.registerCommand("shipit", {
description: "Finale Freigabe gegen TASK.md + git log → qwen3.5-judge (:8001).", description: "Finale Freigabe gegen TASK.md + git log → Judge-Rolle (:8001).",
handler: async function (args: string, ctx: ExtensionCommandContext) { handler: async function (args: string, ctx: ExtensionCommandContext) {
if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) { if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error"); ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error");
return; return;
} }
await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-judge"); await selectRole(pi, ctx, "judge");
ctx.ui.notify("Judge prüft finale Freigabe — Ergebnis erscheint im Chat (SHIP / NO-SHIP)", "info"); ctx.ui.notify("Judge prüft finale Freigabe — Ergebnis erscheint im Chat (SHIP / NO-SHIP)", "info");
currentActivity = "Judge: finale Freigabe…"; currentActivity = "Judge: finale Freigabe…";
await sendAndWait(pi, ctx, shipitPrompt(args || "")); await sendAndWait(pi, ctx, shipitPrompt(args || ""));
@ -1136,7 +1140,7 @@ export default function (pi: ExtensionAPI) {
ctx.ui.setStatus("optimize", `Starte Optimierung (max ${maxRounds} Runden)…`); ctx.ui.setStatus("optimize", `Starte Optimierung (max ${maxRounds} Runden)…`);
const taskPreview = task.length > 55 ? task.slice(0, 52) + "…" : task; const taskPreview = task.length > 55 ? task.slice(0, 52) + "…" : task;
ctx.ui.setStatus("optimize", `◉ Coder liest Anforderungen + implementiert: ${taskPreview}`); ctx.ui.setStatus("optimize", `◉ Coder liest Anforderungen + implementiert: ${taskPreview}`);
if (!await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder")) { if (!await selectRole(pi, ctx, "coder")) {
finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar"); finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar");
return; return;
} }
@ -1189,32 +1193,27 @@ export default function (pi: ExtensionAPI) {
// Schleife: Judge → (PASS? fertig : Fix → nächste Runde) // Schleife: Judge → (PASS? fertig : Fix → nächste Runde)
for (let round = 1; round <= maxRounds; round++) { for (let round = 1; round <= maxRounds; round++) {
const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round); const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round);
if (!await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-judge")) { if (!await selectRole(pi, ctx, "judge")) {
finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar"); finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar");
return; return;
} }
// Runde 1 ohne --continue: Quick-Judge (kein TASK.md, kürzerer Prompt). // Immer der vollständige Judge — auch in Runde 1. Findet Beanstandungen sofort
// Bei FAIL folgt Runde 2 mit vollem judgePrompt für detaillierte Analyse. // (kein nachsichtiger Quick-Check, der triviale Fälle vorschnell durchwinkt).
const useQuickJudge = round === 1 && !continueMode;
if (autoTestCmds.length > 0) { if (autoTestCmds.length > 0) {
const label = autoTestCmds.length === 1 const label = autoTestCmds.length === 1
? autoTestCmds[0].split(" ")[0] ? autoTestCmds[0].split(" ")[0]
: `${autoTestCmds.length} Suiten parallel`; : `${autoTestCmds.length} Suiten parallel`;
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${label}, max. ${testTimeout}s)…`); ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${label}, max. ${testTimeout}s)…`);
const testOutput = await runTestsParallel(pi, ctx, autoTestCmds, testTimeout); const testOutput = await runTestsParallel(pi, ctx, autoTestCmds, testTimeout);
const judgeLabel = useQuickJudge ? "Quick-Check" : "Judge analysiert"; currentActivity = `Judge Runde ${round}/${maxRounds}`;
currentActivity = `Judge Runde ${round}/${maxRounds}${useQuickJudge ? " (Quick)" : ""}`; startPhaseTimer(ctx, "optimize", `${prog} Runde ${round}/${maxRounds}: Judge analysiert Test-Ergebnis`);
startPhaseTimer(ctx, "optimize", `${prog} Runde ${round}/${maxRounds}: ${judgeLabel} Test-Ergebnis`); await sendAndWait(pi, ctx, judgeWithTestsPrompt(testOutput, ""));
await sendAndWait(pi, ctx, useQuickJudge
? quickJudgeWithTestsPrompt(testOutput, "")
: judgeWithTestsPrompt(testOutput, ""));
stopPhaseTimer(); stopPhaseTimer();
} else { } else {
const judgeLabel = useQuickJudge ? "Quick-Check" : "Judge — TASK.md + letzter Commit + Tests"; currentActivity = `Judge Runde ${round}/${maxRounds}`;
currentActivity = `Judge Runde ${round}/${maxRounds}${useQuickJudge ? " (Quick)" : ""}`; startPhaseTimer(ctx, "optimize", `${prog} Runde ${round}/${maxRounds}: Judge — TASK.md + letzter Commit + Tests`);
startPhaseTimer(ctx, "optimize", `${prog} Runde ${round}/${maxRounds}: ${judgeLabel}`); await sendAndWait(pi, ctx, judgePrompt(""));
await sendAndWait(pi, ctx, useQuickJudge ? quickJudgePrompt("") : judgePrompt(""));
stopPhaseTimer(); stopPhaseTimer();
} }
if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Judge Runde ${round}`); return; } if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Judge Runde ${round}`); return; }
@ -1222,16 +1221,22 @@ export default function (pi: ExtensionAPI) {
const judgeText = getLastAssistantText(ctx); const judgeText = getLastAssistantText(ctx);
verdict = parseVerdict(judgeText); verdict = parseVerdict(judgeText);
if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") { // Bestanden ist nur sauberes PASS. "PASS WITH CONCERNS" gilt nur mit --approve-concerns
// als bestanden — sonst fällt es (wie FAIL) in die Fix-Phase, damit der Coder die
// gefundenen Beanstandungen behebt.
const passed = verdict === "PASS" || (verdict === "PASS WITH CONCERNS" && approveConcerns);
if (passed) {
await tickTaskMdStatus(pi, ctx, "Review bestanden (PASS)"); await tickTaskMdStatus(pi, ctx, "Review bestanden (PASS)");
const nextStep = interactive ? "warte auf /continue…" : "ShipIt…"; const nextStep = interactive ? "warte auf /continue…" : "ShipIt…";
ctx.ui.setStatus("optimize", `${"●".repeat(round)}${verdict} nach Runde ${round}/${maxRounds}${nextStep}`); ctx.ui.setStatus("optimize", `${"●".repeat(round)}${verdict} nach Runde ${round}/${maxRounds}${nextStep}`);
break; break;
} }
// Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen. // Loop-Erkennung: gleiche Beanstandung zweimal → manuell eingreifen.
// Fallback auf den gesamten Judge-Text, wenn kein Blocker-Abschnitt vorhanden ist
// (z.B. reine Minor-Concerns) — sonst würde die Erkennung dort nie greifen.
// Normalisierung verhindert False-Negatives durch minimale Formulierungsunterschiede. // Normalisierung verhindert False-Negatives durch minimale Formulierungsunterschiede.
const currentBlockers = parseBlockers(judgeText); const currentBlockers = parseBlockers(judgeText) || judgeText;
if (currentBlockers && normalizeForComparison(currentBlockers) === normalizeForComparison(lastBlockers)) { if (currentBlockers && normalizeForComparison(currentBlockers) === normalizeForComparison(lastBlockers)) {
ctx.ui.setStatus("optimize", `${prog} ⚠ Gleicher Blocker in Runde ${round} manuelle Intervention nötig`); ctx.ui.setStatus("optimize", `${prog} ⚠ Gleicher Blocker in Runde ${round} manuelle Intervention nötig`);
finalNotify(ctx, "⚠ Schleife", "Gleicher Blocker zweimal manuelle Intervention nötig"); finalNotify(ctx, "⚠ Schleife", "Gleicher Blocker zweimal manuelle Intervention nötig");
@ -1253,7 +1258,7 @@ export default function (pi: ExtensionAPI) {
const blockerHint = currentBlockers const blockerHint = currentBlockers
? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers) ? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers)
: "Kritikpunkte aus Judge-Bericht"; : "Kritikpunkte aus Judge-Bericht";
if (!await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder")) { if (!await selectRole(pi, ctx, "coder")) {
finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar"); finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar");
return; return;
} }
@ -1297,7 +1302,7 @@ export default function (pi: ExtensionAPI) {
: interactivePauseTask; : interactivePauseTask;
ctx.ui.setStatus("optimize", `◉ Coder implementiert Zusatzauftrag: ${addPreview}`); ctx.ui.setStatus("optimize", `◉ Coder implementiert Zusatzauftrag: ${addPreview}`);
await writeTaskMd(pi, ctx, interactivePauseTask); await writeTaskMd(pi, ctx, interactivePauseTask);
if (!await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder")) { if (!await selectRole(pi, ctx, "coder")) {
finalNotify(ctx, "⛔ Modell-Fehler", "Coder-Modell nicht verfügbar"); finalNotify(ctx, "⛔ Modell-Fehler", "Coder-Modell nicht verfügbar");
return; return;
} }
@ -1314,9 +1319,9 @@ export default function (pi: ExtensionAPI) {
} }
} }
// Finaler SHIP-Schritt: klares PASS → direkt SHIP ohne zweiten Inference-Aufruf. // Finaler SHIP-Schritt: nur erreichbar mit sauberem PASS (oder PASS WITH CONCERNS +
// "PASS WITH CONCERNS" + --approve-concerns → direkt SHIP (ShipIt-Runde überspringen). // --approve-concerns; beides via break aus der Schleife). PASS WITH CONCERNS ohne Flag
// "PASS WITH CONCERNS" ohne Flag → ShipIt-Runde als finale Abwägung. // führt zu Coder-Fixes (Änderung 3) und kommt nicht mehr hierher.
if (verdict === "PASS" || (verdict === "PASS WITH CONCERNS" && approveConcerns)) { if (verdict === "PASS" || (verdict === "PASS WITH CONCERNS" && approveConcerns)) {
ctx.ui.setStatus("optimize", "🚀 SHIP produktionsreif"); ctx.ui.setStatus("optimize", "🚀 SHIP produktionsreif");
await autoCommitIfDirty(pi, ctx); await autoCommitIfDirty(pi, ctx);
@ -1328,38 +1333,6 @@ export default function (pi: ExtensionAPI) {
} else { } else {
ctx.ui.notify("Nächster Schritt: /update_doku für Code-Kommentare, README.md und BEDIENUNGSANLEITUNG.md", "info"); ctx.ui.notify("Nächster Schritt: /update_doku für Code-Kommentare, README.md und BEDIENUNGSANLEITUNG.md", "info");
} }
} else if (verdict === "PASS WITH CONCERNS") {
ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)}◉ ShipIt — PASS WITH CONCERNS, finale Freigabe?…`);
if (!await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-judge")) {
finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar");
return;
}
currentActivity = "Judge: finale Freigabe…";
startPhaseTimer(ctx, "optimize", `${"●".repeat(maxRounds)}◉ ShipIt — finale Freigabe`);
await sendAndWait(pi, ctx, shipitPrompt(""));
stopPhaseTimer();
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");
await autoCommitIfDirty(pi, ctx);
notifyShipSuccess(ctx);
finalNotify(ctx, "🚀 SHIP", "Programm ist produktionsreif");
await runVersionBump(pi, ctx);
if (withDoku) {
await runUpdateDoku(pi, ctx);
} else {
ctx.ui.notify("Nächster Schritt: /update_doku für Code-Kommentare, README.md und BEDIENUNGSANLEITUNG.md", "info");
}
} else if (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");
}
} }
} catch (e: any) { } catch (e: any) {
finalNotify(ctx, "⛔ Fehler", String(e?.message ?? e)); finalNotify(ctx, "⛔ Fehler", String(e?.message ?? e));
@ -1368,6 +1341,9 @@ export default function (pi: ExtensionAPI) {
// Sicherstellen dass keine Zustandsvariable in späteren /optimize-Aufruf leckt // Sicherstellen dass keine Zustandsvariable in späteren /optimize-Aufruf leckt
cancelRequested = false; cancelRequested = false;
currentModelKey = ""; currentModelKey = "";
// Rolle auf den Standard (coder) zurücksetzen, damit freier Chat nach einem
// Lauf nicht versehentlich die Judge-Persona behält (before_agent_start-Hook).
currentRole = "coder";
interactivePauseActive = false; interactivePauseActive = false;
interactiveContinueRequested = false; interactiveContinueRequested = false;
interactivePauseTask = ""; interactivePauseTask = "";
@ -1378,7 +1354,7 @@ export default function (pi: ExtensionAPI) {
// ── Schlanke Kommandos für kleine Änderungen ───────────────────────────── // ── Schlanke Kommandos für kleine Änderungen ─────────────────────────────
pi.registerCommand("patch", { pi.registerCommand("patch", {
description: "Gezielte Minimaländerung ohne Refactoring, committet → qwen3.5-coder (:8001).", description: "Gezielte Minimaländerung ohne Refactoring, committet → Coder-Rolle (:8001).",
handler: async function (args: string, ctx: ExtensionCommandContext) { handler: async function (args: string, ctx: ExtensionCommandContext) {
const change = (args || "").trim(); const change = (args || "").trim();
if (!change) { if (!change) {
@ -1389,20 +1365,20 @@ export default function (pi: ExtensionAPI) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error"); ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error");
return; return;
} }
await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder"); await selectRole(pi, ctx, "coder");
currentActivity = "Coder patcht…"; currentActivity = "Coder patcht…";
await sendAndWait(pi, ctx, patchPrompt(change)); await sendAndWait(pi, ctx, patchPrompt(change));
} }
}); });
pi.registerCommand("quick_check", { pi.registerCommand("quick_check", {
description: "Schnelle OK/PROBLEM-Prüfung einer kleinen Codeänderung → qwen3.5-judge (:8001).", description: "Schnelle OK/PROBLEM-Prüfung einer kleinen Codeänderung → Judge-Rolle (:8001).",
handler: async function (args: string, ctx: ExtensionCommandContext) { handler: async function (args: string, ctx: ExtensionCommandContext) {
if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) { if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error"); ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error");
return; return;
} }
await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-judge"); await selectRole(pi, ctx, "judge");
currentActivity = "Judge: Schnellcheck…"; currentActivity = "Judge: Schnellcheck…";
await sendAndWait(pi, ctx, quickCheckPrompt(args || "")); await sendAndWait(pi, ctx, quickCheckPrompt(args || ""));
} }
@ -1459,7 +1435,7 @@ export default function (pi: ExtensionAPI) {
// ── Planungsmodus ──────────────────────────────────────────────────────── // ── Planungsmodus ────────────────────────────────────────────────────────
pi.registerCommand("plan", { pi.registerCommand("plan", {
description: "Erstellt Implementierungsplan in PLAN.md ohne Dateiänderungen → qwen3.5-coder.", description: "Erstellt Implementierungsplan in PLAN.md ohne Dateiänderungen → Coder-Rolle.",
handler: async function (args: string, ctx: ExtensionCommandContext) { handler: async function (args: string, ctx: ExtensionCommandContext) {
const task = (args || "").trim(); const task = (args || "").trim();
if (!task) { if (!task) {
@ -1471,7 +1447,7 @@ export default function (pi: ExtensionAPI) {
return; return;
} }
await writeTaskMd(pi, ctx, task); await writeTaskMd(pi, ctx, task);
await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder"); await selectRole(pi, ctx, "coder");
ctx.ui.setStatus("plan", "Analysiere und plane (keine Dateiänderungen)…"); ctx.ui.setStatus("plan", "Analysiere und plane (keine Dateiänderungen)…");
currentActivity = "Coder plant (kein Code)…"; currentActivity = "Coder plant (kein Code)…";
await sendAndWait(pi, ctx, planPrompt(task)); await sendAndWait(pi, ctx, planPrompt(task));
@ -1552,7 +1528,7 @@ export default function (pi: ExtensionAPI) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error"); ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error");
return; return;
} }
await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder"); await selectRole(pi, ctx, "coder");
ctx.ui.setStatus("continue", "Analysiere unterbrochenen Prozess…"); ctx.ui.setStatus("continue", "Analysiere unterbrochenen Prozess…");
currentActivity = "Coder analysiert Stand…"; currentActivity = "Coder analysiert Stand…";
await sendAndWait(pi, ctx, [ await sendAndWait(pi, ctx, [

View file

@ -1,30 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LOG_CODER=$(mktemp /tmp/coder_XXXXXX.log)
LOG_JUDGE=$(mktemp /tmp/judge_XXXXXX.log)
echo "[*] Starte beide Server parallel ..."
bash "$SCRIPT_DIR/start-coder.sh" > "$LOG_CODER" 2>&1 &
PID_CODER=$!
bash "$SCRIPT_DIR/start-judge.sh" > "$LOG_JUDGE" 2>&1 &
PID_JUDGE=$!
wait_result() {
local PID="$1" NAME="$2" LOG="$3"
if wait "$PID"; then
echo "[✓] $NAME bereit"
else
echo "[✗] $NAME fehlgeschlagen — Log:"
cat "$LOG"
return 1
fi
}
RC=0
wait_result "$PID_CODER" "Coder (:8001)" "$LOG_CODER" || RC=1
wait_result "$PID_JUDGE" "Judge (:8002)" "$LOG_JUDGE" || RC=1
rm -f "$LOG_CODER" "$LOG_JUDGE"
exit $RC

View file

@ -30,5 +30,4 @@ check_server() {
} }
echo "=== LLaMA-Server Status ===" echo "=== LLaMA-Server Status ==="
check_server "qwen36-27b-coder" 8001 "qwen3.5-coder" check_server "qwen36-27b-single" 8001 "qwen3.5-single"
check_server "qwen36-27b-judge" 8002 "qwen3.5-judge"

View file

@ -1,14 +1,12 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
CODER="qwen36-27b-coder" # Single-GPU-Architektur: nur noch ein Container (Coder + Judge in einem).
JUDGE="qwen36-27b-judge" SINGLE="qwen36-27b-single"
for NAME in "$CODER" "$JUDGE"; do if docker ps -a --format '{{.Names}}' | grep -q "^${SINGLE}\$"; then
if docker ps -a --format '{{.Names}}' | grep -q "^${NAME}\$"; then docker rm -f "$SINGLE" >/dev/null
docker rm -f "$NAME" >/dev/null echo "[*] Gestoppt: $SINGLE"
echo "[*] Gestoppt: $NAME"
else else
echo "[-] Nicht gefunden: $NAME" echo "[-] Nicht gefunden: $SINGLE"
fi fi
done