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.
**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:
- `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
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.
Die Umschaltung zwischen den Rollen erfolgt über unterschiedliche System-Prompts, die die
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
```
### 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
@ -234,21 +254,22 @@ Urteil: PASS WITH CONCERNS
Blocker: keine
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:
- Keine --version Flag
- Fehlermeldung bei nicht-existenter Datei gibt keinen Exit-Code 1 zurück
- Fehlermeldung nennt den Dateinamen nicht
Fehlende Tests:
- Test für leere Datei 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
- Keine --version Flag
Konkrete Fix-Aufträge:
1. exit(1) bei FileNotFoundError
1. FileNotFoundError abfangen, Meldung mit Dateiname, exit(2)
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
Wenn zweimal hintereinander genau dieselben Blocker oder Concerns auftreten, bricht
`/optimize` ab:
Wenn zweimal hintereinander dieselbe Beanstandung auftritt, bricht `/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.
### Max. Runden ohne PASS
@ -842,6 +866,13 @@ 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"
```

View file

@ -48,6 +48,9 @@ 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=<provider>/<id>`,
siehe [BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md#anderes-modell-für-die-schleife).
---
## 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
(`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
./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
```
@ -256,7 +264,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 + 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/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.
Wann der Judge welches Urteil gibt (Urteilsregeln, Abschnitt „Hinweise“): siehe
[BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md), Judge-Persona.
---
## Live-Aktivitätsstatus

View file

@ -1,13 +1,20 @@
#!/usr/bin/env bash
# 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.
# 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.
set -euo pipefail
REPO="$(cd "$(dirname "$0")" && pwd)"
mkdir -p ~/.pi/agent/extensions
cp "$REPO/pi-coding-extension.ts" ~/.pi/agent/extensions/pi-coding-extension.ts
echo "Kopiert: pi-coding-extension.ts → ~/.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/models.json" ~/.pi/agent/models.json
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": {
"models": [
{ "id": "qwen/qwen3-235b-a22b:free", "name": "Qwen3 235B (Free)" },

View file

@ -1,6 +1,6 @@
// pi-coding-extension.ts
// 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 } from "typebox";
@ -51,6 +51,28 @@ 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 [
@ -65,14 +87,7 @@ function judgePrompt(extra: string): string {
"4. Bewerte Korrektheit, Robustheit, Fehlerbehandlung, Sicherheit, Logging, Wartbarkeit und Produktionsreife.",
"5. Wenn etwas fehlt, sage es klar und direkt.",
"",
"Ausgabeformat:",
"- Urteil: PASS | PASS WITH CONCERNS | FAIL",
"- Blocker",
"- Major",
"- Minor",
"- Fehlende Tests",
"- Produktionsrisiken",
"- Konkrete Fix-Aufträge an den Coder",
...judgeVerdictSection(),
"",
"Wenn du etwas behauptest, nenne die Datei, den Befehl, den Test oder den Reproduktionshinweis."
].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).",
"4. Wenn du etwas behauptest, nenne die Datei, die Zeile oder den Reproduktionshinweis.",
"",
"Ausgabeformat:",
"- Urteil: PASS | PASS WITH CONCERNS | FAIL",
"- Blocker",
"- Major",
"- Minor",
"- Fehlende Tests",
"- Produktionsrisiken",
"- Konkrete Fix-Aufträge an den Coder",
...judgeVerdictSection(),
].join("\n") + suffix;
}
@ -115,7 +123,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.",
"Behebe zuerst Blocker, dann Major, dann Minor. Der Abschnitt „Hinweise“ ist kein Auftrag.",
"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.",
@ -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
// ü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.
const SINGLE_PROVIDER = "llama-cpp-single";
const SINGLE_MODEL_ID = "coding_model";
// PI_CODER_MODEL="<provider>/<id>" 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}`;
}
async function ensureSingleModel(
pi: ExtensionAPI,
@ -518,17 +543,20 @@ async function sendAndWait(
async function waitUntilModelReady(
pi: ExtensionAPI,
ctx: ExtensionCommandContext,
port: number,
modelAlias: string,
maxWaitMs = 180_000
): 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 body = JSON.stringify({
model: modelAlias,
model: SINGLE_MODEL_ID,
messages: [{ role: "user", content: "ping" }],
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`;
await pi.exec("bash", ["-c", `printf "%s" "$1" > "${tmpBody}"`, "_", body], { cwd: ctx.cwd });
let notified = false;
@ -536,13 +564,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 http://localhost:${port}/v1/chat/completions ` +
`-X POST "$1/chat/completions" ` +
`-H "Content-Type: application/json" ` +
`-d "@${tmpBody}"`
`-d "@${tmpBody}"`, "_", endpoint
], { cwd: ctx.cwd });
if (r.stdout?.trim() === "200") return true;
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;
}
await new Promise(res => setTimeout(res, 3000));
@ -644,10 +672,17 @@ function normalizeForComparison(s: 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";
}
// 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 {
@ -657,6 +692,23 @@ 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
@ -1128,15 +1180,15 @@ export default function (pi: ExtensionAPI) {
// ── Manuelle Kommandos ───────────────────────────────────────────────────
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) {
const task = (args || "").trim();
if (!task) {
ctx.ui.notify("Benutzung: /coder <auftrag>", "error");
return;
}
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify(notReadyMessage(ctx), "error");
return;
}
await ensureLocalGitRepo(pi, ctx);
@ -1149,10 +1201,10 @@ export default function (pi: ExtensionAPI) {
});
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) {
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify(notReadyMessage(ctx), "error");
return;
}
await selectRole(pi, ctx, "judge");
@ -1163,10 +1215,10 @@ export default function (pi: ExtensionAPI) {
});
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) {
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify(notReadyMessage(ctx), "error");
return;
}
await ensureLocalGitRepo(pi, ctx);
@ -1178,10 +1230,10 @@ export default function (pi: ExtensionAPI) {
});
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) {
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify(notReadyMessage(ctx), "error");
return;
}
await selectRole(pi, ctx, "judge");
@ -1189,7 +1241,7 @@ export default function (pi: ExtensionAPI) {
currentActivity = "Judge: finale Freigabe…";
await sendAndWait(pi, ctx, shipitPrompt(args || ""));
const shipText = getLastAssistantText(ctx);
const shipVerdict = shipText.match(/Urteil:\s*(SHIP|NO-SHIP)/i)?.[1]?.toUpperCase() ?? "";
const shipVerdict = parseShipVerdict(shipText);
if (shipVerdict === "SHIP") {
await autoCommitIfDirty(pi, ctx);
notifyShipSuccess(ctx);
@ -1246,11 +1298,11 @@ export default function (pi: ExtensionAPI) {
: `--continue: Überspringe Implementierung, starte direkt mit Judge-Prüfung.`;
ctx.ui.notify(continueMsg, "info");
// 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");
// 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);
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;
}
} else {
@ -1260,7 +1312,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 (llama-cpp-single) nicht verfügbar");
finalNotify(ctx, "⛔ Modell-Fehler", `Modell (${SINGLE_LABEL}) nicht verfügbar`);
return;
}
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
// echten Completion-Request bedeutet: Modell ist im VRAM und bereit.
ctx.ui.setStatus("optimize", "Single-Server wird geprüft…");
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");
if (!await waitUntilModelReady(pi, ctx)) {
finalNotify(ctx, "⛔ Server nicht erreichbar", `${notReadyMessage(ctx)} (kein HTTP 200 nach 3 min)`);
return;
}
}
@ -1313,7 +1365,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 (llama-cpp-single) nicht verfügbar");
finalNotify(ctx, "⛔ Modell-Fehler", `Modell (${SINGLE_LABEL}) nicht verfügbar`);
return;
}
@ -1352,10 +1404,9 @@ export default function (pi: ExtensionAPI) {
}
// Loop-Erkennung: gleiche Beanstandung zweimal → manuell eingreifen.
// 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.
// Schlüssel: Blocker, bei „Blocker: Keine“ die Major-Punkte, sonst der ganze Judge-Text (loopKey).
// Normalisierung verhindert False-Negatives durch minimale Formulierungsunterschiede.
const currentBlockers = parseBlockers(judgeText) || judgeText;
const currentBlockers = loopKey(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");
@ -1378,7 +1429,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 (llama-cpp-single) nicht verfügbar");
finalNotify(ctx, "⛔ Modell-Fehler", `Modell (${SINGLE_LABEL}) nicht verfügbar`);
return;
}
currentActivity = `Coder fixt: ${blockerHint}`;
@ -1471,15 +1522,15 @@ export default function (pi: ExtensionAPI) {
// ── Schlanke Kommandos für kleine Änderungen ─────────────────────────────
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) {
const change = (args || "").trim();
if (!change) {
ctx.ui.notify("Benutzung: /patch <beschreibung der änderung>", "error");
return;
}
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify(notReadyMessage(ctx), "error");
return;
}
await ensureLocalGitRepo(pi, ctx);
@ -1491,10 +1542,10 @@ export default function (pi: ExtensionAPI) {
});
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) {
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify(notReadyMessage(ctx), "error");
return;
}
await selectRole(pi, ctx, "judge");
@ -1562,8 +1613,8 @@ export default function (pi: ExtensionAPI) {
ctx.ui.notify("Benutzung: /plan <auftrag>", "error");
return;
}
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify(notReadyMessage(ctx), "error");
return;
}
await writeTaskMd(pi, ctx, task);
@ -1645,8 +1696,8 @@ export default function (pi: ExtensionAPI) {
}
// Standard-Verhalten: unterbrochenen Prozess wieder aufnehmen
if (!await waitUntilModelReady(pi, ctx, 8001, "coding_model")) {
ctx.ui.notify("Server nicht bereit (Port 8001) — start-coding-server.sh ausführen", "error");
if (!await waitUntilModelReady(pi, ctx)) {
ctx.ui.notify(notReadyMessage(ctx), "error");
return;
}
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"]);
});
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`);

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)", () => {
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)", () => {
@ -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, /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 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");
@ -78,3 +80,63 @@ 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/);
});

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, /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");
});