/** * Rule Enforcer Extension * * Zwei Mechanismen: * * 1. before_agent_start — injiziert Kern-Regeln in jeden System-Prompt-Turn * + Selbstkorrektur wenn letzter Turn W06 verletzt hat * + Subagenten-Alerts aus Alert-File (geschrieben von SubConfirm/setInterval) * * 2. message_end — scannt Pi's Ausgabe auf W06-Deliberations-Muster, * setzt Flag für nächsten before_agent_start * * 3. setInterval (30s) — prüft tmux-Sessions auf offene Bestätigungs-Dialoge * und schreibt in Alert-File, damit Pi beim nächsten Turn reagiert. * Ergänzt SubConfirm-Daemon — auch wenn SubConfirm nicht läuft. * * Nur aktiv wenn PI_ORCHESTRATOR=1. */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import * as fs from "node:fs"; import { execSync } from "node:child_process"; const ORCHESTRATOR_AKTIV = !!process.env.PI_ORCHESTRATOR; const ALERT_FILE = "/tmp/.pi-subagent-alert"; const CHECK_INTERVAL_MS = 30_000; // --------------------------------------------------------------------------- // Kern-Regeln — werden in jeden System-Prompt-Turn injiziert // --------------------------------------------------------------------------- const KERN_REGELN = ` ## PFLICHT-REGELN — gelten für JEDE Antwort ohne Ausnahme **W06** Kein Deliberieren: Niemals "Let me", "Actually", "I should", "Ich sollte", "Das liegt daran", "Weil ich" in Antworten ausgeben. Nur Ergebnis oder Entscheidung. **W07** Subagenten nicht aufgeben: Bei Problemen Benutzer kurz informieren, auf Anweisung warten. Nie eigenständig auf direkte Ausführung umschwenken. **W09** sleep verboten: Stattdessen watch_subagents nutzen oder Benutzer informieren. **W10** Subagent nie doppelt starten: Einmal starten, dann watch_subagents abwarten. **WATCH** watch_subagents SOFORT nach jeder Antwort erneut aufrufen — permanent, ohne Ausnahme. `.trim(); // --------------------------------------------------------------------------- // W06-Deliberations-Muster // --------------------------------------------------------------------------- const DELIBERATIONS_MUSTER: RegExp[] = [ /\bLet me\b/, /\bActually,?\s/, /\bI should\b/, /\bI need to\b/, /\bI'll check\b/, /\bI'll look\b/, /\bLet me check\b/, /\bLet me look\b/, /\bLet me verify\b/, /\bLet me first\b/, /\bHmm,?\s/i, /\bWait,?\s/, /\bIch sollte\b/, /\bLass mich\b/, /\bDas liegt daran\b/, /\bWeil ich\b/, /\bIch muss\b/, /\bIch werde\b.*\bprüfen\b/, ]; function hatDeliberation(text: string): boolean { return DELIBERATIONS_MUSTER.some((p) => p.test(text)); } function textAusNachricht(message: any): string { const content = message?.content; if (!Array.isArray(content)) return ""; return content .filter((c: any) => c.type === "text") .map((c: any) => String(c.text ?? "")) .join(" "); } // --------------------------------------------------------------------------- // Alert-File lesen und leeren // --------------------------------------------------------------------------- function leseAlerts(): string | null { try { if (!fs.existsSync(ALERT_FILE)) return null; const content = fs.readFileSync(ALERT_FILE, "utf-8").trim(); if (!content) return null; fs.writeFileSync(ALERT_FILE, ""); return content; } catch { return null; } } function schreibeAlert(text: string): void { try { const existing = fs.existsSync(ALERT_FILE) ? fs.readFileSync(ALERT_FILE, "utf-8") : ""; const neuerInhalt = existing.trim() ? existing.trim() + "\n" + text : text; fs.writeFileSync(ALERT_FILE, neuerInhalt); } catch {} } // --------------------------------------------------------------------------- // tmux-Sessions auf Bestätigungs-Dialoge prüfen // --------------------------------------------------------------------------- function pruefeSubagentenBestaetigungen(): string | null { try { const raw = execSync("tmux ls -F '#{session_name}' 2>/dev/null || true", { encoding: "utf-8", timeout: 2000, }).trim(); if (!raw) return null; const sessions = raw.split("\n").map((s) => s.trim()).filter(Boolean); const wartend: string[] = []; for (const name of sessions) { try { const pane = execSync( `tmux capture-pane -t '${name}' -p 2>/dev/null | tail -10`, { encoding: "utf-8", timeout: 1000 }, ).trim(); const wartetAufBestaetigung = pane.includes("Erlauben?") || pane.includes("→ Yes") || (pane.includes("navigate") && pane.includes("select")) || pane.includes("ACHTUNG:") || pane.includes("Überschreiben?") || pane.includes("Bestätigung"); if (wartetAufBestaetigung) { wartend.push(` • ${name}: WARTET AUF BESTÄTIGUNG`); } } catch {} } return wartend.length > 0 ? wartend.join("\n") : null; } catch { return null; } } // --------------------------------------------------------------------------- // Extension // --------------------------------------------------------------------------- export default function (pi: ExtensionAPI) { if (!ORCHESTRATOR_AKTIV) return; let letzterVerstoss: string | null = null; // ── 30s Background-Check ───────────────────────────────────────────────── // Prüft tmux-Sessions alle 30s auf offene Dialoge, unabhängig von Pi's // Tool-Calls. Schreibt in Alert-File → wird in before_agent_start injiziert. const interval = setInterval(() => { const wartend = pruefeSubagentenBestaetigungen(); if (wartend) { schreibeAlert(`[30s-Check] Subagent wartet auf Bestätigung:\n${wartend}`); } }, CHECK_INTERVAL_MS); pi.on("session_shutdown", async () => { clearInterval(interval); }); // ── message_end: W06-Scan ───────────────────────────────────────────────── // Scannt Pi's abgeschlossene Antwort auf Deliberations-Muster. // Setzt Flag für nächsten before_agent_start-Turn. pi.on("message_end", async (event) => { const text = textAusNachricht(event.message); if (hatDeliberation(text)) { letzterVerstoss = "W06 (Internes Deliberieren)"; } }); // ── before_agent_start: System-Prompt-Injection ─────────────────────────── // Injiziert Kern-Regeln + Selbstkorrektur + Subagenten-Alerts // in den System-Prompt jedes Turns. pi.on("before_agent_start", async (event) => { let zusatz = `\n\n---\n${KERN_REGELN}`; // Selbstkorrektur wenn letzter Turn W06 verletzt hat if (letzterVerstoss) { zusatz += `\n\n⚠️ REGELVERSTOSS erkannt (${letzterVerstoss}): Diese Antwort OHNE Gedankenkommentar. Nur Ergebnis oder Entscheidung.`; letzterVerstoss = null; } // Subagenten-Alerts const alerts = leseAlerts(); if (alerts) { zusatz += `\n\n🚨 SUBAGENT WARTET AUF BESTÄTIGUNG:\n${alerts}\n→ Sofort watch_subagents aufrufen oder Benutzer informieren.`; } return { systemPrompt: event.systemPrompt + zusatz }; }); }