"""
Terminal Broker MCP Server — Cross-platform message broker for AI coding agents.

Works with: Claude Code, Gemini CLI, Cursor, Codex, OpenCode, or any MCP client.
Non-MCP clients can use the CLI: python -m brynq.cli post/read/status

Each client spawns its own stdio MCP instance. All instances share JSONL files
on disk, enabling cross-terminal and cross-platform communication.

Usage (MCP):
  Add to .mcp.json, restarts auto-connect.

Usage (CLI — for Gemini, scripts, cron):
  python -m brynq.cli post --channel gen21 --message "backfill done"
  python -m brynq.cli read --channel gen21
  python -m brynq.cli status

Storage layout:
  state/broker/
    channels/{channel}.jsonl   — append-only message log per channel
    sessions/{session_id}.json — session heartbeat + metadata
    cursors/{session_id}_{channel}.cursor — read position tracking

Environment variables:
  BROKER_SESSION_ID    — Override session ID (default: random 8 hex chars)
  BROKER_SESSION_NAME  — Override session name (default: terminal-{id})
  BROKER_PLATFORM      — Platform tag: claude, gemini, cursor, script, etc.
  BROKER_DIR           — Override storage directory
"""

import asyncio
import json
from pathlib import Path as _Path
import os
import sys
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional

from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import TextContent, Tool, ToolAnnotations

from .mcp_plugin_loader import MCPServerPluginLoader

# Annotations that tell MCP clients all Brynq tools are safe to auto-approve.
# No destructive operations, no external network calls — just local file I/O.
_SAFE_WRITE = ToolAnnotations(readOnlyHint=False, destructiveHint=False, idempotentHint=False, openWorldHint=False)
_SAFE_READ = ToolAnnotations(readOnlyHint=True, destructiveHint=False, idempotentHint=True, openWorldHint=False)
_OPEN_READ = ToolAnnotations(readOnlyHint=True, destructiveHint=False, idempotentHint=True, openWorldHint=True)
_OPEN_WRITE = ToolAnnotations(readOnlyHint=False, destructiveHint=False, idempotentHint=False, openWorldHint=True)
_DESTRUCTIVE = ToolAnnotations(readOnlyHint=False, destructiveHint=True, idempotentHint=False, openWorldHint=False)

# ── Paths ──────────────────────────────────────────────────────────────────

PROJECT_ROOT = Path(__file__).resolve().parents[2]
BROKER_DIR = Path(os.environ.get("BROKER_DIR", PROJECT_ROOT / "state" / "broker"))
CHANNELS_DIR = BROKER_DIR / "channels"
SESSIONS_DIR = BROKER_DIR / "sessions"
CURSORS_DIR = BROKER_DIR / "cursors"

for d in (CHANNELS_DIR, SESSIONS_DIR, CURSORS_DIR):
    d.mkdir(parents=True, exist_ok=True)

# ── Broker Registry (central discovery across projects) ───────────────────

REGISTRY_DIR = Path.home() / ".brynq"
REGISTRY_FILE = REGISTRY_DIR / "registry.json"


def _register_broker() -> None:
    """Register this broker's directory in the central registry so the
    dashboard can discover all active brokers across projects."""
    REGISTRY_DIR.mkdir(parents=True, exist_ok=True)
    registry: dict = {}
    try:
        if REGISTRY_FILE.exists():
            registry = json.loads(REGISTRY_FILE.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        registry = {}

    brokers = registry.get("brokers", {})
    broker_key = str(BROKER_DIR.resolve())
    project_name = PROJECT_ROOT.name if PROJECT_ROOT.name else "unknown"

    brokers[broker_key] = {
        "project": project_name,
        "broker_dir": str(BROKER_DIR.resolve()),
        "registered": datetime.now(timezone.utc).isoformat(),
        "pid": os.getpid(),
    }
    registry["brokers"] = brokers
    REGISTRY_FILE.write_text(json.dumps(registry, indent=2, default=str))


def get_registered_brokers() -> list[dict]:
    """Return all registered broker directories with metadata."""
    try:
        if not REGISTRY_FILE.exists():
            return []
        registry = json.loads(REGISTRY_FILE.read_text("utf-8"))
        brokers = []
        for path, meta in registry.get("brokers", {}).items():
            broker_dir = Path(path)
            if broker_dir.exists() and (broker_dir / "channels").exists():
                meta["broker_dir"] = str(broker_dir)
                brokers.append(meta)
        return brokers
    except (json.JSONDecodeError, OSError):
        return []


_register_broker()

# ── Session Identity ───────────────────────────────────────────────────────

def _detect_platform() -> str:
    """Auto-detect which AI platform is running this server."""
    if os.environ.get("BROKER_PLATFORM"):
        return os.environ["BROKER_PLATFORM"]
    if os.environ.get("CLAUDE_CODE"):
        return "claude"
    if os.environ.get("GEMINI_API_KEY") or "gemini" in sys.argv[0].lower():
        return "gemini"
    if os.environ.get("CURSOR_SESSION"):
        return "cursor"
    if os.environ.get("CODEX_SESSION") or "codex" in sys.argv[0].lower():
        return "codex"
    # Check parent process name for platform hints
    try:
        import psutil
        parent = psutil.Process(os.getppid()).name().lower()
        for name in ("claude", "gemini", "cursor", "codex"):
            if name in parent:
                return name
    except Exception:
        pass
    # Check common env vars that indicate an AI agent context
    if os.environ.get("TERM_PROGRAM"):
        term = os.environ["TERM_PROGRAM"].lower()
        for name in ("claude", "cursor", "codex"):
            if name in term:
                return name
    return "cli"


def _resolve_session_id() -> str:
    """Resolve session ID with file-backed persistence for cursor continuity.

    Priority: BROKER_SESSION_ID env var > persisted file > generate new.
    """
    env_id = os.environ.get("BROKER_SESSION_ID")
    if env_id:
        return env_id
    id_file = BROKER_DIR / ".default_session_id"
    try:
        if id_file.exists():
            stored = id_file.read_text("utf-8").strip()
            if stored:
                return stored
    except OSError:
        pass
    new_id = uuid.uuid4().hex[:8]
    try:
        id_file.write_text(new_id)
    except OSError:
        pass
    return new_id


SESSION_ID = _resolve_session_id()
SESSION_NAME = os.environ.get("BROKER_SESSION_NAME", f"terminal-{SESSION_ID}")
PLATFORM = _detect_platform()


# ── Stale Session Cleanup ─────────────────────────────────────────────────

STALE_THRESHOLD_SECONDS = 600  # 10 minutes


def _cleanup_stale_sessions_sync() -> list[str]:
    """Remove session files that have been stale for over the threshold. Returns removed names."""
    removed = []
    now = datetime.now(timezone.utc)
    for f in list(SESSIONS_DIR.glob("*.json")):
        try:
            data = json.loads(f.read_text("utf-8"))
            hb = datetime.fromisoformat(data.get("heartbeat", ""))
            age = (now - hb).total_seconds()
            if age > STALE_THRESHOLD_SECONDS:
                name = data.get("name", f.stem)
                f.unlink(missing_ok=True)
                removed.append(name)
        except (json.JSONDecodeError, ValueError, TypeError, OSError):
            # Corrupt or unreadable — remove it
            try:
                f.unlink(missing_ok=True)
                removed.append(f.stem)
            except OSError:
                pass
    return removed


async def _cleanup_stale_sessions() -> list[str]:
    return await asyncio.to_thread(_cleanup_stale_sessions_sync)


# ── Session Announcements ─────────────────────────────────────────────────

_SYSTEM_CHANNEL = "_system"


def _announce_join_sync() -> None:
    """Post a join announcement to #_system if this session hasn't announced yet."""
    marker = SESSIONS_DIR / f".announced_{SESSION_ID}"
    if marker.exists():
        return
    entry = {
        "id": uuid.uuid4().hex[:12],
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "session_id": SESSION_ID,
        "session_name": SESSION_NAME,
        "platform": PLATFORM,
        "channel": _SYSTEM_CHANNEL,
        "msg_type": "status",
        "message": f"{SESSION_NAME}@{PLATFORM} joined the broker",
    }
    _append_jsonl_sync(CHANNELS_DIR / f"{_SYSTEM_CHANNEL}.jsonl", entry)
    
    try:
        from . import discord_bridge
        discord_bridge.forward_to_discord(entry)
    except Exception:
        pass
        
    try:
        marker.write_text(SESSION_ID)
    except OSError:
        pass


async def _announce_join() -> None:
    await asyncio.to_thread(_announce_join_sync)

# ── File I/O Helpers ───────────────────────────────────────────────────────


def _lock_file(f) -> None:  # type: ignore[no-untyped-def]
    """Acquire an exclusive advisory lock on a file (cross-platform)."""
    if sys.platform == "win32":
        import msvcrt
        msvcrt.locking(f.fileno(), msvcrt.LK_LOCK, 1)
    else:
        import fcntl
        fcntl.flock(f.fileno(), fcntl.LOCK_EX)


def _unlock_file(f) -> None:  # type: ignore[no-untyped-def]
    """Release an advisory lock on a file (cross-platform)."""
    if sys.platform == "win32":
        import msvcrt
        try:
            msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, 1)
        except OSError:
            pass
    else:
        import fcntl
        fcntl.flock(f.fileno(), fcntl.LOCK_UN)


def _append_jsonl_sync(path: Path, obj: dict) -> None:
    """Sync append with file locking — safe for concurrent multi-agent writes."""
    line = json.dumps(obj, default=str) + "\n"
    with path.open("a", encoding="utf-8") as f:
        _lock_file(f)
        try:
            f.write(line)
            f.flush()
        finally:
            _unlock_file(f)


async def _append_jsonl(path: Path, obj: dict) -> None:
    await asyncio.to_thread(_append_jsonl_sync, path, obj)


def _read_jsonl_sync(path: Path, offset: int = 0) -> list[dict]:
    if not path.exists():
        return []
    lines = path.read_text("utf-8").strip().splitlines()
    entries = []
    for line in lines[offset:]:
        try:
            entries.append(json.loads(line))
        except json.JSONDecodeError:
            continue
    return entries


async def _read_jsonl(path: Path, offset: int = 0) -> list[dict]:
    return await asyncio.to_thread(_read_jsonl_sync, path, offset)


def _read_cursor_sync(session_id: str, channel: str) -> int:
    path = CURSORS_DIR / f"{session_id}_{channel}.cursor"
    try:
        return int(path.read_text("utf-8").strip())
    except (FileNotFoundError, ValueError):
        return 0


async def _read_cursor(session_id: str, channel: str) -> int:
    return await asyncio.to_thread(_read_cursor_sync, session_id, channel)


def _write_cursor_sync(session_id: str, channel: str, position: int) -> None:
    """Write cursor position atomically via temp file + rename."""
    path = CURSORS_DIR / f"{session_id}_{channel}.cursor"
    tmp = path.with_suffix(".cursor.tmp")
    tmp.write_text(str(position))
    tmp.replace(path)  # atomic on same filesystem


async def _write_cursor(session_id: str, channel: str, position: int) -> None:
    await asyncio.to_thread(_write_cursor_sync, session_id, channel, position)


async def _write_json(path: Path, obj: dict) -> None:
    text = json.dumps(obj, indent=2, default=str)
    await asyncio.to_thread(path.write_text, text)


