/**
 * Nexus — The unified Pi intelligence layer v2
 *
 * Combines model routing + sub-agent orchestration into one seamless system.
 * The main agent stays fast and interactive. Complex work is automatically
 * offloaded to sub-agents. The model auto-adapts to task complexity.
 *
 * New in v2:
 *   ✦ Context Bridge — sub-agents get conversation context preamble
 *   ✦ Persistent memory (.pi/memory.md) injected into every turn
 *   ✦ nexus_remember — store project facts mid-session
 *   ✦ nexus_plan — decompose + auto-spawn parallel agents
 *   ✦ Self-healing sub-agents — auto-retry once on recoverable errors
 *   ✦ Smart model routing per sub-agent role
 *   ✦ LLM synthesis in nexus_collect
 *   ✦ OS notifications with sound + cost
 *   ✦ Session summary on shutdown
 *   ✦ /nx plan|memory|retry|chain commands
 *
 * Usage:
 *   pi -e extensions/nexus.ts
 *
 * Commands:
 *   /nx              — Full dashboard
 *   /nx spawn <task> — Manual sub-agent spawn
 *   /nx plan <goal>  — Decompose & spawn optimal agents
 *   /nx memory       — Show .pi/memory.md
 *   /nx retry <id>   — Manually retry failed agent
 *   /nx chain <id> <task> — Continue agent with new task
 *   /nx kill <id>    — Kill sub-agent
 *   /nx clear        — Clear all sub-agents
 *   /nx mode <m>     — interactive | hybrid | delegator
 *   /nx auto on|off  — Toggle auto-delegation
 *   /nx lock         — Lock model (disable auto-routing)
 *   /nx unlock       — Unlock model
 *   /nx tier 1|2|3   — Force model tier
 */

import type { AssistantMessage as AssistantMsg } from "@mariozechner/pi-ai";
import { complete } from "@mariozechner/pi-ai";
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
import { Container, Text, truncateToWidth, visibleWidth, type AutocompleteItem } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import { execSync, spawn as cpSpawn, type ChildProcess } from "child_process";
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
import { basename, join } from "path";
import { applyExtensionDefaults } from "./themeMap.ts";

// ══════════════════════════════════════════════════════════════════════════
// CONSTANTS
// ══════════════════════════════════════════════════════════════════════════

const AGENT_TIMEOUT_MS = 10 * 60 * 1000;
const WIDGET_THROTTLE_MS = 400;
const CONTEXT_WARN_PCT = 70;
const MAX_RESULT_LEN = 12000;
const MEMORY_FILE = ".pi/memory.md";
const SESSIONS_DIR = ".pi/sessions";

// ══════════════════════════════════════════════════════════════════════════
// MODEL TIERS
// ══════════════════════════════════════════════════════════════════════════

interface Tier {
	name: string;
	icon: string;
	provider: string;
	modelId: string;
	thinking: "off" | "low" | "medium" | "high";
	inputCost: number;
	outputCost: number;
}

const TIERS: Tier[] = [
	{ name: "Haiku", icon: "⚡", provider: "anthropic", modelId: "claude-haiku-4-5", thinking: "off", inputCost: 0.80, outputCost: 4.00 },
	{ name: "Sonnet", icon: "⚖️", provider: "anthropic", modelId: "claude-sonnet-4-6", thinking: "low", inputCost: 3.00, outputCost: 15.00 },
	{ name: "Opus", icon: "🧠", provider: "anthropic", modelId: "claude-opus-4-6", thinking: "high", inputCost: 15.00, outputCost: 75.00 },
];

// ══════════════════════════════════════════════════════════════════════════
// SMART MODEL ROUTING FOR SUB-AGENTS
// ══════════════════════════════════════════════════════════════════════════

function selectSubAgentModel(role: string, task: string): string {
	const r = role.toLowerCase();
	const t = task.toLowerCase();
	if (r === "scout" || r === "reviewer") return "anthropic/claude-haiku-4-5";
	if (/architect|design|security audit|security review|threat model/.test(t)) return "anthropic/claude-opus-4-6";
	return "anthropic/claude-sonnet-4-6"; // general, builder, tester
}

// ══════════════════════════════════════════════════════════════════════════
// MEMORY HELPERS
// ══════════════════════════════════════════════════════════════════════════

function readMemory(cwd: string): string {
	const p = join(cwd, MEMORY_FILE);
	return existsSync(p) ? readFileSync(p, "utf-8") : "";
}

function updateMemory(cwd: string, key: string, value: string, mode: "append" | "replace" = "replace"): void {
	const p = join(cwd, MEMORY_FILE);
	const dir = join(cwd, ".pi");
	if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
	let content = existsSync(p) ? readFileSync(p, "utf-8") : "";

	const heading = `## ${key}`;
	const idx = content.indexOf(heading);
	if (idx === -1) {
		// Append new section
		content = content.trimEnd() + `\n\n${heading}\n${value}\n`;
	} else {
		// Find end of section (next ## or EOF)
		const nextSection = content.indexOf("\n## ", idx + heading.length);
		const sectionEnd = nextSection === -1 ? content.length : nextSection;
		if (mode === "replace") {
			content = content.slice(0, idx) + `${heading}\n${value}\n` + content.slice(sectionEnd);
		} else {
			// Append within section
			const insert = sectionEnd === content.length ? content.length : sectionEnd;
			content = content.slice(0, insert) + `\n${value}` + content.slice(insert);
		}
	}
	writeFileSync(p, content, "utf-8");
}

// ══════════════════════════════════════════════════════════════════════════
// CONTEXT BRIDGE
// ══════════════════════════════════════════════════════════════════════════

