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:
parent
5dee5f25e4
commit
11ac46e565
4 changed files with 248 additions and 79 deletions
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue