feat: /plan, /cancel, /continue, /discard + Context 262144 + KV-Cache q4_0

- Neue Befehle: /plan (Planungsmodus, nur PLAN.md), /cancel (Loop-Abbruch),
  /continue (Resume nach Unterbrechung), /discard (PLAN.md verwerfen)
- contextWindow in models.json und llama.cpp-Servern: 131072 → 262144
- KV-Cache: q8_0 → q4_0 (weniger VRAM, passt zu 262k-Kontext auf 2× 3090)
- parallel: 2 → 1 beim Coder (stabiler bei großem Kontext)
- Optimize-Status mit ASCII-Fortschrittsbalken + Blocker-Preview
- cancelRequested-Flag prüft nach jedem Loop-Schritt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-05-20 20:02:20 +02:00
commit 4a31535b76
4 changed files with 126 additions and 26 deletions

View file

@ -239,6 +239,37 @@ function bedienungsanleitungPromptIncremental(files: string[]): string {
].join("\n");
}
function planPrompt(task: string): string {
return [
"Du bist ein erfahrener Software-Architekt im PLANUNGSMODUS.",
"",
"ABSOLUTE VERBOTE — du darfst NICHT:",
"- Dateien editieren, schreiben oder löschen (kein edit, write, apply_patch)",
"- Git-Commits durchführen",
"- Tests oder Skripte ausführen die Seiteneffekte haben",
"",
"ERLAUBT:",
"- Dateien lesen (read, cat, grep, find)",
"- Git-History lesen (git log, git show, git diff)",
"- PLAN.md anlegen oder überschreiben (das ist dein Ausgabe-Dokument)",
"",
"Analysiere den Auftrag gründlich und erstelle einen konkreten Implementierungsplan.",
"",
"Auftrag:",
task,
"",
"Struktur deiner Ausgabe:",
"1. IST-Analyse (relevante Dateien, Architektur, Abhängigkeiten)",
"2. Implementierungsplan (nummerierte Schritte, konkret und umsetzbar)",
"3. Kritische Entscheidungen (Alternativen + Empfehlung)",
"4. Risiken und offene Fragen",
"5. Geschätzte Komplexität: einfach / mittel / komplex",
"",
"Schreibe den vollständigen Plan in PLAN.md.",
"Schließe ab mit: 'Plan bereit. Starte Umsetzung mit /coder oder /optimize --continue'",
].join("\n");
}
// ── Hilfsfunktionen ─────────────────────────────────────────────────────────
// Legt TASK.md neu an oder hängt einen Zusatzauftrag an.
@ -471,24 +502,30 @@ function finalNotify(
ctx.ui.setWidget("coder-judge", [
`Letzter Lauf: ${verdict}${detail} (${timestamp})`,
"─────────────────────────────────────────",
"Workflow: /coder <auftrag> | /judge | /fix | /shipit",
"Workflow: /coder <auftrag> | /judge | /fix | /shipit",
"Auto-Loop: /optimize <auftrag> [--rounds N] [--with-doku] [--continue]",
"Kleine Änderung: /patch <änderung> → /quick_check [was]",
"Finale Doku: /update_doku | Neues Projekt: /new_project <pfad>",
"Planung: /plan <auftrag> → /coder | /optimize --continue | /discard",
"Patch: /patch <änderung> → /quick_check [was]",
"Doku: /update_doku | Neues Projekt: /new_project <pfad>",
"Abbruch: Escape (Generation laufend) | /cancel (Loop nach aktuellem Schritt)",
"Resume: /continue | Modell: auto (Coder→:8001, Judge→:8002)",
]);
}
// ── Extension ────────────────────────────────────────────────────────────────
let cancelRequested = false;
export default function (pi: ExtensionAPI) {
pi.on("session_start", async function (_event, ctx) {
ctx.ui.setWidget("coder-judge", [
"Workflow: /coder <auftrag> | /judge | /fix | /shipit",
"Auto-Loop: /optimize <auftrag> [--rounds N] [--with-doku]",
"Kleine Änderung: /patch <änderung> → /quick_check [was]",
"Finale Doku: /update_doku (nach SHIP Kommentare + README + Bedienungsanleitung)",
"Neues Projekt: /new_project <pfad>",
"Modell wird automatisch gewechselt (Coder→:8001, Judge→:8002)"
"Workflow: /coder <auftrag> | /judge | /fix | /shipit",
"Auto-Loop: /optimize <auftrag> [--rounds N] [--with-doku] [--continue]",
"Planung: /plan <auftrag> → /coder | /optimize --continue | /discard",
"Patch: /patch <änderung> → /quick_check [was]",
"Doku: /update_doku | Neues Projekt: /new_project <pfad>",
"Abbruch: Escape (Generation laufend) | /cancel (Loop nach aktuellem Schritt)",
"Resume: /continue | Modell: auto (Coder→:8001, Judge→:8002)",
]);
});
@ -595,10 +632,12 @@ export default function (pi: ExtensionAPI) {
// TASK.md anlegen und Implementierung starten
await writeTaskMd(pi, ctx, task);
ctx.ui.setStatus("optimize", `Starte Optimierung (max ${maxRounds} Runden)…`);
ctx.ui.setStatus("optimize", "Phase 1: Coder implementiert…");
const taskPreview = task.length > 55 ? task.slice(0, 52) + "…" : task;
ctx.ui.setStatus("optimize", `◉ Coder liest Anforderungen + implementiert: ${taskPreview}`);
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
await sendAndWait(pi, ctx, coderKickoff(task));
await tickTaskMdStatus(pi, ctx, "Implementierung");
if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", "Nach Implementierung"); return; }
}
let lastBlockers = "";
@ -606,43 +645,49 @@ export default function (pi: ExtensionAPI) {
// Schleife: Judge → (PASS? fertig : Fix → nächste Runde)
for (let round = 1; round <= maxRounds; round++) {
ctx.ui.setStatus("optimize", `Runde ${round}/${maxRounds}: Judge prüft…`);
const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round);
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge — TASK.md + letzter Commit + Tests…`);
await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge");
await sendAndWait(pi, ctx, judgePrompt(""));
if (cancelRequested) { cancelRequested = false; 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", `${verdict} nach Runde ${round}`);
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", "⚠ Schleife: gleicher Blocker manuelle Intervention nötig");
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", `⚠ Max. ${maxRounds} Runden ohne PASS`);
ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)} ⚠ Max. ${maxRounds} Runden ohne PASS`);
finalNotify(ctx, "⚠ Kein PASS", `${maxRounds} Runden ohne PASS bitte /judge und /fix manuell`);
return;
}
// Fix-Phase
ctx.ui.setStatus("optimize", `Runde ${round}/${maxRounds}: Coder fixt…`);
// 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}`);
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
await sendAndWait(pi, ctx, fixPrompt(""));
if (cancelRequested) { cancelRequested = false; 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", "Finale ShipIt-Prüfung…");
ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)}◉ ShipIt — SHIP oder NO-SHIP?…`);
await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge");
await sendAndWait(pi, ctx, shipitPrompt(""));
@ -739,6 +784,61 @@ export default function (pi: ExtensionAPI) {
}
});
// ── Planungsmodus ────────────────────────────────────────────────────────
pi.registerCommand("plan", {
description: "Analysiert Auftrag, schmiedet Implementierungsplan in PLAN.md — macht keine Dateiänderungen. → qwen3.5-coder (:8001)",
handler: async function (args: string, ctx: ExtensionCommandContext) {
const task = (args || "").trim();
if (!task) {
ctx.ui.notify("Benutzung: /plan <auftrag>", "error");
return;
}
await writeTaskMd(pi, ctx, task);
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
ctx.ui.setStatus("plan", "Analysiere und plane (keine Dateiänderungen)…");
pi.sendUserMessage(planPrompt(task));
await ctx.waitForIdle();
ctx.ui.setStatus("plan", "");
finalNotify(ctx, "📋 Plan", "Analyse abgeschlossen — PLAN.md + Chat");
}
});
pi.registerCommand("cancel", {
description: "Bricht laufenden Optimize-Loop nach dem aktuellen Schritt ab.",
handler: async function (_args: string, ctx: ExtensionCommandContext) {
cancelRequested = true;
ctx.ui.notify("Abbruch angefordert — wird nach aktuellem Schritt gestoppt", "warning");
}
});
pi.registerCommand("discard", {
description: "Verwirft PLAN.md und setzt den Planungsstatus zurück.",
handler: async function (_args: string, ctx: ExtensionCommandContext) {
await pi.exec("bash", ["-c", "rm -f PLAN.md"], { cwd: ctx.cwd });
ctx.ui.notify("PLAN.md gelöscht — Plan verworfen", "info");
finalNotify(ctx, "🗑 Plan verworfen", "Neu starten mit /plan oder /coder");
}
});
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) {
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
ctx.ui.setStatus("continue", "Analysiere unterbrochenen Prozess…");
pi.sendUserMessage([
"Ein Prozess wurde unterbrochen. Analysiere den aktuellen Stand und führe ihn sinnvoll fort:",
"1. Lies TASK.md für den Auftrag",
"2. Lies PLAN.md falls vorhanden (war ein Plan in Arbeit?)",
"3. Führe 'git log --oneline -5' aus um zu sehen was bereits committed wurde",
"4. Entscheide: Muss noch implementiert werden? Ist ein Review fällig? Müssen Fixes nachgezogen werden?",
"5. Fahre direkt mit dem nächsten sinnvollen Schritt fort — kein langer Bericht, einfach weitermachen.",
].join("\n"));
await ctx.waitForIdle();
ctx.ui.setStatus("continue", "");
}
});
// ── Projekt-Scaffolding ──────────────────────────────────────────────────
pi.registerCommand("new_project", {