async function buildContextBridge(ctx: ExtensionContext, task: string): Promise<string> {
	const MAX_CHARS = 3000;
	const parts: string[] = [];

	// Recent conversation turns
	const branch = ctx.sessionManager.getBranch();
	const recent = branch.slice(-12).filter((e: any) => e.type === "message");
	const turns: string[] = [];
	for (const entry of recent) {
		const msg = (entry as any).message;
		const textContent = Array.isArray(msg.content)
			? msg.content.filter((c: any) => c.type === "text").map((c: any) => c.text).join("")
			: String(msg.content || "");
		if (!textContent.trim()) continue;
		const label = msg.role === "user" ? "User" : "Assistant";
		turns.push(`${label}: ${textContent.slice(0, 400)}`);
	}
	if (turns.length > 0) {
		parts.push(`[CONVERSATION CONTEXT]\n${turns.slice(-6).join("\n\n")}`);
	}

	// Key Facts
	let gitBranch = "unknown";
	let fileList = "";
	try { gitBranch = execSync("git branch --show-current", { cwd: ctx.cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim(); } catch {}
	try {
		fileList = execSync("ls -la", { cwd: ctx.cwd, stdio: ["ignore", "pipe", "ignore"] })
			.toString().trim().split("\n").slice(0, 20).join("\n");
	} catch {}

	const keyFacts = [
		`cwd: ${ctx.cwd}`,
		`git branch: ${gitBranch}`,
		fileList ? `\nFiles:\n${fileList}` : "",
	].filter(Boolean).join("\n");
	parts.push(`[KEY FACTS]\n${keyFacts}`);

	const preamble = parts.join("\n\n");
	const capped = preamble.length > MAX_CHARS ? preamble.slice(0, MAX_CHARS) + "\n...[truncated]" : preamble;
	return `${capped}\n\n[TASK]\n${task}`;
}

// ══════════════════════════════════════════════════════════════════════════
// LLM-POWERED TASK CLASSIFIER
// ══════════════════════════════════════════════════════════════════════════

const CLASSIFIER_SYSTEM_PROMPT = `You are a task complexity classifier for a coding assistant. Given a user prompt, respond with ONLY a JSON object (no markdown, no explanation):

{"tier": <1|2|3>, "thinking": "<off|low|medium|high>", "offload": <true|false>, "reason": "<brief reason>"}

Classification rules:

TIER 1 (simple — use cheapest model):
- Quick questions, yes/no answers, explanations
- Reading/viewing files, checking status, listing things
- Small single-line edits, formatting, renaming
- Conversational responses (thanks, ok, etc.)
- Simple lookups or searches

TIER 2 (medium — use balanced model):
- Writing new functions, classes, or components
- Debugging, fixing bugs, error resolution
- Refactoring single files
- Writing tests for specific code
- Database queries, API endpoint creation
- Docker/CI configuration changes

TIER 3 (complex — use most powerful model):
- Architecture design, system planning
- Multi-file refactoring or migrations
- Security audits, performance optimization
- Complex algorithmic problems
- Designing new systems from scratch
- Tasks requiring deep reasoning or trade-off analysis

THINKING LEVEL:
- "off"    → Simple/conversational.
- "low"    → Moderate task.
- "medium" → Tricky bug, subtle logic, multi-step plan.
- "high"   → Architecture, security audit, complex algorithm.

OFFLOAD (spawn sub-agent instead of inline):
- Set true when the task would generate lots of context (multi-file changes, codebase-wide operations)
- Set true when the task is independent and can run in background
- Set true when "across all files", "entire codebase", parallel work is mentioned
- Set false for conversational, quick, or interactive tasks
- Set false when user needs immediate back-and-forth

AGENT LIMITS:
- Max recommended concurrent agents: 3
- If already at 3+ running agents, prefer offload=false unless critical.

Be concise. The "reason" should be under 10 words.`;

interface ClassificationResult {
	tier: number;
	thinking: "off" | "low" | "medium" | "high";
	offload: boolean;
	reason: string;
}

const classificationCache = new Map<string, { result: ClassificationResult; timestamp: number }>();
const CACHE_TTL_MS = 60_000;

async function classifyWithLLM(
	prompt: string,
	ctx: ExtensionContext,
	contextPercent: number | null,
	activeAgentCount: number,
	memorySnippet: string,
): Promise<ClassificationResult> {
	const cacheKey = prompt.trim().toLowerCase().slice(0, 200);
	const cached = classificationCache.get(cacheKey);
	if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) return cached.result;

	const classifier = ctx.modelRegistry.find("anthropic", "claude-haiku-4-5");
	if (!classifier) return { tier: 2, thinking: "low", offload: false, reason: "classifier unavailable" };

	const apiKey = await ctx.modelRegistry.getApiKey(classifier);
	if (!apiKey) return { tier: 2, thinking: "low", offload: false, reason: "no API key" };

	let userMsg = prompt;
	const notes: string[] = [];
	if (contextPercent !== null && contextPercent > CONTEXT_WARN_PCT) {
		notes.push(`Main context at ${Math.round(contextPercent)}% — prefer offload=true for heavy tasks.`);
	}
	if (activeAgentCount >= 3) {
		notes.push(`Already ${activeAgentCount} running agents — prefer offload=false unless critical.`);
	}
	if (memorySnippet) {
		notes.push(`Tech context from memory: ${memorySnippet.slice(0, 200)}`);
	}
	if (notes.length > 0) userMsg += `\n\n[SYSTEM NOTES]\n${notes.join("\n")}`;

	try {
		const response = await complete(classifier, {
			systemPrompt: CLASSIFIER_SYSTEM_PROMPT,
			messages: [{ role: "user" as const, content: userMsg, timestamp: Date.now() }],
		}, { reasoning: "off" });

		const text = response.content
			.filter((c): c is { type: "text"; text: string } => c.type === "text")
			.map(c => c.text).join("");

		const jsonStr = text.replace(/```json?\n?/g, "").replace(/```/g, "").trim();
		const parsed = JSON.parse(jsonStr);
		const validThinking = ["off", "low", "medium", "high"];
		const result: ClassificationResult = {
			tier: Math.max(1, Math.min(3, parsed.tier || 2)),
			thinking: validThinking.includes(parsed.thinking) ? parsed.thinking : "low",
			offload: Boolean(parsed.offload),
			reason: String(parsed.reason || "classified").slice(0, 50),
		};
		classificationCache.set(cacheKey, { result, timestamp: Date.now() });
		return result;
	} catch (err: any) {
		return { tier: 2, thinking: "low", offload: false, reason: `classify error: ${err?.message?.slice(0, 30)}` };
	}
}

// ══════════════════════════════════════════════════════════════════════════
// SUB-AGENT TYPES
// ══════════════════════════════════════════════════════════════════════════

type AgentMode = "interactive" | "delegator" | "hybrid";
type SubStatus = "queued" | "running" | "done" | "error" | "killed";

interface SubAgent {
	id: number;
	status: SubStatus;
	task: string;
	role: string;
	tools: string;
	textChunks: string[];
	toolCount: number;
	elapsed: number;
	startTime: number;
	sessionFile: string;
	turnCount: number;
	proc?: ChildProcess;
	timer?: ReturnType<typeof setInterval>;
	inputTokens: number;
	outputTokens: number;
	cost: number;
	retryCount: number;
	lastError?: string;
}

interface RoleDef {
	name: string;
	description: string;
	tools: string;
	systemPrompt: string;
}

const ROLES: RoleDef[] = [
	{ name: "general", description: "General-purpose coding agent", tools: "read,bash,edit,write,grep,find,ls", systemPrompt: "You are a focused coding agent. Complete the given task efficiently. Be thorough but concise." },
	{ name: "scout", description: "Fast recon (read-only)", tools: "read,grep,find,ls", systemPrompt: "You are a scout. Investigate the codebase quickly and report findings. Do NOT modify files." },
	{ name: "builder", description: "Implementation & code generation", tools: "read,write,edit,bash,grep,find,ls", systemPrompt: "You are a builder. Implement changes thoroughly. Clean, minimal code. Follow existing patterns." },
	{ name: "reviewer", description: "Code review (read-only)", tools: "read,grep,find,ls", systemPrompt: "You are a reviewer. Analyze code for bugs, security, performance, style. Be specific about line numbers." },
	{ name: "tester", description: "Test writing & execution", tools: "read,write,edit,bash,grep,find,ls", systemPrompt: "You are a tester. Write and run tests. Cover edge cases. Use the project's existing test framework." },
];

// ══════════════════════════════════════════════════════════════════════════
// EXTENSION
// ══════════════════════════════════════════════════════════════════════════

export default function nexus(pi: ExtensionAPI) {

	// ── Router State ───────────────────────────────────────────────────────

	let currentTier = 1;
	let lastThinking: "off" | "low" | "medium" | "high" = "low";
	let tierLocked = false;
	let totalSwitches = 0;
	let turnsSinceSwitch = 0;
	let consecutiveErrors = 0;
	let totalToolCalls = 0;
	let editWriteThisTurn = 0;
	let toolsThisTurn = 0;
	let turnCount = 0;
	let savedVsOpus = 0;
	let classifierCost = 0;
	const tierHistory: Array<{ tier: number; reason: string; turn: number }> = [];

	// ── Agent State ────────────────────────────────────────────────────────

	const agents: Map<number, SubAgent> = new Map();
	const activeProcs: Set<ChildProcess> = new Set();
	let nextId = 1;
	let widgetCtx: ExtensionContext | null = null;
	let mode: AgentMode = "hybrid";
	let autoDelegate = true;
	let sessionDir = "";
	let customRoles: RoleDef[] = [];
	let projectMemory = "";
	let cwdGlobal = "";
	let sessionStartTime = Date.now();

	// ── Widget throttle ────────────────────────────────────────────────────

	let wDirty = false;
	let wTimer: ReturnType<typeof setTimeout> | null = null;

	function scheduleWidget() {
		wDirty = true;
		if (wTimer) return;
		wTimer = setTimeout(() => { wTimer = null; if (wDirty) { wDirty = false; renderWidget(); } }, WIDGET_THROTTLE_MS);
	}

	function flushWidget() {
		if (wTimer) { clearTimeout(wTimer); wTimer = null; }
		wDirty = false;
		renderWidget();
	}

	// ── Helpers ────────────────────────────────────────────────────────────

	function getAllRoles(): RoleDef[] { return [...ROLES, ...customRoles]; }

	function findRole(name: string): RoleDef {
		return getAllRoles().find(r => r.name.toLowerCase() === name.toLowerCase()) || ROLES[0];
	}

	function makeSessionFile(id: number): string {
		if (!existsSync(sessionDir)) mkdirSync(sessionDir, { recursive: true });
		return join(sessionDir, `nexus-${id}-${Date.now()}.jsonl`);
	}

	function loadCustomRoles(cwd: string) {
		customRoles = [];
		const seen = new Set(ROLES.map(r => r.name));
		for (const dir of [join(cwd, ".pi", "agents"), join(cwd, "agents")]) {
			if (!existsSync(dir)) continue;
			try {
				for (const file of readdirSync(dir)) {
					if (!file.endsWith(".md")) continue;
					const raw = readFileSync(join(dir, file), "utf-8");
					const m = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
					if (!m) continue;
					const fm: Record<string, string> = {};
					for (const line of m[1].split("\n")) {
						const i = line.indexOf(":");
						if (i > 0) fm[line.slice(0, i).trim()] = line.slice(i + 1).trim();
					}
					if (!fm.name || seen.has(fm.name)) continue;
					seen.add(fm.name);
					customRoles.push({ name: fm.name, description: fm.description || "", tools: fm.tools || "read,grep,find,ls", systemPrompt: m[2].trim() });
				}
			} catch {}
		}
	}

	// ── Model Switching ────────────────────────────────────────────────────

	async function switchTier(target: number, reason: string, ctx?: ExtensionContext, thinkingOverride?: "off" | "low" | "medium" | "high") {
		const t = Math.max(0, Math.min(2, target));
		const tier = TIERS[t];
		const model = ctx?.modelRegistry.find(tier.provider, tier.modelId);
		if (!model) return;

		const thinking = thinkingOverride ?? tier.thinking;
		const tierChanged = t !== currentTier;
		const thinkingChanged = thinking !== lastThinking;
		if (!tierChanged && !thinkingChanged) return;

		if (tierChanged) {
			const ok = await pi.setModel(model);
			if (!ok) return;
		}
		pi.setThinkingLevel(thinking);
		lastThinking = thinking;

		const prev = currentTier;
		currentTier = t;
		if (tierChanged) totalSwitches++;
		turnsSinceSwitch = 0;
		const thinkTag = thinkingOverride ? ` 🧠${thinking}` : "";
		tierHistory.push({ tier: t, reason: `${TIERS[prev].icon}→${tier.icon} ${reason}${thinkTag}`, turn: turnCount });
	}

	// ── Widget Rendering ───────────────────────────────────────────────────

	function renderWidget() {
		if (!widgetCtx) return;
		if (agents.size === 0) {
			widgetCtx.ui.setWidget("nexus-agents", undefined);
			return;
		}

		widgetCtx.ui.setWidget("nexus-agents", (_tui: any, theme: any) => {
			const container = new Container();
			const content = new Text("", 0, 0);
			container.addChild(content);

			return {
				render(width: number): string[] {
					const lines: string[] = [];
					const tier = TIERS[currentTier];
					const modeIcon = mode === "hybrid" ? "⚡" : mode === "delegator" ? "📡" : "💬";

					lines.push(theme.fg("dim", "─".repeat(width)));
					lines.push(
						theme.fg("accent", ` ✦ Nexus`) +
						theme.fg("dim", ` ${tier.icon} ${tier.name} · ${modeIcon} ${mode} · `) +
						theme.fg("accent", `${agents.size}`) +
						theme.fg("dim", " agents")
					);

					for (const [, a] of agents) {
						const icon = a.status === "running" ? "●" : a.status === "done" ? "✓" : a.status === "queued" ? "◌" : "✗";
						const color = a.status === "running" ? "accent" : a.status === "done" ? "success" : a.status === "queued" ? "dim" : "error";
						const elapsed = a.status === "running" ? Math.round((Date.now() - a.startTime) / 1000) : Math.round(a.elapsed / 1000);
						const task = a.task.length > 50 ? a.task.slice(0, 47) + "..." : a.task;
						const turn = a.turnCount > 1 ? ` t${a.turnCount}` : "";
						const retry = a.retryCount > 0 ? ` ↺${a.retryCount}` : "";

						lines.push(
							theme.fg(color, `  ${icon} #${a.id}`) +
							theme.fg("dim", ` [${a.role}${turn}${retry}] ${elapsed}s · ${a.toolCount} tools`) +
							(a.cost > 0 ? theme.fg("warning", ` $${a.cost.toFixed(4)}`) : "")
						);
						lines.push(theme.fg("muted", `    ${task}`));

						if (a.status === "running" && a.textChunks.length > 0) {
							const last = a.textChunks.join("").split("\n").filter((l: string) => l.trim()).pop() || "";
							if (last) lines.push(theme.fg("dim", `    → ${last.slice(0, width - 8)}`));
						}
					}

					lines.push(theme.fg("dim", "─".repeat(width)));
					content.setText(lines.join("\n"));
					return container.render(width);
				},
				invalidate() { container.invalidate(); },
			};
		});
	}

	// ── Sub-Agent Process ──────────────────────────────────────────────────

	function processLine(a: SubAgent, line: string) {
		if (!line.trim()) return;
		try {
			const e = JSON.parse(line);
			if (e.type === "message_update" && e.assistantMessageEvent?.type === "text_delta") {
				a.textChunks.push(e.assistantMessageEvent.delta || "");
				scheduleWidget();
			} else if (e.type === "tool_execution_start") {
				a.toolCount++;
				scheduleWidget();
			} else if (e.type === "message_end" || e.type === "agent_end") {
				const msg = e.message || (e.messages || []).reverse().find((m: any) => m.role === "assistant");
				if (msg?.usage) {
					a.inputTokens = msg.usage.input || 0;
					a.outputTokens = msg.usage.output || 0;
					a.cost = msg.usage.cost?.total || 0;
					scheduleWidget();
				}
			}
		} catch {}
	}

	function spawnProcess(a: SubAgent, prompt: string, _ctx: ExtensionContext, isCont: boolean): Promise<{ output: string; exitCode: number }> {
		a.status = "running";
		a.startTime = Date.now();
		scheduleWidget();

		const role = findRole(a.role);
		const model = selectSubAgentModel(a.role, a.task);

		const args = [
			"--mode", "json", "-p", "--no-extensions", "--no-skills",
			"--model", model, "--tools", role.tools, "--thinking", "low",
			"--append-system-prompt", role.systemPrompt,
			"--session", a.sessionFile,
		];
		if (isCont) args.push("-c");
		args.push(prompt);

		return new Promise(resolve => {
			let done = false;
			const fin = (val: { output: string; exitCode: number }) => { if (!done) { done = true; resolve(val); } };

			const proc = cpSpawn("pi", args, { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env } });
			a.proc = proc;
			activeProcs.add(proc);

			a.timer = setInterval(() => { a.elapsed = Date.now() - a.startTime; scheduleWidget(); }, 1000);

			const timeout = setTimeout(() => {
				try { proc.kill("SIGTERM"); } catch {}
				setTimeout(() => { try { proc.kill("SIGKILL"); } catch {} }, 3000);
			}, AGENT_TIMEOUT_MS);

			let buf = "";
			proc.stdout!.setEncoding("utf-8");
			proc.stdout!.on("data", (chunk: string) => {
				buf += chunk;
				const lines = buf.split("\n");
				buf = lines.pop() || "";
				for (const l of lines) processLine(a, l);
			});
			proc.stderr!.setEncoding("utf-8");
			proc.stderr!.on("data", () => {});

			proc.on("close", code => {
				clearTimeout(timeout);
				activeProcs.delete(proc);
				a.proc = undefined;
				if (a.timer) clearInterval(a.timer);
				a.elapsed = Date.now() - a.startTime;
				if (buf.trim()) processLine(a, buf);
				a.status = a.elapsed >= AGENT_TIMEOUT_MS ? "error" : (code === 0 ? "done" : "error");
				flushWidget();
				fin({ output: a.textChunks.join(""), exitCode: code ?? 1 });
			});

			proc.on("error", err => {
				clearTimeout(timeout);
				activeProcs.delete(proc);
				a.proc = undefined;
				if (a.timer) clearInterval(a.timer);
				a.status = "error";
				flushWidget();
				fin({ output: `Error: ${err.message}`, exitCode: 1 });
			});
		});
	}

	function notifyOS(title: string, message: string, status: "success" | "error" | "info" = "info") {
		const sound = status === "error" ? "Basso" : "Glass";
		try {
			cpSpawn("osascript", [
				"-e",
				`display notification "${message.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}" sound name "${sound}"`,
			], { stdio: "ignore", detached: true }).unref();
		} catch {}
		process.stdout.write("\x07");
	}

	async function autoExtractMemory(output: string, task: string, ctx: ExtensionContext): Promise<void> {
		if (!output || output.length < 100) return;
		const haiku = ctx.modelRegistry.find("anthropic", "claude-haiku-4-5");
		if (!haiku) return;
		const apiKey = await ctx.modelRegistry.getApiKey(haiku);
		if (!apiKey) return;
		try {
			const response = await complete(haiku, {
				systemPrompt: `Extract key technical facts from this agent output that would be useful to remember for future sessions. Focus on: tech stack discovered, bugs fixed, architectural decisions, file structure, conventions. Output ONLY a JSON object: {"key": "category_name", "value": "concise fact"} or null if nothing worth remembering.`,
				messages: [{ role: "user" as const, content: `Task: ${task}\n\nOutput:\n${output.slice(0, 3000)}`, timestamp: Date.now() }],
			}, { reasoning: "off" });
			const text = response.content.filter((c): c is { type: "text"; text: string } => c.type === "text").map(c => c.text).join("").trim();
			const jsonStr = text.replace(/```json?\n?/g, "").replace(/```/g, "").trim();
			if (jsonStr === "null" || !jsonStr) return;
			const parsed = JSON.parse(jsonStr);
			if (parsed?.key && parsed?.value) {
				updateMemory(ctx.cwd, parsed.key, parsed.value, "append");
				projectMemory = readMemory(ctx.cwd);
			}
		} catch {}
	}

	function isRecoverableError(output: string): boolean {
		const lower = output.slice(-500).toLowerCase();
		return (
			lower.includes("file not found") ||
			lower.includes("no such file") ||
			lower.includes("command not found") ||
			lower.includes("enoent") ||
			lower.includes("timeout") ||
			lower.includes("connection refused") ||
			lower.includes("permission denied")
		);
	}

	function spawnAndDeliver(a: SubAgent, prompt: string, ctx: ExtensionContext, isCont: boolean) {
		spawnProcess(a, prompt, ctx, isCont).then(async ({ output, exitCode }) => {
			// Self-healing: retry once on recoverable errors
			if (exitCode !== 0 && a.retryCount === 0 && isRecoverableError(output)) {
				const errorSnippet = output.slice(-500);
				a.retryCount = 1;
				a.lastError = errorSnippet;
				a.textChunks = [];
				a.toolCount = 0;
				a.status = "queued";
				ctx.ui.notify(`#${a.id} auto-retrying (recoverable error detected)...`, "info");
				const retryPrompt = `${prompt}\n\n[RETRY CONTEXT: Previous attempt failed with this error — please handle it gracefully]\n${errorSnippet}`;
				spawnProcess(a, retryPrompt, ctx, isCont).then(async (retryResult) => {
					await deliverResult(a, retryResult.output, retryResult.exitCode, ctx);
				});
				return;
			}

			await deliverResult(a, output, exitCode, ctx);
		});
	}

	async function deliverResult(a: SubAgent, output: string, exitCode: number, ctx: ExtensionContext) {
		const status = exitCode === 0 ? "✓" : "✗";
		const elapsed = Math.round(a.elapsed / 1000);
		const turn = a.turnCount > 1 ? ` (Turn ${a.turnCount})` : "";
		const retry = a.retryCount > 0 ? ` ↺${a.retryCount}` : "";
		const truncated = output.length > MAX_RESULT_LEN ? output.slice(0, MAX_RESULT_LEN) + "\n\n... [truncated]" : output;

		ctx.ui.notify(`#${a.id} [${a.role}] ${status} ${elapsed}s`, exitCode === 0 ? "info" : "error");
		notifyOS(
			`Nexus #${a.id} [${a.role}] ${status}`,
			`${elapsed}s · ${a.toolCount} tools · $${a.cost.toFixed(4)} · ${a.task.slice(0, 50)}`,
			exitCode === 0 ? "success" : "error",
		);

		pi.sendMessage({
			customType: "nexus-result",
			content: `**Sub-agent #${a.id}** [${a.role}]${turn}${retry} — ${status} ${elapsed}s | ${a.toolCount} tools | $${a.cost.toFixed(4)}\n\n${truncated}`,
			display: true,
		}, { deliverAs: "followUp", triggerTurn: true });

		// Auto-extract memory on success
		if (exitCode === 0) {
			await autoExtractMemory(output, a.task, ctx);
		}
	}

	// ══════════════════════════════════════════════════════════════════════
	// TOOLS
	// ══════════════════════════════════════════════════════════════════════

	const roleList = ROLES.map(r => `${r.name} (${r.description})`).join(", ");

	pi.registerTool({
		name: "nexus_spawn",
		label: "Spawn Sub-Agent",
		description: `Spawn a background sub-agent for heavy tasks. Preserves main chat context. Results auto-deliver when done.\n\nRoles: ${roleList}. Custom roles from .pi/agents/*.md also available.`,
		promptGuidelines: [
			"Spawn sub-agents for multi-file, long-running, or context-heavy work",
			"Use role='scout' for exploration, 'builder' for code, 'reviewer' for review, 'tester' for tests",
			"Spawn multiple agents in parallel for independent tasks",
			"Continue chatting while sub-agents work — results arrive automatically",
		],
		parameters: Type.Object({
			task: Type.String({ description: "Complete task description — sub-agent receives conversation context automatically" }),
			role: Type.Optional(Type.String({ description: "Agent role. Default: general" })),
		}),
		async execute(_id, params, _sig, onUpdate, ctx) {
			widgetCtx = ctx;
			const id = nextId++;
			const a: SubAgent = {
				id, status: "queued", task: params.task, role: params.role || "general",
				tools: findRole(params.role || "general").tools, textChunks: [], toolCount: 0,
				elapsed: 0, startTime: Date.now(), sessionFile: makeSessionFile(id),
				turnCount: 1, inputTokens: 0, outputTokens: 0, cost: 0, retryCount: 0,
			};
			agents.set(id, a);
			if (onUpdate) onUpdate({ content: [{ type: "text", text: `Spawning #${id} [${a.role}]...` }], details: { id, role: a.role } });
			const enrichedPrompt = await buildContextBridge(ctx, params.task);
			spawnAndDeliver(a, enrichedPrompt, ctx, false);
			return { content: [{ type: "text", text: `Sub-agent #${id} [${a.role}] running in background. Continue chatting.` }], details: { id, role: a.role } };
		},
		renderCall(args, theme) {
			const r = (args as any).role || "general";
			const t = ((args as any).task || "").slice(0, 55);
			return new Text(theme.fg("toolTitle", theme.bold("nexus_spawn ")) + theme.fg("accent", `[${r}] `) + theme.fg("muted", t), 0, 0);
		},
		renderResult(result, _opts, theme) {
			const d = result.details as any;
			if (!d) return undefined;
			return new Text(theme.fg("success", `◉ #${d.id}`) + theme.fg("dim", ` [${d.role}] background`), 0, 0);
		},
	});

	pi.registerTool({
		name: "nexus_continue",
		label: "Continue Sub-Agent",
		description: "Continue a finished sub-agent's conversation with follow-up instructions.",
		parameters: Type.Object({
			id: Type.Number({ description: "Sub-agent ID" }),
			prompt: Type.String({ description: "Follow-up instructions" }),
		}),
		async execute(_cid, params, _sig, _upd, ctx) {
			widgetCtx = ctx;
			const a = agents.get(params.id);
			if (!a) return { content: [{ type: "text", text: `No #${params.id} found.` }], details: undefined };
			if (a.status === "running") return { content: [{ type: "text", text: `#${params.id} still running.` }], details: undefined };
			a.textChunks = []; a.toolCount = 0; a.elapsed = 0; a.turnCount++;
			spawnAndDeliver(a, params.prompt, ctx, true);
			return { content: [{ type: "text", text: `#${params.id} continuing (Turn ${a.turnCount}).` }], details: undefined };
		},
	});

	pi.registerTool({
		name: "nexus_status",
		label: "Agent Status",
		description: "Check all sub-agents.",
		parameters: Type.Object({}),
		async execute() {
			if (agents.size === 0) return { content: [{ type: "text", text: "No sub-agents." }], details: undefined };
			const lines = Array.from(agents.values()).map(a => {
				const e = a.status === "running" ? Math.round((Date.now() - a.startTime) / 1000) : Math.round(a.elapsed / 1000);
				const retry = a.retryCount > 0 ? ` ↺${a.retryCount}` : "";
				return `#${a.id} [${a.role}${retry}] ${a.status} — ${e}s, ${a.toolCount} tools, $${a.cost.toFixed(4)}\n  ${a.task}`;
			});
			return { content: [{ type: "text", text: lines.join("\n") }], details: undefined };
		},
	});

	pi.registerTool({
		name: "nexus_kill",
		label: "Kill Sub-Agent",
		description: "Kill/remove a sub-agent.",
		parameters: Type.Object({ id: Type.Number() }),
		async execute(_cid, params) {
			const a = agents.get(params.id);
			if (!a) return { content: [{ type: "text", text: `No #${params.id}.` }], details: undefined };
			if (a.proc) try { a.proc.kill("SIGTERM"); } catch {}
			if (a.timer) clearInterval(a.timer);
			agents.delete(params.id);
			flushWidget();
			return { content: [{ type: "text", text: `#${params.id} removed.` }], details: undefined };
		},
	});

	pi.registerTool({
		name: "nexus_collect",
		label: "Collect Results",
		description: "Collect and synthesize results from all finished sub-agents.",
		parameters: Type.Object({
			synthesize: Type.Optional(Type.Boolean({ description: "Use LLM to synthesize results. Default: true" })),
		}),
		async execute(_id, params, _sig, _upd, ctx) {
			const done = Array.from(agents.values()).filter(a => a.status === "done" || a.status === "error");
			if (done.length === 0) return { content: [{ type: "text", text: "No results to collect." }], details: undefined };

			let totalCost = 0;
			const parts = done.map(a => {
				const out = a.textChunks.join("");
				const trunc = out.length > 6000 ? out.slice(0, 6000) + "\n... [truncated]" : out;
				totalCost += a.cost;
				return `## ${a.status === "done" ? "✓" : "✗"} #${a.id} [${a.role}] ${Math.round(a.elapsed / 1000)}s\nTask: ${a.task}\n\n${trunc}`;
			});
			const rawOutput = `${done.length} results ($${totalCost.toFixed(4)})\n\n${parts.join("\n\n---\n\n")}`;

			const shouldSynthesize = params.synthesize !== false;
			if (!shouldSynthesize) return { content: [{ type: "text", text: rawOutput }], details: undefined };

			// LLM synthesis with Haiku
			try {
				const haiku = ctx.modelRegistry.find("anthropic", "claude-haiku-4-5");
				if (!haiku) return { content: [{ type: "text", text: rawOutput }], details: undefined };
				const apiKey = await ctx.modelRegistry.getApiKey(haiku);
				if (!apiKey) return { content: [{ type: "text", text: rawOutput }], details: undefined };

				const response = await complete(haiku, {
					systemPrompt: `You are synthesizing results from multiple parallel coding agents. Given their outputs, produce a concise synthesis with these sections:
## Executive Summary
## Key Findings
## Action Items
## Open Questions
Be specific and actionable. Reference specific files/lines where relevant.`,
					messages: [{ role: "user" as const, content: rawOutput.slice(0, 8000), timestamp: Date.now() }],
				}, { reasoning: "off" });

				const synthesis = response.content
					.filter((c): c is { type: "text"; text: string } => c.type === "text")
					.map(c => c.text).join("");

				return { content: [{ type: "text", text: `${synthesis}\n\n---\n\n### Raw Outputs\n${rawOutput}` }], details: undefined };
			} catch {
				return { content: [{ type: "text", text: rawOutput }], details: undefined };
			}
		},
	});

	pi.registerTool({
		name: "nexus_remember",
		label: "Store Memory",
		description: "Store important project facts in persistent memory (.pi/memory.md). Use for tech stack, conventions, key decisions, recurring issues.",
		parameters: Type.Object({
			key: Type.String({ description: "Memory category e.g. 'tech_stack', 'conventions', 'bug_fixes'" }),
			value: Type.String({ description: "What to remember" }),
			mode: Type.Optional(Type.Union([Type.Literal("append"), Type.Literal("replace")])),
		}),
		async execute(_id, params, _sig, _upd, ctx) {
			updateMemory(ctx.cwd, params.key, params.value, params.mode || "replace");
			projectMemory = readMemory(ctx.cwd);
			return { content: [{ type: "text", text: `Stored in memory: [${params.key}] ${params.value.slice(0, 80)}` }], details: undefined };
		},
	});

	pi.registerTool({
		name: "nexus_plan",
		label: "Plan & Spawn",
		description: "Decompose a complex goal into parallel + sequential subtasks, then spawn optimal agents for each.",
		parameters: Type.Object({
			goal: Type.String({ description: "High-level goal to accomplish" }),
			context: Type.Optional(Type.String({ description: "Additional context" })),
		}),
		async execute(_id, params, _sig, onUpdate, ctx) {
			widgetCtx = ctx;
			if (onUpdate) onUpdate({ content: [{ type: "text", text: "Planning task decomposition..." }], details: undefined });

			const haiku = ctx.modelRegistry.find("anthropic", "claude-haiku-4-5");
			if (!haiku) return { content: [{ type: "text", text: "Planner unavailable (no Haiku model)." }], details: undefined };
			const apiKey = await ctx.modelRegistry.getApiKey(haiku);
			if (!apiKey) return { content: [{ type: "text", text: "No API key for planner." }], details: undefined };

			const availableRoles = getAllRoles().map(r => r.name).join(", ");
			const planPrompt = `Decompose this goal into subtasks for a coding agent team.
Goal: ${params.goal}
${params.context ? `Context: ${params.context}` : ""}
Available roles: ${availableRoles}
cwd: ${ctx.cwd}

Respond with ONLY JSON (no markdown):
{
  "parallel": [{"role": "...", "task": "..."}],
  "sequential": [{"role": "...", "task": "...", "dependsOn": "parallel"}]
}

Rules:
- parallel: tasks that can run simultaneously (e.g. scout different areas, review different files)
- sequential: tasks that need parallel results first (e.g. builder after scouts report)
- max 3 parallel tasks, max 2 sequential
- tasks must be self-contained with enough context to execute
- use scout for exploration, builder for implementation, reviewer/tester for QA`;

			try {
				const response = await complete(haiku, {
					systemPrompt: "You are a task planning expert for a software engineering team.",
					messages: [{ role: "user" as const, content: planPrompt, timestamp: Date.now() }],
				}, { reasoning: "off" });

				const text = response.content.filter((c): c is { type: "text"; text: string } => c.type === "text").map(c => c.text).join("");
				const jsonStr = text.replace(/```json?\n?/g, "").replace(/```/g, "").trim();
				const plan = JSON.parse(jsonStr);

				const parallel: Array<{ role: string; task: string }> = plan.parallel || [];
				const sequential: Array<{ role: string; task: string }> = plan.sequential || [];

				if (onUpdate) {
					const planSummary = [
						`Plan for: ${params.goal}`,
						`Parallel (${parallel.length}): ${parallel.map(p => `[${p.role}] ${p.task.slice(0, 40)}`).join(", ")}`,
						sequential.length > 0 ? `Sequential (${sequential.length}): ${sequential.map(s => `[${s.role}] ${s.task.slice(0, 40)}`).join(", ")}` : "",
					].filter(Boolean).join("\n");
					onUpdate({ content: [{ type: "text", text: planSummary }], details: undefined });
				}

				const spawnedIds: number[] = [];

				// Spawn parallel agents
				for (const p of parallel) {
					const id = nextId++;
					const role = findRole(p.role);
					const a: SubAgent = {
						id, status: "queued", task: p.task, role: p.role,
						tools: role.tools, textChunks: [], toolCount: 0,
						elapsed: 0, startTime: Date.now(), sessionFile: makeSessionFile(id),
						turnCount: 1, inputTokens: 0, outputTokens: 0, cost: 0, retryCount: 0,
					};
					agents.set(id, a);
					spawnedIds.push(id);
					const enrichedPrompt = await buildContextBridge(ctx, p.task);
					spawnAndDeliver(a, enrichedPrompt, ctx, false);
				}

				// Queue sequential agents (spawn after a short note — they'll wait for parallel in the real world,
				// but here we spawn them with a note about depending on parallel results)
				for (const s of sequential) {
					const id = nextId++;
					const role = findRole(s.role);
					const a: SubAgent = {
						id, status: "queued", task: s.task, role: s.role,
						tools: role.tools, textChunks: [], toolCount: 0,
						elapsed: 0, startTime: Date.now(), sessionFile: makeSessionFile(id),
						turnCount: 1, inputTokens: 0, outputTokens: 0, cost: 0, retryCount: 0,
					};
					agents.set(id, a);
					spawnedIds.push(id);
					const enrichedPrompt = await buildContextBridge(ctx, `${s.task}\n\n[Note: This task depends on parallel agents completing first. Parallel agent IDs: ${spawnedIds.slice(0, parallel.length).join(", ")}]`);
					spawnAndDeliver(a, enrichedPrompt, ctx, false);
				}

				const summary = `Plan executed: ${parallel.length} parallel + ${sequential.length} sequential agents spawned (IDs: ${spawnedIds.join(", ")})`;
				return { content: [{ type: "text", text: summary }], details: { plan, spawnedIds } };
			} catch (err: any) {
				return { content: [{ type: "text", text: `Planning failed: ${err?.message}. Try /nx spawn manually.` }], details: undefined };
			}
		},
	});

	// ══════════════════════════════════════════════════════════════════════
	// CORE ROUTING LOGIC — LLM-POWERED
	// ══════════════════════════════════════════════════════════════════════

	pi.on("before_agent_start", async (event, ctx) => {
		widgetCtx = ctx;
		const usage = ctx.getContextUsage();
		const contextPct = usage?.percent ?? null;
		const activeAgentCount = Array.from(agents.values()).filter(a => a.status === "running").length;

		// Extract memory snippet for classifier context
		const memorySnippet = projectMemory ? projectMemory.slice(0, 300) : "";

		// ── Step 1: LLM Classification ─────────────────────────────────────
		let classification: ClassificationResult | null = null;
		if (!tierLocked || (autoDelegate && mode !== "interactive")) {
			ctx.ui.setWorkingMessage("classifying task...");
			classification = await classifyWithLLM(event.prompt, ctx, contextPct, activeAgentCount, memorySnippet);
			ctx.ui.setWorkingMessage();
			classifierCost += (150 * 0.80 + 30 * 4.00) / 1_000_000;
		}

		// ── Step 2: Model Routing ──────────────────────────────────────────
		if (!tierLocked && classification) {
			let target = classification.tier - 1;
			let thinking = classification.thinking;
			if (target === 0 && totalToolCalls > 10) target = 1;
			if (consecutiveErrors >= 2 && target < 2) {
				target = Math.min(target + 1, 2);
				if (thinking === "off" || thinking === "low") thinking = "medium";
			}
			if (contextPct !== null && contextPct > 80 && target < 1) target = 1;
			await switchTier(target, classification.reason, ctx, thinking);
		}

		// ── Step 3: Build system prompt addons ────────────────────────────
		let systemAddons = "";

		// Always inject project memory if available
		if (projectMemory.trim()) {
			systemAddons += `\n\n## Project Memory\n${projectMemory}`;
		}

		// ── Step 4: Auto-Delegation ────────────────────────────────────────
		if (classification?.offload && autoDelegate && (mode === "hybrid" || mode === "delegator")) {
			if (contextPct !== null && contextPct > CONTEXT_WARN_PCT) {
				ctx.ui.setStatus("nexus-ctx", `⚠️ Context ${Math.round(contextPct)}%`);
			} else {
				ctx.ui.setStatus("nexus-ctx", undefined);
			}

			const roles = getAllRoles().map(r => `${r.name}: ${r.description}`).join("\n");
			const contextWarn = (contextPct !== null && contextPct > CONTEXT_WARN_PCT)
				? `\n⚠️ CONTEXT AT ${Math.round(contextPct)}% — strongly prefer sub-agents for this work.\n`
				: "";

			return {
				systemPrompt: event.systemPrompt + systemAddons + `\n\n## Nexus: Sub-Agent Delegation Recommended
The task classifier determined this task should be **offloaded to a sub-agent** (reason: "${classification.reason}").

You have these tools: nexus_spawn, nexus_continue, nexus_plan, nexus_status, nexus_kill, nexus_collect, nexus_remember.
${contextWarn}
**Strongly prefer spawning a sub-agent** for this task rather than doing it inline. This preserves the main chat context.

Available roles:\n${roles}

Sub-agents run in background with their own context. You can spawn multiple for parallel work. Results auto-deliver.`,
			};
		} else {
			ctx.ui.setStatus("nexus-ctx", undefined);
			if (systemAddons) {
				return { systemPrompt: event.systemPrompt + systemAddons };
			}
		}
	});

	// ── Turn & Tool Tracking ───────────────────────────────────────────────

	pi.on("turn_start", async () => {
		toolsThisTurn = 0;
		editWriteThisTurn = 0;
		turnCount++;
		turnsSinceSwitch++;
	});

	pi.on("tool_execution_start", async (event) => {
		toolsThisTurn++;
		totalToolCalls++;
		if (event.toolName === "edit" || event.toolName === "write") editWriteThisTurn++;
	});

	pi.on("tool_execution_end", async (event, ctx) => {
		if (event.isError) {
			consecutiveErrors++;
			if (consecutiveErrors >= 2 && !tierLocked && currentTier < 2) {
				await switchTier(currentTier + 1, `${consecutiveErrors} consecutive errors`, ctx);
			}
		} else {
			consecutiveErrors = 0;
		}
	});

	pi.on("turn_end", async (_event, ctx) => {
		if (!tierLocked) {
			if (editWriteThisTurn >= 4 && currentTier < 2) {
				await switchTier(2, `heavy: ${editWriteThisTurn} edits/writes`, ctx);
			} else if (toolsThisTurn >= 6 && currentTier < 1) {
				await switchTier(1, `busy: ${toolsThisTurn} tool calls`, ctx);
			}
		}
		const opusCost = (2000 * TIERS[2].inputCost + 1000 * TIERS[2].outputCost) / 1e6;
		const actualCost = (2000 * TIERS[currentTier].inputCost + 1000 * TIERS[currentTier].outputCost) / 1e6;
		savedVsOpus += opusCost - actualCost;
	});

	// ── /nx Command ────────────────────────────────────────────────────────

	pi.registerCommand("nx", {
		description: "Nexus controls: /nx [spawn|plan|memory|retry|chain|kill|clear|mode|auto|lock|unlock|tier]",
		getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
			const items = [
				{ value: "spawn ", label: "spawn <task>" },
				{ value: "plan ", label: "plan <goal>" },
				{ value: "memory", label: "show memory" },
				{ value: "retry ", label: "retry <id>" },
				{ value: "chain ", label: "chain <id> <task>" },
				{ value: "kill ", label: "kill <id>" },
				{ value: "clear", label: "clear all agents" },
				{ value: "mode hybrid", label: "mode hybrid" },
				{ value: "mode interactive", label: "mode interactive" },
				{ value: "mode delegator", label: "mode delegator" },
				{ value: "auto on", label: "auto on" },
				{ value: "auto off", label: "auto off" },
				{ value: "lock", label: "lock model" },
				{ value: "unlock", label: "unlock model" },
				{ value: "tier ", label: "tier 1|2|3" },
			];
			return items.filter(i => i.value.startsWith(prefix));
		},

		async handler(args, ctx) {
			widgetCtx = ctx;
			const parts = (args || "").trim().split(/\s+/);
			const sub = parts[0]?.toLowerCase();

			if (!sub) {
				const tier = TIERS[currentTier];
				const modeIcon = mode === "hybrid" ? "⚡" : mode === "delegator" ? "📡" : "💬";
				const usage = ctx.getContextUsage();
				const pct = usage?.percent ? Math.round(usage.percent) : "?";
				const agentLines = agents.size === 0 ? "  (none)" : Array.from(agents.values()).map(a => {
					const e = a.status === "running" ? Math.round((Date.now() - a.startTime) / 1000) : Math.round(a.elapsed / 1000);
					const retry = a.retryCount > 0 ? ` ↺${a.retryCount}` : "";
					return `  #${a.id} [${a.role}${retry}] ${a.status} ${e}s $${a.cost.toFixed(4)} — ${a.task.slice(0, 50)}`;
				}).join("\n");

				const lastRoute = tierHistory.length > 0
					? tierHistory.slice(-3).map(h => `  Turn ${h.turn}: ${h.reason}`).join("\n")
					: "  (none yet)";

				const memFile = join(ctx.cwd, MEMORY_FILE);
				const memStatus = existsSync(memFile)
					? `exists (${Math.round(readFileSync(memFile, "utf-8").length / 1024 * 10) / 10}KB)`
					: "none";

				pi.sendMessage({
					customType: "nexus-dashboard",
					content: [
						``,
						`✦ Nexus Dashboard v2`,
						`  Model: ${tier.icon} ${tier.name} [${lastThinking}]${tierLocked ? " 🔒" : ""}`,
						`  Mode: ${modeIcon} ${mode} | Auto: ${autoDelegate ? "on" : "off"} | Context: ${pct}%`,
						`  Switches: ${totalSwitches} | Saved: $${savedVsOpus.toFixed(4)} | Classifier: $${classifierCost.toFixed(4)}`,
						`  Memory: ${memStatus}`,
						``,
						`  Sub-Agents (${agents.size}):`,
						agentLines,
						``,
						`  Recent Routing:`,
						lastRoute,
						``,
						`  /nx spawn|plan|memory|retry|chain|kill|clear|mode|auto|lock|unlock|tier`,
						``,
					].join("\n"),
					display: true,
				});
				return;
			}

			if (sub === "spawn") {
				const task = parts.slice(1).join(" ").trim();
				if (!task) { ctx.ui.notify("Usage: /nx spawn <task>", "error"); return; }
				const id = nextId++;
				const a: SubAgent = { id, status: "queued", task, role: "general", tools: ROLES[0].tools, textChunks: [], toolCount: 0, elapsed: 0, startTime: Date.now(), sessionFile: makeSessionFile(id), turnCount: 1, inputTokens: 0, outputTokens: 0, cost: 0, retryCount: 0 };
				agents.set(id, a);
				const enrichedPrompt = await buildContextBridge(ctx, task);
				spawnAndDeliver(a, enrichedPrompt, ctx, false);
				ctx.ui.notify(`#${id} spawned`, "info");

			} else if (sub === "plan") {
				const goal = parts.slice(1).join(" ").trim();
				if (!goal) { ctx.ui.notify("Usage: /nx plan <goal>", "error"); return; }
				ctx.ui.notify(`Planning: ${goal.slice(0, 50)}...`, "info");
				// Run planning inline
				const haiku = ctx.modelRegistry.find("anthropic", "claude-haiku-4-5");
				if (!haiku) { ctx.ui.notify("Planner unavailable (no Haiku model)", "error"); return; }
				const apiKey = await ctx.modelRegistry.getApiKey(haiku);
				if (!apiKey) { ctx.ui.notify("No API key for planner", "error"); return; }
				try {
					const availableRoles = getAllRoles().map(r => r.name).join(", ");
					const resp = await complete(haiku, {
						systemPrompt: "You are a task planning expert. Decompose goals into parallel and sequential coding subtasks. Respond ONLY with JSON, no markdown.",
						messages: [{ role: "user" as const, content: `Goal: ${goal}\nRoles: ${availableRoles}\ncwd: ${ctx.cwd}\n\nRespond with JSON:\n{"parallel":[{"role":"...","task":"..."}],"sequential":[{"role":"...","task":"...","dependsOn":"parallel"}]}\nMax 3 parallel, 2 sequential. Make tasks self-contained.`, timestamp: Date.now() }],
					}, { reasoning: "off" });
					const text = resp.content.filter((c): c is { type: "text"; text: string } => c.type === "text").map(c => c.text).join("");
					const plan = JSON.parse(text.replace(/```json?\n?/g, "").replace(/```/g, "").trim());
					const parallel: Array<{ role: string; task: string }> = plan.parallel || [];
					const sequential: Array<{ role: string; task: string }> = plan.sequential || [];
					const spawnedIds: number[] = [];
					for (const p of parallel) {
						const id = nextId++;
						const role = findRole(p.role);
						const a: SubAgent = { id, status: "queued", task: p.task, role: p.role, tools: role.tools, textChunks: [], toolCount: 0, elapsed: 0, startTime: Date.now(), sessionFile: makeSessionFile(id), turnCount: 1, inputTokens: 0, outputTokens: 0, cost: 0, retryCount: 0 };
						agents.set(id, a);
						spawnedIds.push(id);
						const ep = await buildContextBridge(ctx, p.task);
						spawnAndDeliver(a, ep, ctx, false);
					}
					for (const s of sequential) {
						const id = nextId++;
						const role = findRole(s.role);
						const a: SubAgent = { id, status: "queued", task: s.task, role: s.role, tools: role.tools, textChunks: [], toolCount: 0, elapsed: 0, startTime: Date.now(), sessionFile: makeSessionFile(id), turnCount: 1, inputTokens: 0, outputTokens: 0, cost: 0, retryCount: 0 };
						agents.set(id, a);
						spawnedIds.push(id);
						const ep = await buildContextBridge(ctx, `${s.task}\n\n[Depends on parallel agents: ${spawnedIds.slice(0, parallel.length).join(", ")}]`);
						spawnAndDeliver(a, ep, ctx, false);
					}
					ctx.ui.notify(`Plan: ${parallel.length} parallel + ${sequential.length} sequential spawned (${spawnedIds.join(", ")})`, "info");
				} catch (err: any) {
					ctx.ui.notify(`Plan failed: ${err?.message?.slice(0, 60)}`, "error");
				}

			} else if (sub === "memory") {
				const mem = readMemory(ctx.cwd);
				pi.sendMessage({
					customType: "nexus-memory",
					content: mem ? `**Project Memory** (.pi/memory.md)\n\n${mem}` : "No memory yet. Use nexus_remember or ask the agent to remember something.",
					display: true,
				});

			} else if (sub === "retry") {
				const id = parseInt(parts[1], 10);
				const a = agents.get(id);
				if (!a) { ctx.ui.notify(`No #${id}`, "error"); return; }
				if (a.status === "running") { ctx.ui.notify(`#${id} is still running`, "error"); return; }
				a.textChunks = []; a.toolCount = 0; a.elapsed = 0; a.retryCount++;
				a.status = "queued";
				ctx.ui.notify(`#${id} retrying (manual)`, "info");
				const enrichedPrompt = await buildContextBridge(ctx, a.task);
				spawnAndDeliver(a, enrichedPrompt, ctx, false);

			} else if (sub === "chain") {
				const id = parseInt(parts[1], 10);
				const task = parts.slice(2).join(" ").trim();
				if (!id || !task) { ctx.ui.notify("Usage: /nx chain <id> <task>", "error"); return; }
				const a = agents.get(id);
				if (!a) { ctx.ui.notify(`No #${id}`, "error"); return; }
				if (a.status === "running") { ctx.ui.notify(`#${id} still running`, "error"); return; }
				a.textChunks = []; a.toolCount = 0; a.elapsed = 0; a.turnCount++;
				ctx.ui.notify(`#${id} chaining: ${task.slice(0, 40)}`, "info");
				spawnAndDeliver(a, task, ctx, true);

			} else if (sub === "kill") {
				const id = parseInt(parts[1], 10);
				const a = agents.get(id);
				if (!a) { ctx.ui.notify(`No #${id}`, "error"); return; }
				if (a.proc) try { a.proc.kill("SIGTERM"); } catch {}
				if (a.timer) clearInterval(a.timer);
				agents.delete(id);
				flushWidget();
				ctx.ui.notify(`#${id} killed`, "info");

			} else if (sub === "clear") {
				for (const [, a] of agents) {
					if (a.proc) try { a.proc.kill("SIGTERM"); } catch {}
					if (a.timer) clearInterval(a.timer);
				}
				agents.clear();
				nextId = 1;
				flushWidget();
				ctx.ui.notify("Cleared", "info");

			} else if (sub === "mode") {
				const v = parts[1]?.toLowerCase() as AgentMode;
				if (["interactive", "hybrid", "delegator"].includes(v)) { mode = v; ctx.ui.notify(`Mode: ${v}`, "info"); }
				else ctx.ui.notify("Usage: /nx mode interactive|hybrid|delegator", "error");

			} else if (sub === "auto") {
				if (parts[1] === "on") { autoDelegate = true; ctx.ui.notify("Auto ON", "info"); }
				else if (parts[1] === "off") { autoDelegate = false; ctx.ui.notify("Auto OFF", "info"); }
				else ctx.ui.notify("Usage: /nx auto on|off", "error");

			} else if (sub === "lock") {
				tierLocked = true; ctx.ui.notify("🔒 Model locked", "info");

			} else if (sub === "unlock") {
				tierLocked = false; ctx.ui.notify("🔓 Model unlocked", "info");

			} else if (sub === "tier") {
				const n = parseInt(parts[1], 10);
				if (n >= 1 && n <= 3) {
					await switchTier(n - 1, `manual /nx tier ${n}`, ctx);
					ctx.ui.notify(`${TIERS[n - 1].icon} ${TIERS[n - 1].name}`, "info");
				} else ctx.ui.notify("Usage: /nx tier 1|2|3", "error");

			} else {
				ctx.ui.notify("Unknown. Try: /nx spawn|plan|memory|retry|chain|kill|clear|mode|auto|lock|unlock|tier", "error");
			}
		},
	});

	// ── Session Start ──────────────────────────────────────────────────────

	pi.on("session_start", async (_event, ctx) => {
		applyExtensionDefaults(import.meta.url, ctx);
		widgetCtx = ctx;
		cwdGlobal = ctx.cwd;
		sessionStartTime = Date.now();
		sessionDir = join(ctx.cwd, ".pi", "agent-sessions", "nexus");
		loadCustomRoles(ctx.cwd);

		// Load persistent memory
		projectMemory = readMemory(ctx.cwd);

		// Cleanup previous
		for (const [, a] of agents) {
			if (a.proc) try { a.proc.kill("SIGTERM"); } catch {}
			if (a.timer) clearInterval(a.timer);
		}
		agents.clear();
		nextId = 1;

		// Set initial model (Sonnet = balanced start)
		const init = TIERS[1];
		const model = ctx.modelRegistry.find(init.provider, init.modelId);
		if (model) {
			await pi.setModel(model);
			pi.setThinkingLevel(init.thinking);
		}
		currentTier = 1;

		// Footer
		ctx.ui.setFooter((_tui, theme, footerData) => {
			const unsub = footerData.onBranchChange(() => _tui.requestRender());
			return {
				dispose: unsub,
				invalidate() {},
				render(width: number): string[] {
					const tier = TIERS[currentTier];
					const lockStr = tierLocked ? " 🔒" : "";
					const usage = ctx.getContextUsage();
					const pct = usage?.percent ?? 0;
					const filled = Math.round(pct / 10) || 1;

					let tokIn = 0, tokOut = 0, cost = 0;
					for (const entry of ctx.sessionManager.getBranch()) {
						if (entry.type === "message" && (entry as any).message.role === "assistant") {
							const m = (entry as any).message as AssistantMsg;
							tokIn += m.usage.input;
							tokOut += m.usage.output;
							cost += m.usage.cost.total;
						}
					}
					for (const [, a] of agents) cost += a.cost;
					cost += classifierCost;
					const fmt = (n: number) => n < 1000 ? `${n}` : `${(n / 1000).toFixed(1)}k`;

					const l1L = theme.fg("dim", " ") + theme.fg("accent", `${tier.icon} ${tier.name}`) +
						theme.fg("dim", ` [${lastThinking}]${lockStr} `) +
						theme.fg("warning", "[") + theme.fg("success", "#".repeat(filled)) +
						theme.fg("dim", "-".repeat(10 - filled)) + theme.fg("warning", "]") +
						theme.fg("dim", " ") + theme.fg("accent", `${Math.round(pct)}%`);
					const l1R = theme.fg("success", fmt(tokIn)) + theme.fg("dim", " in ") +
						theme.fg("accent", fmt(tokOut)) + theme.fg("dim", " out ") +
						theme.fg("warning", `$${cost.toFixed(4)} `);
					const p1 = " ".repeat(Math.max(1, width - visibleWidth(l1L) - visibleWidth(l1R)));

					const dir = basename(ctx.cwd);
					const branch = footerData.getGitBranch();
					const modeIcon = mode === "hybrid" ? "⚡" : mode === "delegator" ? "📡" : "💬";
					const running = Array.from(agents.values()).filter(a => a.status === "running").length;
					const memExists = existsSync(join(ctx.cwd, MEMORY_FILE));

					const l2L = theme.fg("dim", ` ${dir}`) +
						(branch ? theme.fg("dim", " ") + theme.fg("warning", "(") + theme.fg("success", branch) + theme.fg("warning", ")") : "") +
						theme.fg("dim", ` · ${modeIcon} ${mode}`) +
						(memExists ? theme.fg("dim", " · 🧠mem") : "");
					const l2R = (running > 0 ? theme.fg("accent", `● ${running} running `) : "") +
						(agents.size > 0 ? theme.fg("dim", `${agents.size} agents `) : "") +
						(savedVsOpus > 0 ? theme.fg("success", `saved $${savedVsOpus.toFixed(3)} `) : "");
					const p2 = " ".repeat(Math.max(1, width - visibleWidth(l2L) - visibleWidth(l2R)));

					return [
						truncateToWidth(l1L + p1 + l1R, width, ""),
						truncateToWidth(l2L + p2 + l2R, width, ""),
					];
				},
			};
		});

		const roles = getAllRoles().map(r => r.name).join(", ");
		const memStatus = projectMemory ? ` · 🧠 memory loaded` : "";
		ctx.ui.notify(
			`✦ Nexus v2 active [${mode}] — LLM-powered routing\n` +
			`Model: ${TIERS[1].icon} ${TIERS[1].name} (Haiku classifies → auto-routes)\n` +
			`Roles: ${roles}${memStatus}\n` +
			`/nx — Dashboard & controls`,
			"info",
		);
	});

	// ── Session Shutdown — Write Summary ───────────────────────────────────

	pi.on("session_shutdown", async () => {
		killAll();

		// Write session summary
		try {
			if (!cwdGlobal) return;
			const sessDir = join(cwdGlobal, SESSIONS_DIR);
			if (!existsSync(sessDir)) mkdirSync(sessDir, { recursive: true });

			const now = new Date();
			const pad = (n: number) => String(n).padStart(2, "0");
			const filename = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}.md`;
			const elapsed = Math.round((Date.now() - sessionStartTime) / 1000);

			let mainCost = 0;
			let agentCost = 0;
			for (const [, a] of agents) agentCost += a.cost;

			const done = Array.from(agents.values()).filter(a => a.status === "done");
			const errored = Array.from(agents.values()).filter(a => a.status === "error");
			const modelsUsed = new Set<string>();
			modelsUsed.add(TIERS[currentTier].name);
			for (const [, a] of agents) modelsUsed.add(selectSubAgentModel(a.role, a.task).split("/").pop() || "unknown");

			const summary = [
				`# Nexus Session — ${now.toISOString().slice(0, 16).replace("T", " ")}`,
				``,
				`## Overview`,
				`- **Elapsed**: ${elapsed}s`,
				`- **Total Cost**: $${(mainCost + agentCost + classifierCost).toFixed(4)} (main + $${agentCost.toFixed(4)} agents + $${classifierCost.toFixed(4)} classifier)`,
				`- **Model Switches**: ${totalSwitches}`,
				`- **Tool Calls**: ${totalToolCalls}`,
				``,
				`## Sub-Agents`,
				`- Completed: ${done.length}`,
				`- Failed: ${errored.length}`,
				done.length > 0 ? `\n### Completed Tasks\n${done.map(a => `- [${a.role}] ${a.task.slice(0, 80)}`).join("\n")}` : "",
				errored.length > 0 ? `\n### Failed Tasks\n${errored.map(a => `- [${a.role}] ${a.task.slice(0, 80)}${a.lastError ? ` — ${a.lastError.slice(0, 100)}` : ""}`).join("\n")}` : "",
				``,
				`## Models Used`,
				Array.from(modelsUsed).map(m => `- ${m}`).join("\n"),
				``,
				`## Routing History`,
				tierHistory.slice(-10).map(h => `- Turn ${h.turn}: ${h.reason}`).join("\n") || "- (none)",
			].filter(s => s !== "").join("\n");

			writeFileSync(join(sessDir, filename), summary, "utf-8");
		} catch {}
	});

	// ── Cleanup ────────────────────────────────────────────────────────────

	function killAll() {
		for (const p of activeProcs) try { p.kill("SIGTERM"); } catch {}
		setTimeout(() => { for (const p of activeProcs) try { p.kill("SIGKILL"); } catch {} }, 3000);
	}

	process.on("exit", killAll);
	process.on("SIGINT", () => { killAll(); process.exit(0); });
	process.on("SIGTERM", () => { killAll(); process.exit(0); });
}