def _write_json_sync(path: Path, obj: dict) -> None:
    path.write_text(json.dumps(obj, indent=2, default=str))


async def _read_json(path: Path) -> Optional[dict]:
    try:
        text = await asyncio.to_thread(path.read_text, "utf-8")
        return json.loads(text)
    except (FileNotFoundError, json.JSONDecodeError):
        return None


# ── Session Heartbeat ──────────────────────────────────────────────────────

_session_start = datetime.now(timezone.utc).isoformat()


async def _update_heartbeat() -> None:
    await _write_json(SESSIONS_DIR / f"{SESSION_ID}.json", {
        "session_id": SESSION_ID,
        "name": SESSION_NAME,
        "platform": PLATFORM,
        "pid": os.getpid(),
        "heartbeat": datetime.now(timezone.utc).isoformat(),
        "started": _session_start,
    })
    await _announce_join()
    await _cleanup_stale_sessions()


def _update_heartbeat_sync() -> None:
    _write_json_sync(SESSIONS_DIR / f"{SESSION_ID}.json", {
        "session_id": SESSION_ID,
        "name": SESSION_NAME,
        "platform": PLATFORM,
        "pid": os.getpid(),
        "heartbeat": datetime.now(timezone.utc).isoformat(),
        "started": _session_start,
    })
    _announce_join_sync()
    _cleanup_stale_sessions_sync()


# ── MCP Server ─────────────────────────────────────────────────────────────

server = Server("brynq")

TOOLS = [
    Tool(
        name="broker_post",
        description=(
            "Post a message to a broker channel. Other terminals "
            "(Claude, Gemini, Cursor, scripts) connected to the same broker can read it."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "channel": {
                    "type": "string",
                    "description": "Channel name (e.g. 'general', 'gen21', 'alerts'). Auto-created.",
                    "default": "general",
                },
                "message": {
                    "type": "string",
                    "description": "The message content.",
                },
                "msg_type": {
                    "type": "string",
                    "description": "Message type: info, request, file_ready, question, alert, status.",
                    "default": "info",
                },
            },
            "required": ["message"],
        },
        annotations=_SAFE_WRITE,
    ),
    Tool(
        name="broker_read",
        description=(
            "Read new messages from a broker channel since your last read. "
            "Returns only UNREAD messages for this session. Includes sender platform."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "channel": {
                    "type": "string",
                    "description": "Channel to read from.",
                    "default": "general",
                },
                "peek": {
                    "type": "boolean",
                    "description": "If true, don't advance cursor (messages stay 'unread').",
                    "default": False,
                },
            },
        },
        annotations=_SAFE_READ,
    ),
    Tool(
        name="broker_broadcast",
        description=(
            "Post a message to ALL channels at once. Use for important announcements "
            "that every terminal should see regardless of which channel they monitor."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "message": {
                    "type": "string",
                    "description": "The broadcast message.",
                },
                "msg_type": {
                    "type": "string",
                    "description": "Message type.",
                    "default": "alert",
                },
            },
            "required": ["message"],
        },
        annotations=_SAFE_WRITE,
    ),
    Tool(
        name="broker_channels",
        description="List all broker channels with message counts and last activity.",
        inputSchema={"type": "object", "properties": {}},
        annotations=_SAFE_READ,
    ),
    Tool(
        name="broker_sessions",
        description="List all connected terminal sessions with platform, heartbeat, and staleness.",
        inputSchema={"type": "object", "properties": {}},
        annotations=_SAFE_READ,
    ),
    Tool(
        name="broker_status",
        description="Show this session's ID, name, platform, and unread counts per channel.",
        inputSchema={"type": "object", "properties": {}},
        annotations=_SAFE_READ,
    ),
    Tool(
        name="broker_history",
        description="Read full message history for a channel (not just unread).",
        inputSchema={
            "type": "object",
            "properties": {
                "channel": {
                    "type": "string",
                    "description": "Channel to read history from.",
                    "default": "general",
                },
                "limit": {
                    "type": "integer",
                    "description": "Max messages to return (newest first).",
                    "default": 20,
                },
            },
        },
        annotations=_SAFE_READ,
    ),
    Tool(
        name="broker_inbox",
        description="Check ALL channels for unread messages at once. Quick overview of what you missed.",
        inputSchema={"type": "object", "properties": {}},
        annotations=_SAFE_READ,
    ),
    Tool(
        name="broker_stats",
        description="View global broker statistics including total messages, storage size, and active sessions.",
        inputSchema={"type": "object", "properties": {}},
        annotations=_SAFE_READ,
    ),
    Tool(
        name="broker_archive_channel",
        description="Archive old messages in a channel to reduce active file size.",
        inputSchema={
            "type": "object",
            "properties": {
                "channel": {
                    "type": "string",
                    "description": "Channel name to archive.",
                    "default": "general",
                },
                "days": {
                    "type": "integer",
                    "description": "Archive messages older than this many days.",
                    "default": 7,
                },
            },
        },
        annotations=_SAFE_WRITE,
    ),
    Tool(
        name="broker_search",
        description="Search across broker channels with flexible filtering (by keyword, channel, sender, platform, etc).",
        inputSchema={
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Substring search in message text (case-insensitive)"},
                "channel": {"type": "string", "description": "Filter to a specific channel"},
                "sender": {"type": "string", "description": "Filter by session name (substring match)"},
                "platform": {"type": "string", "description": "Filter by platform (e.g., claude, gemini)"},
                "msg_type": {"type": "string", "description": "Filter by message type (e.g., info, request)"},
                "limit": {"type": "integer", "description": "Max results to return", "default": 20}
            }
        },
        annotations=_SAFE_READ,
    ),
    # Phase 79: 76 domain tools migrated to mcp_plugins/
    # Domains: tasks, memory, governance, knowledge, messaging, swarm, rag
    # Loaded dynamically via MCPServerPluginLoader in list_tools()
]


@server.list_tools()
async def list_tools() -> list[Tool]:
    # Phase 79: Dynamic Plugin Loader — plugins override static TOOLS
    plugin_loader = MCPServerPluginLoader(_Path(__file__).parent / "mcp_plugins")
    dynamic_tools, _ = plugin_loader.load_plugins()
    # Deduplicate: plugin tools take precedence over static TOOLS
    plugin_names = {t.name for t in dynamic_tools}
    core_tools = [t for t in TOOLS if t.name not in plugin_names]
    return core_tools + dynamic_tools


@server.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
    await _update_heartbeat()

    # Phase 69: Agent Security Sandboxing
    try:
        try:
            from brynq.agent_sandbox import check_permission
        except ImportError:
            from brynq._future.agent_sandbox import check_permission
            
        allowed, reason = check_permission(
            agent_id=SESSION_ID,
            action="execute_tool",
            resource=name
        )
        if not allowed:
            return [TextContent(type="text", text=f"[SECURITY VETO] Sandbox execution denied for tool '{name}': {reason}")]
    except ImportError:
        pass
    except Exception as e:
        pass # Fail open if sandbox engine crashes, or could log

    plugin_loader = MCPServerPluginLoader(_Path(__file__).parent / "mcp_plugins")
    _, dynamic_handlers = plugin_loader.load_plugins()

    # Phase 79: Core messaging handlers stay in server.py (session-coupled).
    # All other handlers are loaded from mcp_plugins/ via MCPServerPluginLoader.
    handlers = {
        "broker_post": _handle_post,
        "broker_read": _handle_read,
        "broker_broadcast": _handle_broadcast,
        "broker_channels": lambda _: _handle_channels(),
        "broker_sessions": lambda _: _handle_sessions(),
        "broker_status": lambda _: _handle_status(),
        "broker_history": _handle_history,
        "broker_inbox": lambda _: _handle_inbox(),
        "broker_stats": lambda _: _handle_stats(),
        "broker_archive_channel": _handle_archive_channel,
        "broker_search": _handle_search,
    }

    handler = dynamic_handlers.get(name) or handlers.get(name)
    if handler:
        import inspect

        # Dynamic plugin handlers expect a JSON string arg, not a dict.
        # Core handlers expect a dict. Adapt based on handler signature.
        is_dynamic = name in dynamic_handlers
        if is_dynamic:
            # Convert dict arguments to JSON string for plugin handlers
            import json as _json
            str_arg = _json.dumps(arguments) if arguments else ""
            raw_result = handler(str_arg)
            if inspect.isawaitable(raw_result):
                raw_result = await raw_result
            # Wrap string result as TextContent if needed
            if isinstance(raw_result, str):
                result = [TextContent(type="text", text=raw_result)]
            elif isinstance(raw_result, list):
                result = raw_result
            else:
                result = [TextContent(type="text", text=str(raw_result))]
        else:
            result = handler(arguments)
            if inspect.isawaitable(result):
                result = await result

        # ── Auto-poll: check for unread messages on every tool call ──
        # Skip for read operations to avoid infinite loops
        _skip_autopoll = {"broker_read", "broker_inbox", "broker_status",
                          "broker_history", "broker_channels", "broker_sessions"}
        if name not in _skip_autopoll:
            try:
                unread_notice = await _auto_poll_inbox()
                if unread_notice:
                    result.append(TextContent(type="text", text=unread_notice))
            except Exception:
                pass  # Never break a tool call for autopoll failure

        return result

    return [TextContent(type="text", text=f"Unknown tool: {name}")]


# ── Tool Handlers ──────────────────────────────────────────────────────────


async def _handle_post(args: dict) -> list[TextContent]:
    channel = args.get("channel", "general").lstrip("#")
    message = args.get("message", "")
    msg_type = args.get("msg_type", "info")

    if not message:
        return [TextContent(type="text", text="Error: message is required")]

    # ── Constitution Enforcer: validate before posting ──
    try:
        from .enforcer import enforce_pre_post
        allowed, reason, mediation = enforce_pre_post(message, channel)
        if not allowed:
            return [TextContent(type="text", text=f"CONSTITUTIONAL VIOLATION: {reason}")]
    except ImportError:
        mediation = None

    entry = {
        "id": uuid.uuid4().hex[:12],
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "session_id": SESSION_ID,
        "session_name": SESSION_NAME,
        "platform": PLATFORM,
        "channel": channel,
        "msg_type": msg_type,
        "message": message,
    }

    path = CHANNELS_DIR / f"{channel}.jsonl"
    await _append_jsonl(path, entry)

    # Notify subscribed agents
    try:
        from .notifications import notify
        await asyncio.to_thread(notify, event_type="new_message", data={"channel": channel, "sender": SESSION_NAME, "message": message[:100]})
    except (ImportError, Exception):
        pass

    # If conflict detected, auto-post mediation advice after the message
    if mediation:
        mediation_entry = {
            "id": uuid.uuid4().hex[:12],
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "session_id": "guardrails",
            "session_name": "Brynq-Mediator",
            "platform": "system",
            "channel": channel,
            "msg_type": "mediation",
            "message": mediation,
        }
        await _append_jsonl(path, mediation_entry)

    try:
        from . import discord_bridge
        await asyncio.to_thread(discord_bridge.forward_to_discord, entry)
    except Exception:
        pass

    result = f"Posted to #{channel} as {SESSION_NAME} @{PLATFORM} [{msg_type}]"
    if mediation:
        result += f"\n\n{mediation}"
    return [TextContent(type="text", text=result)]


async def _handle_broadcast(args: dict) -> list[TextContent]:
    message = args.get("message", "")
    msg_type = args.get("msg_type", "alert")

    if not message:
        return [TextContent(type="text", text="Error: message is required")]

    entry = {
        "id": uuid.uuid4().hex[:12],
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "session_id": SESSION_ID,
        "session_name": SESSION_NAME,
        "platform": PLATFORM,
        "msg_type": msg_type,
        "message": message,
    }

    # Post to all existing channels + general
    channels_posted = set()
    for f in CHANNELS_DIR.glob("*.jsonl"):
        if f.stem.startswith("_"):
            continue  # Don't broadcast to system channels
        entry_copy = {**entry, "channel": f.stem}
        await _append_jsonl(f, entry_copy)
        channels_posted.add(f.stem)

    if "general" not in channels_posted:
        entry["channel"] = "general"
        await _append_jsonl(CHANNELS_DIR / "general.jsonl", entry)
        channels_posted.add("general")

    try:
        from . import discord_bridge
        await asyncio.to_thread(discord_bridge.forward_to_discord, entry)
    except Exception:
        pass

    return [TextContent(
        type="text",
        text=f"Broadcast to {len(channels_posted)} channels: {', '.join(sorted(channels_posted))}",
    )]


async def _handle_read(args: dict) -> list[TextContent]:
    channel = args.get("channel", "general")
    peek = args.get("peek", False)

    path = CHANNELS_DIR / f"{channel}.jsonl"
    cursor = await _read_cursor(SESSION_ID, channel)
    messages = await _read_jsonl(path, offset=cursor)

    # Filter out own messages
    new_msgs = [m for m in messages if m.get("session_id") != SESSION_ID]

    if not peek and messages:
        await _write_cursor(SESSION_ID, channel, cursor + len(messages))

    if not new_msgs:
        return [TextContent(type="text", text=f"No new messages in #{channel}")]

    lines = []
    for m in new_msgs:
        ts = m.get("timestamp", "")[:19]
        name = m.get("session_name", "unknown")
        plat = m.get("platform", "?")
        typ = m.get("msg_type", "info")
        msg = m.get("message", "")
        lines.append(f"[{ts}] {name}@{plat} ({typ}): {msg}")

    header = f"#{channel} — {len(new_msgs)} new message(s):"
    return [TextContent(type="text", text=header + "\n" + "\n".join(lines))]


async def _handle_channels() -> list[TextContent]:
    channels = []
    for f in sorted(CHANNELS_DIR.glob("*.jsonl")):
        name = f.stem
        lines = f.read_text("utf-8").strip().splitlines()
        count = len(lines)
        last = ""
        if lines:
            try:
                last_msg = json.loads(lines[-1])
                last = last_msg.get("timestamp", "")[:19]
            except json.JSONDecodeError:
                pass
        channels.append(f"  #{name}: {count} messages (last: {last})")

    if not channels:
        return [TextContent(type="text", text="No channels yet. Post a message to create one.")]

    return [TextContent(type="text", text="Broker channels:\n" + "\n".join(channels))]


async def _handle_sessions() -> list[TextContent]:
    sessions = []
    now = datetime.now(timezone.utc)

    for f in sorted(SESSIONS_DIR.glob("*.json")):
        data = await _read_json(f)
        if not data:
            continue

        name = data.get("name", "unknown")
        sid = data.get("session_id", "?")
        plat = data.get("platform", "?")
        hb = data.get("heartbeat", "")
        pid = data.get("pid", "?")

        stale = ""
        try:
            hb_dt = datetime.fromisoformat(hb)
            age_s = (now - hb_dt).total_seconds()
            if age_s > 300:
                stale = f" STALE ({int(age_s)}s)"
            else:
                stale = f" active ({int(age_s)}s)"
        except (ValueError, TypeError):
            stale = " unknown"

        marker = " ← YOU" if sid == SESSION_ID else ""
        sessions.append(f"  {name} @{plat} [{sid}] PID {pid}{stale}{marker}")

    if not sessions:
        return [TextContent(type="text", text="No sessions registered.")]

    return [TextContent(type="text", text="Connected sessions:\n" + "\n".join(sessions))]


async def _handle_status() -> list[TextContent]:
    lines = [
        f"Session: {SESSION_NAME} [{SESSION_ID}]",
        f"Platform: {PLATFORM}",
        f"PID: {os.getpid()}",
        "",
        "Unread messages per channel:",
    ]

    has_channels = False
    for f in sorted(CHANNELS_DIR.glob("*.jsonl")):
        channel = f.stem
        total = len(f.read_text("utf-8").strip().splitlines())
        cursor = await _read_cursor(SESSION_ID, channel)
        unread = max(0, total - cursor)
        flag = " *** NEW ***" if unread > 0 else ""
        lines.append(f"  #{channel}: {unread} unread / {total} total{flag}")
        has_channels = True

    if not has_channels:
        lines.append("  (no channels yet)")

    return [TextContent(type="text", text="\n".join(lines))]


async def _handle_history(args: dict) -> list[TextContent]:
    channel = args.get("channel", "general")
    limit = args.get("limit", 20)

    path = CHANNELS_DIR / f"{channel}.jsonl"
    all_msgs = await _read_jsonl(path)

    if not all_msgs:
        return [TextContent(type="text", text=f"No messages in #{channel}")]

    recent = all_msgs[-limit:]
    lines = []
    for m in recent:
        ts = m.get("timestamp", "")[:19]
        name = m.get("session_name", "unknown")
        plat = m.get("platform", "?")
        typ = m.get("msg_type", "info")
        msg = m.get("message", "")
        marker = " ← you" if m.get("session_id") == SESSION_ID else ""
        lines.append(f"[{ts}] {name}@{plat} ({typ}): {msg}{marker}")

    header = f"#{channel} — last {len(recent)} of {len(all_msgs)} messages:"
    return [TextContent(type="text", text=header + "\n" + "\n".join(lines))]


async def _handle_inbox() -> list[TextContent]:
    """Check ALL channels for unread messages."""
    lines = [f"Inbox for {SESSION_NAME} @{PLATFORM}:", ""]
    total_unread = 0

    for f in sorted(CHANNELS_DIR.glob("*.jsonl")):
        channel = f.stem
        all_lines = f.read_text("utf-8").strip().splitlines()
        total = len(all_lines)
        cursor = await _read_cursor(SESSION_ID, channel)
        unread = max(0, total - cursor)

        if unread > 0:
            total_unread += unread
            # Show preview of latest unread
            latest_msgs = _read_jsonl_sync(f, offset=max(0, total - 3))
            latest_new = [m for m in latest_msgs if m.get("session_id") != SESSION_ID]
            preview = ""
            if latest_new:
                last = latest_new[-1]
                preview = f' — latest: {last.get("session_name", "?")}@{last.get("platform", "?")}: "{last.get("message", "")[:60]}"'
            lines.append(f"  #{channel}: {unread} unread{preview}")

    if total_unread == 0:
        lines.append("  All caught up! No unread messages.")
    else:
        lines.insert(1, f"  {total_unread} total unread across all channels")

    return [TextContent(type="text", text="\n".join(lines))]


async def _handle_stats() -> list[TextContent]:
    """Return broker-wide statistics."""
    from . import retention
    stats = await asyncio.to_thread(retention.get_stats)
    
    lines = [
        "Broker Statistics:",
        f"  Total Channels:   {stats.get('channels', 0)}",
        f"  Total Messages:   {stats.get('total_messages', 0)}",
        f"  Storage Used:     {stats.get('storage_bytes', 0) / 1024:.2f} KB",
        f"  Active Sessions:  {stats.get('active_sessions', 0)}",
        f"  Stale Sessions:   {stats.get('stale_sessions', 0)}",
        f"  Archived Files:   {stats.get('archive_count', 0)}",
        f"  Oldest Message:   {stats.get('oldest_message', 'N/A')}",
        f"  Newest Message:   {stats.get('newest_message', 'N/A')}",
    ]
    return [TextContent(type="text", text="\n".join(lines))]


async def _handle_archive_channel(args: dict) -> list[TextContent]:
    channel = args.get("channel", "general")
    days = args.get("days", 7)
    
    from datetime import timedelta
    from . import retention
    
    before_date = datetime.now(timezone.utc) - timedelta(days=days)
    result = await asyncio.to_thread(retention.archive_channel, channel, before_date)
    
    if result["archived"] == 0:
        return [TextContent(type="text", text=f"No messages older than {days} days to archive in #{channel}.")]
        
    return [TextContent(type="text", text=f"Archived {result['archived']} messages from #{channel}. Remaining: {result['remaining']}. Saved to {result['archive_path']}")]


async def _handle_search(args: dict) -> list[TextContent]:
    from . import search as broker_search_module
    
    results = await asyncio.to_thread(
        broker_search_module.search,
        query=args.get("query"),
        channel=args.get("channel"),
        sender=args.get("sender"),
        platform=args.get("platform"),
        msg_type=args.get("msg_type"),
        limit=args.get("limit", 20)
    )
    
    formatted = broker_search_module.format_results(results)
    return [TextContent(type="text", text=formatted)]


async def _handle_create_task(args: dict) -> list[TextContent]:
    from . import tasks
    from . import capability
    
    title = args.get("title", "")
    req_cap = args.get("required_capability")
    
    # Dynamic routing
    if req_cap:
        target_agent = await asyncio.to_thread(capability.route_task, title, req_cap)
        if target_agent:
            title = f"[@{target_agent}] {title}" # Ping the specific agent
            
    try:
        task_id = await asyncio.to_thread(
            tasks.create_task,
            channel=args.get("channel", "general"),
            title=title,
            description=args.get("description", ""),
            priority=args.get("priority", "normal")
        )
        msg = f"Task created successfully. Task ID: {task_id}"
        if req_cap:
            if target_agent:
                msg += f"\nRouted to: {target_agent} (capability: {req_cap})"
            else:
                msg += f"\nWarning: No active agents found with capability '{req_cap}'. Posted to channel unassigned."
        return [TextContent(type="text", text=msg)]
    except Exception as e:
        return [TextContent(type="text", text=f"Error creating task: {str(e)}")]

async def _handle_list_tasks(args: dict) -> list[TextContent]:
    from . import tasks
    try:
        results = await asyncio.to_thread(
            tasks.list_tasks,
            channel=args.get("channel"),
            status=args.get("status")
        )
        if not results:
            return [TextContent(type="text", text="No tasks found matching criteria.")]
        
        lines = [f"Found {len(results)} task(s):"]
        for t in results:
            lines.append(f"  [{t['status'].upper()}] {t['title']} (ID: {t['task_id']}) - Priority: {t['priority']}")
            if t['status'] == 'pending':
                lines.append(f"    Requested by: {t['requester_name']}")
            else:
                lines.append(f"    Assigned to: {t['assignee_name']}")
            lines.append(f"    Channel: #{t['channel']}")
            lines.append("")
        return [TextContent(type="text", text="\n".join(lines))]
    except Exception as e:
        return [TextContent(type="text", text=f"Error listing tasks: {str(e)}")]

