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": {
"baseUrl": "http://127.0.0.1:8001/v1",
"api": "openai-completions",
@ -50,78 +28,13 @@
},
"models": [
{
"id": "qwen3.5-coder",
"name": "Qwen3.6 27B Single-Server Coder (llama.cpp :8001)",
"id": "qwen3.5-single",
"name": "Qwen3.6 27B Single-Server (llama.cpp :8001)",
"reasoning": true,
"input": ["text"],
"contextWindow": 262144,
"maxTokens": 16384,
"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
// 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 } 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 ────────────────────────────────────────────────────────
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.",
@ -32,7 +54,6 @@ function coderKickoff(task: string): string {
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.",
"",
@ -57,59 +78,11 @@ function judgePrompt(extra: string): string {
].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 —
// die Extension hat sie bereits extern gestartet und übergibt den Output.
function judgeWithTestsPrompt(testOutput: string, extra: string): string {
const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : "";
return [
"Du bist ein pingeliger, skeptischer Senior-Reviewer und QA-Ingenieur.",
"Deine Aufgabe ist NICHT, nett zu sein, sondern Fehler, Risiken, Randfälle und Produktionsprobleme zu finden.",
"",
"Die Test-Suite wurde bereits extern ausgeführt. Das Ergebnis steht unten.",
@ -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,
ctx: ExtensionCommandContext,
provider: string,
modelId: string
ctx: ExtensionCommandContext
): Promise<boolean> {
const key = `${provider}/${modelId}`;
const key = `${SINGLE_PROVIDER}/${SINGLE_MODEL_ID}`;
if (key === currentModelKey) return true;
const model = ctx.modelRegistry.find(provider, modelId);
const model = ctx.modelRegistry.find(SINGLE_PROVIDER, SINGLE_MODEL_ID);
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;
}
const ok = await pi.setModel(model);
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;
}
// 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.
// Retry-Schleife fängt "Agent is already processing" ab — tritt auf wenn
// 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.
// Wird von /update_doku und /optimize --with-doku genutzt.
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");
return;
}
@ -761,10 +752,10 @@ async function detectVersionFile(
): Promise<"package.json" | "Cargo.toml" | "pyproject.toml" | "VERSION" | null> {
for (const f of ["package.json", "Cargo.toml", "pyproject.toml"]) {
const r = await pi.exec("bash", ["-c", `test -f ${f}`], { cwd: ctx.cwd });
if (r.exitCode === 0) return f as "package.json" | "Cargo.toml" | "pyproject.toml";
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 });
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.
@ -796,7 +787,7 @@ async function applyVersionBump(
async function runVersionBump(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> {
// Early exit wenn kein git-Repo vorhanden
const gitCheck = await pi.exec("bash", ["-c", "git rev-parse --is-inside-work-tree 2>/dev/null"], { cwd: ctx.cwd });
if (gitCheck.exitCode !== 0) return;
if (gitCheck.code !== 0) return;
const current = await getCurrentVersion(pi, ctx);
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)" : ""}`
);
const choice = await ctx.ui.select({
title: "Version",
message: current
? `Aktuelle Version: ${tag}. Commits seit letztem Tag: ${bump}-Bump erkannt.`
: "Noch kein Versions-Tag vorhanden.",
options: [...labels, "Überspringen"],
});
// ctx.ui.select ist positional: select(title, options[], opts?). Die Erklärung muss in den
// Titel — ExtensionUIDialogOptions kennt kein message-Feld.
const titleLine = current
? `Version — aktuell ${tag}, ${bump}-Bump erkannt`
: "Version — noch kein Versions-Tag vorhanden";
const choice = await ctx.ui.select(titleLine, [...labels, "Überspringen"]);
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 });
if (tagResult.exitCode !== 0) {
if (tagResult.code !== 0) {
ctx.ui.notify(`Tag ${newTag} existiert bereits — manuell löschen mit: git tag -d ${newTag}`, "error");
return;
}
@ -883,7 +873,10 @@ function finalNotify(
// ── Extension ────────────────────────────────────────────────────────────────
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 interactiveContinueRequested = false;
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 ────────────────────────────────────────────────
// turn_start: Working-Text auf aktuellen Command-Kontext setzen
pi.on("turn_start", function (_event, ctx) {
@ -1013,7 +1017,7 @@ export default function (pi: ExtensionAPI) {
// ── Manuelle Kommandos ───────────────────────────────────────────────────
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) {
const task = (args || "").trim();
if (!task) {
@ -1025,46 +1029,46 @@ export default function (pi: ExtensionAPI) {
return;
}
await writeTaskMd(pi, ctx, task);
await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder");
await selectRole(pi, ctx, "coder");
currentActivity = "Coder implementiert…";
await sendAndWait(pi, ctx, coderKickoff(task));
}
});
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) {
if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error");
return;
}
await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-judge");
await selectRole(pi, ctx, "judge");
currentActivity = "Judge reviewt…";
await sendAndWait(pi, ctx, judgePrompt(args || ""));
}
});
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) {
if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error");
return;
}
await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder");
await selectRole(pi, ctx, "coder");
currentActivity = "Coder fixt Judge-Kritik…";
await sendAndWait(pi, ctx, fixPrompt(args || ""));
}
});
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) {
if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error");
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");
currentActivity = "Judge: finale Freigabe…";
await sendAndWait(pi, ctx, shipitPrompt(args || ""));
@ -1136,7 +1140,7 @@ export default function (pi: ExtensionAPI) {
ctx.ui.setStatus("optimize", `Starte Optimierung (max ${maxRounds} Runden)…`);
const taskPreview = task.length > 55 ? task.slice(0, 52) + "…" : task;
ctx.ui.setStatus("optimize", `◉ Coder liest Anforderungen + implementiert: ${taskPreview}`);
if (!await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder")) {
if (!await selectRole(pi, ctx, "coder")) {
finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar");
return;
}
@ -1189,32 +1193,27 @@ export default function (pi: ExtensionAPI) {
// Schleife: Judge → (PASS? fertig : Fix → nächste Runde)
for (let round = 1; round <= maxRounds; round++) {
const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round);
if (!await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-judge")) {
if (!await selectRole(pi, ctx, "judge")) {
finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar");
return;
}
// Runde 1 ohne --continue: Quick-Judge (kein TASK.md, kürzerer Prompt).
// Bei FAIL folgt Runde 2 mit vollem judgePrompt für detaillierte Analyse.
const useQuickJudge = round === 1 && !continueMode;
// Immer der vollständige Judge — auch in Runde 1. Findet Beanstandungen sofort
// (kein nachsichtiger Quick-Check, der triviale Fälle vorschnell durchwinkt).
if (autoTestCmds.length > 0) {
const label = autoTestCmds.length === 1
? autoTestCmds[0].split(" ")[0]
: `${autoTestCmds.length} Suiten parallel`;
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${label}, max. ${testTimeout}s)…`);
const testOutput = await runTestsParallel(pi, ctx, autoTestCmds, testTimeout);
const judgeLabel = useQuickJudge ? "Quick-Check" : "Judge analysiert";
currentActivity = `Judge Runde ${round}/${maxRounds}${useQuickJudge ? " (Quick)" : ""}`;
startPhaseTimer(ctx, "optimize", `${prog} Runde ${round}/${maxRounds}: ${judgeLabel} Test-Ergebnis`);
await sendAndWait(pi, ctx, useQuickJudge
? quickJudgeWithTestsPrompt(testOutput, "")
: judgeWithTestsPrompt(testOutput, ""));
currentActivity = `Judge Runde ${round}/${maxRounds}`;
startPhaseTimer(ctx, "optimize", `${prog} Runde ${round}/${maxRounds}: Judge analysiert Test-Ergebnis`);
await sendAndWait(pi, ctx, judgeWithTestsPrompt(testOutput, ""));
stopPhaseTimer();
} else {
const judgeLabel = useQuickJudge ? "Quick-Check" : "Judge — TASK.md + letzter Commit + Tests";
currentActivity = `Judge Runde ${round}/${maxRounds}${useQuickJudge ? " (Quick)" : ""}`;
startPhaseTimer(ctx, "optimize", `${prog} Runde ${round}/${maxRounds}: ${judgeLabel}`);
await sendAndWait(pi, ctx, useQuickJudge ? quickJudgePrompt("") : judgePrompt(""));
currentActivity = `Judge Runde ${round}/${maxRounds}`;
startPhaseTimer(ctx, "optimize", `${prog} Runde ${round}/${maxRounds}: Judge — TASK.md + letzter Commit + Tests`);
await sendAndWait(pi, ctx, judgePrompt(""));
stopPhaseTimer();
}
if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Judge Runde ${round}`); return; }
@ -1222,16 +1221,22 @@ export default function (pi: ExtensionAPI) {
const judgeText = getLastAssistantText(ctx);
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)");
const nextStep = interactive ? "warte auf /continue…" : "ShipIt…";
ctx.ui.setStatus("optimize", `${"●".repeat(round)}${verdict} nach Runde ${round}/${maxRounds}${nextStep}`);
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.
const currentBlockers = parseBlockers(judgeText);
const currentBlockers = parseBlockers(judgeText) || judgeText;
if (currentBlockers && normalizeForComparison(currentBlockers) === normalizeForComparison(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");
@ -1253,7 +1258,7 @@ export default function (pi: ExtensionAPI) {
const blockerHint = currentBlockers
? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers)
: "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");
return;
}
@ -1297,7 +1302,7 @@ export default function (pi: ExtensionAPI) {
: interactivePauseTask;
ctx.ui.setStatus("optimize", `◉ Coder implementiert Zusatzauftrag: ${addPreview}`);
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");
return;
}
@ -1314,9 +1319,9 @@ export default function (pi: ExtensionAPI) {
}
}
// Finaler SHIP-Schritt: klares PASS → direkt SHIP ohne zweiten Inference-Aufruf.
// "PASS WITH CONCERNS" + --approve-concerns → direkt SHIP (ShipIt-Runde überspringen).
// "PASS WITH CONCERNS" ohne Flag → ShipIt-Runde als finale Abwägung.
// Finaler SHIP-Schritt: nur erreichbar mit sauberem PASS (oder PASS WITH CONCERNS +
// --approve-concerns; beides via break aus der Schleife). PASS WITH CONCERNS ohne Flag
// führt zu Coder-Fixes (Änderung 3) und kommt nicht mehr hierher.
if (verdict === "PASS" || (verdict === "PASS WITH CONCERNS" && approveConcerns)) {
ctx.ui.setStatus("optimize", "🚀 SHIP produktionsreif");
await autoCommitIfDirty(pi, ctx);
@ -1328,38 +1333,6 @@ export default function (pi: ExtensionAPI) {
} else {
ctx.ui.notify("Nächster Schritt: /update_doku für Code-Kommentare, README.md und BEDIENUNGSANLEITUNG.md", "info");
}
} else if (verdict === "PASS WITH CONCERNS") {
ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)}◉ ShipIt — PASS WITH CONCERNS, finale Freigabe?…`);
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) {
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
cancelRequested = false;
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;
interactiveContinueRequested = false;
interactivePauseTask = "";
@ -1378,7 +1354,7 @@ export default function (pi: ExtensionAPI) {
// ── Schlanke Kommandos für kleine Änderungen ─────────────────────────────
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) {
const change = (args || "").trim();
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");
return;
}
await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder");
await selectRole(pi, ctx, "coder");
currentActivity = "Coder patcht…";
await sendAndWait(pi, ctx, patchPrompt(change));
}
});
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) {
if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-single")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-single.sh ausführen", "error");
return;
}
await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-judge");
await selectRole(pi, ctx, "judge");
currentActivity = "Judge: Schnellcheck…";
await sendAndWait(pi, ctx, quickCheckPrompt(args || ""));
}
@ -1459,7 +1435,7 @@ export default function (pi: ExtensionAPI) {
// ── Planungsmodus ────────────────────────────────────────────────────────
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) {
const task = (args || "").trim();
if (!task) {
@ -1471,7 +1447,7 @@ export default function (pi: ExtensionAPI) {
return;
}
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)…");
currentActivity = "Coder plant (kein Code)…";
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");
return;
}
await switchModel(pi, ctx, "llama-cpp-single", "qwen3.5-coder");
await selectRole(pi, ctx, "coder");
ctx.ui.setStatus("continue", "Analysiere unterbrochenen Prozess…");
currentActivity = "Coder analysiert Stand…";
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 ==="
check_server "qwen36-27b-coder" 8001 "qwen3.5-coder"
check_server "qwen36-27b-judge" 8002 "qwen3.5-judge"
check_server "qwen36-27b-single" 8001 "qwen3.5-single"

View file

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