From bba630cbbb91867540378f08746828e6da97166c Mon Sep 17 00:00:00 2001 From: dschlueter Date: Mon, 15 Jun 2026 06:27:14 +0200 Subject: [PATCH] fix: before_agent_start-Hook nur bei aktivem Coding-Workflow Persona injizieren MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit currentRole erhält dritten Zustand "idle" (Initialwert + Reset nach jedem Kommando). Der Hook injiziert nur noch eine Persona wenn currentRole "coder" oder "judge" ist — im freien Chat bleibt der System-Prompt unverändert, sodass andere Extensions (z. B. research-web.ts) nicht durch die Coding-Persona gestört werden. Alle Kommando-Handler (/coder, /judge, /fix, /shipit, /optimize, /patch, /quick_check, /plan, /continue) und runUpdateDoku setzen currentRole am Ende auf "idle" zurück. Co-Authored-By: Claude Sonnet 4.6 --- pi-coding-extension.ts | 18 ++++++++++++++---- tests/extension-guards.test.mjs | 7 +++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/pi-coding-extension.ts b/pi-coding-extension.ts index 74597de..14ea727 100644 --- a/pi-coding-extension.ts +++ b/pi-coding-extension.ts @@ -762,6 +762,7 @@ async function runUpdateDoku(pi: ExtensionAPI, ctx: ExtensionCommandContext): Pr ctx.ui.setStatus("update_doku", "✓ Dokumentation abgeschlossen"); ctx.ui.notify("Dokumentations-Phase abgeschlossen. Commit angelegt.", "info"); + currentRole = "idle"; } // ── Versions-Verwaltung (SemVer + Git-Tags) ────────────────────────────────── @@ -923,7 +924,8 @@ let cancelRequested = false; let currentModelKey = ""; // Cache für ensureSingleModel() — verhindert redundante setModel()-Aufrufe // Aktive Rolle für die Single-Server-Architektur: steuert, welcher Persona-System-Prompt // im before_agent_start-Hook injiziert wird. Wird ausschließlich über selectRole() gesetzt. -let currentRole: "coder" | "judge" = "coder"; +// "idle" = kein aktiver Coding-Workflow → Hook injiziert keine Persona (freier Chat). +let currentRole: "coder" | "judge" | "idle" = "idle"; let interactivePauseActive = false; let interactiveContinueRequested = false; let interactivePauseTask = ""; @@ -999,6 +1001,7 @@ export default function (pi: ExtensionAPI) { // erhalten bleibt. So unterscheiden sich Coder- und Judge-Turns auf System-Prompt-Ebene, // ohne Server- oder Modellwechsel. pi.on("before_agent_start", function (event) { + if (currentRole === "idle") return {}; const persona = currentRole === "judge" ? judgePersona() : coderPersona(); return { systemPrompt: event.systemPrompt + "\n\n" + persona }; }); @@ -1079,6 +1082,7 @@ export default function (pi: ExtensionAPI) { await selectRole(pi, ctx, "coder"); currentActivity = "Coder implementiert…"; await sendAndWait(pi, ctx, coderKickoff(task)); + currentRole = "idle"; } }); @@ -1092,6 +1096,7 @@ export default function (pi: ExtensionAPI) { await selectRole(pi, ctx, "judge"); currentActivity = "Judge reviewt…"; await sendAndWait(pi, ctx, judgePrompt(args || "")); + currentRole = "idle"; } }); @@ -1105,6 +1110,7 @@ export default function (pi: ExtensionAPI) { await selectRole(pi, ctx, "coder"); currentActivity = "Coder fixt Judge-Kritik…"; await sendAndWait(pi, ctx, fixPrompt(args || "")); + currentRole = "idle"; } }); @@ -1127,6 +1133,7 @@ export default function (pi: ExtensionAPI) { } else if (shipVerdict === "NO-SHIP") { ctx.ui.notify("NO-SHIP — noch Blocker offen. Bitte /fix aufrufen.", "error"); } + currentRole = "idle"; } }); @@ -1388,9 +1395,8 @@ export default function (pi: ExtensionAPI) { // Sicherstellen dass keine Zustandsvariable in späteren /optimize-Aufruf leckt cancelRequested = false; currentModelKey = ""; - // Rolle auf den Standard (coder) zurücksetzen, damit freier Chat nach einem - // Lauf nicht versehentlich die Judge-Persona behält (before_agent_start-Hook). - currentRole = "coder"; + // Zurück auf idle: freier Chat nach dem Lauf soll keine Coding-Persona erhalten. + currentRole = "idle"; interactivePauseActive = false; interactiveContinueRequested = false; interactivePauseTask = ""; @@ -1415,6 +1421,7 @@ export default function (pi: ExtensionAPI) { await selectRole(pi, ctx, "coder"); currentActivity = "Coder patcht…"; await sendAndWait(pi, ctx, patchPrompt(change)); + currentRole = "idle"; } }); @@ -1428,6 +1435,7 @@ export default function (pi: ExtensionAPI) { await selectRole(pi, ctx, "judge"); currentActivity = "Judge: Schnellcheck…"; await sendAndWait(pi, ctx, quickCheckPrompt(args || "")); + currentRole = "idle"; } }); @@ -1500,6 +1508,7 @@ export default function (pi: ExtensionAPI) { await sendAndWait(pi, ctx, planPrompt(task)); ctx.ui.setStatus("plan", ""); finalNotify(ctx, "📋 Plan", "Analyse abgeschlossen — PLAN.md + Chat"); + currentRole = "idle"; } }); @@ -1587,6 +1596,7 @@ export default function (pi: ExtensionAPI) { "5. Fahre direkt mit dem nächsten sinnvollen Schritt fort — kein langer Bericht, einfach weitermachen.", ].join("\n")); ctx.ui.setStatus("continue", ""); + currentRole = "idle"; } }); diff --git a/tests/extension-guards.test.mjs b/tests/extension-guards.test.mjs index 30a173d..158f811 100644 --- a/tests/extension-guards.test.mjs +++ b/tests/extension-guards.test.mjs @@ -41,8 +41,11 @@ test("Loop-Erkennung greift auch ohne Blocker-Abschnitt (Fallback auf Gesamttext assert.match(extSource, /parseBlockers\(judgeText\)\s*\|\|\s*judgeText/); }); -test("currentRole wird nach jedem optimize-Lauf auf 'coder' zurückgesetzt", () => { - assert.match(extSource, /currentRole\s*=\s*"coder"/); +test("currentRole wird nach jedem Kommando auf 'idle' zurückgesetzt (kein Persona-Leak in freien Chat)", () => { + // Initialwert und alle Reset-Stellen müssen "idle" sein, nicht "coder". + assert.match(extSource, /currentRole:\s*"coder"\s*\|\s*"judge"\s*\|\s*"idle"/); + assert.match(extSource, /=\s*"idle"/); + assert.doesNotMatch(extSource, /currentRole\s*=\s*"coder"/); }); test("LICENCE.md: deterministisch in der Doku-Phase, nur wenn nicht vorhanden, proprietär", () => {