feat: automatische SemVer-Versionierung nach SHIP + /version-Command
Neuer /version-Command und automatischer Trigger nach SHIP-Verdikt in /optimize: - getCurrentVersion() liest höchsten vX.Y.Z-Tag (git tag -l | sort -V) - analyzeBumpType() klassifiziert Commits (feat! → major, feat: → minor, fix: → patch) - detectVersionFile() findet package.json / Cargo.toml / pyproject.toml / VERSION - applyVersionBump() schreibt Version in Manifest + chore-Commit - runVersionBump() zeigt ctx.ui.select()-Dialog mit empfohlenem Bump-Typ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a6f7f968b5
commit
e13e9382ff
4 changed files with 568 additions and 251 deletions
|
|
@ -378,14 +378,15 @@ async function switchModel(
|
|||
ctx: ExtensionCommandContext,
|
||||
provider: string,
|
||||
modelId: string
|
||||
): Promise<void> {
|
||||
): Promise<boolean> {
|
||||
const model = ctx.modelRegistry.find(provider, modelId);
|
||||
if (!model) {
|
||||
ctx.ui.notify(`Modell ${provider}/${modelId} nicht gefunden`, "error");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
const ok = await pi.setModel(model);
|
||||
if (!ok) ctx.ui.notify(`Kein API-Key für ${modelId}`, "warning");
|
||||
return ok !== false;
|
||||
}
|
||||
|
||||
// Sendet eine Nachricht und wartet bis der Agent fertig ist.
|
||||
|
|
@ -412,6 +413,47 @@ async function sendAndWait(
|
|||
await ctx.waitForIdle();
|
||||
}
|
||||
|
||||
// Prüft via POST /v1/chat/completions ob das Modell im VRAM bereit ist.
|
||||
// /health und /v1/models antworten bereits während des GPU-Ladevorgangs — nur
|
||||
// ein echter Completion-Request liefert zuverlässig HTTP 200 wenn das Modell ready ist.
|
||||
async function waitUntilModelReady(
|
||||
pi: ExtensionAPI,
|
||||
ctx: ExtensionCommandContext,
|
||||
port: number,
|
||||
modelAlias: string,
|
||||
maxWaitMs = 180_000
|
||||
): Promise<boolean> {
|
||||
const deadline = Date.now() + maxWaitMs;
|
||||
const body = JSON.stringify({
|
||||
model: modelAlias,
|
||||
messages: [{ role: "user", content: "ping" }],
|
||||
max_tokens: 1, temperature: 0.0, stream: false,
|
||||
});
|
||||
// Body als Datei — verhindert Shell-Injection wenn modelAlias Sonderzeichen enthält
|
||||
const tmpBody = `/tmp/pi_ready_${Date.now()}_${Math.random().toString(36).slice(2)}.json`;
|
||||
await pi.exec("bash", ["-c", `printf "%s" "$1" > "${tmpBody}"`, "_", body], { cwd: ctx.cwd });
|
||||
let notified = false;
|
||||
try {
|
||||
while (Date.now() < deadline) {
|
||||
const r = await pi.exec("bash", ["-c",
|
||||
`curl -s -o /dev/null -w "%{http_code}" --max-time 5 ` +
|
||||
`-X POST http://localhost:${port}/v1/chat/completions ` +
|
||||
`-H "Content-Type: application/json" ` +
|
||||
`-d "@${tmpBody}"`
|
||||
], { cwd: ctx.cwd });
|
||||
if (r.stdout?.trim() === "200") return true;
|
||||
if (!notified) {
|
||||
ctx.ui.notify(`Modell-Server (Port ${port}) lädt noch — warte bis zu 3 min…`, "info");
|
||||
notified = true;
|
||||
}
|
||||
await new Promise(res => setTimeout(res, 3000));
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
await pi.exec("bash", ["-c", `rm -f "${tmpBody}"`], { cwd: ctx.cwd });
|
||||
}
|
||||
}
|
||||
|
||||
// Führt einen Shell-Befehl aus und gibt stdout+stderr zurück (max. 6000 Zeichen).
|
||||
// Erkennt Test-Suiten im Projektverzeichnis anhand von Framework-Markern.
|
||||
// Alle Checks laufen parallel — konservativ, keine False Positives.
|
||||
|
|
@ -427,8 +469,8 @@ async function detectTestCommands(
|
|||
], { cwd: ctx.cwd }),
|
||||
pi.exec("bash", ["-c",
|
||||
"test -f package.json && " +
|
||||
"node -e \"const p=require('./package.json');process.exit(" +
|
||||
"p.scripts&&p.scripts.test&&!p.scripts.test.includes('no test')?0:1)\" 2>/dev/null"
|
||||
"grep -q '\"test\"' package.json && " +
|
||||
"! grep -q 'no test' package.json"
|
||||
], { cwd: ctx.cwd }),
|
||||
pi.exec("bash", ["-c", "test -f Cargo.toml"], { cwd: ctx.cwd }),
|
||||
pi.exec("bash", ["-c",
|
||||
|
|
@ -495,14 +537,18 @@ function getLastAssistantText(ctx: ExtensionCommandContext): string {
|
|||
}
|
||||
|
||||
// Extrahiert das Urteil aus einer Judge-Antwort.
|
||||
// "UNREADABLE" wenn kein Urteil erkennbar — unterscheidbar von einem expliziten FAIL.
|
||||
function parseVerdict(text: string): string {
|
||||
const m = text.match(/Urteil:\s*(PASS WITH CONCERNS|PASS|FAIL)/i);
|
||||
return m ? m[1].toUpperCase() : "";
|
||||
return m ? m[1].toUpperCase() : "UNREADABLE";
|
||||
}
|
||||
|
||||
// Extrahiert den Blocker-Abschnitt für die Loop-Erkennung.
|
||||
// Erkennt Bullet-Listen (- / – / *), Bold (**Blocker**) und Headings (## Blocker).
|
||||
function parseBlockers(text: string): string {
|
||||
const m = text.match(/[-–*]\s*Blocker[:\n]([\s\S]*?)(?:\n[-–*]\s*Major|\n[-–*]\s*Minor|$)/i);
|
||||
const m = text.match(
|
||||
/(?:\*\*Blocker\*\*|##\s*Blocker|[-–*]\s*Blocker)[:\n]([\s\S]*?)(?:\n(?:\*\*Major\*\*|##\s*Major|[-–*]\s*Major)|\n(?:\*\*Minor\*\*|##\s*Minor|[-–*]\s*Minor)|$)/i
|
||||
);
|
||||
return m ? m[1].trim() : "";
|
||||
}
|
||||
|
||||
|
|
@ -524,6 +570,9 @@ async function getFilesSinceTag(
|
|||
{ cwd: ctx.cwd }
|
||||
);
|
||||
|
||||
// Bei git-Fehler alles verarbeiten (sicherer als stilles Überspringen)
|
||||
if (diff.code !== 0) return null;
|
||||
|
||||
return diff.stdout.trim()
|
||||
.split("\n")
|
||||
.filter(f =>
|
||||
|
|
@ -539,48 +588,75 @@ async function getFilesSinceTag(
|
|||
// 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");
|
||||
if (!await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder")) {
|
||||
ctx.ui.notify("Coder-Modell nicht verfügbar — Dokumentations-Phase abgebrochen", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Jede Phase läuft unabhängig — Fehler in Phase 1 blockieren nicht Phase 2/3.
|
||||
// Tag wird nur NACH erfolgreichem sendAndWait gesetzt.
|
||||
|
||||
// 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));
|
||||
try {
|
||||
const commentFiles = await getFilesSinceTag(pi, ctx, "docs-last-commented");
|
||||
if (commentFiles === null) {
|
||||
ctx.ui.setStatus("update_doku", "1/3: Code wird kommentiert (alle Dateien)…");
|
||||
currentActivity = "Coder kommentiert Code…";
|
||||
await sendAndWait(pi, ctx, commentCodePrompt());
|
||||
await pi.exec("bash", ["-c", "git tag -f docs-last-commented"], { cwd: ctx.cwd });
|
||||
} 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))…`);
|
||||
currentActivity = "Coder kommentiert Code…";
|
||||
await sendAndWait(pi, ctx, commentCodePromptIncremental(commentFiles));
|
||||
await pi.exec("bash", ["-c", "git tag -f docs-last-commented"], { cwd: ctx.cwd });
|
||||
}
|
||||
} catch (e: any) {
|
||||
ctx.ui.notify(`1/3 Code-Kommentare fehlgeschlagen: ${String(e?.message ?? e)}`, "error");
|
||||
}
|
||||
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));
|
||||
try {
|
||||
const readmeFiles = await getFilesSinceTag(pi, ctx, "docs-last-readme");
|
||||
if (readmeFiles === null) {
|
||||
ctx.ui.setStatus("update_doku", "2/3: README.md wird geschrieben…");
|
||||
currentActivity = "Coder schreibt README…";
|
||||
await sendAndWait(pi, ctx, readmeMdPrompt());
|
||||
await pi.exec("bash", ["-c", "git tag -f docs-last-readme"], { cwd: ctx.cwd });
|
||||
} 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)…`);
|
||||
currentActivity = "Coder schreibt README…";
|
||||
await sendAndWait(pi, ctx, readmeMdPromptIncremental(readmeFiles));
|
||||
await pi.exec("bash", ["-c", "git tag -f docs-last-readme"], { cwd: ctx.cwd });
|
||||
}
|
||||
} catch (e: any) {
|
||||
ctx.ui.notify(`2/3 README.md fehlgeschlagen: ${String(e?.message ?? e)}`, "error");
|
||||
}
|
||||
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));
|
||||
try {
|
||||
const bedFiles = await getFilesSinceTag(pi, ctx, "docs-last-bedienungsanleitung");
|
||||
if (bedFiles === null) {
|
||||
ctx.ui.setStatus("update_doku", "3/3: BEDIENUNGSANLEITUNG.md wird geschrieben…");
|
||||
currentActivity = "Coder schreibt Bedienungsanleitung…";
|
||||
await sendAndWait(pi, ctx, bedienungsanleitungPrompt());
|
||||
await pi.exec("bash", ["-c", "git tag -f docs-last-bedienungsanleitung"], { cwd: ctx.cwd });
|
||||
} 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)…`);
|
||||
currentActivity = "Coder schreibt Bedienungsanleitung…";
|
||||
await sendAndWait(pi, ctx, bedienungsanleitungPromptIncremental(bedFiles));
|
||||
await pi.exec("bash", ["-c", "git tag -f docs-last-bedienungsanleitung"], { cwd: ctx.cwd });
|
||||
}
|
||||
} catch (e: any) {
|
||||
ctx.ui.notify(`3/3 BEDIENUNGSANLEITUNG.md fehlgeschlagen: ${String(e?.message ?? e)}`, "error");
|
||||
}
|
||||
await pi.exec("bash", ["-c", "git tag -f docs-last-bedienungsanleitung"], { cwd: ctx.cwd });
|
||||
|
||||
// Abschließender Dokumentations-Commit
|
||||
// Abschließender Dokumentations-Commit (immer, auch bei Teilfehlern)
|
||||
await pi.exec(
|
||||
"bash",
|
||||
["-c", "git add -A && git commit -m 'docs: update comments, README, BEDIENUNGSANLEITUNG' || true"],
|
||||
|
|
@ -594,6 +670,120 @@ async function runUpdateDoku(pi: ExtensionAPI, ctx: ExtensionCommandContext): Pr
|
|||
ctx.ui.notify("Dokumentations-Phase abgeschlossen. Commit angelegt.", "info");
|
||||
}
|
||||
|
||||
// ── Versions-Verwaltung (SemVer + Git-Tags) ──────────────────────────────────
|
||||
|
||||
// Liest den höchsten vX.Y.Z-Tag via `git tag -l`. Gibt null zurück wenn kein Tag existiert.
|
||||
async function getCurrentVersion(
|
||||
pi: ExtensionAPI,
|
||||
ctx: ExtensionCommandContext
|
||||
): Promise<[number, number, number] | null> {
|
||||
const res = await pi.exec("bash", ["-c", "git tag -l 'v*' | sort -V | tail -1"], { cwd: ctx.cwd });
|
||||
const raw = (res.stdout ?? "").trim();
|
||||
const m = raw.match(/^v?(\d+)\.(\d+)\.(\d+)$/);
|
||||
return m ? [+m[1], +m[2], +m[3]] : null;
|
||||
}
|
||||
|
||||
// Analysiert Commit-Subjects seit dem letzten Tag nach Conventional Commits.
|
||||
// feat! / BREAKING CHANGE → major, feat: → minor, alles andere → patch.
|
||||
async function analyzeBumpType(
|
||||
pi: ExtensionAPI,
|
||||
ctx: ExtensionCommandContext,
|
||||
since?: string
|
||||
): Promise<"major" | "minor" | "patch"> {
|
||||
const range = since ? `${since}..HEAD` : "HEAD";
|
||||
const res = await pi.exec("bash", ["-c", `git log ${range} --format="%s" 2>/dev/null`], { cwd: ctx.cwd });
|
||||
const lines = (res.stdout ?? "").split("\n");
|
||||
if (lines.some(l => /^feat!:|BREAKING CHANGE/.test(l))) return "major";
|
||||
if (lines.some(l => /^feat(\(.+\))?:/.test(l))) return "minor";
|
||||
return "patch";
|
||||
}
|
||||
|
||||
// Findet die erste vorhandene Versions-Manifest-Datei im Arbeitsverzeichnis.
|
||||
async function detectVersionFile(
|
||||
pi: ExtensionAPI,
|
||||
ctx: ExtensionCommandContext
|
||||
): Promise<"package.json" | "Cargo.toml" | "pyproject.toml" | "VERSION" | null> {
|
||||
for (const f of ["package.json", "Cargo.toml", "pyproject.toml"]) {
|
||||
const r = await pi.exec("bash", ["-c", `test -f ${f}`], { cwd: ctx.cwd });
|
||||
if (r.exitCode === 0) return f as "package.json" | "Cargo.toml" | "pyproject.toml";
|
||||
}
|
||||
const r = await pi.exec("bash", ["-c", "test -f VERSION"], { cwd: ctx.cwd });
|
||||
return r.exitCode === 0 ? "VERSION" : null;
|
||||
}
|
||||
|
||||
// Schreibt die neue Version in die Manifest-Datei und erstellt einen chore-Commit.
|
||||
async function applyVersionBump(
|
||||
pi: ExtensionAPI,
|
||||
ctx: ExtensionCommandContext,
|
||||
manifest: string,
|
||||
version: string
|
||||
): Promise<void> {
|
||||
let cmd: string;
|
||||
if (manifest === "package.json") {
|
||||
cmd = `npm version --no-git-tag-version ${version}`;
|
||||
} else if (manifest === "Cargo.toml") {
|
||||
cmd = `sed -i 's/^version = ".*"/version = "${version}"/' Cargo.toml`;
|
||||
} else if (manifest === "pyproject.toml") {
|
||||
cmd = `sed -i 's/^version = ".*"/version = "${version}"/' pyproject.toml`;
|
||||
} else {
|
||||
cmd = `printf 'v%s\\n' '${version}' > VERSION`;
|
||||
}
|
||||
await pi.exec("bash", ["-c", cmd], { cwd: ctx.cwd });
|
||||
await pi.exec(
|
||||
"bash",
|
||||
["-c", `git add ${manifest} && git commit -m "chore: bump version to v${version}"`],
|
||||
{ cwd: ctx.cwd }
|
||||
);
|
||||
}
|
||||
|
||||
// Hauptfunktion: ermittelt aktuelle Version, analysiert Commits, zeigt Dialog, setzt Tag.
|
||||
async function runVersionBump(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promise<void> {
|
||||
// Early exit wenn kein git-Repo vorhanden
|
||||
const gitCheck = await pi.exec("bash", ["-c", "git rev-parse --is-inside-work-tree 2>/dev/null"], { cwd: ctx.cwd });
|
||||
if (gitCheck.exitCode !== 0) return;
|
||||
|
||||
const current = await getCurrentVersion(pi, ctx);
|
||||
const tag = current ? `v${current[0]}.${current[1]}.${current[2]}` : undefined;
|
||||
const bump = await analyzeBumpType(pi, ctx, tag);
|
||||
|
||||
const [maj, min, pat] = current ?? [0, 0, 0];
|
||||
const initial = !current;
|
||||
const versions: Record<"patch" | "minor" | "major", string> = initial
|
||||
? { patch: "v0.0.1", minor: "v0.1.0", major: "v1.0.0" }
|
||||
: { patch: `v${maj}.${min}.${pat + 1}`, minor: `v${maj}.${min + 1}.0`, major: `v${maj + 1}.0.0` };
|
||||
|
||||
const recommended: "patch" | "minor" | "major" = initial ? "minor" : bump;
|
||||
const labels = (["patch", "minor", "major"] as const).map(
|
||||
t => `${t} → ${versions[t]}${t === recommended ? " (empfohlen)" : ""}`
|
||||
);
|
||||
|
||||
const choice = await ctx.ui.select({
|
||||
title: "Version",
|
||||
message: current
|
||||
? `Aktuelle Version: ${tag}. Commits seit letztem Tag: ${bump}-Bump erkannt.`
|
||||
: "Noch kein Versions-Tag vorhanden.",
|
||||
options: [...labels, "Überspringen"],
|
||||
});
|
||||
|
||||
if (!choice || choice.startsWith("Überspringen")) return;
|
||||
|
||||
const chosen = (["patch", "minor", "major"] as const).find(t => choice.startsWith(t))!;
|
||||
const newVersion = versions[chosen].replace(/^v/, "");
|
||||
const newTag = `v${newVersion}`;
|
||||
|
||||
const manifest = await detectVersionFile(pi, ctx);
|
||||
if (manifest) {
|
||||
await applyVersionBump(pi, ctx, manifest, newVersion);
|
||||
}
|
||||
|
||||
const tagResult = await pi.exec("bash", ["-c", `git tag ${newTag}`], { cwd: ctx.cwd });
|
||||
if (tagResult.exitCode !== 0) {
|
||||
ctx.ui.notify(`Tag ${newTag} existiert bereits — manuell löschen mit: git tag -d ${newTag}`, "error");
|
||||
return;
|
||||
}
|
||||
ctx.ui.notify(`Version ${newTag} getaggt.`, "info");
|
||||
}
|
||||
|
||||
// Prominente Abschluss-Notification + Widget-Update mit Uhrzeit und Ergebnis.
|
||||
function finalNotify(
|
||||
ctx: ExtensionCommandContext,
|
||||
|
|
@ -601,41 +791,88 @@ function finalNotify(
|
|||
detail: string
|
||||
): void {
|
||||
const timestamp = new Date().toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
|
||||
const level = verdict.includes("SHIP") && !verdict.includes("NO-SHIP") ? "warning"
|
||||
: verdict.includes("NO-SHIP") ? "error"
|
||||
const level = verdict.startsWith("🚀") ? "info"
|
||||
: verdict.includes("NO-SHIP") || verdict.startsWith("⛔") ? "error"
|
||||
: verdict.includes("⚠") ? "warning"
|
||||
: "info";
|
||||
ctx.ui.notify(`${verdict}: ${detail}`, level);
|
||||
ctx.ui.setWidget("coder-judge", [
|
||||
`Letzter Lauf: ${verdict} — ${detail} (${timestamp})`,
|
||||
"─────────────────────────────────────────",
|
||||
"Workflow: /coder <auftrag> | /judge | /fix | /shipit",
|
||||
"Auto-Loop: /optimize <auftrag> [--rounds N] [--with-doku] [--continue] [--test-cmd \"cmd\"]",
|
||||
"Planung: /plan <auftrag> → /coder | /optimize --continue | /discard",
|
||||
"Patch: /patch <änderung> → /quick_check [was]",
|
||||
"Doku: /update_doku | Neues Projekt: /new_project <pfad>",
|
||||
"Abbruch: Escape (Generation laufend) | /cancel (Loop nach aktuellem Schritt)",
|
||||
"Resume: /continue | Modell: auto (Coder→:8001, Judge→:8002)",
|
||||
"/optimize <auftrag> [--rounds N] [--with-doku] [--continue] [--test-cmd \"cmd\"]",
|
||||
"/fix · /judge · /shipit · /cancel · /continue · /help",
|
||||
]);
|
||||
}
|
||||
|
||||
// ── Extension ────────────────────────────────────────────────────────────────
|
||||
|
||||
let cancelRequested = false;
|
||||
let currentActivity = ""; // Working-Message für den aktuellen Command-Kontext
|
||||
|
||||
// Erzeugt eine knappe Statuszeile aus Tool-Name und Argumenten.
|
||||
function toolExecutionLabel(toolName: string, args: Record<string, any>): string {
|
||||
switch (toolName) {
|
||||
case "edit":
|
||||
return `Editiere ${args.path ?? "Datei"}…`;
|
||||
case "write":
|
||||
return `Schreibe ${args.path ?? "Datei"} neu…`;
|
||||
case "read":
|
||||
return `Lese ${args.path ?? "Datei"}…`;
|
||||
case "grep":
|
||||
return `Suche in ${args.path ?? args.pattern ?? "Dateien"}…`;
|
||||
case "find":
|
||||
return `Suche Dateien: ${args.pattern ?? ""}…`;
|
||||
case "ls":
|
||||
return `Verzeichnis: ${args.path ?? "."}…`;
|
||||
case "bash": {
|
||||
const cmd = String(args.command ?? "").trim().replace(/\n[\s\S]*/s, "");
|
||||
if (/git\s+commit/.test(cmd)) return "Git-Commit…";
|
||||
if (/git\s+add/.test(cmd)) return "Stage Änderungen…";
|
||||
if (/git\s+tag/.test(cmd)) return "Git-Tag setzen…";
|
||||
if (/pytest|npm test|cargo test|go test|make test/.test(cmd)) return "Tests laufen…";
|
||||
if (/git\s+(diff|log|show|tag -l)/.test(cmd)) return "Git-History lesen…";
|
||||
if (/patch\s+-p1/.test(cmd)) return "Wende Patch an…";
|
||||
if (/curl/.test(cmd)) return "HTTP-Request…";
|
||||
return `Shell: ${cmd.slice(0, 55)}${cmd.length > 55 ? "…" : ""}`;
|
||||
}
|
||||
case "apply_patch":
|
||||
return "Wende Patch an…";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
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] [--continue] [--test-cmd \"cmd\"]",
|
||||
"Planung: /plan <auftrag> → /coder | /optimize --continue | /discard",
|
||||
"Patch: /patch <änderung> → /quick_check [was]",
|
||||
"Doku: /update_doku | Neues Projekt: /new_project <pfad>",
|
||||
"Abbruch: Escape (Generation laufend) | /cancel (Loop nach aktuellem Schritt)",
|
||||
"Resume: /continue | Modell: auto (Coder→:8001, Judge→:8002)",
|
||||
"/optimize <auftrag> [--rounds N] [--with-doku] [--continue] [--test-cmd \"cmd\"]",
|
||||
"/fix · /judge · /shipit · /cancel · /continue · /help",
|
||||
]);
|
||||
});
|
||||
|
||||
// ── Live-Aktivitätsstatus ────────────────────────────────────────────────
|
||||
// turn_start: Working-Text auf aktuellen Command-Kontext setzen
|
||||
pi.on("turn_start", function (_event, ctx) {
|
||||
if (currentActivity) ctx.ui.setWorkingMessage(currentActivity);
|
||||
});
|
||||
|
||||
// tool_execution_start: präzise Statuszeile während Tool-Ausführung
|
||||
pi.on("tool_execution_start", function (event, ctx) {
|
||||
const label = toolExecutionLabel(event.toolName, (event as any).args ?? {});
|
||||
if (label) ctx.ui.setStatus("agent", label);
|
||||
});
|
||||
|
||||
// tool_execution_end: Statuszeile löschen
|
||||
pi.on("tool_execution_end", function (_event, ctx) {
|
||||
ctx.ui.setStatus("agent", undefined);
|
||||
});
|
||||
|
||||
// agent_end: Working-Text und Statuszeile zurücksetzen
|
||||
pi.on("agent_end", function (_event, ctx) {
|
||||
ctx.ui.setWorkingMessage();
|
||||
ctx.ui.setStatus("agent", undefined);
|
||||
currentActivity = "";
|
||||
});
|
||||
|
||||
// ── 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.
|
||||
|
|
@ -673,41 +910,61 @@ export default function (pi: ExtensionAPI) {
|
|||
// ── Manuelle Kommandos ───────────────────────────────────────────────────
|
||||
|
||||
pi.registerCommand("coder", {
|
||||
description: "Legt TASK.md an, startet Implementierung → qwen3.5-coder (:8001).",
|
||||
description: "Implementiert <auftrag> ohne Review-Loop → 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;
|
||||
}
|
||||
if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder")) {
|
||||
ctx.ui.notify("Coder-Server nicht bereit (Port 8001) — start-coder.sh ausführen", "error");
|
||||
return;
|
||||
}
|
||||
await writeTaskMd(pi, ctx, task);
|
||||
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
||||
pi.sendUserMessage(coderKickoff(task));
|
||||
currentActivity = "Coder implementiert…";
|
||||
await sendAndWait(pi, ctx, coderKickoff(task));
|
||||
}
|
||||
});
|
||||
|
||||
pi.registerCommand("judge", {
|
||||
description: "Review gegen TASK.md + git show HEAD → qwen3.5-judge (:8002).",
|
||||
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||
if (!await waitUntilModelReady(pi, ctx, 8002, "qwen3.5-judge")) {
|
||||
ctx.ui.notify("Judge-Server nicht bereit (Port 8002) — start-judge.sh ausführen", "error");
|
||||
return;
|
||||
}
|
||||
await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge");
|
||||
pi.sendUserMessage(judgePrompt(args || ""));
|
||||
currentActivity = "Judge reviewt…";
|
||||
await sendAndWait(pi, ctx, judgePrompt(args || ""));
|
||||
}
|
||||
});
|
||||
|
||||
pi.registerCommand("fix", {
|
||||
description: "Fixt Judge-Kritik, committet Ergebnis → qwen3.5-coder (:8001).",
|
||||
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||
if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder")) {
|
||||
ctx.ui.notify("Coder-Server nicht bereit (Port 8001) — start-coder.sh ausführen", "error");
|
||||
return;
|
||||
}
|
||||
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
||||
pi.sendUserMessage(fixPrompt(args || ""));
|
||||
currentActivity = "Coder fixt Judge-Kritik…";
|
||||
await sendAndWait(pi, ctx, fixPrompt(args || ""));
|
||||
}
|
||||
});
|
||||
|
||||
pi.registerCommand("shipit", {
|
||||
description: "Finale Freigabe gegen TASK.md + git log → qwen3.5-judge (:8002).",
|
||||
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||
if (!await waitUntilModelReady(pi, ctx, 8002, "qwen3.5-judge")) {
|
||||
ctx.ui.notify("Judge-Server nicht bereit (Port 8002) — start-judge.sh ausführen", "error");
|
||||
return;
|
||||
}
|
||||
await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge");
|
||||
ctx.ui.notify("Judge prüft finale Freigabe — Ergebnis erscheint im Chat (SHIP / NO-SHIP)", "info");
|
||||
pi.sendUserMessage(shipitPrompt(args || ""));
|
||||
currentActivity = "Judge: finale Freigabe…";
|
||||
await sendAndWait(pi, ctx, shipitPrompt(args || ""));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -720,10 +977,10 @@ export default function (pi: ExtensionAPI) {
|
|||
const maxRounds = roundsMatch ? Math.max(1, parseInt(roundsMatch[1], 10)) : 3;
|
||||
const withDoku = /--with-doku/.test(args || "");
|
||||
const continueMode = /--continue/.test(args || "");
|
||||
const testCmdMatch = (args || "").match(/--test-cmd\s+"([^"]+)"|--test-cmd\s+(\S+)/);
|
||||
const testCmd: string | null = testCmdMatch ? (testCmdMatch[1] ?? testCmdMatch[2]) : null;
|
||||
const testCmdMatch = (args || "").match(/--test-cmd\s+"([^"]+)"|--test-cmd\s+'([^']+)'|--test-cmd\s+(\S+)/);
|
||||
const testCmd: string | null = testCmdMatch ? (testCmdMatch[1] ?? testCmdMatch[2] ?? testCmdMatch[3]) : null;
|
||||
const testTimeoutMatch = (args || "").match(/--test-timeout\s+(\d+)/);
|
||||
const testTimeout = testTimeoutMatch ? parseInt(testTimeoutMatch[1], 10) : 120;
|
||||
const testTimeout = testTimeoutMatch ? Math.max(1, parseInt(testTimeoutMatch[1], 10)) : 120;
|
||||
const task = (args || "")
|
||||
.replace(/--rounds\s+\d+/, "")
|
||||
.replace(/--test-timeout\s+\d+/, "")
|
||||
|
|
@ -738,139 +995,169 @@ export default function (pi: ExtensionAPI) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (continueMode) {
|
||||
// --continue: Implementierungsphase überspringen, direkt in Judge→Fix-Schleife
|
||||
// Erweiterter Auftrag wird als Zusatzauftrag in TASK.md eingetragen (falls angegeben)
|
||||
if (task) await writeTaskMd(pi, ctx, task);
|
||||
ctx.ui.setStatus("optimize", `Setze fort (max ${maxRounds} Runden Judge→Fix)…`);
|
||||
const continueMsg = task
|
||||
? `--continue: Zusatzauftrag in TASK.md eingetragen, überspringe Implementierung.`
|
||||
: `--continue: Überspringe Implementierung, starte direkt mit Judge-Prüfung.`;
|
||||
ctx.ui.notify(continueMsg, "info");
|
||||
} else {
|
||||
// TASK.md anlegen und Implementierung starten
|
||||
await writeTaskMd(pi, ctx, task);
|
||||
ctx.ui.setStatus("optimize", `Starte Optimierung (max ${maxRounds} Runden)…`);
|
||||
const taskPreview = task.length > 55 ? task.slice(0, 52) + "…" : task;
|
||||
ctx.ui.setStatus("optimize", `◉ Coder liest Anforderungen + implementiert: ${taskPreview}`);
|
||||
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
||||
await sendAndWait(pi, ctx, coderKickoff(task));
|
||||
await tickTaskMdStatus(pi, ctx, "Implementierung");
|
||||
if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", "Nach Implementierung"); return; }
|
||||
}
|
||||
try {
|
||||
if (continueMode) {
|
||||
// --continue: Implementierungsphase überspringen, direkt in Judge→Fix-Schleife
|
||||
// Erweiterter Auftrag wird als Zusatzauftrag in TASK.md eingetragen (falls angegeben)
|
||||
if (task) await writeTaskMd(pi, ctx, task);
|
||||
ctx.ui.setStatus("optimize", `Setze fort (max ${maxRounds} Runden Judge→Fix)…`);
|
||||
const continueMsg = task
|
||||
? `--continue: Zusatzauftrag in TASK.md eingetragen, überspringe Implementierung.`
|
||||
: `--continue: Überspringe Implementierung, starte direkt mit Judge-Prüfung.`;
|
||||
ctx.ui.notify(continueMsg, "info");
|
||||
|
||||
// Judge-Server-Bereitschaft prüfen — bei 503 (Modell lädt noch) bis zu 60s warten.
|
||||
ctx.ui.setStatus("optimize", "Judge-Server wird geprüft…");
|
||||
let serverReady = false;
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const hc = await pi.exec("bash", ["-c",
|
||||
"curl -sf --max-time 3 http://localhost:8002/health || " +
|
||||
"curl -sf --max-time 3 http://localhost:8002/v1/models"
|
||||
], { cwd: ctx.cwd });
|
||||
if (hc.code === 0) { serverReady = true; break; }
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
}
|
||||
if (!serverReady) {
|
||||
finalNotify(ctx, "⛔ Judge nicht erreichbar", "Port 8002 antwortet nicht — start-judge.sh ausführen");
|
||||
return;
|
||||
}
|
||||
|
||||
// Test-Suiten einmalig ermitteln: --test-cmd überschreibt Auto-Erkennung.
|
||||
// Läuft nach Coder, damit neu angelegte Test-Dateien bereits erkannt werden.
|
||||
ctx.ui.setStatus("optimize", "Test-Suiten werden erkannt…");
|
||||
const autoTestCmds: string[] = testCmd
|
||||
? [testCmd]
|
||||
: await detectTestCommands(pi, ctx);
|
||||
if (autoTestCmds.length > 0) {
|
||||
const label = autoTestCmds.map(c => c.split(" ")[0]).join(", ");
|
||||
ctx.ui.notify(
|
||||
`${autoTestCmds.length} Test-Suite${autoTestCmds.length > 1 ? "n" : ""} erkannt: ${label}`,
|
||||
"info"
|
||||
);
|
||||
} else {
|
||||
ctx.ui.notify("Keine Test-Suiten erkannt — Judge führt Tests selbst aus.", "info");
|
||||
}
|
||||
|
||||
let lastBlockers = "";
|
||||
let verdict = "";
|
||||
|
||||
// Schleife: Judge → (PASS? fertig : Fix → nächste Runde)
|
||||
for (let round = 1; round <= maxRounds; round++) {
|
||||
const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round);
|
||||
await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge");
|
||||
|
||||
if (autoTestCmds.length > 0) {
|
||||
const label = autoTestCmds.length === 1
|
||||
? autoTestCmds[0].split(" ")[0]
|
||||
: `${autoTestCmds.length} Suiten parallel`;
|
||||
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${label}, max. ${testTimeout}s)…`);
|
||||
const testOutput = await runTestsParallel(pi, ctx, autoTestCmds, testTimeout);
|
||||
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge analysiert Test-Ergebnis…`);
|
||||
await sendAndWait(pi, ctx, judgeWithTestsPrompt(testOutput, ""));
|
||||
} else {
|
||||
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge — TASK.md + letzter Commit + Tests…`);
|
||||
await sendAndWait(pi, ctx, judgePrompt(""));
|
||||
}
|
||||
if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", `Nach Judge Runde ${round}`); return; }
|
||||
|
||||
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", `${"●".repeat(round)} ✓ ${verdict} nach Runde ${round}/${maxRounds} — ShipIt…`);
|
||||
break;
|
||||
}
|
||||
|
||||
// Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen
|
||||
const currentBlockers = parseBlockers(judgeText);
|
||||
if (currentBlockers && currentBlockers === lastBlockers) {
|
||||
ctx.ui.setStatus("optimize", `${prog} ⚠ Gleicher Blocker in Runde ${round} – manuelle Intervention nötig`);
|
||||
finalNotify(ctx, "⚠ Schleife", "Gleicher Blocker zweimal – manuelle Intervention nötig");
|
||||
return;
|
||||
}
|
||||
lastBlockers = currentBlockers;
|
||||
|
||||
if (round === maxRounds) {
|
||||
ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)} ⚠ Max. ${maxRounds} Runden ohne PASS`);
|
||||
finalNotify(ctx, "⚠ Kein PASS", `${maxRounds} Runden ohne PASS – bitte /judge und /fix manuell`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fix-Phase: Blocker-Preview aus Judge-Bericht anzeigen
|
||||
const blockerHint = currentBlockers
|
||||
? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers)
|
||||
: "Kritikpunkte aus Judge-Bericht";
|
||||
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Coder fixt — ${blockerHint}`);
|
||||
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
||||
await sendAndWait(pi, ctx, fixPrompt(""));
|
||||
if (cancelRequested) { cancelRequested = false; finalNotify(ctx, "⛔ Abgebrochen", `Nach Fix Runde ${round}`); return; }
|
||||
}
|
||||
|
||||
// Finale ShipIt-Prüfung nur bei PASS
|
||||
if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") {
|
||||
ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)}◉ ShipIt — SHIP oder NO-SHIP?…`);
|
||||
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");
|
||||
finalNotify(ctx, "🚀 SHIP", "Programm ist 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");
|
||||
// Im --continue-Modus: Coder-Server jetzt prüfen, da er für die Fix-Phase gebraucht wird
|
||||
// (in normalem Modus wird er beim coderKickoff implizit geprüft)
|
||||
ctx.ui.setStatus("optimize", "Coder-Server wird geprüft…");
|
||||
if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder")) {
|
||||
finalNotify(ctx, "⛔ Coder nicht erreichbar", "Port 8001 — kein HTTP 200 nach 3 min. start-coder.sh ausführen");
|
||||
return;
|
||||
}
|
||||
} else if (shipVerdict === "NO-SHIP") {
|
||||
ctx.ui.setStatus("optimize", "⛔ NO-SHIP – noch nicht bereit");
|
||||
finalNotify(ctx, "⛔ NO-SHIP", "Noch Blocker offen – bitte /judge und /fix manuell");
|
||||
} else {
|
||||
ctx.ui.setStatus("optimize", "ShipIt abgeschlossen");
|
||||
finalNotify(ctx, "ShipIt", "Kein klares Urteil – Antwort im Chat prüfen");
|
||||
// TASK.md anlegen und Implementierung starten
|
||||
await writeTaskMd(pi, ctx, task);
|
||||
ctx.ui.setStatus("optimize", `Starte Optimierung (max ${maxRounds} Runden)…`);
|
||||
const taskPreview = task.length > 55 ? task.slice(0, 52) + "…" : task;
|
||||
ctx.ui.setStatus("optimize", `◉ Coder liest Anforderungen + implementiert: ${taskPreview}`);
|
||||
if (!await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder")) {
|
||||
finalNotify(ctx, "⛔ Modell-Fehler", "Coder-Modell (llama-cpp-coder) nicht verfügbar");
|
||||
return;
|
||||
}
|
||||
currentActivity = "Coder implementiert…";
|
||||
await sendAndWait(pi, ctx, coderKickoff(task));
|
||||
await tickTaskMdStatus(pi, ctx, "Implementierung");
|
||||
if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", "Nach Implementierung"); return; }
|
||||
}
|
||||
|
||||
// Judge-Bereitschaft via Completion-Check — /health antwortet bereits während des
|
||||
// GPU-Ladevorgangs und ist kein verlässliches Signal. Nur HTTP 200 auf einen
|
||||
// echten Completion-Request bedeutet: Modell ist im VRAM und bereit.
|
||||
ctx.ui.setStatus("optimize", "Judge-Server wird geprüft…");
|
||||
if (!await waitUntilModelReady(pi, ctx, 8002, "qwen3.5-judge")) {
|
||||
finalNotify(ctx, "⛔ Judge nicht erreichbar", "Port 8002 — kein HTTP 200 nach 3 min. start-judge.sh ausführen");
|
||||
return;
|
||||
}
|
||||
|
||||
// Test-Suiten einmalig ermitteln: --test-cmd überschreibt Auto-Erkennung.
|
||||
// Läuft nach Coder, damit neu angelegte Test-Dateien bereits erkannt werden.
|
||||
ctx.ui.setStatus("optimize", "Test-Suiten werden erkannt…");
|
||||
const autoTestCmds: string[] = testCmd
|
||||
? [testCmd]
|
||||
: await detectTestCommands(pi, ctx);
|
||||
if (autoTestCmds.length > 0) {
|
||||
const label = autoTestCmds.map(c => c.split(" ")[0]).join(", ");
|
||||
ctx.ui.notify(
|
||||
`${autoTestCmds.length} Test-Suite${autoTestCmds.length > 1 ? "n" : ""} erkannt: ${label}`,
|
||||
"info"
|
||||
);
|
||||
} else {
|
||||
ctx.ui.notify("Keine Test-Suiten erkannt — Judge führt Tests selbst aus.", "info");
|
||||
}
|
||||
|
||||
let lastBlockers = "";
|
||||
let verdict = "";
|
||||
|
||||
// Schleife: Judge → (PASS? fertig : Fix → nächste Runde)
|
||||
for (let round = 1; round <= maxRounds; round++) {
|
||||
const prog = "●".repeat(round - 1) + "◉" + "○".repeat(maxRounds - round);
|
||||
if (!await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge")) {
|
||||
finalNotify(ctx, "⛔ Modell-Fehler", "Judge-Modell (llama-cpp-judge) nicht verfügbar");
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoTestCmds.length > 0) {
|
||||
const label = autoTestCmds.length === 1
|
||||
? autoTestCmds[0].split(" ")[0]
|
||||
: `${autoTestCmds.length} Suiten parallel`;
|
||||
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Tests laufen (${label}, max. ${testTimeout}s)…`);
|
||||
const testOutput = await runTestsParallel(pi, ctx, autoTestCmds, testTimeout);
|
||||
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge analysiert Test-Ergebnis…`);
|
||||
currentActivity = `Judge reviewt (Runde ${round}/${maxRounds})…`;
|
||||
await sendAndWait(pi, ctx, judgeWithTestsPrompt(testOutput, ""));
|
||||
} else {
|
||||
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Judge — TASK.md + letzter Commit + Tests…`);
|
||||
currentActivity = `Judge reviewt (Runde ${round}/${maxRounds})…`;
|
||||
await sendAndWait(pi, ctx, judgePrompt(""));
|
||||
}
|
||||
if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Judge Runde ${round}`); return; }
|
||||
|
||||
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", `${"●".repeat(round)} ✓ ${verdict} nach Runde ${round}/${maxRounds} — ShipIt…`);
|
||||
break;
|
||||
}
|
||||
|
||||
// Loop-Erkennung: gleicher Blocker zweimal → manuell eingreifen
|
||||
const currentBlockers = parseBlockers(judgeText);
|
||||
if (currentBlockers && currentBlockers === lastBlockers) {
|
||||
ctx.ui.setStatus("optimize", `${prog} ⚠ Gleicher Blocker in Runde ${round} – manuelle Intervention nötig`);
|
||||
finalNotify(ctx, "⚠ Schleife", "Gleicher Blocker zweimal – manuelle Intervention nötig");
|
||||
return;
|
||||
}
|
||||
lastBlockers = currentBlockers;
|
||||
|
||||
if (round === maxRounds) {
|
||||
ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)} ⚠ Max. ${maxRounds} Runden ohne PASS`);
|
||||
if (verdict === "UNREADABLE") {
|
||||
finalNotify(ctx, "⚠ Urteil unklar", `${maxRounds} Runden – Judge-Urteil nicht erkennbar, Antwort im Chat prüfen`);
|
||||
} else {
|
||||
finalNotify(ctx, "⚠ Kein PASS", `${maxRounds} Runden ohne PASS – bitte /judge und /fix manuell`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fix-Phase: Blocker-Preview aus Judge-Bericht anzeigen
|
||||
const blockerHint = currentBlockers
|
||||
? (currentBlockers.length > 50 ? currentBlockers.slice(0, 47) + "…" : currentBlockers)
|
||||
: "Kritikpunkte aus Judge-Bericht";
|
||||
ctx.ui.setStatus("optimize", `${prog} Runde ${round}/${maxRounds}: Coder fixt — ${blockerHint}`);
|
||||
if (!await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder")) {
|
||||
finalNotify(ctx, "⛔ Modell-Fehler", "Coder-Modell (llama-cpp-coder) nicht verfügbar");
|
||||
return;
|
||||
}
|
||||
currentActivity = "Coder fixt Blocker…";
|
||||
await sendAndWait(pi, ctx, fixPrompt(""));
|
||||
if (cancelRequested) { finalNotify(ctx, "⛔ Abgebrochen", `Nach Fix Runde ${round}`); return; }
|
||||
}
|
||||
|
||||
// Finale ShipIt-Prüfung nur bei PASS
|
||||
if (verdict === "PASS" || verdict === "PASS WITH CONCERNS") {
|
||||
ctx.ui.setStatus("optimize", `${"●".repeat(maxRounds)}◉ ShipIt — SHIP oder NO-SHIP?…`);
|
||||
if (!await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge")) {
|
||||
finalNotify(ctx, "⛔ Modell-Fehler", "Judge-Modell (llama-cpp-judge) nicht verfügbar");
|
||||
return;
|
||||
}
|
||||
currentActivity = "Judge: finale Freigabe…";
|
||||
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");
|
||||
finalNotify(ctx, "🚀 SHIP", "Programm ist produktionsreif");
|
||||
await runVersionBump(pi, ctx);
|
||||
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");
|
||||
finalNotify(ctx, "⛔ NO-SHIP", "Noch Blocker offen – bitte /judge und /fix manuell");
|
||||
} else {
|
||||
ctx.ui.setStatus("optimize", "ShipIt abgeschlossen");
|
||||
finalNotify(ctx, "ShipIt", "Kein klares Urteil – Antwort im Chat prüfen");
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
finalNotify(ctx, "⛔ Fehler", String(e?.message ?? e));
|
||||
} finally {
|
||||
// Sicherstellen dass cancelRequested nie in einen späteren /optimize-Aufruf leckt
|
||||
cancelRequested = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -878,30 +1165,40 @@ export default function (pi: ExtensionAPI) {
|
|||
// ── Schlanke Kommandos für kleine Änderungen ─────────────────────────────
|
||||
|
||||
pi.registerCommand("patch", {
|
||||
description: "Gezielte Minimaländerung ohne vollständigen Review → qwen3.5-coder (:8001).",
|
||||
description: "Gezielte Minimaländerung ohne Refactoring, committet → 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;
|
||||
}
|
||||
if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder")) {
|
||||
ctx.ui.notify("Coder-Server nicht bereit (Port 8001) — start-coder.sh ausführen", "error");
|
||||
return;
|
||||
}
|
||||
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
||||
pi.sendUserMessage(patchPrompt(change));
|
||||
currentActivity = "Coder patcht…";
|
||||
await sendAndWait(pi, ctx, patchPrompt(change));
|
||||
}
|
||||
});
|
||||
|
||||
pi.registerCommand("quick_check", {
|
||||
description: "Schnelle Prüfung der letzten Änderung (OK/PROBLEM) → qwen3.5-judge (:8002).",
|
||||
description: "Schnelle OK/PROBLEM-Prüfung einer kleinen Codeänderung → qwen3.5-judge (:8002).",
|
||||
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||
if (!await waitUntilModelReady(pi, ctx, 8002, "qwen3.5-judge")) {
|
||||
ctx.ui.notify("Judge-Server nicht bereit (Port 8002) — start-judge.sh ausführen", "error");
|
||||
return;
|
||||
}
|
||||
await switchModel(pi, ctx, "llama-cpp-judge", "qwen3.5-judge");
|
||||
pi.sendUserMessage(quickCheckPrompt(args || ""));
|
||||
currentActivity = "Judge: Schnellcheck…";
|
||||
await sendAndWait(pi, ctx, quickCheckPrompt(args || ""));
|
||||
}
|
||||
});
|
||||
|
||||
// ── Dokumentations-Phase ─────────────────────────────────────────────────
|
||||
|
||||
pi.registerCommand("update_doku", {
|
||||
description: "Code kommentieren + README.md + BEDIENUNGSANLEITUNG.md + git commit → qwen3.5-coder (:8001).",
|
||||
description: "Inkrementelle Code-Kommentare + README.md + BEDIENUNGSANLEITUNG.md via Git-Tags.",
|
||||
handler: async function (_args: string, ctx: ExtensionCommandContext) {
|
||||
await runUpdateDoku(pi, ctx);
|
||||
}
|
||||
|
|
@ -924,7 +1221,7 @@ export default function (pi: ExtensionAPI) {
|
|||
}),
|
||||
}),
|
||||
async execute(_id, params, _signal, _onUpdate, ctx) {
|
||||
const tmpFile = `/tmp/pi_patch_${Date.now()}.diff`;
|
||||
const tmpFile = `/tmp/pi_patch_${Date.now()}_${Math.random().toString(36).slice(2)}.diff`;
|
||||
await pi.exec(
|
||||
"bash",
|
||||
["-c", `printf "%s" "$1" > "${tmpFile}"`, "_", params.patch],
|
||||
|
|
@ -949,23 +1246,63 @@ export default function (pi: ExtensionAPI) {
|
|||
// ── Planungsmodus ────────────────────────────────────────────────────────
|
||||
|
||||
pi.registerCommand("plan", {
|
||||
description: "Analysiert Auftrag, schmiedet Implementierungsplan in PLAN.md — macht keine Dateiänderungen. → qwen3.5-coder (:8001)",
|
||||
description: "Erstellt Implementierungsplan in PLAN.md ohne Dateiänderungen → qwen3.5-coder.",
|
||||
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||
const task = (args || "").trim();
|
||||
if (!task) {
|
||||
ctx.ui.notify("Benutzung: /plan <auftrag>", "error");
|
||||
return;
|
||||
}
|
||||
if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder")) {
|
||||
ctx.ui.notify("Coder-Server nicht bereit (Port 8001) — start-coder.sh ausführen", "error");
|
||||
return;
|
||||
}
|
||||
await writeTaskMd(pi, ctx, task);
|
||||
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
||||
ctx.ui.setStatus("plan", "Analysiere und plane (keine Dateiänderungen)…");
|
||||
pi.sendUserMessage(planPrompt(task));
|
||||
await ctx.waitForIdle();
|
||||
currentActivity = "Coder plant (kein Code)…";
|
||||
await sendAndWait(pi, ctx, planPrompt(task));
|
||||
ctx.ui.setStatus("plan", "");
|
||||
finalNotify(ctx, "📋 Plan", "Analyse abgeschlossen — PLAN.md + Chat");
|
||||
}
|
||||
});
|
||||
|
||||
pi.registerCommand("version", {
|
||||
description: "Versionsnummer des Projekts erhöhen (SemVer + Git-Tag). Analysiert Commits seit letztem Tag.",
|
||||
handler: async function (_args: string, ctx: ExtensionCommandContext) {
|
||||
await runVersionBump(pi, ctx);
|
||||
}
|
||||
});
|
||||
|
||||
pi.registerCommand("help", {
|
||||
description: "Zeigt alle Kommandos der pi-coder-judge-Extension.",
|
||||
handler: async function (_args: string, ctx: ExtensionCommandContext) {
|
||||
ctx.ui.notify([
|
||||
"── Kern-Workflow ─────────────────────────────────────────",
|
||||
"/optimize <auftrag> [--rounds N] [--with-doku] [--continue]",
|
||||
" [--test-cmd \"cmd\"] [--test-timeout N]",
|
||||
" Coder→Judge→Fix-Schleife bis PASS (empfohlener Einstieg)",
|
||||
"/fix [kommentar] Fixt Judge-Kritik, committet → Coder",
|
||||
"/judge [kommentar] Review gegen TASK.md + HEAD → Judge",
|
||||
"/shipit [kommentar] Finale Freigabe (SHIP/NO-SHIP) → Judge",
|
||||
"",
|
||||
"── Steuerung ─────────────────────────────────────────────",
|
||||
"/continue Unterbrochenen Prozess fortsetzen",
|
||||
"/cancel Laufenden Loop nach aktuellem Schritt abbrechen",
|
||||
"",
|
||||
"── Erweiterte Kommandos (immer tippbar, nicht im Menü) ───",
|
||||
"/coder <auftrag> Nur Implementierung ohne Review-Loop → Coder",
|
||||
"/patch <änderung> Gezielte Minimaländerung → Coder",
|
||||
"/quick_check [was] Schnelle OK/PROBLEM-Prüfung → Judge",
|
||||
"/plan <auftrag> Implementierungsplan in PLAN.md → Coder",
|
||||
"/update_doku Code-Kommentare + README.md + BEDIENUNGSANLEITUNG.md",
|
||||
"/version Versionsnummer erhöhen (SemVer + Git-Tag)",
|
||||
"/discard Verwirft PLAN.md",
|
||||
"/new_project <pfad> Projektverzeichnis + git init + .gitignore",
|
||||
].join("\n"), "info");
|
||||
}
|
||||
});
|
||||
|
||||
pi.registerCommand("cancel", {
|
||||
description: "Bricht laufenden Optimize-Loop nach dem aktuellen Schritt ab.",
|
||||
handler: async function (_args: string, ctx: ExtensionCommandContext) {
|
||||
|
|
@ -975,7 +1312,7 @@ export default function (pi: ExtensionAPI) {
|
|||
});
|
||||
|
||||
pi.registerCommand("discard", {
|
||||
description: "Verwirft PLAN.md und setzt den Planungsstatus zurück.",
|
||||
description: "Löscht PLAN.md und verwirft den aktuellen Plan.",
|
||||
handler: async function (_args: string, ctx: ExtensionCommandContext) {
|
||||
await pi.exec("bash", ["-c", "rm -f PLAN.md"], { cwd: ctx.cwd });
|
||||
ctx.ui.notify("PLAN.md gelöscht — Plan verworfen", "info");
|
||||
|
|
@ -986,9 +1323,14 @@ export default function (pi: ExtensionAPI) {
|
|||
pi.registerCommand("continue", {
|
||||
description: "Nimmt unterbrochenen Prozess wieder auf — liest TASK.md, PLAN.md, git log und entscheidet den nächsten Schritt.",
|
||||
handler: async function (_args: string, ctx: ExtensionCommandContext) {
|
||||
if (!await waitUntilModelReady(pi, ctx, 8001, "qwen3.5-coder")) {
|
||||
ctx.ui.notify("Coder-Server nicht bereit (Port 8001) — start-coder.sh ausführen", "error");
|
||||
return;
|
||||
}
|
||||
await switchModel(pi, ctx, "llama-cpp-coder", "qwen3.5-coder");
|
||||
ctx.ui.setStatus("continue", "Analysiere unterbrochenen Prozess…");
|
||||
pi.sendUserMessage([
|
||||
currentActivity = "Coder analysiert Stand…";
|
||||
await sendAndWait(pi, ctx, [
|
||||
"Ein Prozess wurde unterbrochen. Analysiere den aktuellen Stand und führe ihn sinnvoll fort:",
|
||||
"1. Lies TASK.md für den Auftrag",
|
||||
"2. Lies PLAN.md falls vorhanden (war ein Plan in Arbeit?)",
|
||||
|
|
@ -996,7 +1338,6 @@ export default function (pi: ExtensionAPI) {
|
|||
"4. Entscheide: Muss noch implementiert werden? Ist ein Review fällig? Müssen Fixes nachgezogen werden?",
|
||||
"5. Fahre direkt mit dem nächsten sinnvollen Schritt fort — kein langer Bericht, einfach weitermachen.",
|
||||
].join("\n"));
|
||||
await ctx.waitForIdle();
|
||||
ctx.ui.setStatus("continue", "");
|
||||
}
|
||||
});
|
||||
|
|
@ -1004,7 +1345,7 @@ export default function (pi: ExtensionAPI) {
|
|||
// ── Projekt-Scaffolding ──────────────────────────────────────────────────
|
||||
|
||||
pi.registerCommand("new_project", {
|
||||
description: "Legt Projektverzeichnis an + git init + .gitignore. /new_project <pfad>",
|
||||
description: "Legt Projektverzeichnis, git-Repo und .gitignore an.",
|
||||
handler: async function (args: string, ctx: ExtensionCommandContext) {
|
||||
const rawPath = (args || "").trim();
|
||||
if (!rawPath) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue