204 lines
6.9 KiB
TypeScript
204 lines
6.9 KiB
TypeScript
|
|
/**
|
|||
|
|
* 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 };
|
|||
|
|
});
|
|||
|
|
}
|