Compare commits

..
9 changed files with 85 additions and 293 deletions

View file

@ -32,16 +32,11 @@ eine andere Persona annimmt:
sie, führt Tests aus und erstellt Git-Commits. sie, führt Tests aus und erstellt Git-Commits.
**Judge-Persona**: Überprüft den Code mit dem Blick eines skeptischen Senior-Entwicklers. **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 Prüft Korrektheit, Robustheit, Randfälle, Sicherheit und Produktionsreife. Gibt ein Urteil:
sind `TASK.md` und, falls vorhanden, `SPEC.md`: - `PASS` — Code ist in Ordnung
- `PASS` — alle Anforderungen erfüllt, Tests grün, kein Blocker und kein Major - `PASS WITH CONCERNS` — hat Anmerkungen, die behoben werden müssen (wird wie `FAIL` behandelt,
- `PASS WITH CONCERNS` — alle Anforderungen erfüllt, aber mindestens ein Major: ein konkretes Risiko im außer du verwendest `--approve-concerns`)
geforderten Einsatz, mit Reproduktion (wird wie `FAIL` behandelt, außer du verwendest `--approve-concerns`) - `FAIL` — enthält Blocker, die behoben werden müssen
- `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.
Die Umschaltung zwischen den Rollen erfolgt über unterschiedliche System-Prompts, die die Die Umschaltung zwischen den Rollen erfolgt über unterschiedliche System-Prompts, die die
Extension automatisch vor jeder Inference einsetzt. Es läuft kein zweiter Server. 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 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 `<provider>/<id>`:
```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 ## 4. Neues Projekt anlegen
@ -254,22 +234,21 @@ Urteil: PASS WITH CONCERNS
Blocker: keine Blocker: keine
Major: Major:
- Nicht-existente Datei: Traceback statt Meldung und Exit-Code 2 (TASK.md verlangt Exit 2). - Stoppwortliste ist hardcoded; große Projekte erwarten --stopwords-file Option
Reproduktion: python3 textcount.py fehlt.txt; echo $? → Traceback, Exit 1
Minor: Minor:
- Fehlermeldung nennt den Dateinamen nicht - Keine --version Flag
- Fehlermeldung bei nicht-existenter Datei gibt keinen Exit-Code 1 zurück
Fehlende Tests: Fehlende Tests:
- Test für leere Datei fehlt - Test für leere Datei fehlt
- Test für Datei mit nur Leerzeichen 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 - Bei sehr großen Dateien (>1 GB) wird alles in den RAM geladen
- Keine --version Flag
Konkrete Fix-Aufträge: Konkrete Fix-Aufträge:
1. FileNotFoundError abfangen, Meldung mit Dateiname, exit(2) 1. exit(1) bei FileNotFoundError
2. Test für leere Eingabedatei 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 ### 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. In diesem Fall: `/judge` manuell ausführen, Blocker lesen, mit `/fix` manuell eingreifen.
### Max. Runden ohne PASS ### Max. Runden ohne PASS
@ -866,13 +842,6 @@ Die Checkboxen werden automatisch abgehakt:
## 12. Fehlermeldungen und Lösungen ## 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" ### "Modell-Datei nicht gefunden"
``` ```

View file

@ -48,9 +48,6 @@ nur die Persona im System-Prompt ändert sich.
Coder- und Judge-Rollen werden durch verschiedene System-Prompts realisiert — Coder- und Judge-Rollen werden durch verschiedene System-Prompts realisiert —
dasselbe Modell, derselbe Container, unterschiedliche Persona. dasselbe Modell, derselbe Container, unterschiedliche Persona.
Ein anderes Modell für beide Rollen (z. B. den lokalen Strata-Server) setzt `PI_CODER_MODEL=<provider>/<id>`,
siehe [BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md#anderes-modell-für-die-schleife).
--- ---
## Voraussetzungen ## 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 kopiert dann `settings.json` aus dem Repo. Damit ist der korrekte Default-Provider
(`llama-cpp-single/coding_model`) automatisch konfiguriert. (`llama-cpp-single/coding_model`) automatisch konfiguriert.
Die Extension wird nicht kopiert, sondern verlinkt Nach späteren Änderungen an `pi-coding-extension.ts` oder `models.json`:
(`~/.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`:
```bash ```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 # 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) | | `start-coding-server.sh` | Single-Server starten (Port 8001, Coder + Judge) |
| `stop-coding-server.sh` | Container stoppen | | `stop-coding-server.sh` | Container stoppen |
| `status-coding-server.sh` | Laufstatus anzeigen | | `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/` | 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 | | `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. 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 ## Live-Aktivitätsstatus

View file

@ -1,20 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Verlinkt die Extension und kopiert models.json/settings.json aus dem Repo nach ~/.pi/agent/. # Kopiert die versionierten Dateien aus dem Repo nach ~/.pi/agent/.
# Die Extension ist ein Symlink: Änderungen im Repo gelten ab dem nächsten pi-Start bzw. /reload. # Nach jeder Änderung im Repo ausführen, damit pi agent die neue Version lädt.
# Erneut ausführen nur nach Änderungen an models.json oder settings.json.
set -euo pipefail set -euo pipefail
REPO="$(cd "$(dirname "$0")" && pwd)" REPO="$(cd "$(dirname "$0")" && pwd)"
mkdir -p ~/.pi/agent/extensions mkdir -p ~/.pi/agent/extensions
# A previous, differing copy is kept as .bak before it is replaced by the symlink. cp "$REPO/pi-coding-extension.ts" ~/.pi/agent/extensions/pi-coding-extension.ts
EXT=~/.pi/agent/extensions/pi-coding-extension.ts echo "Kopiert: pi-coding-extension.ts → ~/.pi/agent/extensions/"
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/models.json" ~/.pi/agent/models.json cp "$REPO/models.json" ~/.pi/agent/models.json
echo "Kopiert: models.json → ~/.pi/agent/" echo "Kopiert: models.json → ~/.pi/agent/"

View file

@ -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": { "openrouter": {
"models": [ "models": [
{ "id": "qwen/qwen3-235b-a22b:free", "name": "Qwen3 235B (Free)" }, { "id": "qwen/qwen3-235b-a22b:free", "name": "Qwen3 235B (Free)" },

View file

@ -1,6 +1,6 @@
// pi-coding-extension.ts // pi-coding-extension.ts
// Automatisierter Coder-Judge-Fix-Workflow für AI-Coding-Assistenten // 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 { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox"; import { Type } from "typebox";
@ -51,28 +51,6 @@ function coderKickoff(task: string): string {
].join("\n"); ].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 { function judgePrompt(extra: string): string {
const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : ""; const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : "";
return [ return [
@ -87,7 +65,14 @@ function judgePrompt(extra: string): string {
"4. Bewerte Korrektheit, Robustheit, Fehlerbehandlung, Sicherheit, Logging, Wartbarkeit und Produktionsreife.", "4. Bewerte Korrektheit, Robustheit, Fehlerbehandlung, Sicherheit, Logging, Wartbarkeit und Produktionsreife.",
"5. Wenn etwas fehlt, sage es klar und direkt.", "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." "Wenn du etwas behauptest, nenne die Datei, den Befehl, den Test oder den Reproduktionshinweis."
].join("\n") + suffix; ].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).", "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.", "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; ].join("\n") + suffix;
} }
@ -123,7 +115,7 @@ function fixPrompt(extra: string): string {
"Wechsle in den Reparaturmodus.", "Wechsle in den Reparaturmodus.",
"Lies TASK.md als Referenz — stelle sicher, dass nach den Fixes alle Anforderungen erfüllt bleiben.", "Lies TASK.md als Referenz — stelle sicher, dass nach den Fixes alle Anforderungen erfüllt bleiben.",
"Nutze den letzten Judge-Bericht als verbindliche Aufgabenliste.", "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.", "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.", "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.", "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 // Der Rollenwechsel Coder/Judge erfolgt NICHT mehr über den Modellwechsel, sondern
// über den System-Prompt (siehe before_agent_start-Hook + currentRole). Diese Funktion // ü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 // 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. // ist der Wechsel via currentModelKey gecacht und damit ein No-op.
// PI_CODER_MODEL="<provider>/<id>" setzt ein anderes Modell für beide Rollen (z. B. strata/qwen3.8-flash-next-iq3_s); const SINGLE_PROVIDER = "llama-cpp-single";
// ohne die Variable bleibt es beim Single-Server auf :8001. const SINGLE_MODEL_ID = "coding_model";
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}`;
}
async function ensureSingleModel( async function ensureSingleModel(
pi: ExtensionAPI, pi: ExtensionAPI,
@ -543,20 +518,17 @@ async function sendAndWait(
async function waitUntilModelReady( async function waitUntilModelReady(
pi: ExtensionAPI, pi: ExtensionAPI,
ctx: ExtensionCommandContext, ctx: ExtensionCommandContext,
port: number,
modelAlias: string,
maxWaitMs = 180_000 maxWaitMs = 180_000
): Promise<boolean> { ): Promise<boolean> {
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 deadline = Date.now() + maxWaitMs;
const body = JSON.stringify({ const body = JSON.stringify({
model: SINGLE_MODEL_ID, model: modelAlias,
messages: [{ role: "user", content: "ping" }], messages: [{ role: "user", content: "ping" }],
max_tokens: 1, temperature: 0.0, stream: false, 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`; 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 }); await pi.exec("bash", ["-c", `printf "%s" "$1" > "${tmpBody}"`, "_", body], { cwd: ctx.cwd });
let notified = false; let notified = false;
@ -564,13 +536,13 @@ async function waitUntilModelReady(
while (Date.now() < deadline) { while (Date.now() < deadline) {
const r = await pi.exec("bash", ["-c", const r = await pi.exec("bash", ["-c",
`curl -s -o /dev/null -w "%{http_code}" --max-time 5 ` + `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" ` + `-H "Content-Type: application/json" ` +
`-d "@${tmpBody}"`, "_", endpoint `-d "@${tmpBody}"`
], { cwd: ctx.cwd }); ], { cwd: ctx.cwd });
if (r.stdout?.trim() === "200") return true; if (r.stdout?.trim() === "200") return true;
if (!notified) { 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; notified = true;
} }
await new Promise(res => setTimeout(res, 3000)); await new Promise(res => setTimeout(res, 3000));
@ -672,17 +644,10 @@ function normalizeForComparison(s: string): string {
} }
function parseVerdict(text: 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"; 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. // Extrahiert den Blocker-Abschnitt für die Loop-Erkennung.
// Erkennt Bullet-Listen (- / – / *), Bold (**Blocker**) und Headings (## Blocker). // Erkennt Bullet-Listen (- / – / *), Bold (**Blocker**) und Headings (## Blocker).
function parseBlockers(text: string): string { function parseBlockers(text: string): string {
@ -692,23 +657,6 @@ function parseBlockers(text: string): string {
return m ? m[1].trim() : ""; 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. // Gibt geänderte Quelldateien seit einem Git-Tag zurück.
// null = Tag existiert nicht (erster Lauf) → alles verarbeiten // null = Tag existiert nicht (erster Lauf) → alles verarbeiten
// [] = nichts geändert → Phase überspringen // [] = nichts geändert → Phase überspringen
@ -1180,15 +1128,15 @@ export default function (pi: ExtensionAPI) {
// ── Manuelle Kommandos ─────────────────────────────────────────────────── // ── Manuelle Kommandos ───────────────────────────────────────────────────
pi.registerCommand("coder", { pi.registerCommand("coder", {
description: "Implementiert <auftrag> ohne Review-Loop → Coder-Rolle (Single-Modell).", description: "Implementiert <auftrag> ohne Review-Loop → Coder-Rolle (:8001).",
handler: async function (args: string, ctx: ExtensionCommandContext) { handler: async function (args: string, ctx: ExtensionCommandContext) {
const task = (args || "").trim(); const task = (args || "").trim();
if (!task) { if (!task) {
ctx.ui.notify("Benutzung: /coder <auftrag>", "error"); ctx.ui.notify("Benutzung: /coder <auftrag>", "error");
return; return;
} }
if (!await waitUntilModelReady(pi, ctx)) { if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify(notReadyMessage(ctx), "error"); ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
return; return;
} }
await ensureLocalGitRepo(pi, ctx); await ensureLocalGitRepo(pi, ctx);
@ -1201,10 +1149,10 @@ export default function (pi: ExtensionAPI) {
}); });
pi.registerCommand("judge", { 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) { handler: async function (args: string, ctx: ExtensionCommandContext) {
if (!await waitUntilModelReady(pi, ctx)) { if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify(notReadyMessage(ctx), "error"); ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
return; return;
} }
await selectRole(pi, ctx, "judge"); await selectRole(pi, ctx, "judge");
@ -1215,10 +1163,10 @@ export default function (pi: ExtensionAPI) {
}); });
pi.registerCommand("fix", { 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) { handler: async function (args: string, ctx: ExtensionCommandContext) {
if (!await waitUntilModelReady(pi, ctx)) { if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify(notReadyMessage(ctx), "error"); ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
return; return;
} }
await ensureLocalGitRepo(pi, ctx); await ensureLocalGitRepo(pi, ctx);
@ -1230,10 +1178,10 @@ export default function (pi: ExtensionAPI) {
}); });
pi.registerCommand("shipit", { 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) { handler: async function (args: string, ctx: ExtensionCommandContext) {
if (!await waitUntilModelReady(pi, ctx)) { if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify(notReadyMessage(ctx), "error"); ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
return; return;
} }
await selectRole(pi, ctx, "judge"); await selectRole(pi, ctx, "judge");
@ -1241,7 +1189,7 @@ export default function (pi: ExtensionAPI) {
currentActivity = "Judge: finale Freigabe…"; currentActivity = "Judge: finale Freigabe…";
await sendAndWait(pi, ctx, shipitPrompt(args || "")); await sendAndWait(pi, ctx, shipitPrompt(args || ""));
const shipText = getLastAssistantText(ctx); const shipText = getLastAssistantText(ctx);
const shipVerdict = parseShipVerdict(shipText); const shipVerdict = shipText.match(/Urteil:\s*(SHIP|NO-SHIP)/i)?.[1]?.toUpperCase() ?? "";
if (shipVerdict === "SHIP") { if (shipVerdict === "SHIP") {
await autoCommitIfDirty(pi, ctx); await autoCommitIfDirty(pi, ctx);
notifyShipSuccess(ctx); notifyShipSuccess(ctx);
@ -1298,11 +1246,11 @@ export default function (pi: ExtensionAPI) {
: `--continue: Überspringe Implementierung, starte direkt mit Judge-Prüfung.`; : `--continue: Überspringe Implementierung, starte direkt mit Judge-Prüfung.`;
ctx.ui.notify(continueMsg, "info"); ctx.ui.notify(continueMsg, "info");
// Im --continue-Modus: Single-Modell prüfen (Coder + Judge in einem). // Im --continue-Modus: Single-Server prüfen (Port 8001 — Coder + Judge in einem).
ctx.ui.setStatus("optimize", `Single-Modell wird geprüft (${SINGLE_LABEL})…`); ctx.ui.setStatus("optimize", "Single-Server wird geprüft (Port 8001)…");
const serverReady = await waitUntilModelReady(pi, ctx); const serverReady = await waitUntilModelReady(pi, ctx, 8001, "coding_model");
if (!serverReady) { 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; return;
} }
} else { } else {
@ -1312,7 +1260,7 @@ export default function (pi: ExtensionAPI) {
const taskPreview = task.length > 55 ? task.slice(0, 52) + "…" : task; const taskPreview = task.length > 55 ? task.slice(0, 52) + "…" : task;
ctx.ui.setStatus("optimize", `◉ Coder liest Anforderungen + implementiert: ${taskPreview}`); ctx.ui.setStatus("optimize", `◉ Coder liest Anforderungen + implementiert: ${taskPreview}`);
if (!await selectRole(pi, ctx, "coder")) { 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; return;
} }
currentActivity = "Coder implementiert…"; 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 // GPU-Ladevorgangs und ist kein verlässliches Signal. Nur HTTP 200 auf einen
// echten Completion-Request bedeutet: Modell ist im VRAM und bereit. // echten Completion-Request bedeutet: Modell ist im VRAM und bereit.
ctx.ui.setStatus("optimize", "Single-Server wird geprüft…"); ctx.ui.setStatus("optimize", "Single-Server wird geprüft…");
if (!await waitUntilModelReady(pi, ctx)) { if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
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; return;
} }
} }
@ -1365,7 +1313,7 @@ export default function (pi: ExtensionAPI) {
for (let round = 1; round <= maxRounds; round++) { for (let round = 1; round <= maxRounds; round++) {
const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round); const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round);
if (!await selectRole(pi, ctx, "judge")) { 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; return;
} }
@ -1404,9 +1352,10 @@ export default function (pi: ExtensionAPI) {
} }
// Loop-Erkennung: gleiche Beanstandung zweimal → manuell eingreifen. // 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. // Normalisierung verhindert False-Negatives durch minimale Formulierungsunterschiede.
const currentBlockers = loopKey(judgeText); const currentBlockers = parseBlockers(judgeText) || judgeText;
if (currentBlockers && normalizeForComparison(currentBlockers) === normalizeForComparison(lastBlockers)) { if (currentBlockers && normalizeForComparison(currentBlockers) === normalizeForComparison(lastBlockers)) {
ctx.ui.setStatus("optimize", `${prog} ⚠ Gleicher Blocker in Runde ${round} – 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"); 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) ? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers)
: "Kritikpunkte aus Judge-Bericht"; : "Kritikpunkte aus Judge-Bericht";
if (!await selectRole(pi, ctx, "coder")) { 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; return;
} }
currentActivity = `Coder fixt: ${blockerHint}`; currentActivity = `Coder fixt: ${blockerHint}`;
@ -1522,15 +1471,15 @@ export default function (pi: ExtensionAPI) {
// ── Schlanke Kommandos für kleine Änderungen ───────────────────────────── // ── Schlanke Kommandos für kleine Änderungen ─────────────────────────────
pi.registerCommand("patch", { 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) { handler: async function (args: string, ctx: ExtensionCommandContext) {
const change = (args || "").trim(); const change = (args || "").trim();
if (!change) { if (!change) {
ctx.ui.notify("Benutzung: /patch <beschreibung der änderung>", "error"); ctx.ui.notify("Benutzung: /patch <beschreibung der änderung>", "error");
return; return;
} }
if (!await waitUntilModelReady(pi, ctx)) { if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify(notReadyMessage(ctx), "error"); ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
return; return;
} }
await ensureLocalGitRepo(pi, ctx); await ensureLocalGitRepo(pi, ctx);
@ -1542,10 +1491,10 @@ export default function (pi: ExtensionAPI) {
}); });
pi.registerCommand("quick_check", { 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) { handler: async function (args: string, ctx: ExtensionCommandContext) {
if (!await waitUntilModelReady(pi, ctx)) { if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify(notReadyMessage(ctx), "error"); ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
return; return;
} }
await selectRole(pi, ctx, "judge"); await selectRole(pi, ctx, "judge");
@ -1613,8 +1562,8 @@ export default function (pi: ExtensionAPI) {
ctx.ui.notify("Benutzung: /plan <auftrag>", "error"); ctx.ui.notify("Benutzung: /plan <auftrag>", "error");
return; return;
} }
if (!await waitUntilModelReady(pi, ctx)) { if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify(notReadyMessage(ctx), "error"); ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
return; return;
} }
await writeTaskMd(pi, ctx, task); await writeTaskMd(pi, ctx, task);
@ -1696,8 +1645,8 @@ export default function (pi: ExtensionAPI) {
} }
// Standard-Verhalten: unterbrochenen Prozess wieder aufnehmen // Standard-Verhalten: unterbrochenen Prozess wieder aufnehmen
if (!await waitUntilModelReady(pi, ctx)) { if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify(notReadyMessage(ctx), "error"); ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
return; return;
} }
await selectRole(pi, ctx, "coder"); await selectRole(pi, ctx, "coder");

View file

@ -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"]); 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", () => { test("models.json: tote Dual-/Alt-Provider sind entfernt", () => {
for (const dead of ["llama-cpp", "llama-cpp-coder", "llama-cpp-judge"]) { for (const dead of ["llama-cpp", "llama-cpp-coder", "llama-cpp-judge"]) {
assert.ok(!(dead in models.providers), `Toter Provider '${dead}' noch vorhanden`); assert.ok(!(dead in models.providers), `Toter Provider '${dead}' noch vorhanden`);

View file

@ -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)", () => { 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)", () => { 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, /git config user\.name/);
assert.match(extSource, /new Date\(\)\.getFullYear\(\)/); 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/);
});

View file

@ -9,8 +9,6 @@ const parseVerdict = extractFn("parseVerdict");
const parseBlockers = extractFn("parseBlockers"); const parseBlockers = extractFn("parseBlockers");
const normalizeForComparison = extractFn("normalizeForComparison"); const normalizeForComparison = extractFn("normalizeForComparison");
const licenceMd = extractFn("licenceMd"); const licenceMd = extractFn("licenceMd");
const loopKey = extractFn("loopKey");
const parseShipVerdict = extractFn("parseShipVerdict");
test("parseVerdict: klares PASS", () => { test("parseVerdict: klares PASS", () => {
assert.equal(parseVerdict("Review fertig.\nUrteil: PASS\nKeine Blocker."), "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", () => { test("licenceMd: Platzhalter-Inhaber wird unverändert eingesetzt", () => {
assert.match(licenceMd("2030", "[Rechteinhaber eintragen]"), /Copyright \(c\) 2030 \[Rechteinhaber eintragen\]/); 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/);
});

View file

@ -44,9 +44,3 @@ test("install-Skript deployt settings.json mit vorherigem Backup", () => {
assert.match(s, /cp "\$REPO\/models\.json"/); assert.match(s, /cp "\$REPO\/models\.json"/);
assert.match(s, /pi-coding-extension\.ts/); 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");
});