diff --git a/models.json b/models.json index 2e10757..25ed185 100644 --- a/models.json +++ b/models.json @@ -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 - } } ] }, diff --git a/pi-coder-judge-extension.ts b/pi-coder-judge-extension.ts index c091399..8d13b5b 100644 --- a/pi-coder-judge-extension.ts +++ b/pi-coder-judge-extension.ts @@ -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 { - 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 { + 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 { - 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 { // 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 ohne Review-Loop → qwen3.5-coder (:8001).", + description: "Implementiert 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, [ diff --git a/start-servers.sh b/start-servers.sh deleted file mode 100755 index c6f424f..0000000 --- a/start-servers.sh +++ /dev/null @@ -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 diff --git a/status.sh b/status.sh index 9eacbf7..fe734ca 100755 --- a/status.sh +++ b/status.sh @@ -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" diff --git a/stop-servers.sh b/stop-servers.sh index 1d25229..1321287 100755 --- a/stop-servers.sh +++ b/stop-servers.sh @@ -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