Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2e83daa98 | |||
| 75ad2be498 | |||
| 65555b25f1 | |||
| 2acfc85482 | |||
| a608f1dda8 |
9 changed files with 293 additions and 85 deletions
|
|
@ -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"
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
17
README.md
17
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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/"
|
||||||
|
|
|
||||||
22
models.json
22
models.json
|
|
@ -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)" },
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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`);
|
||||||
|
|
|
||||||
|
|
@ -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/);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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/);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue