feat: --interactive-Checkpoint, direktes SHIP bei PASS, default rounds 2

- /optimize --interactive pausiert nach erstem PASS; /continue setzt fort,
  /continue "Zusatz" hängt weiteren Auftrag an und wiederholt den Judge-Loop
- Klares PASS → direkt SHIP ohne zweiten ShipIt-Inference-Call (1-3 min gespart)
- PASS WITH CONCERNS → ShipIt-Runde weiterhin als finale Abwägung
- Default --rounds 3→2 (~30 % schnellere Durchläufe für typische Tasks)
- /continue-Command erkennt interactivePauseActive und leitet Signal weiter
- Alle drei Interactive-Zustandsvariablen werden im finally-Block resettet
- Dokumentation (README, BEDIENUNGSANLEITUNG, CLAUDE.md) vollständig aktualisiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-05-29 17:51:54 +02:00
commit 11ac46e565
4 changed files with 248 additions and 79 deletions

View file

@ -827,6 +827,9 @@ function finalNotify(
// ── Extension ────────────────────────────────────────────────────────────────
let cancelRequested = false;
let interactivePauseActive = false;
let interactiveContinueRequested = false;
let interactivePauseTask = "";
let currentActivity = ""; // Working-Message für den aktuellen Command-Kontext
// Erzeugt eine knappe Statuszeile aus Tool-Name und Argumenten.
@ -1000,12 +1003,13 @@ export default function (pi: ExtensionAPI) {
// ── Automatische Optimierungsschleife ────────────────────────────────────
pi.registerCommand("optimize", {
description: "Coder→Judge→Fix-Schleife bis PASS. Tests werden automatisch erkannt und parallel ausgeführt. /optimize <auftrag> [--rounds N] [--with-doku] [--continue] [--test-cmd \"override\"] [--test-timeout N]",
description: "Coder→Judge→Fix-Schleife bis PASS (default 2 Runden). Klares PASS → direkt SHIP; PASS WITH CONCERNS → ShipIt-Runde. --interactive pausiert nach PASS für Zusatzaufträge via /continue. /optimize <auftrag> [--rounds N] [--with-doku] [--continue] [--interactive] [--test-cmd \"override\"] [--test-timeout N]",
handler: async function (args: string, ctx: ExtensionCommandContext) {
const roundsMatch = (args || "").match(/--rounds\s+(\d+)/);
const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 3;
const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 2;
const withDoku = /--with-doku/.test(args || "");
const continueMode = /--continue/.test(args || "");
const interactive = /--interactive/.test(args || "");
const testCmdMatch = (args || "").match(/--test-cmd\s+"([^"]+)"|--test-cmd\s+'([^']+)'|--test-cmd\s+(\S+)/);
const testCmd: string | null = testCmdMatch ? (testCmdMatch[1] ?? testCmdMatch[2] ?? testCmdMatch[3]) : null;
const testTimeoutMatch = (args || "").match(/--test-timeout\s+(\d+)/);
@ -1015,6 +1019,7 @@ export default function (pi: ExtensionAPI) {
.replace(/--test-timeout\s+\d+/, "")
.replace(/--with-doku/, "")
.replace(/--continue/, "")
.replace(/--interactive/, "")
.replace(/--test-cmd\s+"[^"]*"/, "")
.replace(/--test-cmd\s+\S+/, "")
.trim();
@ -1085,76 +1090,144 @@ export default function (pi: ExtensionAPI) {
let lastBlockers = "";
let verdict = "";
let keepGoing = true;
// 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-judge", "qwen3.5-judge")) {
finalNotify(ctx, "⛔ Modell-Fehler", "Judge-Modell (llama-cpp-judge) nicht verfügbar");
return;
}
// Äußere Schleife für --interactive: nach PASS pausieren, Zusatzaufträge ermöglichen.
while (keepGoing) {
keepGoing = false;
verdict = "";
lastBlockers = "";
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);
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge analysiert Test-Ergebnis…`);
currentActivity = `Judge reviewt (Runde ${round}/${maxRounds})…`;
await sendAndWait(pi, ctx, judgeWithTestsPrompt(testOutput, ""));
} else {
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge — TASK.md + letzter Commit + Tests…`);
currentActivity = `Judge reviewt (Runde ${round}/${maxRounds})…`;
await sendAndWait(pi, ctx, judgePrompt(""));
}
if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Judge Runde ${round}`); return; }
const judgeText = getLastAssistantText(ctx);
verdict = parseVerdict(judgeText);
if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") {
await tickTaskMdStatus(pi, ctx, "Review bestanden (PASS)");
ctx.ui.setStatus("optimize", `${"●".repeat(round)}${verdict} nach Runde ${round}/${maxRounds} — ShipIt…`);
break;
}
// Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen
const currentBlockers = parseBlockers(judgeText);
if (currentBlockers && currentBlockers === lastBlockers) {
ctx.ui.setStatus("optimize", `${prog} ⚠ Gleicher Blocker in Runde ${round} manuelle Intervention nötig`);
finalNotify(ctx, "⚠ Schleife", "Gleicher Blocker zweimal manuelle Intervention nötig");
return;
}
lastBlockers = currentBlockers;
if (round === maxRounds) {
ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)} ⚠ Max. ${maxRounds} Runden ohne PASS`);
if (verdict === "UNREADABLE") {
finalNotify(ctx, "⚠ Urteil unklar", `${maxRounds} Runden Judge-Urteil nicht erkennbar, Antwort im Chat prüfen`);
} else {
finalNotify(ctx, "⚠ Kein PASS", `${maxRounds} Runden ohne PASS bitte /judge und /fix manuell`);
// 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-judge", "qwen3.5-judge")) {
finalNotify(ctx, "⛔ Modell-Fehler", "Judge-Modell (llama-cpp-judge) nicht verfügbar");
return;
}
return;
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);
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge analysiert Test-Ergebnis…`);
currentActivity = `Judge reviewt (Runde ${round}/${maxRounds})…`;
await sendAndWait(pi, ctx, judgeWithTestsPrompt(testOutput, ""));
} else {
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge — TASK.md + letzter Commit + Tests…`);
currentActivity = `Judge reviewt (Runde ${round}/${maxRounds})…`;
await sendAndWait(pi, ctx, judgePrompt(""));
}
if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Judge Runde ${round}`); return; }
const judgeText = getLastAssistantText(ctx);
verdict = parseVerdict(judgeText);
if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") {
await tickTaskMdStatus(pi, ctx, "Review bestanden (PASS)");
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
const currentBlockers = parseBlockers(judgeText);
if (currentBlockers && currentBlockers === lastBlockers) {
ctx.ui.setStatus("optimize", `${prog} ⚠ Gleicher Blocker in Runde ${round} manuelle Intervention nötig`);
finalNotify(ctx, "⚠ Schleife", "Gleicher Blocker zweimal manuelle Intervention nötig");
return;
}
lastBlockers = currentBlockers;
if (round === maxRounds) {
ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)} ⚠ Max. ${maxRounds} Runden ohne PASS`);
if (verdict === "UNREADABLE") {
finalNotify(ctx, "⚠ Urteil unklar", `${maxRounds} Runden Judge-Urteil nicht erkennbar, Antwort im Chat prüfen`);
} else {
finalNotify(ctx, "⚠ Kein PASS", `${maxRounds} Runden ohne PASS bitte /judge und /fix manuell`);
}
return;
}
// Fix-Phase: Blocker-Preview aus Judge-Bericht anzeigen
const blockerHint = currentBlockers
? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers)
: "Kritikpunkte aus Judge-Bericht";
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Coder fixt — ${blockerHint}`);
if (!await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder")) {
finalNotify(ctx, "⛔ Modell-Fehler", "Coder-Modell (llama-cpp-coder) nicht verfügbar");
return;
}
currentActivity = "Coder fixt Blocker…";
await sendAndWait(pi, ctx, fixPrompt(""));
if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Fix Runde ${round}`); return; }
}
// Fix-Phase: Blocker-Preview aus Judge-Bericht anzeigen
const blockerHint = currentBlockers
? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers)
: "Kritikpunkte aus Judge-Bericht";
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Coder fixt — ${blockerHint}`);
if (!await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder")) {
finalNotify(ctx, "⛔ Modell-Fehler", "Coder-Modell (llama-cpp-coder) nicht verfügbar");
return;
// Interactive-Modus: nach PASS pausieren und auf /continue warten (max 30 min).
// /continue ohne Args → direkt ShipIt. /continue "Zusatz" → Coder implementiert, Loop nochmal.
if (interactive && (verdict === "PASS" || verdict === "PASS WITH CONCERNS")) {
interactivePauseActive = true;
interactiveContinueRequested = false;
interactivePauseTask = "";
ctx.ui.setStatus("optimize", `${verdict} warte auf /continue…`);
ctx.ui.notify(
`${verdict} erreicht. Weitere Features? /continue "Zusatzauftrag" — oder /continue zum Shippern.`,
"info"
);
const waitStart = Date.now();
while (!interactiveContinueRequested && !cancelRequested) {
if (Date.now() - waitStart > 30 * 60 * 1000) {
interactivePauseActive = false;
finalNotify(ctx, "⚠ Timeout", "30 min ohne /continue — abgebrochen");
return;
}
await new Promise(r => setTimeout(r, 500));
}
interactivePauseActive = false;
if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", "Im Interactive-Modus"); return; }
if (interactivePauseTask) {
// Zusatzauftrag: Coder implementiert, dann Judge-Loop erneut
const addPreview = interactivePauseTask.length > 50
? interactivePauseTask.slice(0, 47) + "…"
: interactivePauseTask;
ctx.ui.setStatus("optimize", `◉ Coder implementiert Zusatzauftrag: ${addPreview}`);
await writeTaskMd(pi, ctx, interactivePauseTask);
if (!await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder")) {
finalNotify(ctx, "⛔ Modell-Fehler", "Coder-Modell nicht verfügbar");
return;
}
currentActivity = "Coder implementiert Zusatzauftrag…";
await sendAndWait(pi, ctx, coderKickoff(interactivePauseTask));
await tickTaskMdStatus(pi, ctx, "Implementierung");
if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", "Nach Zusatz-Implementierung"); return; }
keepGoing = true;
continue;
}
// /continue ohne Args → direkt zu ShipIt (verdict bleibt PASS)
}
currentActivity = "Coder fixt Blocker…";
await sendAndWait(pi, ctx, fixPrompt(""));
if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Fix Runde ${round}`); return; }
}
// Finale ShipIt-Prüfung nur bei PASS
if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") {
ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)}◉ ShipIt — SHIP oder NO-SHIP?…`);
// Finaler SHIP-Schritt: klares PASS → direkt SHIP ohne zweiten Inference-Aufruf.
// "PASS WITH CONCERNS" → ShipIt-Runde als finale Abwägung.
if (verdict === "PASS") {
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 (verdict === "PASS WITH CONCERNS") {
ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)}◉ ShipIt — PASS WITH CONCERNS, finale Freigabe?…`);
if (!await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge")) {
finalNotify(ctx, "⛔ Modell-Fehler", "Judge-Modell (llama-cpp-judge) nicht verfügbar");
return;
@ -1187,8 +1260,11 @@ export default function (pi: ExtensionAPI) {
} catch (e: any) {
finalNotify(ctx, "⛔ Fehler", String(e?.message ?? e));
} finally {
// Sicherstellen dass cancelRequested nie in einen späteren /optimize-Aufruf leckt
// Sicherstellen dass keine Zustandsvariable in späteren /optimize-Aufruf leckt
cancelRequested = false;
interactivePauseActive = false;
interactiveContinueRequested = false;
interactivePauseTask = "";
}
}
});
@ -1352,8 +1428,20 @@ export default function (pi: ExtensionAPI) {
});
pi.registerCommand("continue", {
description: "Nimmt unterbrochenen Prozess wieder auf — liest TASK.md, PLAN.md, git log und entscheidet den nächsten Schritt.",
handler: async function (_args: string, ctx: ExtensionCommandContext) {
description: "Im --interactive-Modus: bestätigt PASS und geht zu ShipIt — oder /continue \"Zusatz\" für weiteren Auftrag. Sonst: nimmt unterbrochenen Prozess wieder auf.",
handler: async function (args: string, ctx: ExtensionCommandContext) {
// Interactive-Pause-Handler: Signal an laufenden /optimize-Loop
if (interactivePauseActive) {
interactivePauseTask = (args || "").trim();
interactiveContinueRequested = true;
const msg = interactivePauseTask
? `Zusatzauftrag eingetragen: "${interactivePauseTask}" — Coder startet`
: "Fortfahren — ShipIt wird gestartet";
ctx.ui.notify(msg, "info");
return;
}
// Standard-Verhalten: unterbrochenen Prozess wieder aufnehmen
if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder")) {
ctx.ui.notify("Coder-Server nicht bereit (Port 8001) — start-coder.sh ausführen", "error");
return;