/**
 * Guardrails Extension — Security & Redaction
 *
 * Ported from the pi community "agents" extension pack.
 *
 * Features:
 * - Redacts secrets from tool results before the LLM sees them
 * - Blocks dangerous bash commands (rm -rf, sudo, mkfs, etc.)
 * - Blocks writes to protected paths (.env, .git, .ssh, etc.)
 * - Monitors streaming output against regex patterns (TTSR)
 * - Registers SF slash commands: /safegit, /safegit-level, /safegit-status, /yolo
 */
import { readFileSync } from "node:fs";
import * as path from "node:path";
import { join } from "node:path";
import { TtsrManager } from "./ttsr-manager.js";
import { loadRules } from "./ttsr-rule-loader.js";

const SENSITIVE_PATTERNS = [
	{
		pattern: /\b(sk-[a-zA-Z0-9]{20,})\b/g,
		replacement: "[OPENAI_KEY_REDACTED]",
	},
	{
		pattern: /\b(ghp_[a-zA-Z0-9]{36,})\b/g,
		replacement: "[GITHUB_TOKEN_REDACTED]",
	},
	{
		pattern: /\b(gho_[a-zA-Z0-9]{36,})\b/g,
		replacement: "[GITHUB_OAUTH_REDACTED]",
	},
	{
		pattern: /\b(xox[baprs]-[a-zA-Z0-9-]{10,})\b/g,
		replacement: "[SLACK_TOKEN_REDACTED]",
	},
	{ pattern: /\b(AKIA[A-Z0-9]{16})\b/g, replacement: "[AWS_KEY_REDACTED]" },
	{
		pattern: /\b(api[_-]?key|apikey)\s*[=:]\s*['"]?([a-zA-Z0-9_-]{20,})['"]?/gi,
		replacement: "$1=[REDACTED]",
	},
	{
		pattern:
			/\b(secret|token|password|passwd|pwd)\s*[=:]\s*['"]?([^\s'"]{8,})['"]?/gi,
		replacement: "$1=[REDACTED]",
	},
	{
		pattern: /\b(bearer)\s+([a-zA-Z0-9._-]{20,})\b/gi,
		replacement: "Bearer [REDACTED]",
	},
	{
		pattern: /(mongodb(\+srv)?:\/\/[^:]+:)[^@]+(@)/gi,
		replacement: "$1[REDACTED]$3",
	},
	{
		pattern: /(postgres(ql)?:\/\/[^:]+:)[^@]+(@)/gi,
		replacement: "$1[REDACTED]$3",
	},
	{ pattern: /(mysql:\/\/[^:]+:)[^@]+(@)/gi, replacement: "$1[REDACTED]$3" },
	{ pattern: /(redis:\/\/[^:]+:)[^@]+(@)/gi, replacement: "$1[REDACTED]$3" },
	{
		pattern:
			/-----BEGIN (RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END \1PRIVATE KEY-----/g,
		replacement: "[PRIVATE_KEY_REDACTED]",
	},
];
const SENSITIVE_FILES = [
	{ pattern: /\.env$/, desc: ".env" },
	{ pattern: /\.env\.(?!example$)[^/]+$/, desc: ".env local/override" },
	{ pattern: /\.dev\.vars($|\.[ˆ/]+$)/, desc: ".dev.vars" },
	{ pattern: /secrets?\.(json|ya?ml|toml)$/i, desc: "secrets file" },
	{ pattern: /credentials/i, desc: "credentials file" },
];
function redactToolResult(toolName, filePath, text, ctx) {
	if (toolName === "Read" && filePath) {
		if (/(^|\/)\.env\.example$/i.test(filePath)) {
			return undefined;
		}
		for (const { pattern, desc } of SENSITIVE_FILES) {
			if (pattern.test(filePath)) {
				ctx.ui.notify(
					`🔒 Redacted contents of sensitive file: ${filePath}`,
					"info",
				);
				return {
					content: [
						{
							type: "text",
							text: `[Contents of ${desc} (${filePath}) redacted for security]`,
						},
					],
				};
			}
		}
	}
	let result = text;
	let modified = false;
	for (const { pattern, replacement } of SENSITIVE_PATTERNS) {
		const next = result.replace(pattern, replacement);
		if (next !== result) {
			modified = true;
			result = next;
		}
	}
	if (modified) {
		ctx.ui.notify("🔒 Sensitive data redacted from output", "info");
		return { content: [{ type: "text", text: result }] };
	}
	return undefined;
}
const DANGEROUS_COMMANDS = [
	{ pattern: /\brm\s+(-[^\s]*r|--recursive)/, desc: "recursive delete" },
	{ pattern: /\bsudo\b/, desc: "sudo command" },
	{ pattern: /\b(chmod|chown)\b.*777/, desc: "dangerous permissions" },
	{ pattern: /\bmkfs\b/, desc: "filesystem format" },
	{ pattern: /\bdd\b.*\bof=\/dev\//, desc: "raw device write" },
	{ pattern: />\s*\/dev\/sd[a-z]/, desc: "raw device overwrite" },
	{ pattern: /\bkill\s+-9\s+-1\b/, desc: "kill all processes" },
	{ pattern: /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;/, desc: "fork bomb" },
];
const PROTECTED_PATHS = [
	{ pattern: /\.env($|\.(?!example))/, desc: "environment file" },
	{ pattern: /\.dev\.vars($|\.[ˆ/]+$)/, desc: "dev vars file" },
	{ pattern: /node_modules\//, desc: "node_modules" },
	{ pattern: /^\.git\/|\/\.git\//, desc: "git directory" },
	{ pattern: /\.pem$|\.key$/, desc: "private key file" },
	{ pattern: /id_rsa|id_ed25519|id_ecdsa/, desc: "SSH key" },
	{ pattern: /\.ssh\//, desc: ".ssh directory" },
	{ pattern: /secrets?\.(json|ya?ml|toml)$/i, desc: "secrets file" },
	{ pattern: /credentials/i, desc: "credentials file" },
];
const SOFT_PROTECTED_PATHS = [
	{ pattern: /package-lock\.json$/, desc: "package-lock.json" },
	{ pattern: /yarn\.lock$/, desc: "yarn.lock" },
	{ pattern: /pnpm-lock\.yaml$/, desc: "pnpm-lock.yaml" },
];
const DANGEROUS_BASH_WRITES = [
	/>\s*\.env(?!\.example)(\b|$)/,
	/>\s*\.dev\.vars/,
	/>\s*.*\.pem/,
	/>\s*.*\.key/,
	/tee\s+.*\.env(?!\.example)(\b|$)/,
	/tee\s+.*\.dev\.vars/,
	/cp\s+.*\s+\.env(?!\.example)(\b|$)/,
	/mv\s+.*\s+\.env(?!\.example)(\b|$)/,
];
async function checkBashCommand(command, ctx) {
	for (const { pattern, desc } of DANGEROUS_COMMANDS) {
		if (pattern.test(command)) {
			if (!ctx.hasUI) {
				return { block: true, reason: `Blocked ${desc} (no UI to confirm)` };
			}
			const ok = await ctx.ui.confirm(`⚠️ Dangerous command: ${desc}`, command);
			if (!ok) {
				return { block: true, reason: `Blocked ${desc} by user` };
			}
			break;
		}
	}
	for (const pattern of DANGEROUS_BASH_WRITES) {
		if (pattern.test(command)) {
			ctx.ui.notify("🛡️ Blocked bash write to protected path", "warning");
			return { block: true, reason: "Bash command writes to protected path" };
		}
	}
	return undefined;
}
async function checkWritePath(filePath, ctx) {
	const normalized = path.normalize(filePath);
	for (const { pattern, desc } of PROTECTED_PATHS) {
		if (pattern.test(normalized)) {
			ctx.ui.notify(`🛡️ Blocked write to ${desc}: ${filePath}`, "warning");
			return { block: true, reason: `Protected path: ${desc}` };
		}
	}
	for (const { pattern, desc } of SOFT_PROTECTED_PATHS) {
		if (pattern.test(normalized)) {
			if (!ctx.hasUI) {
				return { block: true, reason: `Protected path (no UI): ${desc}` };
			}
			const ok = await ctx.ui.confirm(
				`⚠️ Modifying ${desc}`,
				`Are you sure you want to modify ${filePath}?`,
			);
			if (!ok) {
				return { block: true, reason: `User blocked write to ${desc}` };
			}
			break;
		}
	}
	return undefined;
}
const SAFE_GIT_DEFAULTS = {
	promptLevel: "medium",
	enabledByDefault: true,
};
const RECENT_ONCE_APPROVAL_TTL_MS = 5_000;
const GIT_PATTERNS = [
	// High risk
	{
		pattern: /\bgit\s+push\s+.*--force(-with-lease)?\b/i,
		action: "force push",
		severity: "high",
	},
	{
		pattern: /\bgit\s+reset\s+--hard\b/i,
		action: "hard reset",
		severity: "high",
	},
	{
		pattern: /\bgit\s+clean\s+-[a-z]*f/i,
		action: "clean (remove untracked files)",
		severity: "high",
	},
	{
		pattern: /\bgit\s+stash\s+(drop|clear)\b/i,
		action: "drop/clear stash",
		severity: "high",
	},
	{
		pattern: /\bgit\s+branch\s+-[dD]\b/i,
		action: "delete branch",
		severity: "high",
	},
	{
		pattern: /\bgit\s+reflog\s+expire\b/i,
		action: "expire reflog",
		severity: "high",
	},
	// Medium risk
	{ pattern: /\bgit\s+push\b/i, action: "push", severity: "medium" },
	// git commit removed - commits are now allowed without prompting
	{ pattern: /\bgit\s+rebase\b/i, action: "rebase", severity: "medium" },
	{ pattern: /\bgit\s+merge\b/i, action: "merge", severity: "medium" },
	{
		pattern: /\bgit\s+tag\b/i,
		action: "create/modify tag",
		severity: "medium",
	},
	{
		pattern: /\bgit\s+cherry-pick\b/i,
		action: "cherry-pick",
		severity: "medium",
	},
	{ pattern: /\bgit\s+revert\b/i, action: "revert", severity: "medium" },
	{ pattern: /\bgit\s+am\b/i, action: "apply patches", severity: "medium" },
	// GitHub CLI
	{ pattern: /\bgh\s+\S+/i, action: "GitHub CLI", severity: "medium" },
];
const severityIcons = {
	high: "🔴",
	medium: "🟡",
};
function getSafeGitConfig(ctx, enabledOverride, promptLevelOverride) {
	const settings = ctx.settingsManager?.getSettings() ?? {};
	const config = {
		...SAFE_GIT_DEFAULTS,
		...(settings.safeGit ?? {}),
	};
	return {
		enabled:
			enabledOverride !== null && enabledOverride !== undefined
				? enabledOverride
				: config.enabledByDefault,
		promptLevel:
			promptLevelOverride !== null && promptLevelOverride !== undefined
				? promptLevelOverride
				: config.promptLevel,
	};
}
function shouldPrompt(severity, promptLevel) {
	if (promptLevel === "none") return false;
	if (promptLevel === "high") return severity === "high";
	return true;
}
function gitGateKey(action, command) {
	return `${action}\0${command.trim().replace(/\s+/g, " ")}`;
}
function pruneRecentOnceApprovals(state, now = Date.now()) {
	for (const [key, expiresAt] of state.recentOnceApprovals) {
		if (expiresAt <= now) state.recentOnceApprovals.delete(key);
	}
}
async function promptForGitCommand(
	action,
	severity,
	gateKey,
	ctx,
	sessionApprovedActions,
	sessionBlockedActions,
	gateState,
) {
	const icon = severityIcons[severity];
	const title =
		severity === "high"
			? `${icon} ⚠️ HIGH RISK: Git ${action} requires approval`
			: `${icon} Git ${action} requires approval`;
	let choice;
	try {
		choice = await ctx.ui.select(title, [
			"✅ Allow this command once",
			"⏭️  Decline this time (ask again later)",
			`✅✅ Auto-approve all "git ${action}" for this session only`,
			`🚫 Auto-block all "git ${action}" for this session only`,
		]);
	} catch {
		choice = undefined;
	}
	if (typeof choice !== "string") {
		ctx.ui.notify(
			`Git ${action} approval not answered; command paused`,
			"warning",
		);
		return {
			block: true,
			reason: `Git ${action} approval not answered; command paused`,
		};
	}
	if (!choice || choice.startsWith("⏭️")) {
		ctx.ui.notify(`Git ${action} declined`, "info");
		return { block: true, reason: `Git ${action} declined by user` };
	}
	if (choice.startsWith("🚫")) {
		sessionBlockedActions.add(action);
		ctx.ui.notify(
			`🚫 All "git ${action}" commands auto-blocked for this session`,
			"warning",
		);
		return {
			block: true,
			reason: `Git ${action} blocked by user (session setting)`,
		};
	}
	if (choice.startsWith("✅✅")) {
		sessionApprovedActions.add(action);
		ctx.ui.notify(
			`✅ All "git ${action}" commands auto-approved for this session`,
			"info",
		);
	} else {
		gateState.recentOnceApprovals.set(
			gateKey,
			Date.now() + RECENT_ONCE_APPROVAL_TTL_MS,
		);
		ctx.ui.notify(`Git ${action} approved once`, "info");
	}
	return undefined;
}
async function checkGitCommand(
	command,
	ctx,
	sessionApprovedActions,
	sessionBlockedActions,
	gateState,
	enabledOverride,
	promptLevelOverride,
) {
	const { enabled, promptLevel } = getSafeGitConfig(
		ctx,
		enabledOverride,
		promptLevelOverride,
	);
	if (!enabled || promptLevel === "none") return undefined;
	for (const { pattern, action, severity } of GIT_PATTERNS) {
		if (pattern.test(command)) {
			if (sessionBlockedActions.has(action)) {
				ctx.ui.notify(
					`🚫 Git ${action} auto-blocked (session setting)`,
					"warning",
				);
				return {
					block: true,
					reason: `Git ${action} blocked by user (session setting)`,
				};
			}
			if (sessionApprovedActions.has(action)) {
				ctx.ui.notify(
					`✅ Git ${action} auto-approved (session setting)`,
					"info",
				);
				return undefined;
			}
			const gateKey = gitGateKey(action, command);
			pruneRecentOnceApprovals(gateState);
			if (gateState.recentOnceApprovals.has(gateKey)) {
				ctx.ui.notify(
					`Git ${action} approval reused for duplicate request`,
					"info",
				);
				return undefined;
			}
			if (!shouldPrompt(severity, promptLevel)) {
				return undefined;
			}
			if (!ctx.hasUI) {
				return {
					block: true,
					reason: `Git ${action} blocked: requires explicit user approval (no UI available)`,
				};
			}
			const existingDecision = gateState.pendingDecisions.get(gateKey);
			if (existingDecision) return existingDecision;
			const pendingDecision = promptForGitCommand(
				action,
				severity,
				gateKey,
				ctx,
				sessionApprovedActions,
				sessionBlockedActions,
				gateState,
			);
			gateState.pendingDecisions.set(gateKey, pendingDecision);
			const cleanup = () => {
				if (gateState.pendingDecisions.get(gateKey) === pendingDecision) {
					gateState.pendingDecisions.delete(gateKey);
				}
			};
			pendingDecision.then(cleanup, cleanup);
			return pendingDecision;
		}
	}
	return undefined;
}
/**
 * Register SF slash commands for session-local safe-git controls.
 *
 * Purpose: expose Git guardrail controls through the SF command surface rather
 * than as shell binaries, so users can type `/safegit` in chat/TUI and update
 * the current session's Git approval policy.
 *
 * Consumer: guardrails(pi) during extension registration.
 */
function registerSafeGitCommands(
	pi,
	sessionEnabledOverride,
	sessionPromptLevelOverride,
	yoloPreviousPromptLevel,
) {
	pi.registerCommand("safegit", {
		description:
			"Slash command /safegit: toggle safe-git protection for this session",
		handler: async (_, ctx) => {
			const { enabled } = getSafeGitConfig(
				ctx,
				sessionEnabledOverride.value,
				sessionPromptLevelOverride.value,
			);
			sessionEnabledOverride.value = !enabled;
			ctx.ui.notify(
				sessionEnabledOverride.value
					? "🔒 Safe-git protection ON"
					: "🔓 Safe-git protection OFF",
				"info",
			);
			ctx.ui.notify("(Temporary for this session)", "info");
		},
	});
	pi.registerCommand("safegit-level", {
		description:
			"Slash command /safegit-level: set prompt level to high, medium, or none",
		handler: async (args, ctx) => {
			const arg = typeof args === "string" ? args.trim().toLowerCase() : "";
			if (arg === "high" || arg === "medium" || arg === "none") {
				sessionPromptLevelOverride.value = arg;
				const desc = {
					high: "🔴 Only high-risk operations require approval",
					medium: "🟡 Medium and high-risk operations require approval",
					none: "⚠️ No approval required (protection disabled)",
				};
				ctx.ui.notify(`Prompt level: ${arg}`, "info");
				ctx.ui.notify(desc[arg], "info");
				ctx.ui.notify("(Temporary for this session)", "info");
				return;
			}
			const { promptLevel } = getSafeGitConfig(
				ctx,
				sessionEnabledOverride.value,
				sessionPromptLevelOverride.value,
			);
			const options = [
				`🔴 high - Only high-risk (force push, hard reset, etc.)`,
				`🟡 medium - Medium and high-risk (push, commit, etc.)`,
				`⚠️ none - No prompts (disable protection)`,
				`❌ Cancel`,
			];
			ctx.ui.notify(`Current level: ${promptLevel}\n`, "info");
			const choice = await ctx.ui.select("Set prompt level:", options);
			const selectedChoice = typeof choice === "string" ? choice : undefined;
			if (!selectedChoice || selectedChoice.startsWith("❌")) {
				ctx.ui.notify("Cancelled.", "info");
				return;
			}
			const level = selectedChoice.split(" ")[1];
			sessionPromptLevelOverride.value = level;
			ctx.ui.notify(`Prompt level set to: ${selectedChoice}`, "info");
			ctx.ui.notify("(Temporary for this session)", "info");
		},
	});
	pi.registerCommand("yolo", {
		description:
			"Slash command /yolo: toggle session-only safe-git prompt bypass",
		handler: async (_, ctx) => {
			const { promptLevel } = getSafeGitConfig(
				ctx,
				sessionEnabledOverride.value,
				sessionPromptLevelOverride.value,
			);
			if (promptLevel === "none") {
				sessionPromptLevelOverride.value =
					yoloPreviousPromptLevel.value ?? SAFE_GIT_DEFAULTS.promptLevel;
				yoloPreviousPromptLevel.value = null;
				ctx.ui.notify(
					`YOLO mode OFF - safe-git prompt level restored to ${sessionPromptLevelOverride.value}`,
					"info",
				);
			} else {
				yoloPreviousPromptLevel.value = promptLevel;
				sessionPromptLevelOverride.value = "none";
				ctx.ui.notify(
					"YOLO mode ON - safe-git prompts disabled for this session",
					"info",
				);
			}
			ctx.ui.notify("(Temporary for this session)", "info");
		},
	});
	pi.registerCommand("safegit-status", {
		description:
			"Slash command /safegit-status: show safe-git status and settings",
		handler: async (_, ctx) => {
			const settings = ctx.settingsManager?.getSettings() ?? {};
			const globalConfig = {
				...SAFE_GIT_DEFAULTS,
				...(settings.safeGit ?? {}),
			};
			const { enabled, promptLevel } = getSafeGitConfig(
				ctx,
				sessionEnabledOverride.value,
				sessionPromptLevelOverride.value,
			);
			const lines = [
				"─── Safe Git Status ───",
				"",
				"Session State:",
				`  Enabled: ${enabled ? "🔒 ON" : "🔓 OFF"}${sessionEnabledOverride.value !== null ? " (session override)" : ""}`,
				`  Prompt Level: ${promptLevel}${sessionPromptLevelOverride.value !== null ? " (session override)" : ""}`,
				"",
				"Global Defaults:",
				`  Enabled: ${globalConfig.enabledByDefault ? "ON" : "OFF"}`,
				`  Prompt Level: ${globalConfig.promptLevel}`,
				"",
				"Prompt Levels:",
				`  🔴 high   - force push, hard reset, clean, delete branch`,
				`  🟡 medium - push, commit, rebase, merge, tag, gh CLI`,
				"",
				"Commands: /yolo /safegit /safegit-level /safegit-status",
				"───────────────────────",
			];
			ctx.ui.notify(lines.join("\n"), "info");
		},
	});
}
const __dirname = import.meta.dirname;
function buildInterruptContent(rule) {
	const template = readFileSync(join(__dirname, "ttsr-interrupt.md"), "utf-8");
	return template
		.replace("{{name}}", rule.name)
		.replace("{{path}}", rule.path)
		.replace("{{content}}", rule.content);
}
function extractDeltaContext(event) {
	if (event.type === "text_delta") {
		return {
			delta: event.delta,
			context: { source: "text", streamKey: "text" },
		};
	}
	if (event.type === "thinking_delta") {
		return {
			delta: event.delta,
			context: { source: "thinking", streamKey: "thinking" },
		};
	}
	if (event.type === "toolcall_delta") {
		const partial = event.partial;
		const contentBlock = partial?.content?.[event.contentIndex];
		const toolName =
			contentBlock && "name" in contentBlock ? contentBlock.name : undefined;
		const filePaths = [];
		if (contentBlock && "partialJson" in contentBlock) {
			const json = contentBlock.partialJson;
			if (json) {
				const pathMatch = json.match(/"(?:file_path|path)"\s*:\s*"([^"]+)"/);
				if (pathMatch) filePaths.push(pathMatch[1]);
			}
		}
		return {
			delta: event.delta,
			context: {
				source: "tool",
				toolName,
				filePaths: filePaths.length > 0 ? filePaths : undefined,
				streamKey: `toolcall:${event.contentIndex}`,
			},
		};
	}
	return null;
}
// ============================================================================
// Entry Point
// ============================================================================
export default function guardrails(pi) {
	const sessionApprovedActions = new Set();
	const sessionBlockedActions = new Set();
	const gateState = {
		pendingDecisions: new Map(),
		recentOnceApprovals: new Map(),
	};
	const sessionEnabledOverride = { value: null };
	const sessionPromptLevelOverride = {
		value: null,
	};
	const yoloPreviousPromptLevel = {
		value: null,
	};
	registerSafeGitCommands(
		pi,
		sessionEnabledOverride,
		sessionPromptLevelOverride,
		yoloPreviousPromptLevel,
	);
	pi.on("session_start", async (_, ctx) => {
		sessionEnabledOverride.value = null;
		sessionPromptLevelOverride.value = null;
		yoloPreviousPromptLevel.value = null;
		sessionApprovedActions.clear();
		sessionBlockedActions.clear();
		gateState.pendingDecisions.clear();
		gateState.recentOnceApprovals.clear();
		const { enabled, promptLevel } = getSafeGitConfig(
			ctx,
			sessionEnabledOverride.value,
			sessionPromptLevelOverride.value,
		);
		if (ctx.hasUI && enabled && promptLevel !== "none") {
			const promptDesc =
				promptLevel === "high" ? "🔴 high-risk only" : "🟡 medium+high";
			ctx.ui.notify(`Safe-git: Protection ${promptDesc}`, "info");
		}
	});
	pi.on("tool_call", async (event, ctx) => {
		if (event.toolName === "Bash") {
			const command = event.input.command;
			const gitResult = await checkGitCommand(
				command,
				ctx,
				sessionApprovedActions,
				sessionBlockedActions,
				gateState,
				sessionEnabledOverride.value,
				sessionPromptLevelOverride.value,
			);
			if (gitResult) return gitResult;
			return checkBashCommand(command, ctx);
		}
		if (event.toolName === "Write" || event.toolName === "Edit") {
			const filePath = event.input.path;
			return checkWritePath(filePath, ctx);
		}
		return undefined;
	});
	pi.on("tool_result", async (event, ctx) => {
		if (event.isError) return undefined;
		const textContent = event.content.find((c) => c.type === "text");
		if (!textContent) return undefined;
		return redactToolResult(
			event.toolName,
			event.input.path,
			textContent.text,
			ctx,
		);
	});
	// ── TTSR: Time Traveling Stream Rules ───────────────────────────────────
	let ttsrManager = null;
	let pendingViolation = null;
	pi.on("session_start", async (_event, ctx) => {
		const rules = loadRules(ctx.cwd);
		if (rules.length === 0) {
			ttsrManager = null;
			return;
		}
		ttsrManager = new TtsrManager();
		let loaded = 0;
		for (const rule of rules) {
			if (ttsrManager.addRule(rule)) loaded++;
		}
		if (loaded === 0) ttsrManager = null;
	});
	pi.on("turn_start", async () => {
		if (!ttsrManager) return;
		ttsrManager.resetBuffer();
		pendingViolation = null;
	});
	pi.on("message_update", async (event, ctx) => {
		if (!ttsrManager || !ttsrManager.hasRules()) return;
		if (pendingViolation) return;
		const extracted = extractDeltaContext(event.assistantMessageEvent);
		if (!extracted) return;
		const { delta, context } = extracted;
		const matches = ttsrManager.checkDelta(delta, context);
		if (matches.length === 0) return;
		pendingViolation = { rules: matches };
		ttsrManager.markInjected(matches);
		ctx.abort();
	});
	pi.on("turn_end", async () => {
		if (!ttsrManager) return;
		ttsrManager.incrementMessageCount();
	});
	pi.on("agent_end", async () => {
		if (!ttsrManager || !pendingViolation) return;
		const violation = pendingViolation;
		pendingViolation = null;
		const interruptParts = violation.rules.map(buildInterruptContent);
		const fullInterrupt = interruptParts.join("\n\n");
		pi.sendMessage(
			{ customType: "ttsr-violation", content: fullInterrupt, display: false },
			{ triggerTurn: true },
		);
	});
}
