diff --git a/extensions/rule-enforcer.ts b/extensions/rule-enforcer.ts new file mode 100644 index 0000000..cfce5e5 --- /dev/null +++ b/extensions/rule-enforcer.ts @@ -0,0 +1,203 @@ +/** + * 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 }; + }); +}