chore: init pi_coder repository
Pi agent extension, model config, and LLaMA server startup scripts for the coder/judge workflow (ports 8001/8002).
This commit is contained in:
commit
4074e10c1a
6 changed files with 1075 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
*.bak
|
||||||
|
node_modules/
|
||||||
|
.DS_Store
|
||||||
19
install.sh
Executable file
19
install.sh
Executable file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
REPO="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
# Extension symlink
|
||||||
|
mkdir -p ~/.pi/agent/extensions
|
||||||
|
ln -sf "$REPO/pi-coder-judge-extension.ts" ~/.pi/agent/extensions/pi-coder-judge-extension.ts
|
||||||
|
echo "Symlink: ~/.pi/agent/extensions/pi-coder-judge-extension.ts -> $REPO/pi-coder-judge-extension.ts"
|
||||||
|
|
||||||
|
# models.json symlink (Backup wenn reguläre Datei vorhanden)
|
||||||
|
if [ -f ~/.pi/agent/models.json ] && [ ! -L ~/.pi/agent/models.json ]; then
|
||||||
|
cp ~/.pi/agent/models.json ~/.pi/agent/models.json.bak
|
||||||
|
echo "Backup: ~/.pi/agent/models.json.bak"
|
||||||
|
fi
|
||||||
|
ln -sf "$REPO/models.json" ~/.pi/agent/models.json
|
||||||
|
echo "Symlink: ~/.pi/agent/models.json -> $REPO/models.json"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Fertig. Bitte /reload in pi agent ausführen."
|
||||||
111
models.json
Normal file
111
models.json
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
{
|
||||||
|
"providers": {
|
||||||
|
"ollama": {
|
||||||
|
"baseUrl": "http://localhost:11434/v1",
|
||||||
|
"api": "openai-completions",
|
||||||
|
"apiKey": "ollama",
|
||||||
|
"compat": {
|
||||||
|
"supportsDeveloperRole": false,
|
||||||
|
"supportsReasoningEffort": false
|
||||||
|
},
|
||||||
|
"models": [
|
||||||
|
{ "id": "qwen2.5-coder:7b", "name": "Qwen2.5 Coder 7B (schnell)" },
|
||||||
|
{ "id": "qwen3-coder-30b-gpu:latest", "name": "Qwen3 Coder 30B GPU (Standard)" },
|
||||||
|
{ "id": "mistral-small3.2:24b", "name": "Mistral Small 3.2 24B" },
|
||||||
|
{ "id": "deepseek-r1:32b", "name": "DeepSeek R1 32B (Reasoning)" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"llama-cpp": {
|
||||||
|
"baseUrl": "http://127.0.0.1:8000/v1",
|
||||||
|
"api": "openai-completions",
|
||||||
|
"apiKey": "none",
|
||||||
|
"compat": {
|
||||||
|
"supportsDeveloperRole": false,
|
||||||
|
"supportsReasoningEffort": false,
|
||||||
|
"maxTokensField": "max_tokens",
|
||||||
|
"thinkingFormat": "qwen-chat-template"
|
||||||
|
},
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "qwen35b-uncensored",
|
||||||
|
"name": "Qwen3.6 35B Uncensored (llama.cpp :8000)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "qwen35b-moe-tools",
|
||||||
|
"name": "Qwen3.6 35B MoE Tools (llama.cpp :8000)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"llama-cpp-coder": {
|
||||||
|
"baseUrl": "http://127.0.0.1:8001/v1",
|
||||||
|
"api": "openai-completions",
|
||||||
|
"apiKey": "none",
|
||||||
|
"compat": {
|
||||||
|
"supportsDeveloperRole": false,
|
||||||
|
"supportsReasoningEffort": false,
|
||||||
|
"maxTokensField": "max_tokens",
|
||||||
|
"thinkingFormat": "qwen-chat-template"
|
||||||
|
},
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "qwen3.5-coder",
|
||||||
|
"name": "Qwen3.6 27B Coder (llama.cpp :8001)",
|
||||||
|
"reasoning": true,
|
||||||
|
"input": ["text"],
|
||||||
|
"contextWindow": 131072,
|
||||||
|
"maxTokens": 16384,
|
||||||
|
"cost": {
|
||||||
|
"input": 0,
|
||||||
|
"output": 0,
|
||||||
|
"cacheRead": 0,
|
||||||
|
"cacheWrite": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"llama-cpp-judge": {
|
||||||
|
"baseUrl": "http://127.0.0.1:8002/v1",
|
||||||
|
"api": "openai-completions",
|
||||||
|
"apiKey": "none",
|
||||||
|
"compat": {
|
||||||
|
"supportsDeveloperRole": false,
|
||||||
|
"supportsReasoningEffort": false,
|
||||||
|
"maxTokensField": "max_tokens",
|
||||||
|
"thinkingFormat": "qwen-chat-template"
|
||||||
|
},
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"id": "qwen3.5-judge",
|
||||||
|
"name": "Qwen3.6 27B Judge (llama.cpp :8002)",
|
||||||
|
"reasoning": true,
|
||||||
|
"input": ["text"],
|
||||||
|
"contextWindow": 65536,
|
||||||
|
"maxTokens": 8192,
|
||||||
|
"cost": {
|
||||||
|
"input": 0,
|
||||||
|
"output": 0,
|
||||||
|
"cacheRead": 0,
|
||||||
|
"cacheWrite": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"openrouter": {
|
||||||
|
"models": [
|
||||||
|
{ "id": "qwen/qwen3-235b-a22b:free", "name": "Qwen3 235B (Free)" },
|
||||||
|
{ "id": "deepseek/deepseek-r1:free", "name": "DeepSeek R1 (Free)" },
|
||||||
|
{ "id": "google/gemini-2.5-pro-exp-03-25:free", "name": "Gemini 2.5 Pro (Free)" },
|
||||||
|
{ "id": "meta-llama/llama-4-maverick:free", "name": "Llama 4 Maverick (Free)" },
|
||||||
|
{ "id": "microsoft/phi-4:free", "name": "Phi-4 (Free)" },
|
||||||
|
{ "id": "qwen/qwen-2.5-coder-32b-instruct", "name": "Qwen2.5 Coder 32B (günstig)" },
|
||||||
|
{ "id": "deepseek/deepseek-r1", "name": "DeepSeek R1 Full (Reasoning)" },
|
||||||
|
{ "id": "qwen/qwen3-235b-a22b", "name": "Qwen3 235B Full" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
762
pi-coder-judge-extension.ts
Normal file
762
pi-coder-judge-extension.ts
Normal file
|
|
@ -0,0 +1,762 @@
|
||||||
|
// pi-coder-judge-extension.ts
|
||||||
|
// Automatisierter Coder-Judge-Fix-Workflow für AI-Coding-Assistenten
|
||||||
|
// Modelle: qwen3.5-coder (Port 8001), qwen3.5-judge (Port 8002)
|
||||||
|
|
||||||
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
||||||
|
import { Type } from "typebox";
|
||||||
|
|
||||||
|
// ── Prompt-Bausteine ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function coderKickoff(task: string): string {
|
||||||
|
return [
|
||||||
|
"Du bist der Coding-Agent.",
|
||||||
|
"Lies TASK.md für die vollständige Aufgabenbeschreibung.",
|
||||||
|
"Halte dich strikt an die dort beschriebenen Anforderungen.",
|
||||||
|
"Arbeite sorgfältig, konkret und produktionsorientiert.",
|
||||||
|
"Lies jede Datei unmittelbar vor dem Editieren neu 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.",
|
||||||
|
"",
|
||||||
|
"Git-Pflichten:",
|
||||||
|
"- Falls noch kein git-Repository existiert, initialisiere es mit 'git init'.",
|
||||||
|
"- Führe nach der Implementierung einen Commit durch: git add -A && git commit -m 'feat: ...'",
|
||||||
|
"",
|
||||||
|
"Führe nach der Implementierung passende Tests oder Checks aus.",
|
||||||
|
"Melde knapp, was du geändert hast, welche Risiken bleiben und welche Tests du ausgeführt hast.",
|
||||||
|
"",
|
||||||
|
"Auftrag:",
|
||||||
|
task
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function judgePrompt(extra: string): string {
|
||||||
|
const suffix = extra?.trim() ? "\n\nZusätzlicher Fokus des Users:\n" + extra.trim() : "";
|
||||||
|
return [
|
||||||
|
"Du bist ein pingeliger, skeptischer Senior-Reviewer und QA-Ingenieur.",
|
||||||
|
"Deine Aufgabe ist NICHT, nett zu sein, sondern Fehler, Risiken, Randfälle und Produktionsprobleme zu finden.",
|
||||||
|
"Arbeite reproduzierbar und konkret.",
|
||||||
|
"",
|
||||||
|
"Pflichten:",
|
||||||
|
"0. Lies TASK.md und prüfe, ob alle dort beschriebenen Anforderungen vollständig umgesetzt sind.",
|
||||||
|
"1. Sieh dir den letzten Commit an: 'git log -1 --stat' und 'git show HEAD'.",
|
||||||
|
"2. Führe relevante Tests, Linter oder Startchecks aus.",
|
||||||
|
"3. Versuche Fehler aktiv zu finden.",
|
||||||
|
"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",
|
||||||
|
"",
|
||||||
|
"Wenn du etwas behauptest, nenne die Datei, den Befehl, den Test oder den Reproduktionshinweis."
|
||||||
|
].join("\n") + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixPrompt(extra: string): string {
|
||||||
|
const suffix = extra?.trim() ? "\n\nZusätzlicher User-Hinweis:\n" + extra.trim() : "";
|
||||||
|
return [
|
||||||
|
"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.",
|
||||||
|
"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.",
|
||||||
|
"Führe nach den Fixes passende Tests aus.",
|
||||||
|
"Führe danach einen Commit durch: git add -A && git commit -m 'fix: ...'",
|
||||||
|
"Wenn ein Punkt nicht sinnvoll umsetzbar ist, begründe das präzise.",
|
||||||
|
"Liefere am Ende nur:",
|
||||||
|
"- Was geändert wurde",
|
||||||
|
"- Welche Judge-Punkte geschlossen wurden",
|
||||||
|
"- Welche Punkte offen bleiben",
|
||||||
|
"- Welche Tests ausgeführt wurden"
|
||||||
|
].join("\n") + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shipitPrompt(extra: string): string {
|
||||||
|
return [
|
||||||
|
"Führe die finale Freigabeprüfung durch.",
|
||||||
|
"Lies TASK.md und prüfe, ob alle dort beschriebenen Anforderungen im finalen Stand enthalten sind.",
|
||||||
|
"Lies die relevanten geänderten Dateien und 'git log --oneline -10'.",
|
||||||
|
"Führe sinnvolle Tests, Linter und Startchecks aus.",
|
||||||
|
"Beurteile, ob der Stand produktionsreif ist.",
|
||||||
|
"",
|
||||||
|
"Ausgabeformat:",
|
||||||
|
"- Urteil: SHIP | NO-SHIP",
|
||||||
|
"- Letzte Blocker",
|
||||||
|
"- Restrisiken",
|
||||||
|
"- Empfohlene Sofortmaßnahmen vor Deployment"
|
||||||
|
].join("\n") + (extra?.trim() ? "\n\nZusätzlicher Fokus:\n" + extra.trim() : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchPrompt(change: string): string {
|
||||||
|
return [
|
||||||
|
"Du machst eine kleine, gezielte Codeänderung. Nichts weiter.",
|
||||||
|
"Ändere AUSSCHLIESSLICH das Folgende:",
|
||||||
|
change,
|
||||||
|
"",
|
||||||
|
"Regeln:",
|
||||||
|
"- Kein Refactoring, keine weiteren Verbesserungen, keine Umbenennungen",
|
||||||
|
"- Lies die Datei unmittelbar vor dem Editieren neu ein",
|
||||||
|
"- Wenn du mehrere Stellen in derselben Datei änderst: Erzeuge einen unified diff und nutze das apply_patch-Tool",
|
||||||
|
"- Fallback: Datei komplett neu schreiben",
|
||||||
|
"- Finde die betroffene Stelle direkt (grep oder gezielte Datei-Suche)",
|
||||||
|
"- Ändere nur die notwendigen Zeilen",
|
||||||
|
"- Prüfe danach nur: Kompiliert/startet es noch?",
|
||||||
|
"- Commit: git add -A && git commit -m 'fix: <ein Satz>'",
|
||||||
|
"- Melde: Datei, Zeile(n), was geändert wurde. Fertig."
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function quickCheckPrompt(what: string): string {
|
||||||
|
const focus = what?.trim() ? "\n\nZu prüfende Änderung:\n" + what.trim() : "";
|
||||||
|
return [
|
||||||
|
"Schnelle Prüfung einer kleinen Codeänderung.",
|
||||||
|
"Lies 'git show HEAD' um zu sehen was geändert wurde.",
|
||||||
|
"Prüfe NUR:",
|
||||||
|
"- Ist die Änderung korrekt umgesetzt?",
|
||||||
|
"- Gibt es offensichtliche Fehler oder Randfälle die übersehen wurden?",
|
||||||
|
"- Kompiliert/startet der Code?",
|
||||||
|
"",
|
||||||
|
"Ausgabeformat (kurz):",
|
||||||
|
"- Urteil: OK | PROBLEM",
|
||||||
|
"- Falls PROBLEM: konkret was falsch ist und wie zu fixen",
|
||||||
|
"",
|
||||||
|
"Kein vollständiger Review, keine Stilkritik, kein Refactoring-Vorschlag."
|
||||||
|
].join("\n") + focus;
|
||||||
|
}
|
||||||
|
|
||||||
|
function commentCodePrompt(): string {
|
||||||
|
return [
|
||||||
|
"Lies TASK.md und alle Quelldateien des Projekts.",
|
||||||
|
"Füge wartungsfreundliche Kommentare ein, die Entwicklern ohne Vorkenntnis helfen, den Code zu verstehen und zu warten.",
|
||||||
|
"",
|
||||||
|
"Regeln:",
|
||||||
|
"- Kommentiere das WARUM, nicht das WAS (kein 'x += 1 // increment x')",
|
||||||
|
"- Erkläre nicht-offensichtliche Algorithmen, Randfälle und Design-Entscheidungen",
|
||||||
|
"- Füge Modul-/Datei-Level-Kommentare ein, die den Gesamtzweck der Datei erklären",
|
||||||
|
"- Keine trivialen Kommentare, die nur den Code wiederholen",
|
||||||
|
"- Sprache der Kommentare: Deutsch",
|
||||||
|
"",
|
||||||
|
"Wenn du mehrere Stellen in derselben Datei kommentierst: 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.",
|
||||||
|
"",
|
||||||
|
"Führe danach einen Build/Test aus um sicherzustellen, dass die Kommentare nichts kaputt gemacht haben.",
|
||||||
|
"Melde welche Dateien du kommentiert hast."
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function readmeMdPrompt(): string {
|
||||||
|
return [
|
||||||
|
"Lies TASK.md und alle Quelldateien des Projekts.",
|
||||||
|
"Schreibe oder aktualisiere README.md aus Entwicklerperspektive.",
|
||||||
|
"",
|
||||||
|
"Pflichtabschnitte:",
|
||||||
|
"- Projektbeschreibung (Was macht das Programm? Warum?)",
|
||||||
|
"- Voraussetzungen (Dependencies, Toolchain)",
|
||||||
|
"- Installation und Build",
|
||||||
|
"- Verwendung (alle Kommandozeilenoptionen und Flags)",
|
||||||
|
"- Beispiele mit konkreter Ausgabe",
|
||||||
|
"- Projektstruktur (Dateien und ihre Aufgabe)",
|
||||||
|
"",
|
||||||
|
"Halte es technisch präzise und korrekt. Sprache: Deutsch."
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function bedienungsanleitungPrompt(): string {
|
||||||
|
return [
|
||||||
|
"Lies TASK.md und README.md.",
|
||||||
|
"Schreibe oder aktualisiere BEDIENUNGSANLEITUNG.md aus Endnutzer-Perspektive.",
|
||||||
|
"Setze kein Entwicklerwissen voraus — die Zielgruppe sind normale Anwender.",
|
||||||
|
"",
|
||||||
|
"Pflichtabschnitte:",
|
||||||
|
"- Zweck des Programms (was kann der Nutzer damit tun?)",
|
||||||
|
"- Installation für Endnutzer (Schritt für Schritt)",
|
||||||
|
"- Erste Schritte / Schnellstart",
|
||||||
|
"- Alle Optionen mit verständlicher Erklärung und Beispielen",
|
||||||
|
"- Typische Anwendungsfälle",
|
||||||
|
"- Fehlermeldungen und ihre Lösung",
|
||||||
|
"",
|
||||||
|
"Sprache: Deutsch. Einfach, klar, ohne Jargon."
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function commentCodePromptIncremental(files: string[]): string {
|
||||||
|
const fileList = files.map(f => ` - ${f}`).join("\n");
|
||||||
|
return [
|
||||||
|
"Kommentiere NUR die folgenden Quelldateien — sie haben sich seit dem letzten Kommentar-Update geändert:",
|
||||||
|
fileList,
|
||||||
|
"Alle anderen Dateien haben bereits aktuelle Kommentare — lass sie vollständig unberührt.",
|
||||||
|
"",
|
||||||
|
"Regeln:",
|
||||||
|
"- Kommentiere das WARUM, nicht das WAS (kein 'x += 1 // increment x')",
|
||||||
|
"- Erkläre nicht-offensichtliche Algorithmen, Randfälle und Design-Entscheidungen",
|
||||||
|
"- Füge Modul-/Datei-Level-Kommentare ein, die den Gesamtzweck der Datei erklären",
|
||||||
|
"- Keine trivialen Kommentare, die nur den Code wiederholen",
|
||||||
|
"- Sprache der Kommentare: Deutsch",
|
||||||
|
"",
|
||||||
|
"Wenn du mehrere Stellen in derselben Datei kommentierst: 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.",
|
||||||
|
"",
|
||||||
|
"Führe danach einen Build/Test aus um sicherzustellen, dass die Kommentare nichts kaputt gemacht haben.",
|
||||||
|
"Melde welche Dateien du kommentiert hast."
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function readmeMdPromptIncremental(files: string[]): string {
|
||||||
|
const fileList = files.map(f => ` - ${f}`).join("\n");
|
||||||
|
return [
|
||||||
|
"Folgende Quelldateien haben sich seit dem letzten README-Update geändert:",
|
||||||
|
fileList,
|
||||||
|
"",
|
||||||
|
"Prüfe: Haben diese Änderungen Auswirkungen auf Installation, Verwendung, Optionen oder Projektstruktur?",
|
||||||
|
"- Falls JA: Lies README.md und aktualisiere NUR die betroffenen Abschnitte.",
|
||||||
|
"- Falls NEIN: Antworte nur mit dem Satz: 'README.md ist aktuell – keine Änderung nötig.'",
|
||||||
|
"",
|
||||||
|
"Wenn du mehrere Abschnitte in README.md aktualisierst: Erzeuge einen unified diff und nutze das apply_patch-Tool.",
|
||||||
|
"Halte es technisch präzise und korrekt. Sprache: Deutsch."
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function bedienungsanleitungPromptIncremental(files: string[]): string {
|
||||||
|
const fileList = files.map(f => ` - ${f}`).join("\n");
|
||||||
|
return [
|
||||||
|
"Folgende Quelldateien haben sich seit dem letzten Bedienungsanleitung-Update geändert:",
|
||||||
|
fileList,
|
||||||
|
"",
|
||||||
|
"Prüfe: Haben diese Änderungen Auswirkungen auf die Benutzung des Programms durch Endnutzer?",
|
||||||
|
"(z.B. neue Optionen, geändertes Verhalten, neue Fehlermeldungen)",
|
||||||
|
"- Falls JA: Lies BEDIENUNGSANLEITUNG.md und aktualisiere NUR die betroffenen Abschnitte.",
|
||||||
|
"- Falls NEIN: Antworte nur mit dem Satz: 'BEDIENUNGSANLEITUNG.md ist aktuell – keine Änderung nötig.'",
|
||||||
|
"",
|
||||||
|
"Wenn du mehrere Abschnitte in BEDIENUNGSANLEITUNG.md aktualisierst: Erzeuge einen unified diff und nutze das apply_patch-Tool.",
|
||||||
|
"Sprache: Deutsch. Einfach, klar, ohne Jargon."
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hilfsfunktionen ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Legt TASK.md neu an oder hängt einen Zusatzauftrag an.
|
||||||
|
async function writeTaskMd(
|
||||||
|
pi: ExtensionAPI,
|
||||||
|
ctx: ExtensionCommandContext,
|
||||||
|
task: string
|
||||||
|
): Promise<void> {
|
||||||
|
const check = await pi.exec("bash", ["-c", "test -f TASK.md && echo exists"], { cwd: ctx.cwd });
|
||||||
|
const exists = check.stdout.trim() === "exists";
|
||||||
|
|
||||||
|
let content: string;
|
||||||
|
if (exists) {
|
||||||
|
content = [
|
||||||
|
"",
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"## Zusatzauftrag",
|
||||||
|
"",
|
||||||
|
new Date().toISOString(),
|
||||||
|
"",
|
||||||
|
task,
|
||||||
|
"",
|
||||||
|
"## Status",
|
||||||
|
"- [ ] Implementierung",
|
||||||
|
"- [ ] Review bestanden (PASS)",
|
||||||
|
"- [ ] Produktionsreif (SHIP)",
|
||||||
|
].join("\n") + "\n";
|
||||||
|
} else {
|
||||||
|
content = [
|
||||||
|
"# Aufgabe",
|
||||||
|
"",
|
||||||
|
task,
|
||||||
|
"",
|
||||||
|
"## Erstellt",
|
||||||
|
new Date().toISOString(),
|
||||||
|
"",
|
||||||
|
"## Status",
|
||||||
|
"- [ ] Implementierung",
|
||||||
|
"- [ ] Review bestanden (PASS)",
|
||||||
|
"- [ ] Produktionsreif (SHIP)",
|
||||||
|
].join("\n") + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirect = exists ? ">>" : ">";
|
||||||
|
await pi.exec("bash", ["-c", `printf "%s" "$1" ${redirect} TASK.md`, "_", content], { cwd: ctx.cwd });
|
||||||
|
ctx.ui.notify(exists ? "TASK.md erweitert" : "TASK.md angelegt", "info");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hakt einen Status-Eintrag in TASK.md ab.
|
||||||
|
// label: exakt wie in der Checkbox, z.B. "Implementierung"
|
||||||
|
async function tickTaskMdStatus(
|
||||||
|
pi: ExtensionAPI,
|
||||||
|
ctx: ExtensionCommandContext,
|
||||||
|
label: string
|
||||||
|
): Promise<void> {
|
||||||
|
const check = await pi.exec("bash", ["-c", "test -f TASK.md && echo exists"], { cwd: ctx.cwd });
|
||||||
|
if (check.stdout.trim() !== "exists") return;
|
||||||
|
// Python übernimmt den String-Ersatz — kein Shell-Escaping-Problem
|
||||||
|
await pi.exec(
|
||||||
|
"python3",
|
||||||
|
["-c",
|
||||||
|
"import sys; f=open('TASK.md','r'); c=f.read(); f.close(); " +
|
||||||
|
"c=c.replace('- [ ] '+sys.argv[1], '- [x] '+sys.argv[1]); " +
|
||||||
|
"f=open('TASK.md','w'); f.write(c); f.close()",
|
||||||
|
label
|
||||||
|
],
|
||||||
|
{ cwd: ctx.cwd }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function switchModel(
|
||||||
|
pi: ExtensionAPI,
|
||||||
|
ctx: ExtensionCommandContext,
|
||||||
|
provider: string,
|
||||||
|
modelId: string
|
||||||
|
): Promise<void> {
|
||||||
|
const model = ctx.modelRegistry.find(provider, modelId);
|
||||||
|
if (!model) {
|
||||||
|
ctx.ui.notify(`Modell ${provider}/${modelId} nicht gefunden`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ok = await pi.setModel(model);
|
||||||
|
if (!ok) ctx.ui.notify(`Kein API-Key für ${modelId}`, "warning");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sendet eine Nachricht und wartet bis der Agent fertig ist.
|
||||||
|
// Erst idle abwarten, dann als followUp einstellen — verhindert "Agent is already processing".
|
||||||
|
async function sendAndWait(
|
||||||
|
pi: ExtensionAPI,
|
||||||
|
ctx: ExtensionCommandContext,
|
||||||
|
content: string
|
||||||
|
): Promise<void> {
|
||||||
|
await ctx.waitForIdle();
|
||||||
|
pi.sendUserMessage(content, { deliverAs: "followUp" });
|
||||||
|
await new Promise(r => setTimeout(r, 400));
|
||||||
|
await ctx.waitForIdle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Liest den Text der letzten Assistenten-Antwort aus dem Session-Branch.
|
||||||
|
function getLastAssistantText(ctx: ExtensionCommandContext): string {
|
||||||
|
const entries = ctx.sessionManager.getBranch();
|
||||||
|
for (let i = entries.length - 1; i >= 0; i--) {
|
||||||
|
const entry = entries[i];
|
||||||
|
if (entry.type === "message") {
|
||||||
|
const msg = (entry as any).message;
|
||||||
|
if (msg?.role === "assistant" && Array.isArray(msg.content)) {
|
||||||
|
return msg.content
|
||||||
|
.filter((c: any) => c.type === "text")
|
||||||
|
.map((c: any) => c.text as string)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extrahiert das Urteil aus einer Judge-Antwort.
|
||||||
|
function parseVerdict(text: string): string {
|
||||||
|
const m = text.match(/Urteil:\s*(PASS WITH CONCERNS|PASS|FAIL)/i);
|
||||||
|
return m ? m[1].toUpperCase() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extrahiert den Blocker-Abschnitt für die Loop-Erkennung.
|
||||||
|
function parseBlockers(text: string): string {
|
||||||
|
const m = text.match(/[-–*]\s*Blocker[:\n]([\s\S]*?)(?:\n[-–*]\s*Major|\n[-–*]\s*Minor|$)/i);
|
||||||
|
return m ? m[1].trim() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gibt geänderte Quelldateien seit einem Git-Tag zurück.
|
||||||
|
// null = Tag existiert nicht (erster Lauf) → alles verarbeiten
|
||||||
|
// [] = nichts geändert → Phase überspringen
|
||||||
|
// [...] = nur diese Dateien verarbeiten
|
||||||
|
async function getFilesSinceTag(
|
||||||
|
pi: ExtensionAPI,
|
||||||
|
ctx: ExtensionCommandContext,
|
||||||
|
tagName: string
|
||||||
|
): Promise<string[] | null> {
|
||||||
|
const tagCheck = await pi.exec("bash", ["-c", `git tag -l "${tagName}"`], { cwd: ctx.cwd });
|
||||||
|
if (!tagCheck.stdout.trim()) return null;
|
||||||
|
|
||||||
|
const diff = await pi.exec(
|
||||||
|
"bash",
|
||||||
|
["-c", `git diff "${tagName}" --name-only 2>/dev/null`],
|
||||||
|
{ cwd: ctx.cwd }
|
||||||
|
);
|
||||||
|
|
||||||
|
return diff.stdout.trim()
|
||||||
|
.split("\n")
|
||||||
|
.filter(f =>
|
||||||
|
f.length > 0 &&
|
||||||
|
!f.endsWith(".md") &&
|
||||||
|
!f.endsWith(".lock") &&
|
||||||
|
!f.endsWith(".toml") &&
|
||||||
|
!f.startsWith("target/") &&
|
||||||
|
!f.endsWith(".gitignore")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dokumentations-Phase: inkrementell via Git-Tags, nur geänderte Dateien werden verarbeitet.
|
||||||
|
// Wird von /update_doku und /optimize --with-doku genutzt.
|
||||||
|
async function runUpdateDoku(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> {
|
||||||
|
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
||||||
|
|
||||||
|
// Phase 1: Code-Kommentare
|
||||||
|
const commentFiles = await getFilesSinceTag(pi, ctx, "docs-last-commented");
|
||||||
|
if (commentFiles === null) {
|
||||||
|
ctx.ui.setStatus("update_doku", "1/3: Code wird kommentiert (alle Dateien)…");
|
||||||
|
await sendAndWait(pi, ctx, commentCodePrompt());
|
||||||
|
} else if (commentFiles.length === 0) {
|
||||||
|
ctx.ui.notify("Code-Kommentare: keine Änderungen seit letztem Lauf – übersprungen.", "info");
|
||||||
|
} else {
|
||||||
|
ctx.ui.setStatus("update_doku", `1/3: Code wird kommentiert (${commentFiles.length} Datei(en))…`);
|
||||||
|
await sendAndWait(pi, ctx, commentCodePromptIncremental(commentFiles));
|
||||||
|
}
|
||||||
|
await pi.exec("bash", ["-c", "git tag -f docs-last-commented"], { cwd: ctx.cwd });
|
||||||
|
|
||||||
|
// Phase 2: README.md
|
||||||
|
const readmeFiles = await getFilesSinceTag(pi, ctx, "docs-last-readme");
|
||||||
|
if (readmeFiles === null) {
|
||||||
|
ctx.ui.setStatus("update_doku", "2/3: README.md wird geschrieben…");
|
||||||
|
await sendAndWait(pi, ctx, readmeMdPrompt());
|
||||||
|
} else if (readmeFiles.length === 0) {
|
||||||
|
ctx.ui.notify("README.md: keine Änderungen seit letztem Lauf – übersprungen.", "info");
|
||||||
|
} else {
|
||||||
|
ctx.ui.setStatus("update_doku", `2/3: README.md wird geprüft (${readmeFiles.length} Datei(en) geändert)…`);
|
||||||
|
await sendAndWait(pi, ctx, readmeMdPromptIncremental(readmeFiles));
|
||||||
|
}
|
||||||
|
await pi.exec("bash", ["-c", "git tag -f docs-last-readme"], { cwd: ctx.cwd });
|
||||||
|
|
||||||
|
// Phase 3: BEDIENUNGSANLEITUNG.md
|
||||||
|
const bedFiles = await getFilesSinceTag(pi, ctx, "docs-last-bedienungsanleitung");
|
||||||
|
if (bedFiles === null) {
|
||||||
|
ctx.ui.setStatus("update_doku", "3/3: BEDIENUNGSANLEITUNG.md wird geschrieben…");
|
||||||
|
await sendAndWait(pi, ctx, bedienungsanleitungPrompt());
|
||||||
|
} else if (bedFiles.length === 0) {
|
||||||
|
ctx.ui.notify("BEDIENUNGSANLEITUNG.md: keine Änderungen seit letztem Lauf – übersprungen.", "info");
|
||||||
|
} else {
|
||||||
|
ctx.ui.setStatus("update_doku", `3/3: BEDIENUNGSANLEITUNG.md wird geprüft (${bedFiles.length} Datei(en) geändert)…`);
|
||||||
|
await sendAndWait(pi, ctx, bedienungsanleitungPromptIncremental(bedFiles));
|
||||||
|
}
|
||||||
|
await pi.exec("bash", ["-c", "git tag -f docs-last-bedienungsanleitung"], { cwd: ctx.cwd });
|
||||||
|
|
||||||
|
// Abschließender Dokumentations-Commit
|
||||||
|
await pi.exec(
|
||||||
|
"bash",
|
||||||
|
["-c", "git add -A && git commit -m 'docs: update comments, README, BEDIENUNGSANLEITUNG' || true"],
|
||||||
|
{ cwd: ctx.cwd }
|
||||||
|
);
|
||||||
|
|
||||||
|
// TASK.md: Produktionsreif abhaken
|
||||||
|
await tickTaskMdStatus(pi, ctx, "Produktionsreif (SHIP)");
|
||||||
|
|
||||||
|
ctx.ui.setStatus("update_doku", "✓ Dokumentation abgeschlossen");
|
||||||
|
ctx.ui.notify("Dokumentations-Phase abgeschlossen. Commit angelegt.", "info");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Extension ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export default function (pi: ExtensionAPI) {
|
||||||
|
pi.on("session_start", async function (_event, ctx) {
|
||||||
|
ctx.ui.setWidget("coder-judge", [
|
||||||
|
"Workflow: /coder <auftrag> | /judge | /fix | /shipit",
|
||||||
|
"Auto-Loop: /optimize <auftrag> [--rounds N] [--with-doku]",
|
||||||
|
"Kleine Änderung: /patch <änderung> → /quick_check [was]",
|
||||||
|
"Finale Doku: /update_doku (nach SHIP – Kommentare + README + Bedienungsanleitung)",
|
||||||
|
"Neues Projekt: /new_project <pfad>",
|
||||||
|
"Modell wird automatisch gewechselt (Coder→:8001, Judge→:8002)"
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Robustes edit: Bottom-up-Reordering via tool_call-Hook ─────────────
|
||||||
|
// Behebt "edits[n] doesn't match": Mehrere Edits auf dieselbe Datei werden
|
||||||
|
// von hinten nach vorne sortiert, damit frühere Edits spätere Positionen nicht verschieben.
|
||||||
|
|
||||||
|
pi.on("tool_call", async function (event, ctx) {
|
||||||
|
if (event.toolName !== "edit") return;
|
||||||
|
|
||||||
|
const input = event.input as {
|
||||||
|
path: string;
|
||||||
|
edits: Array<{ oldText: string; newText: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!input?.edits || input.edits.length <= 1) return;
|
||||||
|
|
||||||
|
const readResult = await pi.exec(
|
||||||
|
"bash",
|
||||||
|
["-c", `cat "$1"`, "_", input.path],
|
||||||
|
{ cwd: ctx.cwd }
|
||||||
|
);
|
||||||
|
if (readResult.code !== 0) return;
|
||||||
|
|
||||||
|
const content = readResult.stdout;
|
||||||
|
|
||||||
|
const positioned = input.edits.map(edit => ({
|
||||||
|
edit,
|
||||||
|
idx: content.indexOf(edit.oldText)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Nicht gefundene Einträge (idx === -1) ans Ende — sie schlagen sowieso fehl
|
||||||
|
positioned.sort((a, b) => b.idx - a.idx);
|
||||||
|
|
||||||
|
input.edits.splice(0, input.edits.length, ...positioned.map(p => p.edit));
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Manuelle Kommandos ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pi.registerCommand("coder", {
|
||||||
|
description: "Legt TASK.md an, startet Implementierung → qwen3.5-coder (:8001).",
|
||||||
|
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||||
|
const task = (args || "").trim();
|
||||||
|
if (!task) {
|
||||||
|
ctx.ui.notify("Benutzung: /coder <auftrag>", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await writeTaskMd(pi, ctx, task);
|
||||||
|
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
||||||
|
pi.sendUserMessage(coderKickoff(task));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.registerCommand("judge", {
|
||||||
|
description: "Review gegen TASK.md + git show HEAD → qwen3.5-judge (:8002).",
|
||||||
|
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||||
|
await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge");
|
||||||
|
pi.sendUserMessage(judgePrompt(args || ""));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.registerCommand("fix", {
|
||||||
|
description: "Fixt Judge-Kritik, committet Ergebnis → qwen3.5-coder (:8001).",
|
||||||
|
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||||
|
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
||||||
|
pi.sendUserMessage(fixPrompt(args || ""));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.registerCommand("shipit", {
|
||||||
|
description: "Finale Freigabe gegen TASK.md + git log → qwen3.5-judge (:8002).",
|
||||||
|
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||||
|
await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge");
|
||||||
|
pi.sendUserMessage(shipitPrompt(args || ""));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Automatische Optimierungsschleife ────────────────────────────────────
|
||||||
|
|
||||||
|
pi.registerCommand("optimize", {
|
||||||
|
description: "Coder→Judge→Fix-Schleife bis PASS + optional Doku. /optimize <auftrag> [--rounds N] [--with-doku]",
|
||||||
|
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||||
|
const roundsMatch = (args || "").match(/--rounds\s+(\d+)/);
|
||||||
|
const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 3;
|
||||||
|
const withDoku = /--with-doku/.test(args || "");
|
||||||
|
const task = (args || "")
|
||||||
|
.replace(/--rounds\s+\d+/, "")
|
||||||
|
.replace(/--with-doku/, "")
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
if (!task) {
|
||||||
|
ctx.ui.notify("Benutzung: /optimize <auftrag> [--rounds N] [--with-doku]", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TASK.md anlegen
|
||||||
|
await writeTaskMd(pi, ctx, task);
|
||||||
|
|
||||||
|
ctx.ui.setStatus("optimize", `Starte Optimierung (max ${maxRounds} Runden)…`);
|
||||||
|
|
||||||
|
// Phase 1: Initiale Implementierung
|
||||||
|
ctx.ui.setStatus("optimize", "Phase 1: Coder implementiert…");
|
||||||
|
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
||||||
|
await sendAndWait(pi, ctx, coderKickoff(task));
|
||||||
|
await tickTaskMdStatus(pi, ctx, "Implementierung");
|
||||||
|
|
||||||
|
let lastBlockers = "";
|
||||||
|
let verdict = "";
|
||||||
|
|
||||||
|
// Schleife: Judge → (PASS? fertig : Fix → nächste Runde)
|
||||||
|
for (let round = 1; round <= maxRounds; round++) {
|
||||||
|
ctx.ui.setStatus("optimize", `Runde ${round}/${maxRounds}: Judge prüft…`);
|
||||||
|
await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge");
|
||||||
|
await sendAndWait(pi, ctx, judgePrompt(""));
|
||||||
|
|
||||||
|
const judgeText = getLastAssistantText(ctx);
|
||||||
|
verdict = parseVerdict(judgeText);
|
||||||
|
|
||||||
|
if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") {
|
||||||
|
await tickTaskMdStatus(pi, ctx, "Review bestanden (PASS)");
|
||||||
|
ctx.ui.setStatus("optimize", `✓ ${verdict} nach Runde ${round}`);
|
||||||
|
ctx.ui.notify(`Optimierung abgeschlossen: ${verdict} nach ${round} Runde(n)`, "info");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen
|
||||||
|
const currentBlockers = parseBlockers(judgeText);
|
||||||
|
if (currentBlockers && currentBlockers === lastBlockers) {
|
||||||
|
ctx.ui.setStatus("optimize", "⚠ Schleife: gleicher Blocker – manuelle Intervention nötig");
|
||||||
|
ctx.ui.notify(
|
||||||
|
"Derselbe Blocker tritt erneut auf – Schleife abgebrochen. Bitte manuell prüfen.",
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastBlockers = currentBlockers;
|
||||||
|
|
||||||
|
if (round === maxRounds) {
|
||||||
|
ctx.ui.setStatus("optimize", `⚠ Max. ${maxRounds} Runden ohne PASS`);
|
||||||
|
ctx.ui.notify(`${maxRounds} Runden durchlaufen ohne PASS. Bitte manuell prüfen.`, "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix-Phase
|
||||||
|
ctx.ui.setStatus("optimize", `Runde ${round}/${maxRounds}: Coder fixt…`);
|
||||||
|
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
||||||
|
await sendAndWait(pi, ctx, fixPrompt(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finale ShipIt-Prüfung nur bei PASS
|
||||||
|
if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") {
|
||||||
|
ctx.ui.setStatus("optimize", "Finale ShipIt-Prüfung…");
|
||||||
|
await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge");
|
||||||
|
await sendAndWait(pi, ctx, shipitPrompt(""));
|
||||||
|
|
||||||
|
const shipText = getLastAssistantText(ctx);
|
||||||
|
const shipVerdict = shipText.match(/Urteil:\s*(SHIP|NO-SHIP)/i)?.[1]?.toUpperCase() ?? "";
|
||||||
|
|
||||||
|
if (shipVerdict === "SHIP") {
|
||||||
|
ctx.ui.setStatus("optimize", "🚀 SHIP – produktionsreif");
|
||||||
|
if (withDoku) {
|
||||||
|
await runUpdateDoku(pi, ctx);
|
||||||
|
} else {
|
||||||
|
ctx.ui.notify("Nächster Schritt: /update_doku für Code-Kommentare, README.md und BEDIENUNGSANLEITUNG.md", "info");
|
||||||
|
}
|
||||||
|
} else if (shipVerdict === "NO-SHIP") {
|
||||||
|
ctx.ui.setStatus("optimize", "⛔ NO-SHIP – noch nicht bereit");
|
||||||
|
} else {
|
||||||
|
ctx.ui.setStatus("optimize", "ShipIt abgeschlossen");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Schlanke Kommandos für kleine Änderungen ─────────────────────────────
|
||||||
|
|
||||||
|
pi.registerCommand("patch", {
|
||||||
|
description: "Gezielte Minimaländerung ohne vollständigen Review → qwen3.5-coder (:8001).",
|
||||||
|
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||||
|
const change = (args || "").trim();
|
||||||
|
if (!change) {
|
||||||
|
ctx.ui.notify("Benutzung: /patch <beschreibung der änderung>", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
||||||
|
pi.sendUserMessage(patchPrompt(change));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pi.registerCommand("quick_check", {
|
||||||
|
description: "Schnelle Prüfung der letzten Änderung (OK/PROBLEM) → qwen3.5-judge (:8002).",
|
||||||
|
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||||
|
await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge");
|
||||||
|
pi.sendUserMessage(quickCheckPrompt(args || ""));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Dokumentations-Phase ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pi.registerCommand("update_doku", {
|
||||||
|
description: "Code kommentieren + README.md + BEDIENUNGSANLEITUNG.md + git commit → qwen3.5-coder (:8001).",
|
||||||
|
handler: async function (_args: string, ctx: ExtensionCommandContext) {
|
||||||
|
await runUpdateDoku(pi, ctx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Robustes Editieren via GNU patch ─────────────────────────────────────
|
||||||
|
|
||||||
|
pi.registerTool({
|
||||||
|
name: "apply_patch",
|
||||||
|
label: "Patch anwenden",
|
||||||
|
description: [
|
||||||
|
"Wendet einen unified diff (git-Format) auf Dateien an.",
|
||||||
|
"Zuverlässiger als das edit-Tool bei mehrfachen Änderungen an derselben Datei.",
|
||||||
|
"Format: --- a/pfad/datei +++ b/pfad/datei @@ -n,m +n,m @@ ...",
|
||||||
|
"Verwende dieses Tool wenn du mehrere Stellen in einer Datei änderst."
|
||||||
|
].join(" "),
|
||||||
|
parameters: Type.Object({
|
||||||
|
patch: Type.String({
|
||||||
|
description: "Unified diff im git-Format mit --- a/... und +++ b/... Headern."
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
||||||
|
const tmpFile = `/tmp/pi_patch_${Date.now()}.diff`;
|
||||||
|
await pi.exec(
|
||||||
|
"bash",
|
||||||
|
["-c", `printf "%s" "$1" > "${tmpFile}"`, "_", params.patch],
|
||||||
|
{ cwd: ctx.cwd }
|
||||||
|
);
|
||||||
|
// -p1 entfernt führende a/ b/ Präfixe (git-Standard)
|
||||||
|
const result = await pi.exec(
|
||||||
|
"bash",
|
||||||
|
["-c", `patch -p1 < "${tmpFile}"; rm -f "${tmpFile}"`],
|
||||||
|
{ cwd: ctx.cwd }
|
||||||
|
);
|
||||||
|
if (result.code !== 0) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Patch fehlgeschlagen:\n${result.stderr}\n${result.stdout}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { content: [{ type: "text", text: result.stdout || "Patch erfolgreich angewendet." }] };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Projekt-Scaffolding ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pi.registerCommand("new_project", {
|
||||||
|
description: "Legt Projektverzeichnis an + git init + .gitignore. /new_project <pfad>",
|
||||||
|
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||||
|
const rawPath = (args || "").trim();
|
||||||
|
if (!rawPath) {
|
||||||
|
ctx.ui.notify("Benutzung: /new_project <pfad>", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ~ expandieren
|
||||||
|
const projectPath = rawPath.startsWith("~/")
|
||||||
|
? rawPath.replace("~/", (process.env.HOME || "") + "/")
|
||||||
|
: rawPath;
|
||||||
|
|
||||||
|
// Verzeichnis anlegen
|
||||||
|
const mkResult = await pi.exec("bash", ["-c", 'mkdir -p "$1"', "_", projectPath], { cwd: ctx.cwd });
|
||||||
|
if (mkResult.code !== 0) {
|
||||||
|
ctx.ui.notify(`Fehler: ${mkResult.stderr}`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// git init (nur wenn noch kein Repo vorhanden)
|
||||||
|
const gitCheck = await pi.exec("bash", ["-c", "test -d .git && echo exists"], { cwd: projectPath });
|
||||||
|
if (gitCheck.stdout.trim() !== "exists") {
|
||||||
|
await pi.exec("bash", ["-c", "git init"], { cwd: projectPath });
|
||||||
|
}
|
||||||
|
|
||||||
|
// .gitignore anlegen
|
||||||
|
const gitignore = "target/\n*.o\n*.d\n*.swp\n.env\n.DS_Store\n";
|
||||||
|
await pi.exec("bash", ["-c", 'printf "%s" "$1" > .gitignore', "_", gitignore], { cwd: projectPath });
|
||||||
|
await pi.exec(
|
||||||
|
"bash",
|
||||||
|
["-c", "git add .gitignore && git commit -m 'chore: init project' || true"],
|
||||||
|
{ cwd: projectPath }
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.ui.notify(`Projekt angelegt: ${projectPath}`, "info");
|
||||||
|
ctx.ui.notify(
|
||||||
|
`⚠ Pi läuft noch in: ${ctx.cwd} — Session-Verzeichnis kann nicht gewechselt werden.\n` +
|
||||||
|
`Neues Projekt starten: cd ${projectPath} && pi`,
|
||||||
|
"warning"
|
||||||
|
);
|
||||||
|
ctx.ui.setStatus("new_project", `Neues Projekt → cd ${projectPath} && pi`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
90
start-coder.sh
Executable file
90
start-coder.sh
Executable file
|
|
@ -0,0 +1,90 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HF_HOME="${HF_HOME:-/home/dschlueter/nvme2n1p7_home/huggingface}"
|
||||||
|
MODEL_REL_PATH="models/qwen3/Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS.gguf"
|
||||||
|
IMAGE="ghcr.io/ggml-org/llama.cpp:server-cuda"
|
||||||
|
CONTAINER_NAME="qwen36-27b-coder"
|
||||||
|
HOST_PORT=8001
|
||||||
|
CONTAINER_PORT=8000
|
||||||
|
MODEL_ALIAS="qwen3.5-coder"
|
||||||
|
|
||||||
|
echo "[*] Verwende HF_HOME = $HF_HOME"
|
||||||
|
if [ ! -f "$HF_HOME/$MODEL_REL_PATH" ]; then
|
||||||
|
echo "[!] Modell-Datei nicht gefunden: $HF_HOME/$MODEL_REL_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}\$"; then
|
||||||
|
echo "[*] Stoppe existierenden Container $CONTAINER_NAME ..."
|
||||||
|
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[*] Starte llama.cpp-Server für Coder ..."
|
||||||
|
docker run -d \
|
||||||
|
--gpus '"device=1,2"' \
|
||||||
|
--name "$CONTAINER_NAME" \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-e HF_HOME="/hf_home" \
|
||||||
|
-v "$HF_HOME:/hf_home:ro" \
|
||||||
|
-p "${HOST_PORT}:${CONTAINER_PORT}" \
|
||||||
|
"$IMAGE" \
|
||||||
|
-m "/hf_home/${MODEL_REL_PATH}" \
|
||||||
|
--alias "${MODEL_ALIAS}" \
|
||||||
|
-c 131072 \
|
||||||
|
-n 16384 \
|
||||||
|
--jinja \
|
||||||
|
--no-context-shift \
|
||||||
|
--temp 0.2 \
|
||||||
|
--top-p 0.95 \
|
||||||
|
--top-k 40 \
|
||||||
|
--min-p 0.01 \
|
||||||
|
--repeat-penalty 1.05 \
|
||||||
|
--main-gpu 0 \
|
||||||
|
--tensor-split 0.5,0.5 \
|
||||||
|
-ngl 999 \
|
||||||
|
-fa on \
|
||||||
|
--kv-unified \
|
||||||
|
--cache-type-k q8_0 \
|
||||||
|
--cache-type-v q8_0 \
|
||||||
|
--batch-size 1024 \
|
||||||
|
--ubatch-size 512 \
|
||||||
|
--parallel 2 \
|
||||||
|
--cont-batching \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port "$CONTAINER_PORT"
|
||||||
|
|
||||||
|
echo "[*] Warte auf HTTP ..."
|
||||||
|
HTTP_READY=0
|
||||||
|
for i in {1..90}; do
|
||||||
|
if curl -s "http://localhost:${HOST_PORT}/health" >/dev/null 2>&1 || \
|
||||||
|
curl -s "http://localhost:${HOST_PORT}/v1/models" >/dev/null 2>&1; then
|
||||||
|
HTTP_READY=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$HTTP_READY" -ne 1 ]; then
|
||||||
|
echo "[!] HTTP-Server wurde nicht rechtzeitig erreichbar." >&2
|
||||||
|
docker logs --tail 200 "$CONTAINER_NAME" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[*] Teste Chat-Completion ..."
|
||||||
|
curl -s -X POST "http://localhost:${HOST_PORT}/v1/chat/completions" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"model\": \"${MODEL_ALIAS}\",
|
||||||
|
\"messages\": [
|
||||||
|
{ \"role\": \"system\", \"content\": \"Du bist ein präziser Coding-Assistent.\" },
|
||||||
|
{ \"role\": \"user\", \"content\": \"Antworte nur mit dem Wort: bereit\" }
|
||||||
|
],
|
||||||
|
\"max_tokens\": 8,
|
||||||
|
\"temperature\": 0.0,
|
||||||
|
\"stream\": false
|
||||||
|
}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "[*] Server bereit auf http://0.0.0.0:${HOST_PORT}"
|
||||||
|
echo "[*] Stoppen mit: docker rm -f ${CONTAINER_NAME}"
|
||||||
90
start-judge.sh
Executable file
90
start-judge.sh
Executable file
|
|
@ -0,0 +1,90 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HF_HOME="${HF_HOME:-/home/dschlueter/nvme2n1p7_home/huggingface}"
|
||||||
|
MODEL_REL_PATH="models/qwen3/Qwen3.6-27B-Uncensored-HauhauCS-Aggressive-IQ4_XS.gguf"
|
||||||
|
IMAGE="ghcr.io/ggml-org/llama.cpp:server-cuda"
|
||||||
|
CONTAINER_NAME="qwen36-27b-judge"
|
||||||
|
HOST_PORT=8002
|
||||||
|
CONTAINER_PORT=8000
|
||||||
|
MODEL_ALIAS="qwen3.5-judge"
|
||||||
|
|
||||||
|
echo "[*] Verwende HF_HOME = $HF_HOME"
|
||||||
|
if [ ! -f "$HF_HOME/$MODEL_REL_PATH" ]; then
|
||||||
|
echo "[!] Modell-Datei nicht gefunden: $HF_HOME/$MODEL_REL_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}\$"; then
|
||||||
|
echo "[*] Stoppe existierenden Container $CONTAINER_NAME ..."
|
||||||
|
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[*] Starte llama.cpp-Server für Judge ..."
|
||||||
|
docker run -d \
|
||||||
|
--gpus '"device=1,2"' \
|
||||||
|
--name "$CONTAINER_NAME" \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-e HF_HOME="/hf_home" \
|
||||||
|
-v "$HF_HOME:/hf_home:ro" \
|
||||||
|
-p "${HOST_PORT}:${CONTAINER_PORT}" \
|
||||||
|
"$IMAGE" \
|
||||||
|
-m "/hf_home/${MODEL_REL_PATH}" \
|
||||||
|
--alias "${MODEL_ALIAS}" \
|
||||||
|
-c 65536 \
|
||||||
|
-n 8192 \
|
||||||
|
--jinja \
|
||||||
|
--no-context-shift \
|
||||||
|
--temp 0.1 \
|
||||||
|
--top-p 0.9 \
|
||||||
|
--top-k 40 \
|
||||||
|
--min-p 0.01 \
|
||||||
|
--repeat-penalty 1.05 \
|
||||||
|
--main-gpu 0 \
|
||||||
|
--tensor-split 0.5,0.5 \
|
||||||
|
-ngl 999 \
|
||||||
|
-fa on \
|
||||||
|
--kv-unified \
|
||||||
|
--cache-type-k q8_0 \
|
||||||
|
--cache-type-v q8_0 \
|
||||||
|
--batch-size 512 \
|
||||||
|
--ubatch-size 256 \
|
||||||
|
--parallel 1 \
|
||||||
|
--cont-batching \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port "$CONTAINER_PORT"
|
||||||
|
|
||||||
|
echo "[*] Warte auf HTTP ..."
|
||||||
|
HTTP_READY=0
|
||||||
|
for i in {1..90}; do
|
||||||
|
if curl -s "http://localhost:${HOST_PORT}/health" >/dev/null 2>&1 || \
|
||||||
|
curl -s "http://localhost:${HOST_PORT}/v1/models" >/dev/null 2>&1; then
|
||||||
|
HTTP_READY=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$HTTP_READY" -ne 1 ]; then
|
||||||
|
echo "[!] HTTP-Server wurde nicht rechtzeitig erreichbar." >&2
|
||||||
|
docker logs --tail 200 "$CONTAINER_NAME" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[*] Teste Judge-Endpoint ..."
|
||||||
|
curl -s -X POST "http://localhost:${HOST_PORT}/v1/chat/completions" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"model\": \"${MODEL_ALIAS}\",
|
||||||
|
\"messages\": [
|
||||||
|
{ \"role\": \"system\", \"content\": \"Du bist ein strenger Code-Reviewer.\" },
|
||||||
|
{ \"role\": \"user\", \"content\": \"Antworte nur mit dem Wort: bereit\" }
|
||||||
|
],
|
||||||
|
\"max_tokens\": 8,
|
||||||
|
\"temperature\": 0.0,
|
||||||
|
\"stream\": false
|
||||||
|
}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "[*] Server bereit auf http://0.0.0.0:${HOST_PORT}"
|
||||||
|
echo "[*] Stoppen mit: docker rm -f ${CONTAINER_NAME}"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue