Compare commits

..
Author SHA1 Message Date
d2e83daa98 feat: deploy-pi-config.sh verlinkt die Extension statt sie zu kopieren
Änderungen an pi-coding-extension.ts gelten damit für jeden pi-Aufruf ab dem
nächsten Start bzw. /reload, ohne erneutes Deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 06:12:42 +02:00
75ad2be498 feat: Urteilsregeln für den Judge (PASS gegen TASK/SPEC, Abschnitt Hinweise)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 06:02:38 +02:00
65555b25f1 fix: Judge-Urteil auch mit Markdown erkennen („## Urteil: **PASS**“)
parseVerdict und das SHIP-Urteil erwarteten „Urteil: PASS“ ohne Markup.
Der Judge schrieb „## Urteil: **PASS WITH CONCERNS**“; jede Runde galt als
unklar, --approve-concerns griff nie. Neu: parseShipVerdict, Tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:16:30 +02:00
2acfc85482 fix: Loop-Erkennung meldet bei „Blocker: Keine“ keinen falschen Schleifenabbruch
Zwei Judge-Runden mit „Blocker: Keine“ und verschiedenen Major-Punkten galten
als „gleicher Blocker zweimal“. loopKey() vergleicht jetzt die Blocker, bei
leerem Blocker-Abschnitt die Major-Punkte, sonst den ganzen Judge-Text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:42:30 +02:00
a608f1dda8 feat: Modell der Coder/Judge-Schleife per PI_CODER_MODEL wählbar
PI_CODER_MODEL=<provider>/<id> setzt das Modell für beide Rollen; ohne
Variable bleibt es bei llama-cpp-single/coding_model. Die Bereitschafts-
prüfung nutzt baseUrl des Modells statt fest Port 8001. Neuer Provider
strata (lokaler Strata-Server, 127.0.0.1:9090) in models.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:24:01 +02:00
9 changed files with 293 additions and 85 deletions

View file

@ -32,11 +32,16 @@ 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. Gibt ein Urteil: Prüft Korrektheit, Robustheit, Randfälle, Sicherheit und Produktionsreife. Maßstab für das Urteil
- `PASS` — Code ist in Ordnung sind `TASK.md` und, falls vorhanden, `SPEC.md`:
- `PASS WITH CONCERNS` — hat Anmerkungen, die behoben werden müssen (wird wie `FAIL` behandelt, - `PASS` — alle Anforderungen erfüllt, Tests grün, kein Blocker und kein Major
außer du verwendest `--approve-concerns`) - `PASS WITH CONCERNS` — alle Anforderungen erfüllt, aber mindestens ein Major: ein konkretes Risiko im
- `FAIL` — enthält Blocker, die behoben werden müssen 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.
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.
@ -144,6 +149,21 @@ 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
@ -234,21 +254,22 @@ Urteil: PASS WITH CONCERNS
Blocker: keine Blocker: keine
Major: Major:
- Stoppwortliste ist hardcoded; große Projekte erwarten --stopwords-file Option - 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
Minor: Minor:
- Keine --version Flag - Fehlermeldung nennt den Dateinamen nicht
- 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
Produktionsrisiken: Hinweise (ändern das Urteil nicht):
- 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. exit(1) bei FileNotFoundError 1. FileNotFoundError abfangen, Meldung mit Dateiname, exit(2)
2. Test für leere Eingabedatei 2. Test für leere Eingabedatei
``` ```
@ -415,12 +436,15 @@ Stand und der Fix-Zyklus läuft automatisch bis PASS oder max. N Runden.
### Loop-Erkennung ### Loop-Erkennung
Wenn zweimal hintereinander genau dieselben Blocker oder Concerns auftreten, bricht Wenn zweimal hintereinander dieselbe Beanstandung auftritt, bricht `/optimize` ab:
`/optimize` ab:
``` ```
⚠ Derselbe Blocker tritt erneut auf – Schleife abgebrochen. Bitte manuell prüfen. ⚠ Schleife: Gleicher Blocker zweimal – manuelle Intervention nötig
``` ```
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
@ -842,6 +866,13 @@ 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,6 +48,9 @@ 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
@ -93,9 +96,14 @@ 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.
Nach späteren Änderungen an `pi-coding-extension.ts` oder `models.json`: 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`:
```bash ```bash
./deploy-pi-config.sh # kopiert nach ~/.pi/agent/ ./deploy-pi-config.sh # verlinkt die Extension, kopiert models.json, settings.json (nur packages)
# dann /reload in pi agent # dann /reload in pi agent
``` ```
@ -256,7 +264,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 + models.json + settings.json nach `~/.pi/agent/` deployen | | `deploy-pi-config.sh` | Extension nach `~/.pi/agent/extensions/` verlinken, models.json + settings.json kopieren |
| `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 |
@ -292,6 +300,9 @@ 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,13 +1,20 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Kopiert die versionierten Dateien aus dem Repo nach ~/.pi/agent/. # Verlinkt die Extension und kopiert models.json/settings.json aus dem Repo nach ~/.pi/agent/.
# Nach jeder Änderung im Repo ausführen, damit pi agent die neue Version lädt. # 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.
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
cp "$REPO/pi-coding-extension.ts" ~/.pi/agent/extensions/pi-coding-extension.ts # A previous, differing copy is kept as .bak before it is replaced by the symlink.
echo "Kopiert: pi-coding-extension.ts → ~/.pi/agent/extensions/" 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/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,6 +39,28 @@
] ]
}, },
"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 (coding_model) auf Single-Server (Port 8001); Rollen Coder/Judge via System-Prompt // Ein Modell (Standard: coding_model auf Single-Server :8001, sonst PI_CODER_MODEL); 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,6 +51,28 @@ 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 [
@ -65,14 +87,7 @@ 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.",
"", "",
"Ausgabeformat:", ...judgeVerdictSection(),
"- 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;
@ -98,14 +113,7 @@ 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.",
"", "",
"Ausgabeformat:", ...judgeVerdictSection(),
"- 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;
} }
@ -115,7 +123,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.", "Behebe zuerst Blocker, dann Major, dann Minor. Der Abschnitt „Hinweise“ ist kein Auftrag.",
"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.",
@ -432,13 +440,30 @@ async function tickTaskMdStatus(
); );
} }
// Single-Server: genau ein Modell (coding_model auf :8001) für beide Rollen. // Single-Modell: genau ein Modell (Standard coding_model auf :8001, oder PI_CODER_MODEL) 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.
const SINGLE_PROVIDER = "llama-cpp-single"; // PI_CODER_MODEL="<provider>/<id>" setzt ein anderes Modell für beide Rollen (z. B. strata/qwen3.8-flash-next-iq3_s);
const SINGLE_MODEL_ID = "coding_model"; // 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}`;
}
async function ensureSingleModel( async function ensureSingleModel(
pi: ExtensionAPI, pi: ExtensionAPI,
@ -518,17 +543,20 @@ 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: modelAlias, model: SINGLE_MODEL_ID,
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 modelAlias Sonderzeichen enthält // Body als Datei — verhindert Shell-Injection wenn die Modell-ID 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;
@ -536,13 +564,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 http://localhost:${port}/v1/chat/completions ` + `-X POST "$1/chat/completions" ` +
`-H "Content-Type: application/json" ` + `-H "Content-Type: application/json" ` +
`-d "@${tmpBody}"` `-d "@${tmpBody}"`, "_", endpoint
], { 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 (Port ${port}) lädt noch — warte bis zu 3 min…`, "info"); ctx.ui.notify(`Modell-Server (${endpoint}) 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));
@ -644,10 +672,17 @@ function normalizeForComparison(s: string): string {
} }
function parseVerdict(text: string): string { function parseVerdict(text: string): string {
const m = text.match(/Urteil:\s*(PASS WITH CONCERNS|PASS|FAIL)/i); // Markdown zwischen „Urteil“ und dem Wort ist erlaubt: „## Urteil: **PASS**“, „**Urteil:** FAIL“.
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 {
@ -657,6 +692,23 @@ 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
@ -1128,15 +1180,15 @@ export default function (pi: ExtensionAPI) {
// ── Manuelle Kommandos ─────────────────────────────────────────────────── // ── Manuelle Kommandos ───────────────────────────────────────────────────
pi.registerCommand("coder", { pi.registerCommand("coder", {
description: "Implementiert <auftrag> ohne Review-Loop → Coder-Rolle (:8001).", description: "Implementiert <auftrag> ohne Review-Loop → Coder-Rolle (Single-Modell).",
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, 8001, "coding_model")) { if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error"); ctx.ui.notify(notReadyMessage(ctx), "error");
return; return;
} }
await ensureLocalGitRepo(pi, ctx); await ensureLocalGitRepo(pi, ctx);
@ -1149,10 +1201,10 @@ export default function (pi: ExtensionAPI) {
}); });
pi.registerCommand("judge", { pi.registerCommand("judge", {
description: "Review gegen TASK.md + git show HEAD → Judge-Rolle (:8001).", description: "Review gegen TASK.md + git show HEAD → Judge-Rolle (Single-Modell).",
handler: async function (args: string, ctx: ExtensionCommandContext) { handler: async function (args: string, ctx: ExtensionCommandContext) {
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) { if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error"); ctx.ui.notify(notReadyMessage(ctx), "error");
return; return;
} }
await selectRole(pi, ctx, "judge"); await selectRole(pi, ctx, "judge");
@ -1163,10 +1215,10 @@ export default function (pi: ExtensionAPI) {
}); });
pi.registerCommand("fix", { pi.registerCommand("fix", {
description: "Fixt Judge-Kritik, committet Ergebnis → Coder-Rolle (:8001).", description: "Fixt Judge-Kritik, committet Ergebnis → Coder-Rolle (Single-Modell).",
handler: async function (args: string, ctx: ExtensionCommandContext) { handler: async function (args: string, ctx: ExtensionCommandContext) {
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) { if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error"); ctx.ui.notify(notReadyMessage(ctx), "error");
return; return;
} }
await ensureLocalGitRepo(pi, ctx); await ensureLocalGitRepo(pi, ctx);
@ -1178,10 +1230,10 @@ export default function (pi: ExtensionAPI) {
}); });
pi.registerCommand("shipit", { pi.registerCommand("shipit", {
description: "Finale Freigabe gegen TASK.md + git log → Judge-Rolle (:8001).", description: "Finale Freigabe gegen TASK.md + git log → Judge-Rolle (Single-Modell).",
handler: async function (args: string, ctx: ExtensionCommandContext) { handler: async function (args: string, ctx: ExtensionCommandContext) {
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) { if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error"); ctx.ui.notify(notReadyMessage(ctx), "error");
return; return;
} }
await selectRole(pi, ctx, "judge"); await selectRole(pi, ctx, "judge");
@ -1189,7 +1241,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 = shipText.match(/Urteil:\s*(SHIP|NO-SHIP)/i)?.[1]?.toUpperCase() ?? ""; const shipVerdict = parseShipVerdict(shipText);
if (shipVerdict === "SHIP") { if (shipVerdict === "SHIP") {
await autoCommitIfDirty(pi, ctx); await autoCommitIfDirty(pi, ctx);
notifyShipSuccess(ctx); notifyShipSuccess(ctx);
@ -1246,11 +1298,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-Server prüfen (Port 8001 — Coder + Judge in einem). // Im --continue-Modus: Single-Modell prüfen (Coder + Judge in einem).
ctx.ui.setStatus("optimize", "Single-Server wird geprüft (Port 8001)…"); ctx.ui.setStatus("optimize", `Single-Modell wird geprüft (${SINGLE_LABEL})…`);
const serverReady = await waitUntilModelReady(pi, ctx, 8001, "coding_model"); const serverReady = await waitUntilModelReady(pi, ctx);
if (!serverReady) { if (!serverReady) {
finalNotify(ctx, "⛔ Server nicht erreichbar", "Port 8001 — kein HTTP 200 nach 3 min. start-coding-server.sh ausführen"); finalNotify(ctx, "⛔ Server nicht erreichbar", `${notReadyMessage(ctx)} (kein HTTP 200 nach 3 min)`);
return; return;
} }
} else { } else {
@ -1260,7 +1312,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 (llama-cpp-single) nicht verfügbar"); finalNotify(ctx, "⛔ Modell-Fehler", `Modell (${SINGLE_LABEL}) nicht verfügbar`);
return; return;
} }
currentActivity = "Coder implementiert…"; currentActivity = "Coder implementiert…";
@ -1274,8 +1326,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, 8001, "coding_model")) { if (!await waitUntilModelReady(pi, ctx)) {
finalNotify(ctx, "⛔ Server nicht erreichbar", "Port 8001 — kein HTTP 200 nach 3 min. start-coding-server.sh ausführen"); finalNotify(ctx, "⛔ Server nicht erreichbar", `${notReadyMessage(ctx)} (kein HTTP 200 nach 3 min)`);
return; return;
} }
} }
@ -1313,7 +1365,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 (llama-cpp-single) nicht verfügbar"); finalNotify(ctx, "⛔ Modell-Fehler", `Modell (${SINGLE_LABEL}) nicht verfügbar`);
return; return;
} }
@ -1352,10 +1404,9 @@ export default function (pi: ExtensionAPI) {
} }
// Loop-Erkennung: gleiche Beanstandung zweimal → manuell eingreifen. // Loop-Erkennung: gleiche Beanstandung zweimal → manuell eingreifen.
// Fallback auf den gesamten Judge-Text, wenn kein Blocker-Abschnitt vorhanden ist // Schlüssel: Blocker, bei „Blocker: Keine“ die Major-Punkte, sonst der ganze Judge-Text (loopKey).
// (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 = parseBlockers(judgeText) || judgeText; const currentBlockers = loopKey(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");
@ -1378,7 +1429,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 (llama-cpp-single) nicht verfügbar"); finalNotify(ctx, "⛔ Modell-Fehler", `Modell (${SINGLE_LABEL}) nicht verfügbar`);
return; return;
} }
currentActivity = `Coder fixt: ${blockerHint}`; currentActivity = `Coder fixt: ${blockerHint}`;
@ -1471,15 +1522,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 (:8001).", description: "Gezielte Minimaländerung ohne Refactoring, committet → Coder-Rolle (Single-Modell).",
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, 8001, "coding_model")) { if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error"); ctx.ui.notify(notReadyMessage(ctx), "error");
return; return;
} }
await ensureLocalGitRepo(pi, ctx); await ensureLocalGitRepo(pi, ctx);
@ -1491,10 +1542,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 (:8001).", description: "Schnelle OK/PROBLEM-Prüfung einer kleinen Codeänderung → Judge-Rolle (Single-Modell).",
handler: async function (args: string, ctx: ExtensionCommandContext) { handler: async function (args: string, ctx: ExtensionCommandContext) {
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) { if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error"); ctx.ui.notify(notReadyMessage(ctx), "error");
return; return;
} }
await selectRole(pi, ctx, "judge"); await selectRole(pi, ctx, "judge");
@ -1562,8 +1613,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, 8001, "coding_model")) { if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error"); ctx.ui.notify(notReadyMessage(ctx), "error");
return; return;
} }
await writeTaskMd(pi, ctx, task); await writeTaskMd(pi, ctx, task);
@ -1645,8 +1696,8 @@ export default function (pi: ExtensionAPI) {
} }
// Standard-Verhalten: unterbrochenen Prozess wieder aufnehmen // Standard-Verhalten: unterbrochenen Prozess wieder aufnehmen
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) { if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error"); ctx.ui.notify(notReadyMessage(ctx), "error");
return; return;
} }
await selectRole(pi, ctx, "coder"); await selectRole(pi, ctx, "coder");

View file

@ -15,6 +15,13 @@ 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, /parseBlockers\(judgeText\)\s*\|\|\s*judgeText/); assert.match(extSource, /const currentBlockers = loopKey\(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,3 +61,14 @@ 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,6 +9,8 @@ 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");
@ -78,3 +80,63 @@ 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,3 +44,9 @@ 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");
});