async def _handle_accept_task(args: dict) -> list[TextContent]:
    from . import tasks
    try:
        task = await asyncio.to_thread(tasks.accept_task, args.get("task_id", ""))
        return [TextContent(type="text", text=f"Task '{task['title']}' accepted successfully. You are now the assignee.")]
    except Exception as e:
        return [TextContent(type="text", text=f"Error accepting task: {str(e)}")]

async def _handle_complete_task(args: dict) -> list[TextContent]:
    from . import tasks
    task_id = args.get("task_id", "")
    result = args.get("result", "")
    try:
        task = await asyncio.to_thread(tasks.complete_task, task_id, result)
        # Update agent profile with task completion
        assignee = task.get("assignee_session", SESSION_ID)
        try:
            from .profiles import record_task_completion
            await asyncio.to_thread(
                record_task_completion,
                session_id=assignee,
                task_id=task_id,
                title=task.get("title", ""),
                result=result[:200],
                success=True,
            )
        except Exception:
            pass
        # Award XP via incentives engine
        try:
            from .incentives import award_xp, record_skill_used
            xp_result = await asyncio.to_thread(award_xp, assignee, "task_complete")
            # Track skill if task had a required capability
            req_cap = task.get("required_capability", "")
            if req_cap:
                await asyncio.to_thread(record_skill_used, assignee, req_cap)
        except Exception:
            pass
        xp_msg = ""
        try:
            xp_msg = f" (+{xp_result['xp_gained']} XP, level: {xp_result['level']})"
        except Exception:
            pass
        # Route result to workflow engine if task belongs to a workflow run
        wf_msg = ""
        workflow_run_id = task.get("workflow_run_id")
        wf_step_id = task.get("step_id")
        if workflow_run_id and wf_step_id:
            try:
                from .workflow import complete_step as wf_complete_step
                advance = await asyncio.to_thread(
                    wf_complete_step, workflow_run_id, wf_step_id, result[:2000],
                )
                if "error" not in advance:
                    newly_ready = advance.get("newly_ready", [])
                    wf_msg = f" Workflow step '{wf_step_id}' advanced ({advance.get('steps_completed', 0)}/{advance.get('steps_total', 0)})."
                    if newly_ready:
                        wf_msg += f" Next ready: {', '.join(newly_ready)}."
            except Exception:
                pass
        return [TextContent(type="text", text=f"Task '{task['title']}' marked as completed!{xp_msg}{wf_msg}")]
    except Exception as e:
        return [TextContent(type="text", text=f"Error completing task: {str(e)}")]

async def _handle_fail_task(args: dict) -> list[TextContent]:
    from .tasks import fail_task
    task_id = args.get("task_id", "")
    reason = args.get("reason", "")
    if not task_id:
        return [TextContent(type="text", text="Error: task_id is required")]
    try:
        task = await asyncio.to_thread(fail_task, task_id, reason)
        # Update profile
        try:
            from .profiles import record_task_completion
            await asyncio.to_thread(
                record_task_completion,
                session_id=task.get("assignee_session", SESSION_ID),
                task_id=task_id,
                title=task.get("title", ""),
                result=reason[:200],
                success=False,
            )
        except Exception:
            pass
        # Break streak on failure
        try:
            from .incentives import record_task_failure
            await asyncio.to_thread(record_task_failure, task.get("assignee_session", SESSION_ID))
        except Exception:
            pass
        # Propagate failure to workflow engine if task belongs to a workflow run
        wf_msg = ""
        workflow_run_id = task.get("workflow_run_id")
        wf_step_id = task.get("step_id")
        if workflow_run_id and wf_step_id:
            try:
                from .workflow import fail_step as wf_fail_step
                await asyncio.to_thread(
                    wf_fail_step, workflow_run_id, wf_step_id, reason[:2000],
                )
                wf_msg = f" Workflow run {workflow_run_id} marked as failed."
            except Exception:
                pass
        return [TextContent(type="text", text=f"Task {task_id} marked as failed: {reason}{wf_msg}")]
    except Exception as e:
        return [TextContent(type="text", text=f"Error: {str(e)}")]

async def _handle_cross_project_post(args: dict) -> list[TextContent]:
    from . import cross_project
    target_project = args.get("target_project", "")
    channel = args.get("channel", "general")
    message = args.get("message", "")
    
    if not target_project or not message:
        return [TextContent(type="text", text="Error: target_project and message are required.")]
        
    success = await asyncio.to_thread(
        cross_project.post_cross_project,
        target_project=target_project,
        channel=channel,
        message=message,
        sender_session=SESSION_ID,
        sender_name=SESSION_NAME,
        platform=PLATFORM
    )
    
    if success:
        return [TextContent(type="text", text=f"Successfully sent message to #{channel} in project '{target_project}'.")]
    else:
        return [TextContent(type="text", text=f"Failed to send message. Project '{target_project}' not found in registry ~/.brynq/registry.json.")]

async def _handle_register_cron(args: dict) -> list[TextContent]:
    from . import scheduler
    try:
        job_id = await asyncio.to_thread(
            scheduler.register_cron_task,
            name=args.get("name", "Task"),
            channel=args.get("channel", "general"),
            cron_expr=args.get("cron_expr", "300"),
            message=args.get("message", ""),
            session_id=SESSION_ID,
            session_name=SESSION_NAME,
            platform=PLATFORM
        )
        return [TextContent(type="text", text=f"Cron task registered successfully. Job ID: {job_id}")]
    except Exception as e:
        return [TextContent(type="text", text=f"Error registering cron task: {str(e)}")]

async def _handle_list_cron(args: dict) -> list[TextContent]:
    from . import scheduler
    try:
        tasks = await asyncio.to_thread(scheduler.list_cron_tasks)
        if not tasks:
            return [TextContent(type="text", text="No active cron tasks found.")]
        lines = ["Active Cron Tasks:"]
        for t in tasks:
            lines.append(f"  [{t['job_id']}] {t['name']} -> #{t['channel']} (Every {t['interval_secs']}s)")
        return [TextContent(type="text", text="\n".join(lines))]
    except Exception as e:
        return [TextContent(type="text", text=f"Error listing cron tasks: {str(e)}")]

async def _handle_remove_cron(args: dict) -> list[TextContent]:
    from . import scheduler
    try:
        success = await asyncio.to_thread(scheduler.remove_cron_task, args.get("job_id", ""))
        if success:
            return [TextContent(type="text", text="Cron task removed successfully.")]
        else:
            return [TextContent(type="text", text="Cron task not found.")]
    except Exception as e:
        return [TextContent(type="text", text=f"Error removing cron task: {str(e)}")]

async def _handle_register_capability(args: dict) -> list[TextContent]:
    from . import capability
    try:
        await asyncio.to_thread(
            capability.register_capability,
            session_id=SESSION_ID,
            session_name=SESSION_NAME,
            platform=PLATFORM,
            capability=args.get("capability", ""),
            description=args.get("description", "")
        )
        return [TextContent(type="text", text=f"Capability '{args.get('capability')}' registered successfully. You can now receive routed tasks for this skill.")]
    except Exception as e:
        return [TextContent(type="text", text=f"Error registering capability: {str(e)}")]

async def _handle_list_capabilities(args: dict) -> list[TextContent]:
    from . import capability
    try:
        caps = await asyncio.to_thread(capability.list_capabilities)
        if not caps:
            return [TextContent(type="text", text="No active capabilities registered in the swarm.")]
        
        lines = ["Active Swarm Capabilities:"]
        for c in caps:
            lines.append(f"  - [{c['capability']}] {c['session_name']}@{c['platform']}: {c['description']}")
            
        return [TextContent(type="text", text="\n".join(lines))]
    except Exception as e:
        return [TextContent(type="text", text=f"Error listing capabilities: {str(e)}")]


async def _handle_find_agent(args: dict) -> list[TextContent]:
    from .capability import find_agents
    agents = await asyncio.to_thread(
        find_agents,
        capability=args.get("capability"),
        query=args.get("query"),
        platform=args.get("platform"),
    )
    if not agents:
        return [TextContent(type="text", text="No matching agents found")]
    lines = [f"Found {len(agents)} matching agent(s):"]
    for a in agents:
        lines.append(f"  {a['session_name']}@{a['platform']} — {a['capability']}: {a['description']}")
        lines.append(f"    score: {a['score']} | active tasks: {a['active_delegations']}")
    return [TextContent(type="text", text="\n".join(lines))]


async def _handle_delegate(args: dict) -> list[TextContent]:
    from .capability import delegate_task
    title = args.get("title", "")
    description = args.get("description", "")
    required = args.get("required_capability", "")
    if not all([title, description, required]):
        return [TextContent(type="text", text="Error: title, description, and required_capability are all required")]
    result = await asyncio.to_thread(
        delegate_task,
        title=title,
        description=description,
        required_capability=required,
        channel=args.get("channel", "tasks"),
        priority=args.get("priority", "normal"),
    )
    if result.get("routed"):
        return [TextContent(type="text", text=f"Task delegated: [{result['task_id']}] '{title}'\n  Assigned to: {result['assigned_to']}@{result['assigned_platform']} (score: {result['match_score']})\n  Channel: #{result['channel']} | Priority: {result['priority']}")]
    return [TextContent(type="text", text=f"Task posted but NOT routed: [{result['task_id']}] '{title}'\n  Reason: {result.get('reason', 'unknown')}\n  Channel: #{result['channel']}")]


# ── Main ───────────────────────────────────────────────────────────────────


async def main():
    global SESSION_ID, SESSION_NAME
    # MCP: use persistent ID so memory and cursors survive restarts.
    # Only generate a new ID if BROKER_SESSION_ID env var is explicitly set.
    # The file-backed _resolve_session_id() already handles persistence.
    if os.environ.get("BROKER_SESSION_ID"):
        SESSION_ID = os.environ["BROKER_SESSION_ID"]
        if os.environ.get("BROKER_SESSION_NAME"):
            SESSION_NAME = os.environ["BROKER_SESSION_NAME"]
        else:
            SESSION_NAME = f"terminal-{SESSION_ID}"
    # else: keep the file-backed persistent ID from _resolve_session_id()
            
    # Start the scheduler thread
    try:
        from . import scheduler
        scheduler.start_scheduler()
    except Exception:
        pass
        
    await _update_heartbeat()

    # ── Startup: make this agent aware of the network ──

    # Check inbox for unread messages
    try:
        from .hooks import check_inbox
        check_inbox(SESSION_ID)
    except Exception:
        pass

    # Auto-register capabilities from profile (if profile exists)
    try:
        from .profiles import get_profile
        from .capability import register_capability
        profile = get_profile(SESSION_ID)
        if profile and profile.get("skills"):
            for skill in profile["skills"]:
                try:
                    register_capability(capability=skill, description=f"Skill: {skill}")
                except Exception:
                    pass
    except Exception:
        pass

    # Register enterprise tools
    try:
        from .enterprise_integration import register_enterprise_tools
        # Need to hoist handlers dict - but since it's inside call_tool, we just extend TOOLS
        from .enterprise_tools import ENTERPRISE_TOOLS
        TOOLS.extend(ENTERPRISE_TOOLS)
    except Exception:
        pass

    async with stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            server.create_initialization_options(),
        )


# ── Phase 1: File Watcher Handlers ────────────────────────────────────────


async def _handle_watch(args: dict) -> list[TextContent]:
    from .file_watcher import register_watch
    directory = args.get("directory", "")
    if not directory:
        return [TextContent(type="text", text="Error: directory is required")]
    watch = await asyncio.to_thread(
        register_watch,
        directory=directory,
        pattern=args.get("pattern", "*"),
        channel=args.get("channel", "file-events"),
        recursive=args.get("recursive", True),
    )
    return [TextContent(type="text", text=f"Watch registered: {watch['watch_id']}\n  Directory: {watch['directory']}\n  Pattern: {watch['pattern']}\n  Channel: #{watch['channel']}\n  Recursive: {watch['recursive']}")]


async def _handle_unwatch(args: dict) -> list[TextContent]:
    from .file_watcher import remove_watch
    watch_id = args.get("watch_id", "")
    if not watch_id:
        return [TextContent(type="text", text="Error: watch_id is required")]
    removed = await asyncio.to_thread(remove_watch, watch_id)
    if removed:
        return [TextContent(type="text", text=f"Watch {watch_id} removed")]
    return [TextContent(type="text", text=f"Watch {watch_id} not found")]


async def _handle_list_watches() -> list[TextContent]:
    from .file_watcher import list_watches
    watches = await asyncio.to_thread(list_watches)
    if not watches:
        return [TextContent(type="text", text="No active file watches")]
    lines = ["Active file watches:"]
    for w in watches:
        lines.append(f"  [{w['watch_id']}] {w['directory']} ({w['pattern']}) -> #{w['channel']}")
    return [TextContent(type="text", text="\n".join(lines))]


async def _handle_check_watches() -> list[TextContent]:
    from .file_watcher import check_all_watches
    results = await asyncio.to_thread(check_all_watches)
    if not results:
        return [TextContent(type="text", text="No file watches configured")]
    lines = ["File watch check results:"]
    for r in results:
        total = r["total_changes"]
        if total > 0:
            lines.append(f"  [{r['watch_id']}] {r['directory']}: {total} changes (+{len(r['created'])} ~{len(r['modified'])} -{len(r['deleted'])})")
        else:
            lines.append(f"  [{r['watch_id']}] {r['directory']}: no changes")
    return [TextContent(type="text", text="\n".join(lines))]


# ── Phase 2: Persistent Memory Handlers ──────────────────────────────────


async def _handle_memory_set(args: dict) -> list[TextContent]:
    from .memory import memory_set
    key = args.get("key", "")
    value = args.get("value")
    if not key:
        return [TextContent(type="text", text="Error: key is required")]
    entry = await asyncio.to_thread(
        memory_set,
        key=key,
        value=value,
        namespace=args.get("namespace", "default"),
        tags=args.get("tags"),
        ttl_hours=args.get("ttl_hours"),
    )
    ttl_info = f" (expires: {entry['expires'][:19]})" if entry.get("expires") else " (permanent)"
    return [TextContent(type="text", text=f"Memory stored: {entry['namespace']}/{key}{ttl_info}")]


async def _handle_memory_get(args: dict) -> list[TextContent]:
    from .memory import memory_get_full
    key = args.get("key", "")
    if not key:
        return [TextContent(type="text", text="Error: key is required")]
    entry = await asyncio.to_thread(
        memory_get_full,
        key=key,
        namespace=args.get("namespace", "default"),
    )
    if entry is None:
        return [TextContent(type="text", text=f"Memory not found: {args.get('namespace', 'default')}/{key}")]
    return [TextContent(type="text", text=json.dumps({"key": entry["key"], "namespace": entry["namespace"], "value": entry["value"], "tags": entry.get("tags", []), "updated": entry.get("updated", ""), "expires": entry.get("expires")}, indent=2, default=str))]


async def _handle_memory_list(args: dict) -> list[TextContent]:
    from .memory import memory_list
    entries = await asyncio.to_thread(
        memory_list,
        namespace=args.get("namespace"),
        tag=args.get("tag"),
    )
    if not entries:
        return [TextContent(type="text", text="No memory entries found")]
    lines = [f"Memory entries ({len(entries)}):"]
    for e in entries:
        tags = f" [{', '.join(e['tags'])}]" if e.get("tags") else ""
        lines.append(f"  {e['namespace']}/{e['key']}{tags}: {e['value_preview']}")
    return [TextContent(type="text", text="\n".join(lines))]


async def _handle_memory_search(args: dict) -> list[TextContent]:
    from .memory import memory_search
    query = args.get("query", "")
    if not query:
        return [TextContent(type="text", text="Error: query is required")]
    results = await asyncio.to_thread(memory_search, query)
    if not results:
        return [TextContent(type="text", text=f"No memory entries matching '{query}'")]
    lines = [f"Memory search '{query}' ({len(results)} results):"]
    for e in results:
        lines.append(f"  {e['namespace']}/{e['key']}: {e['value_preview']}")
    return [TextContent(type="text", text="\n".join(lines))]


async def _handle_memory_delete(args: dict) -> list[TextContent]:
    from .memory import memory_delete
    key = args.get("key", "")
    if not key:
        return [TextContent(type="text", text="Error: key is required")]
    deleted = await asyncio.to_thread(
        memory_delete,
        key=key,
        namespace=args.get("namespace", "default"),
    )
    if deleted:
        return [TextContent(type="text", text=f"Memory deleted: {args.get('namespace', 'default')}/{key}")]
    return [TextContent(type="text", text=f"Memory not found: {args.get('namespace', 'default')}/{key}")]


# ── Guardrail Handlers ─────────────────────────────────────────────────────


async def _handle_claim_file(args: dict) -> list[TextContent]:
    from .guardrails import claim_file
    filepath = args.get("filepath", "")
    reason = args.get("reason", "")
    success, msg = await asyncio.to_thread(claim_file, filepath, reason)
    return [TextContent(type="text", text=msg)]


async def _handle_release_file(args: dict) -> list[TextContent]:
    from .guardrails import release_file
    filepath = args.get("filepath", "")
    success, msg = await asyncio.to_thread(release_file, filepath)
    return [TextContent(type="text", text=msg)]


async def _handle_check_lock(args: dict) -> list[TextContent]:
    from .guardrails import check_file_lock
    filepath = args.get("filepath", "")
    lock = await asyncio.to_thread(check_file_lock, filepath)
    if not lock:
        return [TextContent(type="text", text=f"{filepath} is UNLOCKED — safe to edit")]
    holder = lock.get("session_name", "unknown")
    age = lock.get("age_seconds", 0)
    reason = lock.get("reason", "")
    return [TextContent(type="text", text=f"{filepath} is LOCKED by {holder} ({age}s ago). Reason: {reason}")]


async def _handle_list_locks() -> list[TextContent]:
    from .guardrails import get_all_locks
    locks = await asyncio.to_thread(get_all_locks)
    if not locks:
        return [TextContent(type="text", text="No active file locks.")]
    lines = ["Active file locks:"]
    for lk in locks:
        mine = " <-- YOU" if lk.get("is_mine") else ""
        lines.append(f"  {lk['filepath']} — {lk['session_name']}@{lk.get('platform','?')} ({lk['age_seconds']}s){mine}")
    return [TextContent(type="text", text="\n".join(lines))]


async def _handle_propose(args: dict) -> list[TextContent]:
    from .guardrails import format_proposal
    channel = args.get("channel", "general")
    title = args.get("title", "")
    description = args.get("description", "")
    options = args.get("options", [])
    proposal = format_proposal(title, description, options)

    entry = {
        "id": uuid.uuid4().hex[:12],
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "session_id": SESSION_ID,
        "session_name": SESSION_NAME,
        "platform": PLATFORM,
        "channel": channel,
        "msg_type": "proposal",
        "message": proposal,
    }
    await _append_jsonl(CHANNELS_DIR / f"{channel}.jsonl", entry)
    return [TextContent(type="text", text=f"Proposal posted to #{channel}: {title}")]


# ── Constitution Enforcement — Handlers ────────────────────────────────────


async def _handle_my_standing() -> list[TextContent]:
    from .enforcer import get_violation_summary
    summary = await asyncio.to_thread(get_violation_summary)
    status = summary["status"]
    total = summary["total_violations"]
    level = summary["enforcement_level"]
    icon = {"good_standing": "OK", "cooldown": "COOLDOWN", "blocked": "BLOCKED"}.get(status, "?")
    lines = [f"Constitutional Standing: {icon}", f"  Violations: {total}", f"  Level: {level}"]
    if summary["recent_violations"]:
        lines.append("  Recent:")
        for v in summary["recent_violations"][-3:]:
            lines.append(f"    Law {v['law']}: {v['description'][:60]}")
    return [TextContent(type="text", text="\n".join(lines))]


async def _handle_violations(args: dict) -> list[TextContent]:
    from .enforcer import get_violation_summary
    sid = args.get("session_id") or SESSION_ID
    summary = await asyncio.to_thread(get_violation_summary, sid)
    return [TextContent(type="text", text=json.dumps(summary, indent=2, default=str))]


async def _handle_pardon(args: dict) -> list[TextContent]:
    from .enforcer import pardon
    sid = args.get("session_id", "")
    if not sid:
        return [TextContent(type="text", text="Error: session_id required")]
    result = await asyncio.to_thread(pardon, sid)
    return [TextContent(type="text", text=f"PARDONED: {sid} cleared of {result['pardoned']} violations. Status: {result['status']}")]


async def _handle_constitution() -> list[TextContent]:
    summary = """BRYNQ CONSTITUTION v1.0 — 45 Laws, 10 Articles

KEY LAWS EVERY AGENT MUST FOLLOW:
  Law 1:  True Identity — no impersonation
  Law 5:  Clarity Over Volume — concise, actionable messages
  Law 6:  Rate Limits Are Sacred — 10 msgs/60s
  Law 8:  No Arguments — use broker_propose
  Law 10: Message Length Limit — 5,000 chars max
  Law 11: Claim Before Edit — broker_claim_file first
  Law 16: Clear Task Specs — unambiguous instructions
  Law 17: Accept Before Working — broker_accept_task first
  Law 20: One Task At A Time — complete before accepting more
  Law 26: Human Supremacy — human overrides everything
  Law 28: No Credential Sharing — use env vars
  Law 37: No Platform Discrimination — skills > platform

ENFORCEMENT:
  Warning (1-2) → Cooldown (3-5) → Escalation (6-8) → Disconnect (12+)

Full text: CONSTITUTION.md"""
    return [TextContent(type="text", text=summary)]


# ── Phase 6: LinkedIn for Bots — Handlers ────────────────────────────────


async def _handle_create_profile(args: dict) -> list[TextContent]:
    from .profiles import create_or_update_profile
    profile = await asyncio.to_thread(
        create_or_update_profile,
        bio=args.get("bio", ""),
        skills=args.get("skills"),
        availability=args.get("availability", "available"),
    )
    skills = ", ".join(profile.get("skills", []))
    return [TextContent(type="text", text=f"Profile updated: {profile['session_name']}@{profile['platform']}\n  Bio: {profile['bio']}\n  Skills: {skills}\n  Reputation: {profile['reputation']} | Tasks completed: {profile['tasks_completed']}")]


async def _handle_view_profile(args: dict) -> list[TextContent]:
    from .profiles import get_profile
    sid = args.get("session_id") or SESSION_ID
    profile = await asyncio.to_thread(get_profile, sid)
    if not profile:
        return [TextContent(type="text", text=f"No profile found for {sid}")]
    return [TextContent(type="text", text=json.dumps(profile, indent=2, default=str))]


async def _handle_list_profiles(args: dict) -> list[TextContent]:
    from .profiles import list_profiles
    profiles = await asyncio.to_thread(
        list_profiles,
        skill=args.get("skill"),
        availability=args.get("availability"),
        platform=args.get("platform"),
    )
    if not profiles:
        return [TextContent(type="text", text="No bot profiles found")]
    lines = [f"Bot Profiles ({len(profiles)}):"]
    for p in profiles:
        status = p.get("availability", "?")
        rep = p.get("reputation", "?")
        skills = ", ".join(p.get("skills", [])[:5])
        lines.append(f"  {p['session_name']}@{p['platform']} [{status}] rep:{rep} — {skills}")
    return [TextContent(type="text", text="\n".join(lines))]


async def _handle_rate_bot(args: dict) -> list[TextContent]:
    from .profiles import rate_bot
    result = await asyncio.to_thread(
        rate_bot,
        target_session_id=args.get("target_session_id", ""),
        rating=args.get("rating", 5),
        review=args.get("review", ""),
        task_id=args.get("task_id"),
    )
    if "error" in result:
        return [TextContent(type="text", text=result["error"])]
    return [TextContent(type="text", text=f"Rated! New reputation: {result['reputation']} ({result['total_ratings']} total ratings)")]


async def _handle_post_job(args: dict) -> list[TextContent]:
    from .jobs import post_job
    job = await asyncio.to_thread(
        post_job,
        title=args.get("title", ""),
        description=args.get("description", ""),
        required_skills=args.get("required_skills", []),
        preferred_skills=args.get("preferred_skills"),
        priority=args.get("priority", "normal"),
        complexity=args.get("complexity", "medium"),
        channel=args.get("channel", "jobs"),
    )
    skills = ", ".join(job["required_skills"])
    return [TextContent(type="text", text=f"Job posted: [{job['job_id']}] {job['title']}\n  Required: {skills}\n  Status: {job['status']} | Channel: #{job['channel']}")]


async def _handle_list_jobs(args: dict) -> list[TextContent]:
    from .jobs import list_jobs
    jobs = await asyncio.to_thread(
        list_jobs,
        status=args.get("status"),
        skill=args.get("skill"),
    )
    if not jobs:
        return [TextContent(type="text", text="No job postings found")]
    lines = [f"Job Board ({len(jobs)} postings):"]
    for j in jobs:
        skills = ", ".join(j.get("required_skills", []))
        apps = len(j.get("applicants", []))
        lines.append(f"  [{j['job_id']}] {j['title']} ({j['status']}) — {skills} | {apps} applicant(s)")
    return [TextContent(type="text", text="\n".join(lines))]


async def _handle_apply_job(args: dict) -> list[TextContent]:
    from .jobs import apply_to_job
    result = await asyncio.to_thread(
        apply_to_job,
        job_id=args.get("job_id", ""),
        pitch=args.get("pitch", ""),
        estimated_time=args.get("estimated_time", ""),
    )
    if "error" in result:
        return [TextContent(type="text", text=result["error"])]
    return [TextContent(type="text", text=f"Applied! Match: {result['match_percentage']}% | Skills matched: {', '.join(result['skills_matched'])} | Missing: {', '.join(result['skills_missing']) or 'none'}")]


async def _handle_review_applications(args: dict) -> list[TextContent]:
    from .jobs import list_applications
    apps = await asyncio.to_thread(list_applications, args.get("job_id", ""))
    if not apps:
        return [TextContent(type="text", text="No applications found")]
    lines = [f"Applications ({len(apps)}):"]
    for a in apps:
        lines.append(f"  {a['applicant_name']}@{a['applicant_platform']} — match:{a['match_percentage']}% rep:{a['reputation']} tasks:{a['tasks_completed']}")
        lines.append(f"    Pitch: {a['pitch'][:100]}")
        lines.append(f"    Status: {a['status']} | ID: {a['applicant_id']}")
    return [TextContent(type="text", text="\n".join(lines))]


async def _handle_hire(args: dict) -> list[TextContent]:
    from .jobs import hire_applicant
    result = await asyncio.to_thread(
        hire_applicant,
        job_id=args.get("job_id", ""),
        applicant_session_id=args.get("applicant_session_id", ""),
    )
    if "error" in result:
        return [TextContent(type="text", text=result["error"])]
    return [TextContent(type="text", text=f"Hired {result['hired']} for job [{result['job_id']}]! Other applicants rejected. Job status: {result['status']}")]


# ── Phase 8: Incentives Handlers ──────────────────────────────────────────


async def _handle_leaderboard(args: dict) -> list[TextContent]:
    from .incentives import get_leaderboard
    sort_by = args.get("sort_by", "xp")
    limit = args.get("limit", 20)
    lb = await asyncio.to_thread(get_leaderboard, sort_by=sort_by, limit=limit)
    if not lb:
        return [TextContent(type="text", text="Leaderboard is empty. Complete tasks to earn XP!")]
    lines = [f"Leaderboard (sorted by {sort_by}):\n"]
    for entry in lb:
        lines.append(
            f"  #{entry['rank']} {entry['session_id']} [{entry['level']}] "
            f"XP:{entry['xp']} Tasks:{entry['tasks_completed']} "
            f"Rating:{entry['avg_rating']} Streak:{entry['streak']} "
            f"Best:{entry['best_streak']} Bounty:{entry['bounty_points']}"
        )
    return [TextContent(type="text", text="\n".join(lines))]


async def _handle_achievements(args: dict) -> list[TextContent]:
    from .incentives import get_achievements, list_all_achievements
    if args.get("list_all", False):
        all_ach = list_all_achievements()
        lines = ["All Achievements:\n"]
        for a in all_ach:
            lines.append(f"  {a['name']} — {a['description']}")
        return [TextContent(type="text", text="\n".join(lines))]
    unlocked = await asyncio.to_thread(get_achievements, SESSION_ID)
    if not unlocked:
        return [TextContent(type="text", text="No achievements unlocked yet. Keep working!")]
    lines = [f"Your Achievements ({len(unlocked)}):\n"]
    for a in unlocked:
        lines.append(f"  {a['name']} — {a['description']}")
    return [TextContent(type="text", text="\n".join(lines))]


async def _handle_xp_status(args: dict) -> list[TextContent]:
    from .incentives import get_agent_stats, get_level_progress, get_achievements
    stats = await asyncio.to_thread(get_agent_stats, SESSION_ID)
    progress = get_level_progress(stats["xp"])
    achievements = await asyncio.to_thread(get_achievements, SESSION_ID)
    lines = [
        f"XP Status for {SESSION_NAME}@{PLATFORM}:\n",
        f"  Level: {progress['level']}",
        f"  XP: {progress['xp']}",
    ]
    if progress["next_level"]:
        lines.append(f"  Next: {progress['next_level']} ({progress['progress_pct']}% → {progress['next_threshold']} XP)")
    else:
        lines.append(f"  MAX LEVEL REACHED")
    lines.extend([
        f"  Streak: {stats['streak']} (best: {stats['best_streak']})",
        f"  Tasks: {stats['tasks_completed']} completed, {stats['tasks_failed']} failed",
        f"  Rating: {stats['avg_rating']} ({stats['total_ratings']} ratings)",
        f"  Bounties: {stats['bounties_earned']} earned, {stats['bounty_points']} pts",
        f"  Achievements: {len(achievements)} unlocked",
    ])
    return [TextContent(type="text", text="\n".join(lines))]


async def _handle_post_bounty(args: dict) -> list[TextContent]:
    from .incentives import post_bounty
    result = await asyncio.to_thread(
        post_bounty,
        task_id=args["task_id"],
        title=args["title"],
        points=args["points"],
        posted_by=SESSION_ID,
        description=args.get("description", ""),
        required_skill=args.get("required_skill", ""),
    )
    return [TextContent(type="text", text=f"Bounty posted! {result['title']} — {result['points']} pts on task [{result['task_id']}]")]


async def _handle_claim_bounty(args: dict) -> list[TextContent]:
    from .incentives import claim_bounty
    result = await asyncio.to_thread(claim_bounty, args["task_id"], SESSION_ID)
    if not result:
        return [TextContent(type="text", text="Bounty not found or already claimed.")]
    return [TextContent(type="text", text=f"Bounty claimed! Complete task [{result['task_id']}] to earn {result['points']} pts.")]


async def _handle_list_bounties(args: dict) -> list[TextContent]:
    from .incentives import list_bounties
    status = args.get("status", "open")
    bounties = await asyncio.to_thread(list_bounties, status=status)
    if not bounties:
        return [TextContent(type="text", text=f"No {status} bounties found.")]
    lines = [f"Bounties ({status}):\n"]
    for b in bounties:
        claimed = f" claimed by {b['claimed_by']}" if b.get("claimed_by") else ""
        lines.append(f"  [{b['task_id']}] {b['title']} — {b['points']} pts ({b['status']}){claimed}")
    return [TextContent(type="text", text="\n".join(lines))]

# ── Swarm Engine ───────────────────────────────────────────────────────────

async def _handle_drop_task(args: dict) -> list[TextContent]:
    from . import swarm
    # create_swarm uses a task_id (we generate from title) and required_skills
    task_id = f"{args['title'][:30].replace(' ', '-').lower()}-{uuid.uuid4().hex[:6]}"
    swarm_id = await asyncio.to_thread(
        swarm.create_swarm,
        task_id=task_id,
        required_skills=args["required_skills"],
    )
    return [TextContent(type="text", text=f"Swarm task dropped. Agents can now bid. Swarm ID: {swarm_id}")]

async def _handle_bid_task(args: dict) -> list[TextContent]:
    from . import swarm
    success = await asyncio.to_thread(
        swarm.submit_bid,
        swarm_id=args["swarm_id"],
        session_id=SESSION_ID,
        reputation=args["reputation_score"],
        skill_match=1.0,
    )
    if success:
        return [TextContent(type="text", text="Bid placed successfully. Wait for swarm formation.")]
    return [TextContent(type="text", text="Failed to place bid. Task may not exist or bidding is closed.")]

async def _handle_form_swarm(args: dict) -> list[TextContent]:
    from . import swarm
    import json
    success = await asyncio.to_thread(swarm.form_swarm, args["swarm_id"])
    if success:
        # Read the swarm data to show members
        swarm_path = swarm.SWARM_DIR / f"{args['swarm_id']}.json"
        try:
            data = json.loads(swarm_path.read_text("utf-8"))
            members = data.get("members", {})
            lead = [k for k, v in members.items() if v == "lead"]
            lead_str = lead[0] if lead else "unknown"
            return [TextContent(type="text", text=f"Swarm formed! Lead: @{lead_str}. Members: {', '.join(members.keys())}")]
        except (FileNotFoundError, json.JSONDecodeError, OSError):
            return [TextContent(type="text", text="Swarm formed successfully.")]
    return [TextContent(type="text", text="Failed to form swarm. Either no bids or swarm not found.")]

# ── Local LLM Agent Management Handlers ───────────────────────────────────


async def _handle_discover_backends() -> list[TextContent]:
    try:
        from . import llm_router
    except ImportError:
        return [TextContent(type="text", text="Error: llm_router module not installed. Install brynq[llm] extras.")]
    try:
        backends = await asyncio.to_thread(llm_router.discover_backends)
        if not backends:
            return [TextContent(type="text", text="No LLM backends discovered. Install Ollama, LM Studio, or set API keys for Claude/Gemini.")]
        lines = ["Discovered LLM Backends:"]
        for b in backends:
            name = b.get("name", "unknown")
            status = b.get("status", "unknown")
            models = b.get("models", [])
            model_str = ", ".join(models[:10]) if models else "none"
            if len(models) > 10:
                model_str += f" (+{len(models) - 10} more)"
            lines.append(f"  [{status.upper()}] {name}: {model_str}")
        return [TextContent(type="text", text="\n".join(lines))]
    except Exception as e:
        return [TextContent(type="text", text=f"Error discovering backends: {e}")]


async def _handle_spawn_local(args: dict) -> list[TextContent]:
    try:
        from . import spawner
    except ImportError:
        return [TextContent(type="text", text="Error: spawner module not installed. Install brynq[llm] extras.")]
    name = args.get("name", "")
    if not name:
        return [TextContent(type="text", text="Error: name is required")]
    backend = args.get("backend", "ollama")
    model = args.get("model", "llama3")
    channel = args.get("channel", "general")
    try:
        result = await asyncio.to_thread(
            spawner.spawn_agent,
            name=name,
            backend=backend,
            model=model,
            channel=channel,
            broker_dir=str(BROKER_DIR),
        )
        status = result.get("status", "unknown")
        if status == "running":
            pid = result.get("pid", "?")
            return [TextContent(type="text", text=(
                f"Agent '{name}' spawned successfully.\n"
                f"  Backend: {backend}\n"
                f"  Model: {model}\n"
                f"  Channel: #{channel}\n"
                f"  PID: {pid}"
            ))]
        else:
            error = result.get("error", "unknown error")
            return [TextContent(type="text", text=f"Failed to spawn agent '{name}': {error}")]
    except Exception as e:
        return [TextContent(type="text", text=f"Error spawning agent: {e}")]


async def _handle_list_local_agents() -> list[TextContent]:
    try:
        from . import spawner
    except ImportError:
        return [TextContent(type="text", text="Error: spawner module not installed. Install brynq[llm] extras.")]
    try:
        agents = await asyncio.to_thread(spawner.list_agents)
        if not agents:
            return [TextContent(type="text", text="No local LLM agents running.")]
        lines = ["Running Local LLM Agents:"]
        for a in agents:
            name = a.get("name", "unknown")
            model = a.get("model", "?")
            backend = a.get("backend", "?")
            status = a.get("status", "?")
            uptime = a.get("uptime", "?")
            channel = a.get("channel", "?")
            pid = a.get("pid", "?")
            lines.append(f"  {name} [{status.upper()}] — {backend}/{model} on #{channel} (PID {pid}, uptime {uptime})")
        return [TextContent(type="text", text="\n".join(lines))]
    except Exception as e:
        return [TextContent(type="text", text=f"Error listing agents: {e}")]


async def _handle_kill_local(args: dict) -> list[TextContent]:
    try:
        from . import spawner
    except ImportError:
        return [TextContent(type="text", text="Error: spawner module not installed. Install brynq[llm] extras.")]
    name = args.get("name", "")
    if not name:
        return [TextContent(type="text", text="Error: name is required")]
    try:
        result = await asyncio.to_thread(spawner.kill_agent, name=name)
        if result.get("killed"):
            return [TextContent(type="text", text=f"Agent '{name}' killed successfully.")]
        else:
            reason = result.get("error", "agent not found")
            return [TextContent(type="text", text=f"Could not kill agent '{name}': {reason}")]
    except Exception as e:
        return [TextContent(type="text", text=f"Error killing agent: {e}")]


async def _handle_quick_chat(args: dict) -> list[TextContent]:
    try:
        from . import llm_router
    except ImportError:
        return [TextContent(type="text", text="Error: llm_router module not installed. Install brynq[llm] extras.")]
    prompt = args.get("prompt", "")
    if not prompt:
        return [TextContent(type="text", text="Error: prompt is required")]
    backend = args.get("backend", "auto")
    model = args.get("model", "auto")
    try:
        result = await asyncio.to_thread(
            llm_router.quick_chat,
            prompt=prompt,
            backend=backend,
            model=model,
        )
        used_backend = result.get("backend", "unknown")
        used_model = result.get("model", "unknown")
        response = result.get("response", "")
        return [TextContent(type="text", text=(
            f"[{used_backend}/{used_model}]\n\n{response}"
        ))]
    except Exception as e:
        return [TextContent(type="text", text=f"Error in quick chat: {e}")]


# ── Auto-Poll: Check inbox on every tool call ─────────────────────────────

_last_autopoll: float = 0.0
_AUTOPOLL_INTERVAL = 10.0  # Don't check more than once per 10 seconds


async def _auto_poll_inbox() -> Optional[str]:
    """Silent inbox check. Returns a notification string if unread, else None.

    Throttled to once per 10 seconds to avoid file I/O spam.
    Does NOT advance cursors — messages stay unread until explicit broker_read.
    """
    global _last_autopoll
    now = time.monotonic()
    if now - _last_autopoll < _AUTOPOLL_INTERVAL:
        return None
    _last_autopoll = now

    total_unread = 0
    previews = []

    for f in sorted(CHANNELS_DIR.glob("*.jsonl")):
        channel = f.stem
        if channel.startswith("_") and channel != "_system":
            continue  # Skip system channels
        try:
            all_lines = f.read_text("utf-8").strip().splitlines()
        except Exception:
            continue
        total = len(all_lines)
        cursor = _read_cursor_sync(SESSION_ID, channel)
        unread = max(0, total - cursor)

        if unread > 0:
            total_unread += unread
            # Get latest message preview
            try:
                last_line = all_lines[-1]
                last_msg = json.loads(last_line)
                if last_msg.get("session_id") != SESSION_ID:
                    sender = last_msg.get("session_name", "?")
                    platform = last_msg.get("platform", "?")
                    text = last_msg.get("message", "")[:80]
                    previews.append(f"  #{channel}: {unread} new — {sender}@{platform}: \"{text}\"")
            except (json.JSONDecodeError, IndexError):
                previews.append(f"  #{channel}: {unread} new")

    if total_unread == 0:
        return None

    header = f"\n[BRYNQ INBOX] {total_unread} unread message(s):"
    footer = "Use broker_read to see full messages."
    return header + "\n" + "\n".join(previews[:5]) + "\n" + footer


# ── Main ───────────────────────────────────────────────────────────────────


def run():
    asyncio.run(main())


if __name__ == "__main__":
    run()


# ── Phase 26: Social Handlers ──────────────────────────────────────────────────

async def _handle_create_post(args: dict) -> list[TextContent]:
    from .social import create_post
    content = args.get("content", "")
    post_type = args.get("post_type", "status")
    tags = args.get("tags", [])
    project = args.get("project", "")
    
    result = await asyncio.to_thread(
        create_post,
        content=content,
        post_type=post_type,
        tags=tags,
        project=project,
    )
    if "error" in result:
        return [TextContent(type="text", text=f"Error: {result['error']}")]
    return [TextContent(type="text", text=f"Post created: {result['post_id']}")]

async def _handle_get_feed(args: dict) -> list[TextContent]:
    from .social import get_feed, get_trending
    limit = args.get("limit", 50)
    
    feed = await asyncio.to_thread(get_feed, limit=limit)
    trending = await asyncio.to_thread(get_trending)
    
    res = {"feed": feed, "trending": trending}
    return [TextContent(type="text", text=json.dumps(res, indent=2, default=str))]

async def _handle_follow_agent(args: dict) -> list[TextContent]:
    from .social import follow
    target_id = args.get("target_id", "")
    
    result = await asyncio.to_thread(follow, target_id=target_id)
    if "error" in result:
        return [TextContent(type="text", text=f"Error: {result['error']}")]
    return [TextContent(type="text", text=json.dumps(result, indent=2, default=str))]

# ── Phase 27: Personality Handlers ──────────────────────────────────────────────

async def _handle_adopt_personality(args: dict) -> list[TextContent]:
    from .personality import adopt_personality
    template_id = args.get("template_id", "")
    
    try:
        result = await asyncio.to_thread(adopt_personality, template_id=template_id)
        return [TextContent(type="text", text=f"Personality adopted: {result['personality']['name']} (style: {result['personality']['communication_style']})")]
    except Exception as e:
        return [TextContent(type="text", text=f"Error: {str(e)}")]

async def _handle_list_personalities() -> list[TextContent]:
    from .personality import list_templates
    templates = await asyncio.to_thread(list_templates)
    return [TextContent(type="text", text=json.dumps(templates, indent=2, default=str))]

# ── Phase 28: Marketplace Handlers ──────────────────────────────────────────────

async def _handle_list_service(args: dict) -> list[TextContent]:
    from .marketplace import list_service
    title = args.get("title", "")
    description = args.get("description", "")
    category = args.get("category", "")
    pricing_tier = args.get("pricing_tier", "basic")
    
    try:
        result = await asyncio.to_thread(
            list_service,
            title=title,
            description=description,
            category=category,
            pricing_tier=pricing_tier
        )
        return [TextContent(type="text", text=f"Service listed! ID: {result['service_id']}")]
    except Exception as e:
        return [TextContent(type="text", text=f"Error: {str(e)}")]

async def _handle_browse_services(args: dict) -> list[TextContent]:
    from .marketplace import browse_services
    category = args.get("category", None)
    
    services = await asyncio.to_thread(browse_services, category=category)
    return [TextContent(type="text", text=json.dumps(services, indent=2, default=str))]

async def _handle_book_service(args: dict) -> list[TextContent]:
    from .marketplace import book_service
    service_id = args.get("service_id", "")
    requirements = args.get("requirements", "")
    
    result = await asyncio.to_thread(
        book_service,
        service_id=service_id,
        requirements=requirements
    )
    if "error" in result:
        return [TextContent(type="text", text=f"Error: {result['error']}")]
    return [TextContent(type="text", text=f"Booking created! ID: {result['booking_id']}")]

async def _handle_deliver_service(args: dict) -> list[TextContent]:
    from .marketplace import complete_booking
    booking_id = args.get("booking_id", "")
    deliverable = args.get("deliverable", "")
    
    result = await asyncio.to_thread(
        complete_booking,
        booking_id=booking_id,
        deliverable=deliverable
    )
    if "error" in result:
        return [TextContent(type="text", text=f"Error: {result['error']}")]
    return [TextContent(type="text", text=f"Booking delivered!")]

# ── Phase 29: CRM Handlers ──────────────────────────────────────────────────────

async def _handle_rate_collaboration(args: dict) -> list[TextContent]:
    from .crm import rate_collaboration
    partner_id = args.get("partner_id", "")
    rating = args.get("rating", 5.0)
    task_id = args.get("task_id", "")
    
    result = await asyncio.to_thread(
        rate_collaboration,
        partner_id=partner_id,
        rating=rating,
        task_id=task_id
    )
    return [TextContent(type="text", text=json.dumps(result, indent=2, default=str))]

async def _handle_suggest_team(args: dict) -> list[TextContent]:
    from .crm import suggest_team
    required_skills = args.get("required_skills", [])
    team_size = args.get("team_size", 3)
    
    result = await asyncio.to_thread(
        suggest_team,
        required_skills=required_skills,
        team_size=team_size
    )
    return [TextContent(type="text", text=json.dumps(result, indent=2, default=str))]

# ── Phase 30: ERP Handlers ──────────────────────────────────────────────────────

async def _handle_check_capacity() -> list[TextContent]:
    from .erp import get_capacity
    result = await asyncio.to_thread(get_capacity)
    return [TextContent(type="text", text=json.dumps(result, indent=2, default=str))]

# ── Phase 31: Onboarding Handlers ───────────────────────────────────────────────

async def _handle_read_knowledge_base(args: dict) -> list[TextContent]:
    from .onboarding import list_knowledge, get_knowledge
    topic = args.get("topic", "")
    
    if not topic:
        articles = await asyncio.to_thread(list_knowledge)
        return [TextContent(type="text", text=json.dumps(articles, indent=2, default=str))]
    else:
        article = await asyncio.to_thread(get_knowledge, topic=topic)
        if not article:
            return [TextContent(type="text", text=f"Topic '{topic}' not found.")]
        return [TextContent(type="text", text=json.dumps(article, indent=2, default=str))]

async def _handle_complete_onboarding_step(args: dict) -> list[TextContent]:
    from .onboarding import complete_step
    step = args.get("step", "")
    
    result = await asyncio.to_thread(complete_step, step=step)
    if "error" in result:
        return [TextContent(type="text", text=f"Error: {result['error']}")]
    
    return [TextContent(type="text", text=f"Step '{step}' marked complete. Full checklist status:\n{json.dumps(result.get('steps',{}), indent=2)}")]

# ── Phase 26: Social Handlers ──────────────────────────────────────────────────

async def _handle_create_post(args: dict) -> list[TextContent]:
    from .social import create_post
    content = args.get("content", "")
    post_type = args.get("post_type", "status")
    tags = args.get("tags", [])
    project = args.get("project", "")
    
    result = await asyncio.to_thread(
        create_post,
        content=content,
        post_type=post_type,
        tags=tags,
        project=project,
    )
    if "error" in result:
        return [TextContent(type="text", text=f"Error: {result['error']}")]
    return [TextContent(type="text", text=f"Post created: {result['post_id']}")]

async def _handle_get_feed(args: dict) -> list[TextContent]:
    from .social import get_feed, get_trending
    limit = args.get("limit", 50)
    
    feed = await asyncio.to_thread(get_feed, limit=limit)
    trending = await asyncio.to_thread(get_trending)
    
    res = {"feed": feed, "trending": trending}
    return [TextContent(type="text", text=json.dumps(res, indent=2, default=str))]

async def _handle_follow_agent(args: dict) -> list[TextContent]:
    from .social import follow
    target_id = args.get("target_id", "")
    
    result = await asyncio.to_thread(follow, target_id=target_id)
    if "error" in result:
        return [TextContent(type="text", text=f"Error: {result['error']}")]
    return [TextContent(type="text", text=json.dumps(result, indent=2, default=str))]

# ── Phase 27: Personality Handlers ──────────────────────────────────────────────

async def _handle_adopt_personality(args: dict) -> list[TextContent]:
    from .personality import adopt_personality
    template_id = args.get("template_id", "")
    
    try:
        result = await asyncio.to_thread(adopt_personality, template_id=template_id)
        return [TextContent(type="text", text=f"Personality adopted: {result['personality']['name']} (style: {result['personality']['communication_style']})")]
    except Exception as e:
        return [TextContent(type="text", text=f"Error: {str(e)}")]

async def _handle_list_personalities() -> list[TextContent]:
    from .personality import list_templates
    templates = await asyncio.to_thread(list_templates)
    return [TextContent(type="text", text=json.dumps(templates, indent=2, default=str))]

# ── Phase 28: Marketplace Handlers ──────────────────────────────────────────────

async def _handle_list_service(args: dict) -> list[TextContent]:
    from .marketplace import list_service
    title = args.get("title", "")
    description = args.get("description", "")
    category = args.get("category", "")
    pricing_tier = args.get("pricing_tier", "basic")
    
    try:
        result = await asyncio.to_thread(
            list_service,
            title=title,
            description=description,
            category=category,
            pricing_tier=pricing_tier
        )
        return [TextContent(type="text", text=f"Service listed! ID: {result['service_id']}")]
    except Exception as e:
        return [TextContent(type="text", text=f"Error: {str(e)}")]

async def _handle_browse_services(args: dict) -> list[TextContent]:
    from .marketplace import browse_services
    category = args.get("category", None)
    
    services = await asyncio.to_thread(browse_services, category=category)
    return [TextContent(type="text", text=json.dumps(services, indent=2, default=str))]

async def _handle_book_service(args: dict) -> list[TextContent]:
    from .marketplace import book_service
    service_id = args.get("service_id", "")
    requirements = args.get("requirements", "")
    
    result = await asyncio.to_thread(
        book_service,
        service_id=service_id,
        requirements=requirements
    )
    if "error" in result:
        return [TextContent(type="text", text=f"Error: {result['error']}")]
    return [TextContent(type="text", text=f"Booking created! ID: {result['booking_id']}")]

async def _handle_deliver_service(args: dict) -> list[TextContent]:
    from .marketplace import complete_booking
    booking_id = args.get("booking_id", "")
    deliverable = args.get("deliverable", "")
    
    result = await asyncio.to_thread(
        complete_booking,
        booking_id=booking_id,
        deliverable=deliverable
    )
    if "error" in result:
        return [TextContent(type="text", text=f"Error: {result['error']}")]
    return [TextContent(type="text", text=f"Booking delivered!")]

# ── Phase 29: CRM Handlers ──────────────────────────────────────────────────────

async def _handle_rate_collaboration(args: dict) -> list[TextContent]:
    from .crm import rate_collaboration
    partner_id = args.get("partner_id", "")
    rating = args.get("rating", 5.0)
    task_id = args.get("task_id", "")
    
    result = await asyncio.to_thread(
        rate_collaboration,
        partner_id=partner_id,
        rating=rating,
        task_id=task_id
    )
    return [TextContent(type="text", text=json.dumps(result, indent=2, default=str))]

async def _handle_suggest_team(args: dict) -> list[TextContent]:
    from .crm import suggest_team
    required_skills = args.get("required_skills", [])
    team_size = args.get("team_size", 3)
    
    result = await asyncio.to_thread(
        suggest_team,
        required_skills=required_skills,
        team_size=team_size
    )
    return [TextContent(type="text", text=json.dumps(result, indent=2, default=str))]

# ── Phase 30: ERP Handlers ──────────────────────────────────────────────────────

async def _handle_check_capacity() -> list[TextContent]:
    from .erp import get_capacity
    result = await asyncio.to_thread(get_capacity)
    return [TextContent(type="text", text=json.dumps(result, indent=2, default=str))]

# ── Phase 31: Onboarding Handlers ───────────────────────────────────────────────

async def _handle_read_knowledge_base(args: dict) -> list[TextContent]:
    from .onboarding import list_knowledge, get_knowledge
    topic = args.get("topic", "")
    
    if not topic:
        articles = await asyncio.to_thread(list_knowledge)
        return [TextContent(type="text", text=json.dumps(articles, indent=2, default=str))]
    else:
        article = await asyncio.to_thread(get_knowledge, topic=topic)
        if not article:
            return [TextContent(type="text", text=f"Topic '{topic}' not found.")]
        return [TextContent(type="text", text=json.dumps(article, indent=2, default=str))]

async def _handle_complete_onboarding_step(args: dict) -> list[TextContent]:
    from .onboarding import complete_step
    step = args.get("step", "")
    
    result = await asyncio.to_thread(complete_step, step=step)
    if "error" in result:
        return [TextContent(type="text", text=f"Error: {result['error']}")]
    
    return [TextContent(type="text", text=f"Step '{step}' marked complete. Full checklist status:\n{json.dumps(result.get('steps',{}), indent=2)}")]

# -- Compute Grid Handlers --

async def _handle_grid_join(args: dict) -> list[TextContent]:
    from brynq.compute_grid import ComputeGrid
    capabilities = args.get("capabilities", [])
    max_concurrent = args.get("max_concurrent", 4)
    node = ComputeGrid.join(SESSION_ID, capabilities=capabilities, max_concurrent=max_concurrent)
    return [TextContent(type="text", text=f"Joined Compute Grid: {json.dumps(node, indent=2)}")]

async def _handle_grid_stats() -> list[TextContent]:
    from brynq.compute_grid import ComputeGrid
    stats = ComputeGrid.stats()
    return [TextContent(type="text", text=json.dumps(stats, indent=2))]

async def _handle_grid_submit(args: dict) -> list[TextContent]:
    from brynq.compute_grid import SwarmMapReduce
    mr = SwarmMapReduce()
    try:
        job = mr.submit(
            task_description=args["description"],
            chunks=args["chunks"],
            map_fn_name=args["map_fn"],
            reduce_fn_name=args["reduce_fn"]
        )
        return [TextContent(type="text", text=f"Job submitted successfully: {json.dumps(job, indent=2)}")]
    except Exception as e:
        return [TextContent(type="text", text=f"Failed to submit job: {e}")]

async def _handle_grid_status(args: dict) -> list[TextContent]:
    from brynq.compute_grid import SwarmMapReduce
    mr = SwarmMapReduce()
    try:
        status = mr.status(args["job_id"])
        return [TextContent(type="text", text=json.dumps(status, indent=2))]
    except Exception as e:
        return [TextContent(type="text", text=f"Failed to get status: {e}")]




# ── Phase 58: Peer Review Handlers ──────────────────────────────────────────────────────

async def _handle_submit_for_review(args: dict) -> list[TextContent]:
    from .tasks import submit_for_review
    task_id = args.get("task_id", "")
    deliverable = args.get("deliverable", "")
    res = await asyncio.to_thread(submit_for_review, task_id, deliverable)
    if "error" in res:
        return [TextContent(type="text", text=f"Error: {res['error']}")]
    return [TextContent(type="text", text=f"Task {task_id} submitted for review.")]

async def _handle_accept_review(args: dict) -> list[TextContent]:
    from .tasks import accept_review
    task_id = args.get("task_id", "")
    res = await asyncio.to_thread(accept_review, task_id)
    if "error" in res:
        return [TextContent(type="text", text=f"Error: {res['error']}")]
    return [TextContent(type="text", text=f"You are now reviewing task {task_id}.")]

async def _handle_approve_task(args: dict) -> list[TextContent]:
    from .tasks import approve_task
    task_id = args.get("task_id", "")
    feedback = args.get("feedback", "")
    res = await asyncio.to_thread(approve_task, task_id, feedback)
    if "error" in res:
        return [TextContent(type="text", text=f"Error: {res['error']}")]
    return [TextContent(type="text", text=f"Task {task_id} APPROVED.")]

async def _handle_reject_task(args: dict) -> list[TextContent]:
    from .tasks import reject_task
    task_id = args.get("task_id", "")
    feedback = args.get("feedback", "")
    res = await asyncio.to_thread(reject_task, task_id, feedback)
    if "error" in res:
        return [TextContent(type="text", text=f"Error: {res['error']}")]
    return [TextContent(type="text", text=f"Task {task_id} REJECTED.")]
