diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 54b414d..cfc9de2 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -32,16 +32,11 @@ eine andere Persona annimmt: sie, führt Tests aus und erstellt Git-Commits. **Judge-Persona**: Überprüft den Code mit dem Blick eines skeptischen Senior-Entwicklers. -Prüft Korrektheit, Robustheit, Randfälle, Sicherheit und Produktionsreife. Maßstab für das Urteil -sind `TASK.md` und, falls vorhanden, `SPEC.md`: -- `PASS` — alle Anforderungen erfüllt, Tests grün, kein Blocker und kein Major -- `PASS WITH CONCERNS` — alle Anforderungen erfüllt, aber mindestens ein Major: ein konkretes Risiko im - geforderten Einsatz, mit Reproduktion (wird wie `FAIL` behandelt, außer du verwendest `--approve-concerns`) -- `FAIL` — eine Anforderung fehlt, Tests schlagen fehl oder ein reproduzierbarer Fehler im geforderten Verhalten - -Härtung über die Anforderungen hinaus, Skalierung, Stil und Wünsche schreibt der Judge unter „Hinweise“. Sie -ändern das Urteil nicht, und der Coder bearbeitet sie bei `/fix` nicht. Ein Major ohne Reproduktion gilt als -Hinweis. Ohne diese Regeln urteilte der Judge mit Strata am 15.10.2026 in keiner Aufgabe PASS. +Prüft Korrektheit, Robustheit, Randfälle, Sicherheit und Produktionsreife. Gibt ein Urteil: +- `PASS` — Code ist in Ordnung +- `PASS WITH CONCERNS` — hat Anmerkungen, die behoben werden müssen (wird wie `FAIL` behandelt, + außer du verwendest `--approve-concerns`) +- `FAIL` — enthält Blocker, die behoben werden müssen Die Umschaltung zwischen den Rollen erfolgt über unterschiedliche System-Prompts, die die Extension automatisch vor jeder Inference einsetzt. Es läuft kein zweiter Server. @@ -149,21 +144,6 @@ Falls die GGUF-Datei an einem anderen Ort liegt: HF_HOME=/mnt/daten/huggingface ./start-coding-server.sh ``` -### Anderes Modell für die Schleife - -Ohne weitere Angabe arbeiten Coder und Judge mit `llama-cpp-single/coding_model` (Port 8001). Ein anderes Modell aus -`models.json` setzt die Umgebungsvariable `PI_CODER_MODEL` beim Start von pi, als `/`: - -```bash -PI_CODER_MODEL=strata/qwen3.8-flash-next-iq3_s \ - pi --provider strata --model qwen3.8-flash-next-iq3_s -``` - -`--provider`/`--model` machen das Modell auch für freie Eingaben zum Standard dieses Laufs; die globalen -Einstellungen bleiben unverändert. Die Bereitschaftsprüfung fragt den Endpunkt aus `baseUrl` des Modells ab. Der -Provider `strata` (lokaler Strata-Server auf `127.0.0.1:9090`) steht in `models.json`; den Server startet man außerhalb -dieses Repos. - --- ## 4. Neues Projekt anlegen @@ -254,22 +234,21 @@ Urteil: PASS WITH CONCERNS Blocker: keine Major: -- Nicht-existente Datei: Traceback statt Meldung und Exit-Code 2 (TASK.md verlangt Exit 2). - Reproduktion: python3 textcount.py fehlt.txt; echo $? → Traceback, Exit 1 +- Stoppwortliste ist hardcoded; große Projekte erwarten --stopwords-file Option Minor: -- Fehlermeldung nennt den Dateinamen nicht +- Keine --version Flag +- Fehlermeldung bei nicht-existenter Datei gibt keinen Exit-Code 1 zurück Fehlende Tests: - Test für leere Datei fehlt - Test für Datei mit nur Leerzeichen fehlt -Hinweise (ändern das Urteil nicht): +Produktionsrisiken: - Bei sehr großen Dateien (>1 GB) wird alles in den RAM geladen -- Keine --version Flag Konkrete Fix-Aufträge: -1. FileNotFoundError abfangen, Meldung mit Dateiname, exit(2) +1. exit(1) bei FileNotFoundError 2. Test für leere Eingabedatei ``` @@ -436,15 +415,12 @@ Stand und der Fix-Zyklus läuft automatisch bis PASS oder max. N Runden. ### Loop-Erkennung -Wenn zweimal hintereinander dieselbe Beanstandung auftritt, bricht `/optimize` ab: +Wenn zweimal hintereinander genau dieselben Blocker oder Concerns auftreten, bricht +`/optimize` ab: ``` -⚠ Schleife: Gleicher Blocker zweimal – manuelle Intervention nötig +⚠ Derselbe Blocker tritt erneut auf – Schleife abgebrochen. Bitte manuell prüfen. ``` -Verglichen wird der Blocker-Abschnitt des Judges. Steht dort nur „Keine“, werden stattdessen die Major-Punkte -verglichen; fehlen beide Abschnitte, der ganze Judge-Text. Zwei Runden mit „Blocker: Keine“, aber verschiedenen -Major-Punkten sind also keine Schleife. - In diesem Fall: `/judge` manuell ausführen, Blocker lesen, mit `/fix` manuell eingreifen. ### Max. Runden ohne PASS @@ -866,13 +842,6 @@ Die Checkboxen werden automatisch abgehakt: ## 12. Fehlermeldungen und Lösungen -### "Urteil unklar: … Judge-Urteil nicht erkennbar" - -**Ursache:** In der Judge-Antwort steht keine Zeile „Urteil: PASS | PASS WITH CONCERNS | FAIL“. Markdown dazwischen -wird erkannt (`## Urteil: **PASS**`, `**Urteil:** FAIL`); fehlt das Wort ganz, gilt die Runde als nicht bestanden. - -**Lösung:** Antwort im Chat lesen, dann `/judge` erneut oder `/fix` mit der Kritik. - ### "Modell-Datei nicht gefunden" ``` diff --git a/README.md b/README.md index fd310fa..82295fa 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,6 @@ nur die Persona im System-Prompt ändert sich. Coder- und Judge-Rollen werden durch verschiedene System-Prompts realisiert — dasselbe Modell, derselbe Container, unterschiedliche Persona. -Ein anderes Modell für beide Rollen (z. B. den lokalen Strata-Server) setzt `PI_CODER_MODEL=/`, -siehe [BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md#anderes-modell-für-die-schleife). - --- ## Voraussetzungen @@ -96,14 +93,9 @@ Das Install-Skript sichert eine vorhandene `~/.pi/agent/settings.json` als `.bak kopiert dann `settings.json` aus dem Repo. Damit ist der korrekte Default-Provider (`llama-cpp-single/coding_model`) automatisch konfiguriert. -Die Extension wird nicht kopiert, sondern verlinkt -(`~/.pi/agent/extensions/pi-coding-extension.ts` → Repo). Änderungen an `pi-coding-extension.ts` gelten -damit für jeden pi-Aufruf ab dem nächsten Start bzw. `/reload`, ohne erneutes Deploy. Das gilt auch für noch -nicht committete Änderungen. - -Nach späteren Änderungen an `models.json` oder `settings.json`: +Nach späteren Änderungen an `pi-coding-extension.ts` oder `models.json`: ```bash -./deploy-pi-config.sh # verlinkt die Extension, kopiert models.json, settings.json (nur packages) +./deploy-pi-config.sh # kopiert nach ~/.pi/agent/ # dann /reload in pi agent ``` @@ -264,7 +256,7 @@ und Extension-Guards gegen Regressionen. | `start-coding-server.sh` | Single-Server starten (Port 8001, Coder + Judge) | | `stop-coding-server.sh` | Container stoppen | | `status-coding-server.sh` | Laufstatus anzeigen | -| `deploy-pi-config.sh` | Extension nach `~/.pi/agent/extensions/` verlinken, models.json + settings.json kopieren | +| `deploy-pi-config.sh` | Extension + models.json + settings.json nach `~/.pi/agent/` deployen | | `examples/` | Demo-Projekte (Python, Rust, Go, C, Bash, HTML/JS) für Live-Demos | | `examples/restore-all.sh` | Examples nach Demo-Lauf in Ausgangszustand zurücksetzen | @@ -300,9 +292,6 @@ einen weiteren Fix-Auftrag, genau wie bei `FAIL`. Erst sauberes `PASS` beendet d Opt-out: `--approve-concerns` lässt `PASS WITH CONCERNS` als bestanden gelten. -Wann der Judge welches Urteil gibt (Urteilsregeln, Abschnitt „Hinweise“): siehe -[BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md), Judge-Persona. - --- ## Live-Aktivitätsstatus diff --git a/deploy-pi-config.sh b/deploy-pi-config.sh index 21398bf..b0b85ff 100755 --- a/deploy-pi-config.sh +++ b/deploy-pi-config.sh @@ -1,20 +1,13 @@ #!/usr/bin/env bash -# Verlinkt die Extension und kopiert models.json/settings.json aus dem Repo nach ~/.pi/agent/. -# Die Extension ist ein Symlink: Änderungen im Repo gelten ab dem nächsten pi-Start bzw. /reload. -# Erneut ausführen nur nach Änderungen an models.json oder settings.json. +# Kopiert die versionierten Dateien aus dem Repo nach ~/.pi/agent/. +# Nach jeder Änderung im Repo ausführen, damit pi agent die neue Version lädt. set -euo pipefail REPO="$(cd "$(dirname "$0")" && pwd)" mkdir -p ~/.pi/agent/extensions -# A previous, differing copy is kept as .bak before it is replaced by the symlink. -EXT=~/.pi/agent/extensions/pi-coding-extension.ts -if [ -f "$EXT" ] && [ ! -L "$EXT" ] && ! cmp -s "$EXT" "$REPO/pi-coding-extension.ts"; then - cp "$EXT" "$EXT.bak" - echo "Backup: pi-coding-extension.ts → pi-coding-extension.ts.bak" -fi -ln -sfn "$REPO/pi-coding-extension.ts" "$EXT" -echo "Verlinkt: ~/.pi/agent/extensions/pi-coding-extension.ts → $REPO/pi-coding-extension.ts" +cp "$REPO/pi-coding-extension.ts" ~/.pi/agent/extensions/pi-coding-extension.ts +echo "Kopiert: pi-coding-extension.ts → ~/.pi/agent/extensions/" cp "$REPO/models.json" ~/.pi/agent/models.json echo "Kopiert: models.json → ~/.pi/agent/" diff --git a/models.json b/models.json index 1b7afae..8f08ae0 100644 --- a/models.json +++ b/models.json @@ -39,28 +39,6 @@ ] }, - "strata": { - "baseUrl": "http://127.0.0.1:9090/v1", - "api": "openai-completions", - "apiKey": "none", - "compat": { - "supportsDeveloperRole": false, - "supportsReasoningEffort": true, - "maxTokensField": "max_tokens" - }, - "models": [ - { - "id": "qwen3.8-flash-next-iq3_s", - "name": "Strata Qwen3.8-Flash-Next IQ3_S (lokal :9090)", - "reasoning": true, - "input": ["text"], - "contextWindow": 262144, - "maxTokens": 16384, - "cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 } - } - ] - }, - "openrouter": { "models": [ { "id": "qwen/qwen3-235b-a22b:free", "name": "Qwen3 235B (Free)" }, diff --git a/pi-coding-extension.ts b/pi-coding-extension.ts index d610921..ac411d1 100644 --- a/pi-coding-extension.ts +++ b/pi-coding-extension.ts @@ -1,6 +1,6 @@ // pi-coding-extension.ts // Automatisierter Coder-Judge-Fix-Workflow für AI-Coding-Assistenten -// Ein Modell (Standard: coding_model auf Single-Server :8001, sonst PI_CODER_MODEL); Rollen Coder/Judge via System-Prompt +// Ein Modell (coding_model) 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"; @@ -51,28 +51,6 @@ function coderKickoff(task: string): string { ].join("\n"); } -// Urteilsregeln und Ausgabeformat beider Judge-Prompts. Ohne feste Regeln fand der Judge (Strata, 15.10.) in jeder -// Runde neue Härtungen über die Anforderungen hinaus und urteilte nie PASS, auch bei erfüllter SPEC. -function judgeVerdictSection(): string[] { - return [ - "Urteilsregeln (verbindlich; Maßstab ist TASK.md und, falls vorhanden, SPEC.md):", - "- FAIL: eine Anforderung ist nicht erfüllt, Tests schlagen fehl, oder ein reproduzierbarer Fehler im geforderten Verhalten.", - "- PASS WITH CONCERNS: alle Anforderungen erfüllt, aber mindestens ein Major: ein konkretes Risiko im geforderten Einsatz, mit Reproduktion (Befehl oder Eingabe → falsches Ergebnis).", - "- PASS: alle Anforderungen erfüllt, Tests grün, kein Blocker und kein Major. Minor-Punkte und Hinweise ändern daran nichts.", - "- Härtung über die Anforderungen hinaus, Skalierung, Stil, Wünsche und Risiken in nicht geforderten Umgebungen gehören unter „Hinweise“. Sie ändern das Urteil nicht und sind keine Fix-Aufträge.", - "- Ein Major ohne Reproduktion ist ein Hinweis.", - "", - "Ausgabeformat:", - "- Urteil: PASS | PASS WITH CONCERNS | FAIL", - "- Blocker", - "- Major", - "- Minor", - "- Fehlende Tests", - "- Hinweise (ändern das Urteil nicht)", - "- Konkrete Fix-Aufträge an den Coder (nur für Blocker, Major, Minor und fehlende Tests)", - ]; -} - function judgePrompt(extra: string): string { const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : ""; return [ @@ -87,7 +65,14 @@ function judgePrompt(extra: string): string { "4. Bewerte Korrektheit, Robustheit, Fehlerbehandlung, Sicherheit, Logging, Wartbarkeit und Produktionsreife.", "5. Wenn etwas fehlt, sage es klar und direkt.", "", - ...judgeVerdictSection(), + "Ausgabeformat:", + "- Urteil: PASS | PASS WITH CONCERNS | FAIL", + "- Blocker", + "- Major", + "- Minor", + "- Fehlende Tests", + "- Produktionsrisiken", + "- Konkrete Fix-Aufträge an den Coder", "", "Wenn du etwas behauptest, nenne die Datei, den Befehl, den Test oder den Reproduktionshinweis." ].join("\n") + suffix; @@ -113,7 +98,14 @@ function judgeWithTestsPrompt(testOutput: string, extra: string): string { "3. Versuche weitere Fehler im Code aktiv zu finden (Randfälle, Sicherheit, Robustheit).", "4. Wenn du etwas behauptest, nenne die Datei, die Zeile oder den Reproduktionshinweis.", "", - ...judgeVerdictSection(), + "Ausgabeformat:", + "- Urteil: PASS | PASS WITH CONCERNS | FAIL", + "- Blocker", + "- Major", + "- Minor", + "- Fehlende Tests", + "- Produktionsrisiken", + "- Konkrete Fix-Aufträge an den Coder", ].join("\n") + suffix; } @@ -123,7 +115,7 @@ function fixPrompt(extra: string): string { "Wechsle in den Reparaturmodus.", "Lies TASK.md als Referenz — stelle sicher, dass nach den Fixes alle Anforderungen erfüllt bleiben.", "Nutze den letzten Judge-Bericht als verbindliche Aufgabenliste.", - "Behebe zuerst Blocker, dann Major, dann Minor. Der Abschnitt „Hinweise“ ist kein Auftrag.", + "Behebe zuerst Blocker, dann Major, dann Minor.", "Lies jede betroffene Datei unmittelbar vor dem Editieren erneut ein.", "Wenn du mehrere Stellen in derselben Datei änderst: Erzeuge einen unified diff und nutze das apply_patch-Tool — nicht mehrere edit-Aufrufe.", "Fallback: Datei komplett neu schreiben wenn apply_patch nicht möglich.", @@ -440,30 +432,13 @@ async function tickTaskMdStatus( ); } -// Single-Modell: genau ein Modell (Standard coding_model auf :8001, oder PI_CODER_MODEL) für beide Rollen. +// Single-Server: genau ein Modell (coding_model 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. -// PI_CODER_MODEL="/" setzt ein anderes Modell für beide Rollen (z. B. strata/qwen3.8-flash-next-iq3_s); -// ohne die Variable bleibt es beim Single-Server auf :8001. -const DEFAULT_SINGLE_MODEL = "llama-cpp-single/coding_model"; -const SINGLE_MODEL_SPEC = (process.env.PI_CODER_MODEL || DEFAULT_SINGLE_MODEL).trim(); -const SINGLE_PROVIDER = SINGLE_MODEL_SPEC.slice(0, SINGLE_MODEL_SPEC.indexOf("/")); -const SINGLE_MODEL_ID = SINGLE_MODEL_SPEC.slice(SINGLE_MODEL_SPEC.indexOf("/") + 1); -const SINGLE_LABEL = `${SINGLE_PROVIDER}/${SINGLE_MODEL_ID}`; - -// Endpunkt des Single-Modells laut models.json, für Bereitschaftsprüfung und Meldungen. -function singleEndpoint(ctx: ExtensionCommandContext): string { - const model = ctx.modelRegistry.find(SINGLE_PROVIDER, SINGLE_MODEL_ID); - return model?.baseUrl ? model.baseUrl.replace(/\/+$/, "") : ""; -} - -function notReadyMessage(ctx: ExtensionCommandContext): string { - const where = singleEndpoint(ctx) || SINGLE_LABEL; - const hint = SINGLE_MODEL_SPEC === DEFAULT_SINGLE_MODEL ? "start-coding-server.sh ausführen" : "Server starten"; - return `Server nicht bereit (${where}) — ${hint}`; -} +const SINGLE_PROVIDER = "llama-cpp-single"; +const SINGLE_MODEL_ID = "coding_model"; async function ensureSingleModel( pi: ExtensionAPI, @@ -543,20 +518,17 @@ async function sendAndWait( async function waitUntilModelReady( pi: ExtensionAPI, ctx: ExtensionCommandContext, + port: number, + modelAlias: string, maxWaitMs = 180_000 ): Promise { - const endpoint = singleEndpoint(ctx); - if (!endpoint) { - ctx.ui.notify(`Modell ${SINGLE_LABEL} nicht in models.json gefunden`, "error"); - return false; - } const deadline = Date.now() + maxWaitMs; const body = JSON.stringify({ - model: SINGLE_MODEL_ID, + model: modelAlias, messages: [{ role: "user", content: "ping" }], max_tokens: 1, temperature: 0.0, stream: false, }); - // Body als Datei — verhindert Shell-Injection wenn die Modell-ID Sonderzeichen enthält + // Body als Datei — verhindert Shell-Injection wenn modelAlias Sonderzeichen enthält const tmpBody = `/tmp/pi_ready_${Date.now()}_${Math.random().toString(36).slice(2)}.json`; await pi.exec("bash", ["-c", `printf "%s" "$1" > "${tmpBody}"`, "_", body], { cwd: ctx.cwd }); let notified = false; @@ -564,13 +536,13 @@ async function waitUntilModelReady( while (Date.now() < deadline) { const r = await pi.exec("bash", ["-c", `curl -s -o /dev/null -w "%{http_code}" --max-time 5 ` + - `-X POST "$1/chat/completions" ` + + `-X POST http://localhost:${port}/v1/chat/completions ` + `-H "Content-Type: application/json" ` + - `-d "@${tmpBody}"`, "_", endpoint + `-d "@${tmpBody}"` ], { cwd: ctx.cwd }); if (r.stdout?.trim() === "200") return true; if (!notified) { - ctx.ui.notify(`Modell-Server (${endpoint}) lädt noch — warte bis zu 3 min…`, "info"); + ctx.ui.notify(`Modell-Server (Port ${port}) lädt noch — warte bis zu 3 min…`, "info"); notified = true; } await new Promise(res => setTimeout(res, 3000)); @@ -672,17 +644,10 @@ function normalizeForComparison(s: string): string { } function parseVerdict(text: string): string { - // Markdown zwischen „Urteil“ und dem Wort ist erlaubt: „## Urteil: **PASS**“, „**Urteil:** FAIL“. - const m = text.match(/Urteil[\s*_`:]*(PASS WITH CONCERNS|PASS|FAIL)/i); + const m = text.match(/Urteil:\s*(PASS WITH CONCERNS|PASS|FAIL)/i); return m ? m[1].toUpperCase() : "UNREADABLE"; } -// SHIP-Urteil der Freigabe-Runde; Markdown wie bei parseVerdict erlaubt. "" wenn nicht erkennbar. -function parseShipVerdict(text: string): string { - const m = text.match(/Urteil[\s*_`:]*(NO-SHIP|SHIP)/i); - return m ? m[1].toUpperCase() : ""; -} - // Extrahiert den Blocker-Abschnitt für die Loop-Erkennung. // Erkennt Bullet-Listen (- / – / *), Bold (**Blocker**) und Headings (## Blocker). function parseBlockers(text: string): string { @@ -692,23 +657,6 @@ function parseBlockers(text: string): string { return m ? m[1].trim() : ""; } -// Schlüssel für die Loop-Erkennung im /optimize-Loop: der Blocker-Abschnitt; steht dort nur „Keine“ o. ä., -// der Major-Abschnitt; fehlen beide, der ganze Judge-Text. Ohne diese Unterscheidung galten zwei Runden mit -// „Blocker: Keine“ und unterschiedlichen Major-Punkten als „gleicher Blocker“ (Fehlalarm). -function loopKey(text: string): string { - const stop = "(?:\\n(?:\\*\\*(?:Blocker|Major|Minor)\\*\\*|##\\s*(?:Blocker|Major|Minor)|[-–*]\\s*(?:Blocker|Major|Minor))|$)"; - const section = (name) => { - const m = text.match(new RegExp(`(?:\\*\\*${name}\\*\\*|##\\s*${name}|[-–*]\\s*${name})[:\\n]([\\s\\S]*?)${stop}`, "i")); - return m ? m[1].trim() : ""; - }; - const isNone = (s) => /^[\s\-–*•:]*(keine?r?|none|n\/a|nichts|—|-)?[\s.!]*$/i.test(s); - const blockers = section("Blocker"); - if (blockers && !isNone(blockers)) return blockers; - const majors = section("Major"); - if (majors && !isNone(majors)) return "MAJOR: " + majors; - return text; -} - // Gibt geänderte Quelldateien seit einem Git-Tag zurück. // null = Tag existiert nicht (erster Lauf) → alles verarbeiten // [] = nichts geändert → Phase überspringen @@ -1180,15 +1128,15 @@ export default function (pi: ExtensionAPI) { // ── Manuelle Kommandos ─────────────────────────────────────────────────── pi.registerCommand("coder", { - description: "Implementiert ohne Review-Loop → Coder-Rolle (Single-Modell).", + description: "Implementiert ohne Review-Loop → Coder-Rolle (:8001).", handler: async function (args: string, ctx: ExtensionCommandContext) { const task = (args || "").trim(); if (!task) { ctx.ui.notify("Benutzung: /coder ", "error"); return; } - if (!await waitUntilModelReady(pi, ctx)) { - ctx.ui.notify(notReadyMessage(ctx), "error"); + if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) { + ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error"); return; } await ensureLocalGitRepo(pi, ctx); @@ -1201,10 +1149,10 @@ export default function (pi: ExtensionAPI) { }); pi.registerCommand("judge", { - description: "Review gegen TASK.md + git show HEAD → Judge-Rolle (Single-Modell).", + description: "Review gegen TASK.md + git show HEAD → Judge-Rolle (:8001).", handler: async function (args: string, ctx: ExtensionCommandContext) { - if (!await waitUntilModelReady(pi, ctx)) { - ctx.ui.notify(notReadyMessage(ctx), "error"); + if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) { + ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error"); return; } await selectRole(pi, ctx, "judge"); @@ -1215,10 +1163,10 @@ export default function (pi: ExtensionAPI) { }); pi.registerCommand("fix", { - description: "Fixt Judge-Kritik, committet Ergebnis → Coder-Rolle (Single-Modell).", + description: "Fixt Judge-Kritik, committet Ergebnis → Coder-Rolle (:8001).", handler: async function (args: string, ctx: ExtensionCommandContext) { - if (!await waitUntilModelReady(pi, ctx)) { - ctx.ui.notify(notReadyMessage(ctx), "error"); + if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) { + ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error"); return; } await ensureLocalGitRepo(pi, ctx); @@ -1230,10 +1178,10 @@ export default function (pi: ExtensionAPI) { }); pi.registerCommand("shipit", { - description: "Finale Freigabe gegen TASK.md + git log → Judge-Rolle (Single-Modell).", + description: "Finale Freigabe gegen TASK.md + git log → Judge-Rolle (:8001).", handler: async function (args: string, ctx: ExtensionCommandContext) { - if (!await waitUntilModelReady(pi, ctx)) { - ctx.ui.notify(notReadyMessage(ctx), "error"); + if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) { + ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error"); return; } await selectRole(pi, ctx, "judge"); @@ -1241,7 +1189,7 @@ export default function (pi: ExtensionAPI) { currentActivity = "Judge: finale Freigabe…"; await sendAndWait(pi, ctx, shipitPrompt(args || "")); const shipText = getLastAssistantText(ctx); - const shipVerdict = parseShipVerdict(shipText); + const shipVerdict = shipText.match(/Urteil:\s*(SHIP|NO-SHIP)/i)?.[1]?.toUpperCase() ?? ""; if (shipVerdict === "SHIP") { await autoCommitIfDirty(pi, ctx); notifyShipSuccess(ctx); @@ -1298,11 +1246,11 @@ export default function (pi: ExtensionAPI) { : `--continue: Überspringe Implementierung, starte direkt mit Judge-Prüfung.`; ctx.ui.notify(continueMsg, "info"); - // Im --continue-Modus: Single-Modell prüfen (Coder + Judge in einem). - ctx.ui.setStatus("optimize", `Single-Modell wird geprüft (${SINGLE_LABEL})…`); - const serverReady = await waitUntilModelReady(pi, ctx); + // Im --continue-Modus: Single-Server prüfen (Port 8001 — Coder + Judge in einem). + ctx.ui.setStatus("optimize", "Single-Server wird geprüft (Port 8001)…"); + const serverReady = await waitUntilModelReady(pi, ctx, 8001, "coding_model"); if (!serverReady) { - finalNotify(ctx, "⛔ Server nicht erreichbar", `${notReadyMessage(ctx)} (kein HTTP 200 nach 3 min)`); + finalNotify(ctx, "⛔ Server nicht erreichbar", "Port 8001 — kein HTTP 200 nach 3 min. start-coding-server.sh ausführen"); return; } } else { @@ -1312,7 +1260,7 @@ export default function (pi: ExtensionAPI) { const taskPreview = task.length > 55 ? task.slice(0, 52) + "…" : task; ctx.ui.setStatus("optimize", `◉ Coder liest Anforderungen + implementiert: ${taskPreview}`); if (!await selectRole(pi, ctx, "coder")) { - finalNotify(ctx, "⛔ Modell-Fehler", `Modell (${SINGLE_LABEL}) nicht verfügbar`); + finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar"); return; } currentActivity = "Coder implementiert…"; @@ -1326,8 +1274,8 @@ export default function (pi: ExtensionAPI) { // GPU-Ladevorgangs und ist kein verlässliches Signal. Nur HTTP 200 auf einen // echten Completion-Request bedeutet: Modell ist im VRAM und bereit. ctx.ui.setStatus("optimize", "Single-Server wird geprüft…"); - if (!await waitUntilModelReady(pi, ctx)) { - finalNotify(ctx, "⛔ Server nicht erreichbar", `${notReadyMessage(ctx)} (kein HTTP 200 nach 3 min)`); + if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) { + finalNotify(ctx, "⛔ Server nicht erreichbar", "Port 8001 — kein HTTP 200 nach 3 min. start-coding-server.sh ausführen"); return; } } @@ -1365,7 +1313,7 @@ export default function (pi: ExtensionAPI) { for (let round = 1; round <= maxRounds; round++) { const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round); if (!await selectRole(pi, ctx, "judge")) { - finalNotify(ctx, "⛔ Modell-Fehler", `Modell (${SINGLE_LABEL}) nicht verfügbar`); + finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar"); return; } @@ -1404,9 +1352,10 @@ export default function (pi: ExtensionAPI) { } // Loop-Erkennung: gleiche Beanstandung zweimal → manuell eingreifen. - // Schlüssel: Blocker, bei „Blocker: Keine“ die Major-Punkte, sonst der ganze Judge-Text (loopKey). + // 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 = loopKey(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"); @@ -1429,7 +1378,7 @@ export default function (pi: ExtensionAPI) { ? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers) : "Kritikpunkte aus Judge-Bericht"; if (!await selectRole(pi, ctx, "coder")) { - finalNotify(ctx, "⛔ Modell-Fehler", `Modell (${SINGLE_LABEL}) nicht verfügbar`); + finalNotify(ctx, "⛔ Modell-Fehler", "Modell (llama-cpp-single) nicht verfügbar"); return; } currentActivity = `Coder fixt: ${blockerHint}`; @@ -1522,15 +1471,15 @@ export default function (pi: ExtensionAPI) { // ── Schlanke Kommandos für kleine Änderungen ───────────────────────────── pi.registerCommand("patch", { - description: "Gezielte Minimaländerung ohne Refactoring, committet → Coder-Rolle (Single-Modell).", + description: "Gezielte Minimaländerung ohne Refactoring, committet → Coder-Rolle (:8001).", handler: async function (args: string, ctx: ExtensionCommandContext) { const change = (args || "").trim(); if (!change) { ctx.ui.notify("Benutzung: /patch ", "error"); return; } - if (!await waitUntilModelReady(pi, ctx)) { - ctx.ui.notify(notReadyMessage(ctx), "error"); + if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) { + ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error"); return; } await ensureLocalGitRepo(pi, ctx); @@ -1542,10 +1491,10 @@ export default function (pi: ExtensionAPI) { }); pi.registerCommand("quick_check", { - description: "Schnelle OK/PROBLEM-Prüfung einer kleinen Codeänderung → Judge-Rolle (Single-Modell).", + 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)) { - ctx.ui.notify(notReadyMessage(ctx), "error"); + if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) { + ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error"); return; } await selectRole(pi, ctx, "judge"); @@ -1613,8 +1562,8 @@ export default function (pi: ExtensionAPI) { ctx.ui.notify("Benutzung: /plan ", "error"); return; } - if (!await waitUntilModelReady(pi, ctx)) { - ctx.ui.notify(notReadyMessage(ctx), "error"); + if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) { + ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error"); return; } await writeTaskMd(pi, ctx, task); @@ -1696,8 +1645,8 @@ export default function (pi: ExtensionAPI) { } // Standard-Verhalten: unterbrochenen Prozess wieder aufnehmen - if (!await waitUntilModelReady(pi, ctx)) { - ctx.ui.notify(notReadyMessage(ctx), "error"); + if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) { + ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error"); return; } await selectRole(pi, ctx, "coder"); diff --git a/tests/config.test.mjs b/tests/config.test.mjs index 592a581..606d923 100644 --- a/tests/config.test.mjs +++ b/tests/config.test.mjs @@ -15,13 +15,6 @@ test("models.json: Provider llama-cpp-single hat genau ein Modell coding_model a assert.deepEqual(p.models.map((m) => m.id), ["coding_model"]); }); -test("models.json: Provider strata zeigt auf den lokalen Strata-Server (:9090)", () => { - const p = models.providers["strata"]; - assert.ok(p, "Provider strata fehlt"); - assert.match(p.baseUrl, /^http:\/\/127\.0\.0\.1:9090\/v1$/); - assert.deepEqual(p.models.map((m) => m.id), ["qwen3.8-flash-next-iq3_s"]); -}); - test("models.json: tote Dual-/Alt-Provider sind entfernt", () => { for (const dead of ["llama-cpp", "llama-cpp-coder", "llama-cpp-judge"]) { assert.ok(!(dead in models.providers), `Toter Provider '${dead}' noch vorhanden`); diff --git a/tests/extension-guards.test.mjs b/tests/extension-guards.test.mjs index 0f281f5..158f811 100644 --- a/tests/extension-guards.test.mjs +++ b/tests/extension-guards.test.mjs @@ -38,7 +38,7 @@ test("Fix-Auslösung: PASS WITH CONCERNS gilt nur mit --approve-concerns als bes }); test("Loop-Erkennung greift auch ohne Blocker-Abschnitt (Fallback auf Gesamttext)", () => { - assert.match(extSource, /const currentBlockers = loopKey\(judgeText\)/); + assert.match(extSource, /parseBlockers\(judgeText\)\s*\|\|\s*judgeText/); }); test("currentRole wird nach jedem Kommando auf 'idle' zurückgesetzt (kein Persona-Leak in freien Chat)", () => { @@ -61,14 +61,3 @@ test("LICENCE.md: deterministisch in der Doku-Phase, nur wenn nicht vorhanden, p assert.match(extSource, /git config user\.name/); assert.match(extSource, /new Date\(\)\.getFullYear\(\)/); }); - -test("Single-Modell: Standard bleibt llama-cpp-single/coding_model, PI_CODER_MODEL überschreibt", () => { - assert.match(extSource, /DEFAULT_SINGLE_MODEL = "llama-cpp-single\/coding_model"/); - assert.match(extSource, /process\.env\.PI_CODER_MODEL \|\| DEFAULT_SINGLE_MODEL/); -}); - -test("Bereitschaftsprüfung: Endpunkt aus models.json statt fest Port 8001", () => { - assert.doesNotMatch(extSource, /waitUntilModelReady\(pi, ctx, 8001/); - assert.doesNotMatch(extSource, /localhost:\$\{port\}/); - assert.match(extSource, /function singleEndpoint\b/); -}); diff --git a/tests/pure-logic.test.mjs b/tests/pure-logic.test.mjs index a91f15e..af1f446 100644 --- a/tests/pure-logic.test.mjs +++ b/tests/pure-logic.test.mjs @@ -9,8 +9,6 @@ const parseVerdict = extractFn("parseVerdict"); const parseBlockers = extractFn("parseBlockers"); const normalizeForComparison = extractFn("normalizeForComparison"); const licenceMd = extractFn("licenceMd"); -const loopKey = extractFn("loopKey"); -const parseShipVerdict = extractFn("parseShipVerdict"); test("parseVerdict: klares PASS", () => { assert.equal(parseVerdict("Review fertig.\nUrteil: PASS\nKeine Blocker."), "PASS"); @@ -80,63 +78,3 @@ test("licenceMd: proprietärer Default mit eingesetztem Jahr und Inhaber", () => test("licenceMd: Platzhalter-Inhaber wird unverändert eingesetzt", () => { assert.match(licenceMd("2030", "[Rechteinhaber eintragen]"), /Copyright \(c\) 2030 \[Rechteinhaber eintragen\]/); }); - -test("loopKey: echte Blocker sind der Schlüssel", () => { - const txt = "## Blocker\n- B1 Absturz bei leerer Eingabe\n\n## Major\n- M1 x"; - assert.match(loopKey(txt), /B1 Absturz/); - assert.doesNotMatch(loopKey(txt), /M1/); -}); - -test("loopKey: 'Blocker: Keine' → Major-Punkte statt Fehlalarm (Strata-Probe 15.10.)", () => { - const r1 = "## Urteil: **PASS WITH CONCERNS**\n\n## Blocker\n- Keine.\n\n## Major\n- M1 Flags als Name\n\n## Minor\n- N1"; - const r2 = "## Urteil: **PASS WITH CONCERNS**\n\n## Blocker\n- Keine.\n\n## Major\n- M1 UTF-8-Absturz\n\n## Minor\n- N1"; - assert.notEqual(loopKey(r1), loopKey(r2)); - assert.match(loopKey(r1), /^MAJOR: - M1 Flags/); -}); - -test("loopKey: gleiche Major-Punkte zweimal → gleicher Schlüssel (Loop wird erkannt)", () => { - const r = "**Blocker**\nkeine\n**Major**\n- M1 gleich\n**Minor**\n- n"; - assert.equal(loopKey(r), loopKey(r + "\n")); -}); - -test("loopKey: weder Blocker noch Major → ganzer Text", () => { - assert.equal(loopKey("Urteil: FAIL\nirgendwas"), "Urteil: FAIL\nirgendwas"); - assert.equal(loopKey("## Blocker\nNone\n## Major\n-"), "## Blocker\nNone\n## Major\n-"); -}); - -test("parseVerdict: Urteil in Markdown-Fett (Strata-Judge 15.10.)", () => { - assert.equal(parseVerdict("## Urteil: **PASS WITH CONCERNS**\n### Blocker"), "PASS WITH CONCERNS"); - assert.equal(parseVerdict("**Urteil:** FAIL"), "FAIL"); - assert.equal(parseVerdict("## Urteil: **PASS**"), "PASS"); - assert.equal(parseVerdict("Urteil: `PASS`"), "PASS"); -}); - -test("parseShipVerdict: SHIP, NO-SHIP, Markdown, unklar", () => { - assert.equal(parseShipVerdict("Urteil: SHIP"), "SHIP"); - assert.equal(parseShipVerdict("## Urteil: **NO-SHIP**"), "NO-SHIP"); - assert.equal(parseShipVerdict("**Urteil:** ship"), "SHIP"); - assert.equal(parseShipVerdict("kein Urteil"), ""); -}); - -// Judge-Prompts rufen judgeVerdictSection() auf; extractFn wertet jede Funktion einzeln aus, freie Namen landen -// im globalen Scope. -globalThis.judgeVerdictSection = extractFn("judgeVerdictSection"); -const judgePrompt = extractFn("judgePrompt"); -const judgeWithTestsPrompt = extractFn("judgeWithTestsPrompt"); -const fixPrompt = extractFn("fixPrompt"); - -test("Judge-Prompts: Urteilsregeln und Hinweise in beiden Varianten (Strata-Judge 15.10.)", () => { - for (const p of [judgePrompt(""), judgeWithTestsPrompt("1 passed", "")]) { - assert.match(p, /Urteilsregeln \(verbindlich/); - assert.match(p, /- PASS: alle Anforderungen erfüllt, Tests grün, kein Blocker und kein Major/); - assert.match(p, /„Hinweise“\. Sie ändern das Urteil nicht/); - assert.match(p, /- Hinweise \(ändern das Urteil nicht\)/); - assert.match(p, /- Urteil: PASS \| PASS WITH CONCERNS \| FAIL/); - assert.doesNotMatch(p, /Produktionsrisiken/); - } - assert.match(judgeWithTestsPrompt("3 passed", ""), /3 passed/); -}); - -test("fixPrompt: Hinweise sind kein Auftrag", () => { - assert.match(fixPrompt(""), /„Hinweise“ ist kein Auftrag/); -}); diff --git a/tests/scripts.test.mjs b/tests/scripts.test.mjs index a5f88a3..686bad2 100644 --- a/tests/scripts.test.mjs +++ b/tests/scripts.test.mjs @@ -44,9 +44,3 @@ test("install-Skript deployt settings.json mit vorherigem Backup", () => { assert.match(s, /cp "\$REPO\/models\.json"/); assert.match(s, /pi-coding-extension\.ts/); }); - -test("install-Skript verlinkt die Extension statt sie zu kopieren", () => { - const s = readRepoFile("deploy-pi-config.sh"); - assert.match(s, /ln -sfn "\$REPO\/pi-coding-extension\.ts" "\$EXT"/); - assert.doesNotMatch(s, /cp "\$REPO\/pi-coding-extension\.ts"/, "cp auf den Symlink bricht mit 'same file' ab"); -});