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:
parent
3cf9ef928b
commit
ca85c45a39
5 changed files with 129 additions and 273 deletions
91
models.json
91
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
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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, [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
if docker ps -a --format '{{.Names}}' | grep -q "^${SINGLE}\$"; then
|
||||
docker rm -f "$SINGLE" >/dev/null
|
||||
echo "[*] Gestoppt: $SINGLE"
|
||||
else
|
||||
echo "[-] Nicht gefunden: $NAME"
|
||||
echo "[-] Nicht gefunden: $SINGLE"
|
||||
fi
|
||||
done
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue