"""Vastian Network CLI — interact with the agent network from the terminal."""

from __future__ import annotations

import asyncio
import json
import logging
import os
from pathlib import Path

import typer
import yaml
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.prompt import Prompt
from rich.table import Table

try:
    import questionary
    from questionary import Style as QStyle

    _Q_STYLE = QStyle(
        [
            ("qmark", "fg:ansicyan bold"),
            ("question", "fg:ansiwhite bold"),
            ("pointer", "fg:ansicyan bold"),
            ("highlighted", "fg:ansicyan bold"),
            ("selected", "fg:ansicyan"),
            ("answer", "fg:ansicyan bold"),
        ]
    )
    HAS_QUESTIONARY = True
except ImportError:
    HAS_QUESTIONARY = False
    _Q_STYLE = None

from vastian.config import Settings, get_settings
from vastian.orchestrator import Orchestrator

logger = logging.getLogger(__name__)

# ── Worker PID lock ─────────────────────────────────────────────────────


def _acquire_worker_lock(state_dir: Path, host_type: str = "local") -> bool:
    """Acquire a PID lock file. Returns True if lock acquired, False if another worker is alive."""
    lock_path = state_dir / "worker.lock"
    if lock_path.exists():
        try:
            lock_data = json.loads(lock_path.read_text(encoding="utf-8"))
            pid = lock_data.get("pid", 0)
            # Check if process is still alive
            try:
                os.kill(pid, 0)
                # Process alive — lock is held
                return False
            except (OSError, ProcessLookupError):
                # Process dead — stale lock, overwrite
                pass
        except Exception:
            pass  # Corrupt lock file, overwrite
    lock_path.parent.mkdir(parents=True, exist_ok=True)
    from datetime import UTC, datetime

    lock_path.write_text(
        json.dumps(
            {
                "pid": os.getpid(),
                "host": host_type,
                "started": datetime.now(UTC).isoformat(),
            }
        ),
        encoding="utf-8",
    )
    return True


def _release_worker_lock(state_dir: Path) -> None:
    """Remove the PID lock file on clean shutdown."""
    lock_path = state_dir / "worker.lock"
    try:
        if lock_path.exists():
            lock_data = json.loads(lock_path.read_text(encoding="utf-8"))
            if lock_data.get("pid") == os.getpid():
                lock_path.unlink()
    except Exception:
        pass


def _read_worker_lock(state_dir: Path) -> dict | None:
    """Read the worker lock file, or return None if not locked."""
    lock_path = state_dir / "worker.lock"
    if not lock_path.exists():
        return None
    try:
        lock_data = json.loads(lock_path.read_text(encoding="utf-8"))
        pid = lock_data.get("pid", 0)
        try:
            os.kill(pid, 0)
            lock_data["alive"] = True
        except (OSError, ProcessLookupError):
            lock_data["alive"] = False
        return lock_data
    except Exception:
        return None


import contextlib  # noqa: E402
from datetime import UTC  # noqa: E402

from vastian.orchestrator.orchestrator import (  # noqa: E402
    TOOL_DESCRIPTIONS,
    get_tool_names_for_agent,
)


def _version_callback(value: bool) -> None:
    if value:
        try:
            from importlib.metadata import version

            ver = version("vastian")
        except Exception:
            ver = "unknown"
        typer.echo(f"vastian {ver}")
        raise typer.Exit()


app = typer.Typer(
    name="vastian",
    help="The Vastian Network — Distributed multi-agent knowledge management system.",
    no_args_is_help=True,
)
console = Console()


@app.callback()
def _main(
    version: bool = typer.Option(
        False, "--version", "-V", callback=_version_callback, is_eager=True, help="Show version."
    ),
) -> None:
    """The Vastian Network CLI."""


def _open_feed_browser(url: str) -> None:
    """Open the feed URL — tries VS Code Simple Browser command first, falls back to webbrowser."""
    import subprocess
    import webbrowser

    try:
        # Try VS Code CLI command to open Simple Browser
        subprocess.Popen(
            ["code", "--command", f"simpleBrowser.show {url}"],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
    except FileNotFoundError:
        try:  # noqa: SIM105
            webbrowser.open(url)
        except Exception:
            pass  # User can open manually


def _setup_logging(level: str = "INFO") -> None:
    """Configure logging for the CLI."""
    logging.basicConfig(
        level=getattr(logging, level.upper(), logging.INFO),
        format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        datefmt="%H:%M:%S",
    )


# ── Commands ─────────────────────────────────────────────


@app.command()
def chat(
    agent: str = typer.Option(None, "--agent", "-a", help="Target a specific agent by ID."),
    verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging."),
    tier: str = typer.Option(
        None,
        "--tier",
        "-t",
        help="Access tier (open/standard/elevated/admin). Overrides VASTIAN_USER_TIER env var.",
    ),
) -> None:
    """Start an interactive chat session with the Vastian Network."""
    _setup_logging("DEBUG" if verbose else "WARNING")

    console.print(
        Panel.fit(
            "[bold cyan]The Vastian Network[/bold cyan]\n"
            "[dim]Distributed Knowledge Management & Mentor Council System[/dim]",
            border_style="cyan",
        )
    )

    if agent:
        console.print(f"[dim]Targeting agent:[/dim] [bold]{agent}[/bold]")
    else:
        console.print("[dim]Auto-routing enabled. Charles will triage your requests.[/dim]")

    console.print(
        "[dim]Commands: /help (all) | /agents | /status | /tasks | /bg | /feed | /improve\n"
        "          /agent <id> | /council <topic> | /meeting <team> | /audit <id>\n"
        "          /simulate <desc> | /onboard | /inbox | /report <id> | /models\n"
        "          /sessions | /personas | /settings | exit[/dim]\n"
    )

    asyncio.run(_chat_loop(agent, verbose, tier=tier))


@app.command()
def run(
    port: int = typer.Option(4545, "--port", "-p", help="Port for the unified server."),
    host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host to bind."),
) -> None:
    """Start the unified app and open in browser. Vastian launcher (replaces multiple tabs)."""
    _setup_logging("WARNING")
    import os

    if os.environ.get("VASTIAN_PORT"):
        port = int(os.environ["VASTIAN_PORT"])
    asyncio.run(_run_loop(port=port, host=host))


@app.command()
def serve(
    port: int = typer.Option(4545, "--port", "-p", help="Port for the unified server."),
    host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host to bind."),
) -> None:
    """Start the unified HTTP server only (no chat). For Tauri sidecar."""
    _setup_logging("WARNING")
    import os

    if os.environ.get("VASTIAN_PORT"):
        port = int(os.environ["VASTIAN_PORT"])
    asyncio.run(_serve_loop(port=port, host=host))


def _ensure_data_root_seeded(settings: Settings) -> None:
    """Require data folder for Tauri/desktop app: ensure it exists and is seeded."""
    from vastian.seed import (
        install_config_seed,
        install_data_seed,
        install_saved_prompts_seed,
        install_state_seed,
    )
    from vastian.storage.data_root import get_data_root

    data_root = get_data_root(Path.cwd())
    data_root.mkdir(parents=True, exist_ok=True)
    install_config_seed(data_root / "config")
    install_state_seed(settings.state_dir)
    install_saved_prompts_seed(data_root / "saved_prompts")
    install_data_seed(settings.sqlite_path.parent)


async def _serve_loop(port: int = 4545, host: str = "127.0.0.1") -> None:
    """Run server in blocking mode with full orchestrator. Handles SIGTERM for Tauri."""
    settings = get_settings()
    # Write logs to file (for desktop app debugging — no console when launched from .exe)
    settings.log_dir.mkdir(parents=True, exist_ok=True)
    log_file = settings.log_dir / "vastian.log"
    file_handler = logging.FileHandler(log_file, encoding="utf-8")
    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(
        logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
    )
    root = logging.getLogger()
    root.addHandler(file_handler)
    root.setLevel(logging.INFO)  # Capture startup errors in log file
    _ensure_data_root_seeded(settings)

    # Worker singleton — prevent two workers on same data
    if not _acquire_worker_lock(settings.state_dir, host_type="local"):
        lock_info = _read_worker_lock(settings.state_dir)
        pid = lock_info.get("pid", "?") if lock_info else "?"
        host_type = lock_info.get("host", "?") if lock_info else "?"
        console.print(
            f"[yellow]Warning:[/yellow] Another worker is running (PID {pid}, {host_type})."
        )
        console.print("[dim]If this is stale, delete state/worker.lock[/dim]")
        raise typer.Exit(1)

    orchestrator = Orchestrator(settings)
    try:
        await orchestrator.initialize()
    except Exception as e:
        _release_worker_lock(settings.state_dir)
        console.print(f"[red]Initialization error:[/red] {e}")
        raise typer.Exit(1) from e

    from vastian.bridges.cursor_bridge.sessions.engine import SessionEngine
    from vastian.server import create_app, find_available_port, run_server_async
    from vastian.tasks.feed import NotificationFeed
    from vastian.tasks.persona_chart import PersonaChartPage
    from vastian.tasks.roster import RosterPage
    from vastian.tasks.settings_page import SettingsPage

    actual_port = find_available_port(host, port)
    feed = NotificationFeed(
        db_path=settings.sqlite_path.parent / "tasks.db",
        port=actual_port,
        state_dir=settings.state_dir,
        user_tier=orchestrator.user_tier,
    )
    roster = RosterPage(
        port=actual_port, state_dir=settings.state_dir, user_tier=orchestrator.user_tier
    )
    chart_page = PersonaChartPage(port=actual_port)
    settings_page = SettingsPage(port=actual_port, user_tier=orchestrator.user_tier)

    roster.session_engine = SessionEngine(
        registry=orchestrator.registry,
        bus=orchestrator.bus,
        knowledge=orchestrator.knowledge,
        state_dir=settings.state_dir,
    )
    roster.orchestrator = orchestrator

    async def _on_force_start(task_id: str) -> None:
        await orchestrator._execute_background_task(task_id)

    async def _on_kanban_approved(kanban_id: str) -> None:
        if orchestrator.task_queue:
            tasks = orchestrator.task_queue.get_kanban_tasks(status="approved")
            for t in tasks:
                if t["kanban_id"] == kanban_id:
                    orchestrator.task_queue.start_kanban_task(kanban_id)
                    orchestrator.task_queue.add_notification(
                        t["suggested_by"],
                        f"Kanban task started: {t['title']}",
                        "running",
                    )
                    asyncio.create_task(orchestrator._execute_kanban_task(t))
                    break

    feed.on_force_start = _on_force_start
    feed.on_kanban_approved = _on_kanban_approved
    feed._callbacks_handled_externally = True

    app = create_app(
        {
            "feed": feed,
            "roster": roster,
            "chart": chart_page,
            "settings": settings_page,
            "orchestrator": orchestrator,
            "main_loop": asyncio.get_running_loop(),
            "task_queue": orchestrator.task_queue,
            "project_root": Path.cwd(),
            "user_tier": orchestrator.user_tier,
        }
    )
    from vastian.storage.data_root import get_data_root

    base = f"http://{host}:{actual_port}"
    data_root = get_data_root(Path.cwd())

    # First-run detection: check if settings.json exists (created after setup wizard)
    settings_json = data_root / "config" / "settings.json"
    is_first_run = not settings_json.exists()

    if is_first_run:
        console.print("\n[bold green]Welcome to the Vastian Network![/bold green]")
        console.print(
            "[dim]This appears to be your first run. "
            "Open the web UI below to complete setup.[/dim]"
        )
        console.print(
            "[dim]The setup wizard will help you configure your AI provider "
            "(Ollama, OpenAI, or Anthropic) and set your username.[/dim]\n"
        )

    console.print(f"[dim]Web UI: [link={base}]{base}[/link][/dim]")
    console.print(f"[dim]Data folder: {data_root}[/dim]")
    console.print(
        "[dim]  Dashboard | Boards | Sessions | Personas | Meetings | Councils | Settings | Backup[/dim]"
    )
    try:
        await run_server_async(app, port=actual_port, host=host)
    finally:
        _release_worker_lock(settings.state_dir)


async def _run_loop(port: int = 4545, host: str = "127.0.0.1") -> None:
    """Run unified app, open browser, then block. Vastian launcher."""
    from vastian.server import find_available_port

    settings = get_settings()
    _ensure_data_root_seeded(settings)
    orchestrator = Orchestrator(settings)
    try:
        await orchestrator.initialize()
    except Exception as e:
        console.print(f"[red]Initialization error:[/red] {e}")
        raise typer.Exit(1) from e

    from vastian.bridges.cursor_bridge.sessions.engine import SessionEngine
    from vastian.server import create_app, run_server
    from vastian.tasks.feed import NotificationFeed
    from vastian.tasks.persona_chart import PersonaChartPage
    from vastian.tasks.roster import RosterPage
    from vastian.tasks.settings_page import SettingsPage

    actual_port = find_available_port(host, port)
    feed = NotificationFeed(
        db_path=settings.sqlite_path.parent / "tasks.db",
        port=actual_port,
        state_dir=settings.state_dir,
        user_tier=orchestrator.user_tier,
    )
    roster = RosterPage(
        port=actual_port, state_dir=settings.state_dir, user_tier=orchestrator.user_tier
    )
    chart_page = PersonaChartPage(port=actual_port)
    settings_page = SettingsPage(port=actual_port, user_tier=orchestrator.user_tier)

    roster.session_engine = SessionEngine(
        registry=orchestrator.registry,
        bus=orchestrator.bus,
        knowledge=orchestrator.knowledge,
        state_dir=settings.state_dir,
    )
    roster.orchestrator = orchestrator

    async def _on_force_start(task_id: str) -> None:
        await orchestrator._execute_background_task(task_id)

    async def _on_kanban_approved(kanban_id: str) -> None:
        if orchestrator.task_queue:
            tasks = orchestrator.task_queue.get_kanban_tasks(status="approved")
            for t in tasks:
                if t["kanban_id"] == kanban_id:
                    orchestrator.task_queue.start_kanban_task(kanban_id)
                    orchestrator.task_queue.add_notification(
                        t["suggested_by"],
                        f"Kanban task started: {t['title']}",
                        "running",
                    )
                    asyncio.create_task(orchestrator._execute_kanban_task(t))
                    break

    feed.on_force_start = _on_force_start
    feed.on_kanban_approved = _on_kanban_approved
    feed._callbacks_handled_externally = True

    app = create_app(
        {
            "feed": feed,
            "roster": roster,
            "chart": chart_page,
            "settings": settings_page,
            "orchestrator": orchestrator,
            "main_loop": asyncio.get_running_loop(),
            "task_queue": orchestrator.task_queue,
            "project_root": Path.cwd(),
            "user_tier": orchestrator.user_tier,
        }
    )
    base_url = run_server(app, port=actual_port, host=host)
    console.print(f"[green]Unified app:[/green] [link={base_url}]{base_url}[/link]")
    console.print("[dim]Opening browser... Press Ctrl+C to stop.[/dim]")

    # Wait for server to be ready then open browser
    async def _open_browser() -> None:
        await asyncio.sleep(2)
        with contextlib.suppress(Exception):
            _open_feed_browser(base_url)

    asyncio.create_task(_open_browser())

    # Block until Ctrl+C
    try:
        while True:
            await asyncio.sleep(3600)
    except (asyncio.CancelledError, KeyboardInterrupt):
        console.print("\n[dim]Stopping...[/dim]")


async def _chat_loop(target_agent: str | None, verbose: bool, tier: str | None = None) -> None:
    """Main interactive chat loop."""
    settings = get_settings()
    orchestrator = Orchestrator(settings)
    # Set user tier from CLI flag or env var (settings.user_tier defaults to "open")
    orchestrator.user_tier = tier or settings.user_tier

    try:
        await orchestrator.initialize()
    except Exception as e:
        console.print(f"[red]Initialization error:[/red] {e}")
        console.print("[dim]Running in stub mode (no LLM connected).[/dim]")

    # Start unified HTTP server (PSB2: feed + roster + persona + settings on port 4545)
    base_url = ""
    try:
        from vastian.server import create_app, find_available_port, run_server
        from vastian.tasks.feed import NotificationFeed
        from vastian.tasks.persona_chart import PersonaChartPage
        from vastian.tasks.roster import RosterPage
        from vastian.tasks.settings_page import SettingsPage

        actual_port = find_available_port("127.0.0.1", 4545)
        feed = NotificationFeed(
            db_path=settings.sqlite_path.parent / "tasks.db",
            port=actual_port,
            state_dir=settings.state_dir,
            user_tier=orchestrator.user_tier,
        )
        roster = RosterPage(
            port=actual_port,
            state_dir=settings.state_dir,
            user_tier=orchestrator.user_tier,
        )
        chart_page = PersonaChartPage(port=actual_port)
        settings_page = SettingsPage(port=actual_port, user_tier=orchestrator.user_tier)

        from vastian.bridges.cursor_bridge.sessions.engine import SessionEngine

        roster.session_engine = SessionEngine(
            registry=orchestrator.registry,
            bus=orchestrator.bus,
            knowledge=orchestrator.knowledge,
            state_dir=settings.state_dir,
        )
        roster.orchestrator = orchestrator

        async def _on_force_start(task_id: str) -> None:
            await orchestrator._execute_background_task(task_id)

        async def _on_kanban_approved(kanban_id: str) -> None:
            if orchestrator.task_queue:
                tasks = orchestrator.task_queue.get_kanban_tasks(status="approved")
                for task in tasks:
                    if task["kanban_id"] == kanban_id:
                        orchestrator.task_queue.start_kanban_task(kanban_id)
                        orchestrator.task_queue.add_notification(
                            task["suggested_by"],
                            f"Kanban task started: {task['title']}",
                            "running",
                        )
                        asyncio.create_task(orchestrator._execute_kanban_task(task))
                        break

        feed.on_force_start = _on_force_start
        feed.on_kanban_approved = _on_kanban_approved
        feed._callbacks_handled_externally = True  # unified server schedules on main loop

        main_loop = asyncio.get_running_loop()
        app = create_app(
            {
                "feed": feed,
                "roster": roster,
                "chart": chart_page,
                "settings": settings_page,
                "orchestrator": orchestrator,
                "main_loop": main_loop,
                "task_queue": orchestrator.task_queue,
                "project_root": Path.cwd(),
                "user_tier": orchestrator.user_tier,
            }
        )
        base_url = run_server(app, port=actual_port)
        if base_url:
            console.print(f"[dim]Web UI: [link={base_url}]{base_url}[/link][/dim]")
            console.print(
                "[dim]  Unified app: Dashboard, Boards, Sessions, Personas, Meetings, Councils, Settings, Backup[/dim]"
            )
            orchestrator._roster_url = f"{base_url}/#/sessions"
            orchestrator._chart_url = f"{base_url}/#/personas"
            orchestrator._settings_url = f"{base_url}/settings"
    except Exception:
        pass

    # Wire outreach delivery callback so agents can interrupt the user
    async def _outreach_interrupt(item: dict) -> None:
        """Called by the orchestrator's delivery loop for urgent outreach."""
        agent_id = item.get("agent_id", "unknown")
        agent_obj = orchestrator.registry.get_agent(agent_id)
        display_name = agent_obj.config.name if agent_obj else agent_id
        console.print()
        console.print(
            Panel(
                Markdown(item.get("message", "")),
                title=f"[bold yellow]{display_name}[/bold yellow] needs your input",
                subtitle=f"[dim]Priority: {item.get('priority', 'normal')} | Reply to respond, /skip to dismiss[/dim]",
                border_style="yellow",
                padding=(0, 1),
            )
        )

    orchestrator._outreach_callback = _outreach_interrupt

    # Defer heavy startup work to background so the prompt appears immediately
    async def _deferred_startup() -> None:
        """Run heavy startup tasks after the prompt is already displayed."""
        await asyncio.sleep(0.1)  # Yield so the prompt renders first
        try:
            restored = await orchestrator.restore_conversations()
            if restored:
                console.print(
                    f"\n[dim]Restored conversation history for {restored} agent(s).[/dim]"
                )
        except Exception:
            pass
        try:
            if orchestrator.task_queue:
                pending = orchestrator.task_queue.get_running()
                if pending:
                    console.print(
                        f"[dim yellow]{len(pending)} background task(s) still running from last session.[/dim yellow]"
                    )
            _show_sync_suggestions(orchestrator)
            _show_daily_briefing(orchestrator)
        except Exception:
            pass

    asyncio.get_event_loop().call_soon(lambda: asyncio.create_task(_deferred_startup()))

    try:
        await _chat_interaction(orchestrator, target_agent)
    finally:
        orchestrator._user_online = False
        # Generate session journal entry (cross-machine continuity)
        try:
            entry = await orchestrator.generate_session_summary()
            if entry:
                n = len(entry.get("agent_summaries", {}))
                console.print(f"[dim]Session journal saved ({n} agent summary/ies).[/dim]")
        except Exception:
            pass

        # Persist any remaining conversation state before shutdown
        if orchestrator.knowledge:
            for aid, agent in orchestrator.registry.get_all_agents().items():
                if agent.conversation_history:
                    try:
                        await orchestrator.knowledge.structured.clear_conversation(aid)
                        await orchestrator.knowledge.structured.save_conversation_batch(
                            aid, agent.conversation_history[-30:]
                        )
                    except Exception:
                        pass
            with contextlib.suppress(Exception):
                await orchestrator.knowledge.structured.close()
        if orchestrator.task_queue:
            orchestrator.task_queue.close()
        # Note: The feed server is a daemon thread — it stays alive as long as
        # the process runs. The browser tab persists independently because it
        # polls /api/notifications via JS fetch (no meta-refresh reload needed).
        console.print("[dim]Session saved. Feed browser tab stays open. Goodbye![/dim]")


async def _chat_interaction(orchestrator: Orchestrator, target_agent: str | None) -> None:
    """Inner chat interaction loop — separated for clean finally handling."""
    _active_outreach: dict | None = None  # Currently-displayed outreach item

    # Mark user as online (enables outreach delivery loop)
    orchestrator._user_online = True

    while True:
        # Show pending outreach indicator in prompt
        outreach_hint = ""
        if orchestrator.task_queue:
            try:
                breakdown = orchestrator.task_queue.get_outreach_breakdown()
                agent_count = breakdown["agents"]
                # Count Kanban tasks needing attention (separate from outreach)
                kanban_attention = 0
                try:
                    suggested = orchestrator.task_queue.get_kanban_tasks(status="suggested")
                    review_tasks = orchestrator.task_queue.get_kanban_tasks(status="review")
                    user_tasks = [
                        t
                        for t in orchestrator.task_queue.get_kanban_tasks(status="approved")
                        if t.get("assigned_to") == "user"
                    ]
                    kanban_attention = len(suggested) + len(review_tasks) + len(user_tasks)
                except Exception:
                    pass
                # Compact format: "3 agents · 12 tasks"
                parts = []
                if agent_count:
                    parts.append(
                        f"[bold cyan]{agent_count}[/bold cyan] [dim cyan]{'agent' if agent_count == 1 else 'agents'}[/dim cyan]"
                    )
                if kanban_attention:
                    parts.append(
                        f"[bold yellow]{kanban_attention}[/bold yellow] [dim yellow]{'task' if kanban_attention == 1 else 'tasks'}[/dim yellow]"
                    )
                if parts:
                    outreach_hint = f" ({' [dim]·[/dim] '.join(parts)} [dim]— /check-in[/dim])"
            except Exception:
                pass

        try:
            user_input = Prompt.ask(f"\n[bold green]You[/bold green]{outreach_hint}")
        except (KeyboardInterrupt, EOFError):
            orchestrator._user_online = False
            console.print("\n[dim]Goodbye![/dim]")
            break

        if not user_input.strip():
            continue

        if user_input.strip().lower() == "exit":
            orchestrator._user_online = False
            console.print("[dim]Goodbye![/dim]")
            break

        # ── Special Commands ─────────────────────────
        if user_input.startswith("/council "):
            topic = user_input.removeprefix("/council ").strip()
            await _handle_council(orchestrator, topic)
            continue

        # ── /meeting <team_id> [--twin] — convene a team meeting ──
        if user_input.strip().startswith("/meeting"):
            parts = user_input.strip().split()
            if len(parts) < 2:
                console.print("[red]Usage:[/red] /meeting <team_id> [--twin]")
                console.print("[dim]Teams: vv-dev, vv-sync, vv-strategic, vv-scenario[/dim]")
            else:
                meeting_team = parts[1]
                send_twin = "--twin" in parts
                user_joins = not send_twin
                if send_twin:
                    console.print(
                        f"[cyan]Sending your digital twin to {meeting_team} meeting...[/cyan]"
                    )
                else:
                    console.print(f"[cyan]Joining {meeting_team} meeting...[/cyan]")
                try:
                    result = await orchestrator.convene_team_meeting(
                        team_id=meeting_team,
                        user_present=user_joins,
                        send_twin=send_twin if send_twin else None,
                    )
                    if "error" in result:
                        console.print(f"[red]{result['error']}[/red]")
                    elif send_twin:
                        console.print(
                            "[green]Meeting complete. Summary will appear in /check-in.[/green]"
                        )
                except Exception as e:
                    console.print(f"[red]Meeting error:[/red] {e}")
            continue

        if user_input.strip() == "/status":
            _show_status(orchestrator)
            continue

        if user_input.strip() == "/agents":
            _show_agents(orchestrator)
            continue

        if user_input.startswith("/agent "):
            target_agent = user_input.removeprefix("/agent ").strip()
            console.print(f"[dim]Now targeting:[/dim] [bold]{target_agent}[/bold]")
            # Show Theo quality flags if this agent has been flagged
            if target_agent in orchestrator._theo_flags and orchestrator._theo_flags[target_agent]:
                flags = orchestrator._theo_flags[target_agent]
                console.print(
                    f"[dim yellow]  ⚠ Theo has flagged {len(flags)} quality issue(s) "
                    f"for this agent — context injected into conversation.[/dim yellow]"
                )
            continue

        if user_input.strip() == "/auto":
            target_agent = None
            console.print("[dim]Auto-routing re-enabled.[/dim]")
            continue

        if user_input.strip() == "/tasks":
            await _show_tasks(orchestrator)
            continue

        if user_input.startswith("/audit "):
            audit_target = user_input.removeprefix("/audit ").strip()
            await _handle_audit(orchestrator, audit_target)
            continue

        if user_input.strip() == "/tools":
            _show_tools(target_agent or "charles", orchestrator)
            continue

        if user_input.startswith("/tools "):
            tools_target = user_input.removeprefix("/tools ").strip()
            _show_tools(tools_target, orchestrator)
            continue

        if user_input.startswith("/simulate "):
            scenario = user_input.removeprefix("/simulate ").strip()
            await _handle_simulate(orchestrator, scenario)
            continue

        if user_input.strip() == "/metrics":
            _show_metrics()
            continue

        if user_input.startswith("/assess "):
            assess_input = user_input.removeprefix("/assess ").strip()
            await _handle_assess(orchestrator, assess_input)
            continue

        if user_input.strip() == "/models":
            _show_models(orchestrator)
            continue

        if user_input.strip() == "/onboard":
            await _handle_onboard(orchestrator)
            continue

        if user_input.strip() == "/feed":
            _show_feed_url()
            continue

        if user_input.strip() == "/sessions":
            _rurl = getattr(orchestrator, "_roster_url", "")
            if _rurl:
                console.print(
                    f"[bold cyan]Sessions Roster:[/bold cyan] [link={_rurl}]{_rurl}[/link]"
                )
            else:
                console.print(
                    "[dim]Sessions roster not running. Restart the CLI to enable it.[/dim]"
                )
            continue

        if user_input.strip() == "/settings":
            _surl = getattr(orchestrator, "_settings_url", "")
            if _surl:
                console.print(
                    f"[bold cyan]Global Settings:[/bold cyan] [link={_surl}]{_surl}[/link]"
                )
            else:
                console.print("[dim]Settings page not running. Restart the CLI to enable it.[/dim]")
            continue

        if user_input.strip() == "/personas":
            _curl = getattr(orchestrator, "_chart_url", "")
            if _curl:
                console.print(
                    f"[bold cyan]Persona Radar Chart:[/bold cyan] [link={_curl}]{_curl}[/link]"
                )
            else:
                console.print("[dim]Persona chart not running. Restart the CLI to enable it.[/dim]")
            continue

        # ── Wave 7: /health-plans, /spiritual-plans, /plan ─
        if user_input.strip().startswith("/health-plans"):
            from vastian.plans_loader import list_health_plans, load_health_plan

            parts = user_input.strip().split(maxsplit=1)
            plan_name = parts[1].strip() if len(parts) > 1 else ""
            if not plan_name:
                names = list_health_plans()
                console.print("[bold cyan]Health Plans:[/bold cyan]")
                for n in names:
                    console.print(f"  [cyan]{n}[/cyan]")
                console.print("[dim]Usage: /health-plans <name> or /plan health <name>[/dim]")
            else:
                plan = load_health_plan(plan_name)
                if plan:
                    console.print(
                        Panel(
                            yaml.dump(
                                plan, default_flow_style=False, allow_unicode=True, sort_keys=False
                            ),
                            title=f"[bold]Health Plan: {plan_name}[/bold]",
                            border_style="green",
                        )
                    )
                else:
                    console.print(f"[red]Health plan '{plan_name}' not found.[/red]")
            continue

        if user_input.strip().startswith("/spiritual-plans"):
            from vastian.plans_loader import list_spiritual_plans, load_spiritual_plan

            parts = user_input.strip().split(maxsplit=1)
            plan_name = parts[1].strip() if len(parts) > 1 else ""
            if not plan_name:
                names = list_spiritual_plans()
                console.print("[bold cyan]Spiritual Plans:[/bold cyan]")
                for n in names:
                    console.print(f"  [cyan]{n}[/cyan]")
                console.print("[dim]Usage: /spiritual-plans <name> or /plan spiritual <name>[/dim]")
            else:
                plan = load_spiritual_plan(plan_name)
                if plan:
                    console.print(
                        Panel(
                            yaml.dump(
                                plan, default_flow_style=False, allow_unicode=True, sort_keys=False
                            ),
                            title=f"[bold]Spiritual Plan: {plan_name}[/bold]",
                            border_style="magenta",
                        )
                    )
                else:
                    console.print(f"[red]Spiritual plan '{plan_name}' not found.[/red]")
            continue

        if user_input.strip().startswith("/plan"):
            from vastian.plans_loader import load_health_plan, load_spiritual_plan

            parts = user_input.strip().split(maxsplit=2)
            # /plan health chop  or  /plan spiritual living_myths_couple
            if len(parts) >= 3:
                plan_type, plan_name = parts[1].lower(), parts[2].strip()
                if plan_type == "health":
                    plan = load_health_plan(plan_name)
                    if plan:
                        console.print(
                            Panel(
                                yaml.dump(
                                    plan,
                                    default_flow_style=False,
                                    allow_unicode=True,
                                    sort_keys=False,
                                ),
                                title=f"[bold]Health Plan: {plan_name}[/bold]",
                                border_style="green",
                            )
                        )
                    else:
                        console.print(f"[red]Health plan '{plan_name}' not found.[/red]")
                elif plan_type == "spiritual":
                    plan = load_spiritual_plan(plan_name)
                    if plan:
                        console.print(
                            Panel(
                                yaml.dump(
                                    plan,
                                    default_flow_style=False,
                                    allow_unicode=True,
                                    sort_keys=False,
                                ),
                                title=f"[bold]Spiritual Plan: {plan_name}[/bold]",
                                border_style="magenta",
                            )
                        )
                    else:
                        console.print(f"[red]Spiritual plan '{plan_name}' not found.[/red]")
                else:
                    console.print("[red]Usage: /plan health <name> or /plan spiritual <name>[/red]")
            else:
                console.print("[dim]Usage: /plan health <name> or /plan spiritual <name>[/dim]")
            continue

        if user_input.strip() == "/bg" or user_input.strip() == "/background":
            _show_background_tasks(orchestrator)
            continue

        if user_input.strip() == "/clear-failed":
            if orchestrator.task_queue:
                count = orchestrator.task_queue.dismiss_all_failed_tasks()
                console.print(f"[dim]Dismissed {count} failed task(s) from sync suggestions.[/dim]")
            else:
                console.print("[dim]No task queue available.[/dim]")
            continue

        if user_input.startswith("/report"):
            report_arg = user_input.removeprefix("/report").strip()
            _show_report(orchestrator, report_arg)
            continue

        if user_input.strip() == "/inbox":
            _show_inbox_status(orchestrator)
            continue

        if user_input.strip() == "/improve":
            await _handle_improve(orchestrator)
            continue

        # ── E8: /persona command ──────────────────────────
        if user_input.strip().startswith("/persona"):
            parts = user_input.strip().split(maxsplit=1)
            persona_name = parts[1] if len(parts) > 1 else ""
            if not persona_name:
                console.print("[bold cyan]Available Personas:[/bold cyan]")
                try:
                    import yaml

                    pdir = Path("config/personas/archetypes")
                    for f in sorted(pdir.glob("*.yaml")):
                        data = yaml.safe_load(f.read_text(encoding="utf-8"))
                        if data:
                            console.print(
                                f"  [cyan]{data.get('name', f.stem)}[/cyan] - {data.get('dominant_trait', '')} dominant"
                            )
                except Exception:
                    console.print("[dim]Could not load persona configs.[/dim]")
                console.print("[dim]Usage: /persona <name> to start a role-play session[/dim]")
            else:
                # Scenario options: huddle style + metrics domain + optional standard scenario
                style_map = {"1": "labyrinth", "2": "canvas", "3": "portal"}
                metrics_map = {
                    "1": "default",
                    "2": "professional",
                    "3": "personal",
                    "4": "side_venture",
                }
                scenario_block = ""
                try:
                    console.print("[dim]Huddle style: [1] Labyrinth [2] Canvas [3] Portal[/dim]")
                    style_choice = Prompt.ask("[bold]Style[/bold]", default="1").strip() or "1"
                    style = style_map.get(style_choice, "labyrinth")
                    console.print(
                        "[dim]Metrics: [1] Default [2] Professional [3] Personal [4] Side Venture[/dim]"
                    )
                    metrics_choice = Prompt.ask("[bold]Metrics[/bold]", default="1").strip() or "1"
                    metrics_domain = metrics_map.get(metrics_choice, "default")
                    from vastian.scenario_loader import load_standard_scenarios

                    std_scenarios = load_standard_scenarios()
                    scenario_block = ""
                    if std_scenarios:
                        console.print(
                            "[dim]Standard scenario (or Enter to skip): "
                            + ", ".join(
                                f"[{i + 1}] {s['title']}" for i, s in enumerate(std_scenarios[:5])
                            )
                            + "[/dim]"
                        )
                        try:
                            sc_choice = Prompt.ask("[bold]Scenario[/bold]", default="").strip()
                            if (
                                sc_choice
                                and sc_choice.isdigit()
                                and 1 <= int(sc_choice) <= len(std_scenarios)
                            ):
                                chosen = std_scenarios[int(sc_choice) - 1]
                                scenario_block = (
                                    f"\n\n**Scenario**: {chosen.get('title', '')}\n**Entry options** (present these to user): "
                                    + str(chosen.get("entry_options", []))
                                )
                                metrics_domain = chosen.get("domain", metrics_domain)
                        except (KeyboardInterrupt, EOFError):
                            pass
                except (KeyboardInterrupt, EOFError):
                    style, metrics_domain, scenario_block = "labyrinth", "default", ""
                console.print(
                    f"[bold cyan]Starting persona session with: {persona_name}[/bold cyan] (style={style}, metrics={metrics_domain})"
                )
                try:
                    from vastian.scenario_loader import (
                        format_composed_for_prompt,
                        load_composed_prompt,
                        load_metrics_options,
                    )

                    composed = load_composed_prompt(style=style)
                    components_block = format_composed_for_prompt(composed)
                    metrics_list = load_metrics_options(domain=metrics_domain)
                    metrics_block = (
                        f"\n\n**Metrics to score**: {', '.join(metrics_list)}"
                        if metrics_list
                        else ""
                    )
                    adrian = orchestrator.registry.get_agent("adrian")
                    if adrian:
                        prompt = (
                            f"Start an interactive persona role-play session.\n\n"
                            f"The user wants to explore the persona: {persona_name}\n"
                            f"Load the persona config from config/personas/archetypes/ and adopt that personality.\n"
                            f"Use the Session Transfer reference (docs/adrian/reference/session-transfer-casey.md).\n"
                            f"Present the persona, set the scene, offer the first challenge. Use numbered options for decision points.\n"
                            f"Every challenge and wrap-up heading MUST include its emoji (e.g. ### 🌀 First Challenge).\n"
                            f"Be immersive and engaging."
                            f"{components_block}{metrics_block}{scenario_block}"
                        )
                        response = await orchestrator.handle_user_message_direct(
                            "adrian",
                            prompt,
                            delegation_callback=None,
                        )
                        console.print(
                            Panel(
                                Markdown(response[:3000]),
                                title="[bold]Adrian (Persona Guide)[/bold]",
                                border_style="magenta",
                            )
                        )
                except Exception as e:
                    console.print(f"[red]Error: {e}[/red]")
            continue

        # ── E9: /mentor-council command ───────────────────
        if user_input.strip().startswith("/mentor-council"):
            parts = user_input.strip().split(maxsplit=1)
            topic = parts[1] if len(parts) > 1 else ""
            if not topic:
                console.print("[bold cyan]Available Mentors:[/bold cyan]")
                try:
                    import yaml

                    mdir = Path("config/mentors")
                    for f in sorted(mdir.glob("*.yaml")):
                        if f.name.startswith("_"):
                            continue
                        data = yaml.safe_load(f.read_text(encoding="utf-8"))
                        if data:
                            console.print(
                                f"  [cyan]{data.get('name', f.stem)}[/cyan] - {data.get('archetype', '')}"
                            )
                except Exception:
                    console.print("[dim]Could not load mentor configs.[/dim]")
                console.print("[dim]Usage: /mentor-council <topic> to convene a wisdom panel[/dim]")
            else:
                console.print(f"[bold cyan]Convening Mentor Council on: {topic}[/bold cyan]")
                try:
                    # Load mentors and have Victor moderate
                    import yaml

                    mentors = []
                    mdir = Path("config/mentors")
                    for f in sorted(mdir.glob("*.yaml")):
                        if f.name.startswith("_"):
                            continue
                        data = yaml.safe_load(f.read_text(encoding="utf-8"))
                        if data:
                            mentors.append(data)

                    mentor_context = "\n".join(
                        f"- {m['name']} ({m.get('archetype', '')}): {m.get('worldview', '')}"
                        for m in mentors[:6]
                    )
                    victor = orchestrator.registry.get_agent("victor")
                    if victor:
                        prompt = (
                            f"Convene a mentor council on: {topic}\n\n"
                            f"Available mentors:\n{mentor_context}\n\n"
                            f"For each mentor, channel their voice and worldview to give advice on the topic.\n"
                            f"Then synthesize the wisdom into 3 actionable takeaways.\n"
                            f"Speak in each mentor's distinct voice."
                        )
                        response = await orchestrator.handle_user_message_direct(
                            "victor",
                            prompt,
                            delegation_callback=None,
                        )
                        console.print(
                            Panel(
                                Markdown(response[:4000]),
                                title="[bold]Mentor Council[/bold]",
                                border_style="yellow",
                            )
                        )
                except Exception as e:
                    console.print(f"[red]Error: {e}[/red]")
            continue

        # ── E9b: /mentor <name> [question] individual dialogue (Mars M4a) ─────────
        if user_input.strip().startswith("/mentor "):
            rest = user_input.strip()[8:].strip()
            parts = rest.split(maxsplit=1)
            mentor_name = (parts[0] or "").replace(" ", "_").lower()
            user_question = (parts[1] or "").strip() if len(parts) > 1 else ""
            if not mentor_name:
                console.print("[dim]Usage: /mentor <name> e.g. /mentor marcus_aurelius[/dim]")
                try:
                    import yaml

                    mdir = Path("config/mentors")
                    for f in sorted(mdir.glob("*.yaml")):
                        if f.name.startswith("_"):
                            continue
                        data = yaml.safe_load(f.read_text(encoding="utf-8"))
                        if data:
                            console.print(
                                f"  [cyan]{data.get('mentor_id', f.stem)}[/cyan] — {data.get('name', '')}"
                            )
                except Exception:
                    pass
            else:
                mentor_path = Path("config/mentors") / f"{mentor_name}.yaml"
                if not mentor_path.exists():
                    console.print(f"[red]Mentor not found: {mentor_name}[/red]")
                else:
                    try:
                        import yaml

                        mentor_data = yaml.safe_load(mentor_path.read_text(encoding="utf-8"))
                        voice = mentor_data.get("voice", "")
                        worldview = mentor_data.get("worldview", "")
                        name = mentor_data.get("name", mentor_name)
                        archetype = mentor_data.get("archetype", "")
                        mentor_data.get("suggested_topics", [])
                        question_part = (
                            f"\n\nUser's question: {user_question}" if user_question else ""
                        )
                        prompt = (
                            f"You are channeling {name} ({archetype}). "
                            f"Their worldview: {worldview}\n"
                            f"Their voice: {voice}\n\n"
                            f"Victor, moderate: check relevance to the user's context, "
                            f"then respond AS the mentor in their authentic voice."
                            f"{question_part}\n\n"
                            f"If no question was given, invite the user to ask one."
                        )
                        response = await orchestrator.handle_user_message_direct(
                            "victor",
                            prompt,
                            delegation_callback=None,
                        )
                        console.print(
                            Panel(
                                Markdown(response[:4000]),
                                title=f"[bold]{name} — Mentor Dialogue[/bold]",
                                border_style="yellow",
                            )
                        )
                        # Log to docs/sage/mentor-session-log.md
                        log_path = Path("docs/sage/mentor-session-log.md")
                        log_path.parent.mkdir(parents=True, exist_ok=True)
                        from datetime import datetime

                        entry = f"\n## {datetime.now(UTC).strftime('%Y-%m-%d %H:%M')} — {name}\n\n{response[:2000]}\n"
                        if log_path.exists():
                            log_path.write_text(
                                log_path.read_text(encoding="utf-8") + entry, encoding="utf-8"
                            )
                        else:
                            log_path.write_text(
                                "# Mentor Session Log\n\n" + entry, encoding="utf-8"
                            )
                    except Exception as e:
                        console.print(f"[red]Error: {e}[/red]")
            continue

        # ── E10: /sprint command ──────────────────────────
        if user_input.strip().startswith("/sprint"):
            parts = user_input.strip().split(maxsplit=1)
            sub = parts[1].strip() if len(parts) > 1 else ""

            if not sub or sub == "status":
                # Show sprint status
                if orchestrator.task_queue:
                    sprints = orchestrator.task_queue.get_sprints(status="active")
                    if sprints:
                        for s in sprints:
                            burn = orchestrator.task_queue.get_sprint_burndown(s["sprint_id"])
                            console.print(
                                Panel(
                                    f"[bold]{s.get('name', s['sprint_id'])}[/bold]\n"
                                    f"Goal: {s.get('goal', 'none')}\n"
                                    f"Tickets: {burn['total_tickets']} | Points: {burn['done_points']}/{burn['total_points']}\n"
                                    f"Status: {json.dumps(burn['status_counts'], indent=2)}",
                                    title="[bold cyan]Active Sprint[/bold cyan]",
                                    border_style="cyan",
                                )
                            )
                    else:
                        console.print(
                            "[dim]No active sprints. Use /sprint create <name> to start one.[/dim]"
                        )
            elif sub.startswith("create"):
                name = sub.replace("create", "").strip() or "New Sprint"
                sid = orchestrator.task_queue.create_sprint(name)
                console.print(f"[green]Sprint created:[/green] {sid} - {name}")
            elif sub == "plan":
                console.print("[dim]Running cycle planning...[/dim]")
                result = await orchestrator.run_cycle_planning_session()
                console.print(f"[green]Cycle planning complete:[/green] {result.get('notes', '')}")
            elif sub == "close":
                sprints = orchestrator.task_queue.get_sprints(status="active")
                if sprints:
                    sid = sprints[0]["sprint_id"]
                    orchestrator.task_queue.close_sprint(sid)
                    console.print(f"[green]Sprint closed:[/green] {sid}")
                else:
                    console.print("[dim]No active sprint to close.[/dim]")
            else:
                console.print("[dim]Usage: /sprint [status|create <name>|plan|close][/dim]")
            continue

        if user_input.strip() == "/escalate":
            _show_escalation_pathways(orchestrator)
            continue

        if user_input.strip().startswith("/suggest"):
            await _handle_suggest(orchestrator, user_input.strip())
            continue

        if user_input.strip() == "/help":
            console.print(
                "[dim]Commands:[/dim]\n"
                "  /agent <id>      — Switch to a specific agent\n"
                "  /auto            — Re-enable auto-routing (Charles)\n"
                "  /council <topic> — Convene a Mentor Council meeting\n"
                "  /meeting <team>  — Convene a team meeting (vv-dev, vv-sync, etc.)\n"
                "  /meeting <team> --twin — Send your digital twin instead\n"
                "  /audit <id>      — Have Theo audit an agent's prompt quality\n"
                "  /tasks           — Show delegations and pending tasks\n"
                "  /tools [id]      — Show agent's available tools (current agent if no id)\n"
                "  /simulate <desc> — Run a what-if simulation via Concord\n"
                "  /assess <vars>   — Run a metric assessment via Jake\n"
                "  /metrics         — Show all simulation variables and metrics\n"
                "  /models          — Show available LLM models and provider status\n"
                "  /onboard         — Run onboarding: populate docs from saved_prompts/intro.txt\n"
                "  /bg              — Show background task status\n"
                "  /report <id>     — Detailed delegation report (task ID or agent ID)\n"
                "  /inbox           — Show inbox watcher status and folder counts\n"
                "  /improve         — Run Orion's codebase improvement scan\n"
                "  /suggest [id]    — Ask agents to suggest Kanban tasks (or a specific agent)\n"
                "  /escalate        — Show escalation pathways and chains\n"
                "  /feed            — Show notification feed URL (browser + kanban)\n"
                "  /sessions        — Show sessions roster URL (session types + history)\n"
                "  /persona [name]  — List personas or start a role-play session with Adrian\n"
                "  /mentor-council <topic> — Convene a wisdom panel with mentor personas\n"
                "  /sprint [cmd]    — Sprint status, create, plan, or close\n"
                "  /personas        — Show persona radar chart URL (12 archetypes + traits)\n"
                "  /health-plans [name] — List or view health plans (e.g. chop, kathy)\n"
                "  /spiritual-plans [name] — List or view spiritual plans\n"
                "  /plan <health|spiritual> <name> — View a plan by type and name\n"
                "  /settings        — Open global settings (automation frequency, loops, tiers)\n"
                "  /check-in        — Interactive list: pick an item by number to address\n"
                "  /check-in -v     — Verbose view: full outreach panels + reminders\n"
                "  /check-in -k     — Also show Kanban tasks in verbose mode\n"
                "  /skip            — Snooze high-priority outreach (comes back later), dismiss normal\n"
                "  /dismiss         — Permanently dismiss current outreach (no snooze)\n"
                "  /clear-failed    — Dismiss all stale failed tasks from sync suggestions\n"
                "  /assign-me <id>  — Claim a suggested Kanban task for yourself (agents won't auto-start)\n"
                "  /done <id>       — Mark a Kanban task as done by its ID\n"
                "  /giga-sync       — Show pending Giga outbox items (agent-written plans/neurons)\n"
                "  /pending         — List pending transaction approvals\n"
                "  /approve <id>    — Approve a pending transaction\n"
                "  /deny <id>       — Deny a pending transaction\n"
                "  /agents          — List all agents\n"
                "  /status          — Show network status\n"
                "  /help            — Show this help\n"
                "  exit             — Quit\n\n"
                "[dim]Message Modifiers (append to any message):[/dim]\n"
                "  --thorough       — Agent searches more (12 rounds, 8 results/search)\n"
                "  --deep           — Maximum research depth (25 rounds, 10 results/search)\n"
                "  Example: research Denver events --deep\n\n"
                "[dim]Background worker (separate terminal):[/dim]\n"
                "  vastian worker [zo-inbox] — Run background tasks (zo-inbox = run on Zo)\n"
                "  vastian improve  — Run Orion's codebase improvement scan"
            )
            continue

        # ── /pending — list pending approvals ─────────
        if user_input.strip() == "/pending":
            _show_pending_approvals(orchestrator)
            continue

        # ── /approve <id> — approve a pending transaction ─
        if user_input.strip().startswith("/approve"):
            approval_id = user_input.strip().removeprefix("/approve").strip()
            if not approval_id:
                console.print("[red]Usage:[/red] /approve <approval_id>")
            else:
                await _handle_approve(orchestrator, approval_id, approve=True)
            continue

        # ── /deny <id> — deny a pending transaction ──────
        if user_input.strip().startswith("/deny"):
            approval_id = user_input.strip().removeprefix("/deny").strip()
            if not approval_id:
                console.print("[red]Usage:[/red] /deny <approval_id>")
            else:
                await _handle_approve(orchestrator, approval_id, approve=False)
            continue

        # ── /check-in — surface pending agent outreach + Kanban suggestions ────
        if user_input.strip().startswith("/check-in") or user_input.strip().startswith("/checkin"):
            has_anything = False
            tokens = user_input.strip().split()
            show_kanban = "--kanban" in user_input or "-k" in tokens
            # Interactive selector is now the DEFAULT. Use --verbose / -v for old wall-of-text.
            show_verbose = "--verbose" in user_input or "-v" in tokens
            show_list = not show_verbose  # default to interactive list

            # ── Interactive list mode (DEFAULT) ──
            if show_list:
                items: list[dict] = []  # unified list of outreach + kanban for selection

                # Gather outreach items
                all_outreach_list: list[dict] = []
                if orchestrator.task_queue:
                    with contextlib.suppress(Exception):
                        all_outreach_list = orchestrator.task_queue.get_pending_outreach()
                for o in all_outreach_list:
                    a_id = o.get("agent_id", "unknown")
                    a_obj = orchestrator.registry.get_agent(a_id)
                    a_name = a_obj.config.name if a_obj else a_id
                    items.append(
                        {
                            "type": "outreach",
                            "agent_id": a_id,
                            "agent_name": a_name,
                            "title": (o.get("subject") or o.get("message", ""))[:60],
                            "priority": o.get("priority", "normal"),
                            "id": o.get("outreach_id", "?"),
                            "raw": o,
                        }
                    )

                # Gather actionable Kanban items
                if orchestrator.task_queue:
                    suggested = orchestrator.task_queue.get_kanban_tasks(status="suggested")
                    review_tasks = orchestrator.task_queue.get_kanban_tasks(status="review")
                    user_tasks = [
                        t
                        for t in orchestrator.task_queue.get_kanban_tasks(status="approved")
                        if t.get("assigned_to") == "user"
                    ]
                    for t in suggested + review_tasks + user_tasks:
                        a_id = t.get("suggested_by", "?")
                        a_obj = orchestrator.registry.get_agent(a_id)
                        a_name = a_obj.config.name if a_obj else a_id
                        items.append(
                            {
                                "type": "kanban",
                                "agent_id": a_id,
                                "agent_name": a_name,
                                "title": t.get("title", "")[:60],
                                "priority": t.get("priority", "normal"),
                                "id": t.get("kanban_id", "?"),
                                "raw": t,
                            }
                        )

                if not items:
                    console.print("[dim]No agents need your input right now.[/dim]")
                    continue

                # Sort: critical > high > normal > low
                priority_order = {"critical": 0, "high": 1, "normal": 2, "low": 3}
                items.sort(key=lambda x: priority_order.get(x["priority"], 2))

                # ── Arrow-key selector (questionary) ──
                if HAS_QUESTIONARY:
                    _cancel_label = "  ← cancel"
                    choices = []
                    for item in items:
                        p = item["priority"]
                        p_tag = {
                            "critical": "!!!",
                            "high": " ! ",
                            "normal": " · ",
                            "low": "   ",
                        }.get(p, " · ")
                        kind = "agent" if item["type"] == "outreach" else "task"
                        label = f"{p_tag} {item['agent_name']:<12}  [{kind}]  {item['title']}"
                        choices.append(questionary.Choice(title=label, value=item))
                    choices.append(questionary.Choice(title=_cancel_label, value=None))

                    try:
                        selected = await questionary.select(
                            "Select an item to address (↑/↓ then Enter):",
                            choices=choices,
                            style=_Q_STYLE,
                            use_shortcuts=False,
                            use_arrow_keys=True,
                            use_jk_keys=False,
                        ).ask_async()
                    except (KeyboardInterrupt, EOFError):
                        continue

                    if selected is None or not isinstance(selected, dict):
                        continue
                else:
                    # Fallback: numbered list if questionary unavailable
                    console.print("\n[bold]Select an item to address:[/bold]")
                    for idx, item in enumerate(items, 1):
                        p = item["priority"]
                        p_color = (
                            "red"
                            if p in ("high", "critical")
                            else "yellow"
                            if p == "normal"
                            else "dim"
                        )
                        type_tag = (
                            "[cyan]outreach[/cyan]"
                            if item["type"] == "outreach"
                            else "[yellow]task[/yellow]"
                        )
                        console.print(
                            f"  [bold]{idx:>2}.[/bold] [{p_color}]{p.upper():>8}[/{p_color}]  "
                            f"[bold cyan]{item['agent_name']:<12}[/bold cyan]  "
                            f"{type_tag}  {item['title']}"
                        )
                    console.print(
                        f"\n[dim]Enter a number (1-{len(items)}) or press Enter to cancel.[/dim]"
                    )
                    try:
                        choice_raw = Prompt.ask("[bold green]#[/bold green]", default="")
                    except (KeyboardInterrupt, EOFError):
                        continue
                    if not choice_raw.strip():
                        continue
                    try:
                        choice_idx = int(choice_raw.strip()) - 1
                        if choice_idx < 0 or choice_idx >= len(items):
                            console.print("[red]Invalid selection.[/red]")
                            continue
                    except ValueError:
                        console.print("[red]Enter a number.[/red]")
                        continue
                    selected = items[choice_idx]

                if selected["type"] == "outreach":
                    # Show the outreach panel and jump into conversation
                    outreach_item = selected["raw"]
                    _active_outreach = outreach_item
                    agent_name = outreach_item.get("agent_id", "unknown")
                    agent_obj = orchestrator.registry.get_agent(agent_name)
                    display_name = agent_obj.config.name if agent_obj else agent_name
                    console.print()
                    console.print(
                        Panel(
                            Markdown(outreach_item.get("message", "")),
                            title=f"[bold cyan]{display_name}[/bold cyan] needs your input",
                            subtitle=f"[dim]Priority: {outreach_item.get('priority', 'normal')} | ID: {outreach_item.get('outreach_id', '?')}[/dim]",
                            border_style="cyan",
                            padding=(0, 1),
                        )
                    )
                    console.print("[dim]Reply to respond, /skip to dismiss.[/dim]")
                    if orchestrator.task_queue:
                        orchestrator.task_queue.mark_outreach_delivered(
                            outreach_item["outreach_id"]
                        )
                    target_agent = agent_name
                else:
                    # Kanban task — switch to that agent and have them brief the user
                    t = selected["raw"]
                    agent_name = t.get("suggested_by", "unknown")
                    agent_obj = orchestrator.registry.get_agent(agent_name)
                    display_name = agent_obj.config.name if agent_obj else agent_name
                    target_agent = agent_name

                    # Show a compact header, then let the agent brief the user
                    console.print()
                    console.print(
                        f"[bold cyan]{display_name}[/bold cyan] — "
                        f"[bold]{t.get('title', '')}[/bold]  "
                        f"[dim](ID: {t.get('kanban_id', '?')})[/dim]"
                    )
                    console.print("[dim]Connecting you...[/dim]")

                    # Auto-send a briefing request to the agent
                    brief_msg = (
                        f"I'm checking in on this Kanban task you suggested:\n\n"
                        f"**{t.get('title', '')}**\n{t.get('description', '')}\n\n"
                        f"(Status: {t.get('status', '?')} | ID: {t.get('kanban_id', '?')})\n\n"
                        f"Brief me on this — what do you need from me to move it forward?"
                    )
                    try:
                        reply = await orchestrator.chat(brief_msg, agent_id=agent_name)
                        console.print()
                        console.print(
                            Panel(
                                Markdown(reply),
                                title=f"[bold cyan]{display_name}[/bold cyan]",
                                border_style="cyan",
                                padding=(0, 1),
                            )
                        )
                        console.print(
                            f"[dim]Now talking to {display_name}. Reply to continue.[/dim]"
                        )
                    except Exception as e:
                        logger.debug("Briefing failed: %s", e)
                        console.print(
                            Panel(
                                f"[bold]{t.get('title', '')}[/bold]\n\n{t.get('description', '')}\n\n"
                                f"[dim]Status: {t.get('status', '?')} | ID: {t.get('kanban_id', '?')}[/dim]",
                                title=f"[bold cyan]{display_name}[/bold cyan] — Kanban Task",
                                border_style="yellow",
                                padding=(0, 1),
                            )
                        )
                        console.print(
                            f"[dim]Now talking to {display_name}. Type your message.[/dim]"
                        )

                continue

            # ── Verbose /check-in -v (old wall-of-text mode) ──

            # 1. Kanban tasks needing attention
            kanban_count = 0
            if orchestrator.task_queue:
                suggested = orchestrator.task_queue.get_kanban_tasks(status="suggested")
                review_tasks = orchestrator.task_queue.get_kanban_tasks(status="review")
                user_tasks = [
                    t
                    for t in orchestrator.task_queue.get_kanban_tasks(status="approved")
                    if t.get("assigned_to") == "user"
                ]
                actionable = suggested + review_tasks + user_tasks
                kanban_count = len(actionable)

                # Only show full Kanban list with --kanban / -k flag
                if show_kanban and actionable:
                    has_anything = True
                    console.print(
                        "\n[bold yellow]Kanban Tasks Needing Your Attention[/bold yellow]"
                    )
                    for t in actionable:
                        agent_id = t.get("suggested_by", "?")
                        agent_obj = orchestrator.registry.get_agent(agent_id)
                        agent_label = agent_obj.config.name if agent_obj else agent_id
                        if t.get("assigned_to") == "user":
                            status_label = "Assigned to you — waiting for inbox"
                        elif t["status"] == "suggested":
                            status_label = "Awaiting approval"
                        else:
                            status_label = "Review result"
                        priority = t.get("priority", "normal")
                        p_color = (
                            "red"
                            if priority in ("high", "critical")
                            else "yellow"
                            if priority == "normal"
                            else "dim"
                        )
                        yours_tag = (
                            " [bold blue](yours)[/bold blue]"
                            if t.get("assigned_to") == "user"
                            else ""
                        )
                        console.print(
                            f"  [{p_color}]{priority.upper()}[/{p_color}] "
                            f"[cyan]{agent_label}[/cyan] — {t['title']}{yours_tag}"
                        )
                        console.print(f"    [dim]{status_label} | ID: {t['kanban_id']}[/dim]")
                        if t.get("description"):
                            console.print(f"    [dim]{t['description'][:120]}[/dim]")
                    review_ids = [t["kanban_id"] for t in actionable if t["status"] == "review"]
                    if review_ids:
                        console.print("[dim]Tasks in review — mark as done with: /done <id>[/dim]")
                    suggested_ids = [
                        t["kanban_id"] for t in actionable if t["status"] == "suggested"
                    ]
                    if suggested_ids:
                        console.print(
                            "[dim]Suggested tasks — /assign-me <id> to claim, or approve in browser.[/dim]"
                        )
                    if user_tasks:
                        console.print(
                            "[dim]Your tasks — name files task-<id>-yourfile.txt and drop in saved_prompts/inbox/ (or cloud upload if enabled).[/dim]"
                        )
                    console.print(
                        "[dim]Open the Kanban board in the browser (/feed) to approve or review.[/dim]\n"
                    )

            # 2. Show outreach requests (agents needing user input)
            all_outreach = []
            if orchestrator.task_queue:
                with contextlib.suppress(Exception):
                    all_outreach = orchestrator.task_queue.get_pending_outreach()

            if all_outreach:
                has_anything = True
                outreach_item = all_outreach[0]
                remaining = len(all_outreach) - 1
                _active_outreach = outreach_item
                agent_name = outreach_item.get("agent_id", "unknown")
                agent_obj = orchestrator.registry.get_agent(agent_name)
                display_name = agent_obj.config.name if agent_obj else agent_name
                remaining_hint = f" (+{remaining} more)" if remaining else ""
                console.print()
                console.print(
                    Panel(
                        Markdown(outreach_item.get("message", "")),
                        title=f"[bold cyan]{display_name}[/bold cyan] needs your input{remaining_hint}",
                        subtitle=f"[dim]Priority: {outreach_item.get('priority', 'normal')} | ID: {outreach_item.get('outreach_id', '?')}[/dim]",
                        border_style="cyan",
                        padding=(0, 1),
                    )
                )
                if remaining:
                    console.print(
                        f"[dim]Reply to respond, /skip to dismiss. {remaining} more outreach item(s) after this — /check-in again to see.[/dim]"
                    )
                else:
                    console.print(
                        "[dim]Reply to respond, or /skip to dismiss. This is the only pending outreach.[/dim]"
                    )
                # Mark delivered
                if orchestrator.task_queue:
                    orchestrator.task_queue.mark_outreach_delivered(outreach_item["outreach_id"])
                # Switch target agent to the requesting agent
                target_agent = agent_name

            # 3. Show upcoming scheduled reminders
            if orchestrator.task_queue:
                scheduled = orchestrator.task_queue.get_scheduled_outreach()
                if scheduled:
                    has_anything = True
                    console.print("\n[bold blue]Upcoming Scheduled Reminders[/bold blue]")
                    for s in scheduled[:5]:
                        s_agent = s.get("agent_id", "?")
                        s_obj = orchestrator.registry.get_agent(s_agent)
                        s_name = s_obj.config.name if s_obj else s_agent
                        sched_date = (s.get("scheduled_at") or "?")[:10]
                        console.print(
                            f"  [blue]{sched_date}[/blue] [cyan]{s_name}[/cyan] — {s.get('subject', '?')}"
                        )
                    console.print(
                        "[dim]These will be delivered automatically on their scheduled dates.[/dim]\n"
                    )

            # Brief Kanban hint when flag wasn't used but tasks exist
            if not show_kanban and kanban_count > 0:
                console.print(
                    f"[dim]{kanban_count} Kanban task{'s' if kanban_count != 1 else ''} need attention — use /check-in -k to view.[/dim]"
                )

            if not has_anything and kanban_count == 0:
                console.print("[dim]No agents need your input right now.[/dim]")
            elif not has_anything and kanban_count > 0:
                pass  # Kanban hint already shown above
            continue

        # ── /skip — dismiss or snooze current outreach ──────
        if user_input.strip().startswith("/skip"):
            if _active_outreach:
                oid = _active_outreach["outreach_id"]
                priority = _active_outreach.get("priority", "normal")
                # High/critical outreach is snoozed (comes back later), not dismissed
                if priority in ("high", "critical"):
                    if orchestrator.task_queue:
                        new_id = orchestrator.task_queue.snooze_outreach(oid, hours=4)
                        if new_id:
                            console.print("[dim]Snoozed — will check back in ~4 hours.[/dim]")
                        else:
                            console.print("[dim]Dismissed.[/dim]")
                else:
                    if orchestrator.task_queue:
                        orchestrator.task_queue.dismiss_outreach(oid)
                    console.print("[dim]Dismissed.[/dim]")
                _active_outreach = None
                target_agent = None
            else:
                console.print("[dim]No active outreach to skip.[/dim]")
            continue

        # ── /dismiss — force-dismiss (even high priority, no snooze) ──
        if user_input.strip() == "/dismiss":
            if _active_outreach:
                if orchestrator.task_queue:
                    orchestrator.task_queue.dismiss_outreach(_active_outreach["outreach_id"])
                console.print("[dim]Permanently dismissed.[/dim]")
                _active_outreach = None
                target_agent = None
            else:
                console.print("[dim]No active outreach to dismiss.[/dim]")
            continue

        # ── /assign-me <kanban_id> — claim a suggested task for yourself ──
        if user_input.strip().startswith("/assign-me"):
            kanban_id = user_input.strip().removeprefix("/assign-me").strip()
            if not kanban_id:
                console.print("[red]Usage:[/red] /assign-me <kanban_id>")
            elif orchestrator.task_queue:
                task = orchestrator.task_queue.get_kanban_task(kanban_id)
                if not task:
                    console.print(f"[red]Kanban task not found:[/red] {kanban_id}")
                elif task.get("status") != "suggested":
                    console.print(
                        f"[red]Task must be in 'suggested' status to assign.[/red] Current: {task.get('status')}"
                    )
                else:
                    orchestrator.task_queue.assign_kanban_to_user(kanban_id)
                    console.print(f"[green]Assigned to you:[/green] {task.get('title', kanban_id)}")
                    console.print(
                        f"[dim]Agents will not auto-start this task. Name a file "
                        f"task-{kanban_id}-yourfile.txt and drop in saved_prompts/inbox/ (or cloud upload if enabled).[/dim]"
                    )
            else:
                console.print("[red]Task queue not initialized.[/red]")
            continue

        # ── /done <kanban_id> — mark a kanban task as done from CLI ────
        if user_input.strip().startswith("/done"):
            kanban_id = user_input.strip().removeprefix("/done").strip()
            if not kanban_id:
                console.print("[red]Usage:[/red] /done <kanban_id>")
            elif orchestrator.task_queue:
                task = orchestrator.task_queue.get_kanban_task(kanban_id)
                if task:
                    orchestrator.task_queue.update_kanban_status(kanban_id, "done")
                    console.print(f"[green]Marked as done:[/green] {task.get('title', kanban_id)}")
                else:
                    console.print(f"[red]Kanban task not found:[/red] {kanban_id}")
            else:
                console.print("[red]Task queue not initialized.[/red]")
            continue

        # ── /giga-sync — show pending outbox items for Giga ──────
        if user_input.strip().startswith("/giga-sync"):
            await _handle_giga_sync(console)
            continue

        # ── Catch unknown slash commands ─────────────
        if user_input.strip().startswith("/"):
            console.print(
                f"[red]Unknown command:[/red] {user_input.strip().split()[0]}. Type /help for available commands."
            )
            continue

        # ── Normal Message ───────────────────────────
        orchestrator.reset_idle_timer()

        # If responding to an active outreach, record the response
        if _active_outreach and orchestrator.task_queue:
            orchestrator.task_queue.resolve_outreach(
                _active_outreach["outreach_id"],
                user_input[:500],
            )
            _active_outreach = None

        responding_agent = target_agent or "charles"

        # ── Parse depth modifiers (--deep, --thorough) ─────────
        research_depth = "normal"
        clean_input = user_input
        if user_input.rstrip().endswith("--deep"):
            research_depth = "deep"
            clean_input = user_input.rstrip().removesuffix("--deep").rstrip()
        elif user_input.rstrip().endswith("--thorough"):
            research_depth = "thorough"
            clean_input = user_input.rstrip().removesuffix("--thorough").rstrip()

        if research_depth != "normal":
            console.print(
                f"[dim]Research depth: [bold]{research_depth}[/bold] — agent will search more extensively.[/dim]"
            )

        async def _on_delegation(from_name: str, to_name: str, task: str) -> None:
            """Show delegation progress in the UI."""
            console.print(
                f"  [dim yellow]↳ {from_name} delegating to {to_name}:[/dim yellow] "
                f"[dim]{task[:120]}[/dim]"
            )

        status_label = (
            "[cyan]Researching deeply...[/cyan]"
            if research_depth == "deep"
            else "[cyan]Researching thoroughly...[/cyan]"
            if research_depth == "thorough"
            else "[cyan]Thinking...[/cyan]"
        )
        status = console.status(status_label)
        status.start()
        try:
            response = await orchestrator.handle_user_message_direct(
                content=clean_input,
                agent_id=responding_agent,
                delegation_callback=_on_delegation,
                research_depth=research_depth,
            )
        except Exception as e:
            response = f"Error: {e}"
        finally:
            status.stop()

        # Display response
        agent_config = orchestrator.registry.get_config(responding_agent)
        agent_label = agent_config.display_name if agent_config else responding_agent

        console.print(f"\n[bold blue]{agent_label}[/bold blue]")
        console.print(Panel(Markdown(response), border_style="blue", padding=(1, 2)))


async def _show_tasks(orchestrator: Orchestrator) -> None:
    """Show delegations and pending tasks."""
    if not orchestrator.knowledge:
        console.print("[red]Knowledge store not available.[/red]")
        return

    delegations = await orchestrator.knowledge.structured.get_delegations(limit=20)
    tasks = await orchestrator.knowledge.structured.get_tasks(limit=20)

    if delegations:
        console.print("\n[bold]Recent Delegations[/bold]\n")
        for i, d in enumerate(delegations, 1):
            status_color = "green" if d.get("status") == "completed" else "yellow"
            console.print(
                f"  [bold]{i}.[/bold] [{status_color}]{d.get('status', 'unknown')}[/{status_color}] "
                f"[cyan]{d.get('from_agent', '')}[/cyan] → [green]{d.get('to_agent', '')}[/green] "
                f"[dim]({(d.get('created_at', ''))[:16]})[/dim]"
            )
            console.print(f"     [bold]Task:[/bold] {d.get('task_description', '')}")
            response = d.get("response", "")
            if response:
                # Show first 300 chars of the response
                preview = response[:300] + ("..." if len(response) > 300 else "")
                console.print(f"     [bold]Response:[/bold] {preview}")
            console.print()
    else:
        console.print("[dim]No delegations recorded yet.[/dim]")

    if tasks:
        table = Table(title="Tasks", show_header=True)
        table.add_column("Title")
        table.add_column("Assigned To", style="green")
        table.add_column("Status")
        table.add_column("Priority")

        for t in tasks:
            table.add_row(
                t.get("title", ""),
                t.get("assigned_to", ""),
                t.get("status", ""),
                t.get("priority", ""),
            )
        console.print(table)


async def _handle_audit(orchestrator: Orchestrator, agent_id: str) -> None:
    """Have Theo audit an agent's prompt and configuration."""
    target_config = orchestrator.registry.get_config(agent_id)
    if target_config is None:
        console.print(f"[red]Agent not found: {agent_id}[/red]")
        return

    console.print(f"[dim yellow]Theo is auditing {target_config.display_name}...[/dim yellow]")

    # Build audit prompt for Theo with the target agent's full config
    import yaml

    config_dump = yaml.dump(target_config.model_dump(), default_flow_style=False)

    # Also get the target agent's recent conversation history
    target_agent = orchestrator.registry.get_agent(agent_id)
    recent_history = ""
    if target_agent and target_agent.conversation_history:
        recent_msgs = target_agent.conversation_history[-10:]
        recent_history = "\n".join(f"[{m['role']}]: {m['content'][:300]}" for m in recent_msgs)

    audit_prompt = (
        f"AUDIT REQUEST: Review the following agent's configuration and recent behavior.\n\n"
        f"## Agent YAML Config\n```yaml\n{config_dump}```\n\n"
    )
    if recent_history:
        audit_prompt += f"## Recent Conversation History\n{recent_history}\n\n"
    else:
        audit_prompt += "## Recent Conversation History\nNo conversations yet.\n\n"

    audit_prompt += (
        "## Your Audit Tasks\n"
        "1. Is the system prompt well-structured for this agent's role?\n"
        "2. Are there any gaps in responsibilities or owned documents?\n"
        "3. Could the tone or role_description cause confusion or hallucination?\n"
        "4. Are there specific prompt improvements you'd recommend?\n"
        "5. If there's conversation history, did the agent perform well? Any drift?\n"
        "\nProvide specific, actionable fixes — not vague suggestions."
    )

    with console.status("[yellow]Theo is reviewing...[/yellow]"):
        try:
            response = await orchestrator.handle_user_message_direct(
                content=audit_prompt,
                agent_id="theo",
            )
        except Exception as e:
            console.print(f"[red]Audit error:[/red] {e}")
            return

    console.print(f"\n[bold magenta]🛡️🔍 Theo — Prompt Audit: {target_config.name}[/bold magenta]")
    console.print(Panel(Markdown(response), border_style="magenta", padding=(1, 2)))


async def _handle_council(orchestrator: Orchestrator, topic: str) -> None:
    """Handle a council meeting request."""
    console.print(
        Panel(
            f"[bold yellow]Convening Mentor Council[/bold yellow]\nTopic: {topic}",
            border_style="yellow",
        )
    )

    async def _council_progress(phase: str, agent_name: str, detail: str) -> None:
        """Show live council progress with full conversation content."""
        if phase == "contribution":
            # Show the actual agent contribution in a styled panel
            console.print()
            console.print(
                Panel(
                    Markdown(detail),
                    title=f"[bold cyan]{agent_name}[/bold cyan]",
                    border_style="cyan",
                    padding=(0, 1),
                )
            )
        elif phase == "synthesis_result":
            # Show the synthesis as it comes in
            console.print()
            console.print(
                Panel(
                    Markdown(detail),
                    title=f"[bold green]Synthesis by {agent_name}[/bold green]",
                    border_style="green",
                    padding=(0, 1),
                )
            )
        elif phase == "thinking":
            # Brief status line while agent is working
            console.print(f"  [dim yellow]⟐ {agent_name}[/dim yellow] [dim]{detail}[/dim]")
        elif agent_name:
            console.print(f"  [dim yellow]⟐ {agent_name}:[/dim yellow] [dim]{detail}[/dim]")
        else:
            console.print(f"  [dim yellow]⟐[/dim yellow] [dim]{detail}[/dim]")

    # Attach progress callback
    if orchestrator.council:
        orchestrator.council.progress_callback = _council_progress

    try:
        result = await orchestrator.convene_council(topic=topic)
    except Exception as e:
        console.print(f"[red]Council error:[/red] {e}")
        return
    finally:
        # Detach callback
        if orchestrator.council:
            orchestrator.council.progress_callback = None

    if "error" in result:
        console.print(f"[red]{result['error']}[/red]")
        return

    # Display results
    console.print(
        Panel(
            f"[bold green]Council Complete[/bold green]\n"
            f"Type: {result.get('type', 'N/A')}\n"
            f"Rounds: {result.get('rounds_completed', 0)}\n"
            f"Contributions: {result.get('total_contributions', 0)}",
            border_style="green",
        )
    )

    if result.get("synthesis"):
        console.print("\n[bold]Synthesis:[/bold]")
        console.print(Panel(Markdown(result["synthesis"]), border_style="cyan"))

    if result.get("action_items"):
        console.print("\n[bold]Action Items:[/bold]")
        table = Table(show_header=True)
        table.add_column("Priority")
        table.add_column("Description")
        table.add_column("Assigned To")
        for item in result["action_items"]:
            table.add_row(
                item.get("priority", "normal").upper(),
                item.get("description", ""),
                item.get("assigned_to", ""),
            )
        console.print(table)


def _show_status(orchestrator: Orchestrator) -> None:
    """Display network status."""
    status = orchestrator.get_network_status()
    table = Table(title="Vastian Network Status", show_header=True)
    table.add_column("Agent")
    table.add_column("Title")
    table.add_column("State")
    table.add_column("Reports To")

    for _aid, info in status.get("agents", {}).items():
        table.add_row(
            info["name"],
            info["title"],
            info["state"],
            info.get("reports_to", "—"),
        )

    console.print(table)
    console.print(f"\n[dim]Teams: {', '.join(status.get('teams', []))}[/dim]")
    console.print(f"[dim]Messages logged: {status.get('message_history_size', 0)}[/dim]")


def _show_agents(orchestrator: Orchestrator) -> None:
    """Display all agents."""
    agents = orchestrator.list_agents()
    table = Table(title="Vastian Network Agents", show_header=True)
    table.add_column("ID")
    table.add_column("Sigil")
    table.add_column("Name")
    table.add_column("Title")
    table.add_column("Reports To")

    for a in agents:
        table.add_row(
            a["id"],
            a.get("sigil", ""),
            a["name"],
            a["title"],
            a.get("reports_to", "—") or "—",
        )

    console.print(table)


async def _handle_simulate(orchestrator: Orchestrator, scenario: str) -> None:
    """Route a what-if scenario to Concord for simulation."""
    console.print(
        Panel(
            f"[bold cyan]Running Simulation via Concord[/bold cyan]\nScenario: {scenario}",
            border_style="cyan",
        )
    )

    async def _on_delegation(from_name: str, to_name: str, task: str) -> None:
        console.print(
            f"  [dim yellow]↳ {from_name} delegating to {to_name}:[/dim yellow] "
            f"[dim]{task[:120]}[/dim]"
        )

    status = console.status("[cyan]Concord is building the simulation...[/cyan]")
    status.start()
    try:
        response = await orchestrator.handle_user_message_direct(
            content=(
                f"Run a what-if simulation for the following scenario: {scenario}\n\n"
                f"Use your run_simulation tool. Search the VKB first for baseline data. "
                f"Save the full results to your documents."
            ),
            agent_id="concord",
            delegation_callback=_on_delegation,
        )
    except Exception as e:
        response = f"Simulation error: {e}"
    finally:
        status.stop()

    concord_config = orchestrator.registry.get_config("concord")
    label = concord_config.display_name if concord_config else "Concord"
    console.print(f"\n[bold blue]{label}[/bold blue]")
    console.print(Panel(Markdown(response), border_style="cyan", padding=(1, 2)))


async def _handle_assess(orchestrator: Orchestrator, variables_input: str) -> None:
    """Route a metric assessment to Jake."""
    console.print(
        Panel(
            f"[bold green]Running Metric Assessment via Jake[/bold green]\n"
            f"Variables: {variables_input}",
            border_style="green",
        )
    )

    async def _on_delegation(from_name: str, to_name: str, task: str) -> None:
        console.print(
            f"  [dim yellow]↳ {from_name} delegating to {to_name}:[/dim yellow] "
            f"[dim]{task[:120]}[/dim]"
        )

    status = console.status("[green]Jake is running the assessment...[/green]")
    status.start()
    try:
        response = await orchestrator.handle_user_message_direct(
            content=(
                f"Run a metric assessment for these variables: {variables_input}\n\n"
                f"Use your score_metrics tool. Search the VKB first for historical baselines. "
                f"Save the results to your documents."
            ),
            agent_id="jake",
            delegation_callback=_on_delegation,
        )
    except Exception as e:
        response = f"Assessment error: {e}"
    finally:
        status.stop()

    jake_config = orchestrator.registry.get_config("jake")
    label = jake_config.display_name if jake_config else "Jake"
    console.print(f"\n[bold blue]{label}[/bold blue]")
    console.print(Panel(Markdown(response), border_style="green", padding=(1, 2)))


async def _handle_onboard(orchestrator: Orchestrator) -> None:
    """Run the onboarding flow: read intro.txt and delegate doc creation to agents."""
    from pathlib import Path

    from vastian.storage.data_root import get_saved_prompts_dir

    intro_path = get_saved_prompts_dir(Path.cwd()) / "intro.txt"
    if not intro_path.exists():
        console.print(
            "[red]Onboarding requires saved_prompts/intro.txt[/red]\n"
            "[dim]Write a paragraph or two about yourself — your job, goals, finances, "
            "interests, and what you want from this network. Save it to "
            "saved_prompts/intro.txt and run /onboard again.[/dim]"
        )
        return

    intro_content = intro_path.read_text(encoding="utf-8").strip()
    if len(intro_content) < 50:
        console.print(
            "[red]intro.txt is too short. Write at least a paragraph about yourself.[/red]"
        )
        return

    console.print(
        Panel(
            "[bold]Onboarding[/bold]\n\n"
            f"Read intro.txt ({len(intro_content)} chars). Charles is delegating document "
            f"creation to your team in the background.\n\n"
            f"[dim]Check progress: /bg or open the activity feed: /feed[/dim]",
            border_style="cyan",
        )
    )

    if orchestrator.task_queue:
        task = orchestrator.task_queue.enqueue(
            task_type="onboarding",
            agent_id="charles",
            description="Onboarding: populate all agent docs from intro.txt",
            payload={"intro_path": str(intro_path)},
        )
        import asyncio

        asyncio.create_task(orchestrator._execute_background_task(task.task_id))
        console.print(
            f"[green]Onboarding task queued (#{task.task_id}). Your team is working on it.[/green]"
        )
    else:
        console.print("[red]Task queue not initialized.[/red]")


def _show_feed_url() -> None:
    """Show the notification feed URL with a cache-busting parameter."""
    import time

    url = f"http://127.0.0.1:4545?v={int(time.time())}"
    console.print(
        "\n[bold]Activity Feed[/bold]\n"
        f"  URL: [link={url}]{url}[/link]\n"
        "  [dim]Open in VS Code: Ctrl+Shift+P > Simple Browser: Show > paste URL[/dim]\n"
        "  [dim]If the UI looks stale, close the tab and reopen with the URL above (includes cache-buster).[/dim]\n"
        "  [dim]Auto-refreshes every 5 seconds. Shows task completions and notifications.[/dim]\n"
    )


def _show_background_tasks(orchestrator: Orchestrator) -> None:
    """Show current background task status."""
    if not orchestrator.task_queue:
        console.print("[red]Task queue not initialized.[/red]")
        return

    console.print("\n[bold]Background Tasks[/bold]\n")

    table = Table(show_lines=True)
    table.add_column("ID", style="dim")
    table.add_column("Agent", style="bold")
    table.add_column("Description")
    table.add_column("Status")
    table.add_column("Time")

    tasks = orchestrator.task_queue.get_recent(15)
    if not tasks:
        console.print("[dim]No background tasks yet.[/dim]")
        return

    status_colors = {
        "queued": "dim",
        "running": "yellow",
        "completed": "green",
        "failed": "red",
    }

    for t in tasks:
        color = status_colors.get(t.status.value, "white")
        time_str = t.created_at[:19] if t.created_at else "—"
        table.add_row(
            t.task_id[:8],
            t.agent_id,
            t.description[:60],
            f"[{color}]{t.status.value}[/{color}]",
            time_str,
        )

    console.print(table)

    running = [t for t in tasks if t.status.value == "running"]
    if running:
        console.print(f"\n[yellow]{len(running)} task(s) currently running.[/yellow]")


def _show_report(orchestrator: Orchestrator, arg: str) -> None:
    """
    Show a detailed delegation report.
    Accepts a task_id (shows all sub-reports) or an agent_id (shows recent reports).
    """
    from rich.panel import Panel

    if not orchestrator.task_queue:
        console.print("[red]Task queue not initialized.[/red]")
        return

    if not arg:
        console.print(
            "[yellow]Usage:[/yellow] /report <task-id> or /report <agent-id>\n"
            "[dim]  task-id  — Shows all delegation reports for that task[/dim]\n"
            "[dim]  agent-id — Shows recent reports for that agent[/dim]"
        )
        return

    tq = orchestrator.task_queue

    # First try as task_id
    reports = tq.get_reports_for_task(arg)

    # If nothing, try as agent_id
    if not reports:
        reports = tq.get_reports_for_agent(arg, limit=10)

    # Also try partial task_id match
    if not reports:
        all_recent = tq.get_recent(30)
        matched_ids = [t.task_id for t in all_recent if t.task_id.startswith(arg)]
        for tid in matched_ids:
            reports.extend(tq.get_reports_for_task(tid))

    if not reports:
        console.print(f"[dim]No reports found for '{arg}'.[/dim]")
        console.print(
            "[dim]Hint: Use /bg to see task IDs, or pass an agent ID like 'finley'.[/dim]"
        )
        return

    console.print(f"\n[bold]Delegation Reports[/bold] ({len(reports)} found)\n")

    for rpt in reports:
        status_color = "green" if rpt["status"] == "completed" else "red"
        agent_id = rpt["agent_id"]
        desc = rpt["description"]
        created = rpt.get("created_at", "")[:19]

        header = (
            f"[bold]{agent_id.upper()}[/bold] · "
            f"[{status_color}]{rpt['status']}[/{status_color}] · "
            f"[dim]{created}[/dim]"
        )

        full_result = rpt.get("full_result", "")
        # Truncate very long results for display but still show substantially more than summaries
        if len(full_result) > 3000:
            display_result = full_result[:3000] + "\n\n... (truncated, full result stored in DB)"
        else:
            display_result = full_result

        panel_content = f"[dim]Task:[/dim] {desc}\n\n{display_result}"

        tools = rpt.get("tools_used", [])
        docs = rpt.get("documents_written", [])
        if tools:
            panel_content += f"\n\n[dim]Tools used:[/dim] {', '.join(tools)}"
        if docs:
            panel_content += f"\n[dim]Docs written:[/dim] {', '.join(docs)}"

        console.print(Panel(panel_content, title=header, border_style="blue", width=100))
        console.print()


def _show_inbox_status(orchestrator: Orchestrator) -> None:
    """Show inbox watcher status and folder file counts."""

    from rich.panel import Panel

    watcher = getattr(orchestrator, "inbox_watcher", None)
    if not watcher:
        console.print("[red]Inbox watcher not initialized.[/red]")
        return

    inbox_count = len(list(watcher.inbox_dir.glob("*.txt")))
    intake_count = len(list(watcher.intake_dir.glob("*.txt")))
    complete_count = len(list(watcher.complete_dir.glob("*.txt")))

    status_text = (
        f"[bold]Inbox Watcher[/bold] — {'[green]Running[/green]' if watcher._running else '[red]Stopped[/red]'}\n"
        f"  Poll interval: {watcher._poll_interval}s\n\n"
        f"  [yellow]inbox/[/yellow]    {inbox_count} file(s) waiting\n"
        f"  [cyan]intake/[/cyan]   {intake_count} file(s) being processed\n"
        f"  [green]complete/[/green] {complete_count} file(s) done\n\n"
        f"[dim]Drop a .txt file in saved_prompts/inbox/ with your update (or use cloud upload if enabled).\n"
        f"Charles will triage and delegate to the right agents.\n"
        f"Theo + Orion review results for gaps automatically.[/dim]"
    )

    console.print(Panel(status_text, title="Inbox System", border_style="blue"))

    # Show recent inbox files in intake
    if intake_count > 0:
        console.print("\n[yellow]Files being processed:[/yellow]")
        for f in sorted(watcher.intake_dir.glob("*.txt")):
            console.print(f"  [dim]→[/dim] {f.name}")

    if inbox_count > 0:
        console.print("\n[cyan]Files waiting in inbox:[/cyan]")
        for f in sorted(watcher.inbox_dir.glob("*.txt")):
            console.print(f"  [dim]→[/dim] {f.name}")


def _show_escalation_pathways(orchestrator: Orchestrator) -> None:
    """Show all defined escalation pathways and their chains."""
    from rich.panel import Panel

    from vastian.orchestrator.router import ESCALATION_PATHWAYS

    lines = []
    for pid, pathway in ESCALATION_PATHWAYS.items():
        chain_str = " → ".join(pathway["chain"])
        lines.append(
            f"[bold]{pid.replace('_', ' ').title()}[/bold]\n"
            f"  {pathway['description']}\n"
            f"  [cyan]Chain:[/cyan] {chain_str}\n"
        )

    body = "\n".join(lines)
    body += (
        "[dim]The router automatically detects escalation-worthy messages and "
        "routes to the first agent in the chain. If they can't resolve it, "
        "they escalate to the next agent.[/dim]"
    )
    console.print(Panel(body, title="Escalation Pathways", border_style="yellow"))


async def _handle_improve(orchestrator: Orchestrator) -> None:
    """Run Orion's improvement scan from inside the CLI chat."""
    console.print(
        Panel(
            "[bold cyan]Running Orion's Improvement Scan[/bold cyan]\n"
            "[dim]Orion will scan the codebase, suggest improvements on the Kanban board.\n"
            "Managers auto-review for alignment. You approve via the browser.[/dim]",
            border_style="cyan",
        )
    )

    status = console.status("[cyan]Orion is scanning the codebase...[/cyan]")
    status.start()
    try:
        await orchestrator.run_orion_improvement_cycle()
    except Exception as e:
        console.print(f"[red]Improvement scan error:[/red] {e}")
    finally:
        status.stop()

    if orchestrator.task_queue:
        pending = orchestrator.task_queue.get_kanban_tasks(status="pending_review")
        suggested = orchestrator.task_queue.get_kanban_tasks(status="suggested")
        total = len(pending) + len(suggested)
        if total:
            console.print(f"[green]Orion suggested {total} task(s) on the Kanban board.[/green]")
            console.print(
                "[dim]Open the activity feed (/feed) and go to the Kanban tab to review.[/dim]"
            )
        else:
            console.print("[dim]No new improvements suggested this cycle.[/dim]")


async def _handle_giga_sync(console: Console) -> None:
    """Show pending items in state/giga-outbox/ and display a summary table."""

    project_root = Path(__file__).resolve().parents[2]
    outbox_plans = project_root / "state" / "giga-outbox" / "plans"
    outbox_neurons = project_root / "state" / "giga-outbox" / "neurons"

    plan_files = sorted(outbox_plans.glob("*.json")) if outbox_plans.exists() else []
    neuron_files = sorted(outbox_neurons.glob("*.json")) if outbox_neurons.exists() else []

    if not plan_files and not neuron_files:
        console.print("[dim]Giga outbox is empty — no items pending sync.[/dim]")
        return

    import json

    table = Table(title="Giga Outbox — Pending Sync", show_lines=True)
    table.add_column("Type", style="bold cyan", width=8)
    table.add_column("Key / Title", style="white", max_width=40)
    table.add_column("Mind", style="yellow", width=18)
    table.add_column("Created By", style="green", width=12)
    table.add_column("Created At", style="dim", width=20)
    table.add_column("File", style="dim", max_width=30)

    for pf in plan_files:
        try:
            data = json.loads(pf.read_text(encoding="utf-8"))
            table.add_row(
                "Plan",
                data.get("key_name", pf.stem),
                data.get("mind", "—"),
                data.get("created_by", "—"),
                data.get("created_at", "—")[:19] if data.get("created_at") else "—",
                pf.name,
            )
        except Exception:
            table.add_row("Plan", pf.stem, "—", "—", "—", pf.name)

    for nf in neuron_files:
        try:
            data = json.loads(nf.read_text(encoding="utf-8"))
            table.add_row(
                "Neuron",
                data.get("title", nf.stem),
                data.get("mind", "—"),
                data.get("created_by", "—"),
                "—",
                nf.name,
            )
        except Exception:
            table.add_row("Neuron", nf.stem, "—", "—", "—", nf.name)

    console.print(table)
    total = len(plan_files) + len(neuron_files)
    console.print(
        f"\n[bold]{total}[/bold] item(s) pending. "
        f"These were written by agents via the Giga bridge.\n"
        f"[dim]Cursor syncs them to Giga via MCP tools. "
        f"To clear manually, delete the JSON files from state/giga-outbox/.[/dim]"
    )


async def _handle_suggest(orchestrator: Orchestrator, user_input: str) -> None:
    """Ask agents to suggest Kanban tasks. Optionally target a specific agent."""
    parts = user_input.split(maxsplit=1)
    target_agent = parts[1].strip() if len(parts) > 1 else None

    # Agents that can suggest tasks
    suggest_agents = ["orion", "charles", "liam", "dominic", "maria", "arjuna"]

    if target_agent:
        if target_agent not in suggest_agents:
            console.print(
                f"[red]Agent '{target_agent}' doesn't have the suggest_kanban_task tool.[/red]\n"
                f"[dim]Available: {', '.join(suggest_agents)}[/dim]"
            )
            return
        agents_to_run = [target_agent]
    else:
        agents_to_run = suggest_agents

    console.print(
        Panel(
            f"[bold cyan]Running Suggestion Cycle[/bold cyan]\n"
            f"[dim]Asking {', '.join(agents_to_run)} to review their domains and suggest Kanban tasks.\n"
            f"Managers auto-review for alignment. You approve via the browser.[/dim]",
            border_style="cyan",
        )
    )

    for agent_id in agents_to_run:
        status = console.status(f"[cyan]{agent_id} is thinking about suggestions...[/cyan]")
        status.start()
        try:
            await orchestrator.run_agent_suggestion_cycle(agent_id)
        except Exception as e:
            console.print(f"[red]{agent_id} suggestion error:[/red] {e}")
        finally:
            status.stop()

    if orchestrator.task_queue:
        pending = orchestrator.task_queue.get_kanban_tasks(status="pending_review")
        suggested = orchestrator.task_queue.get_kanban_tasks(status="suggested")
        total = len(pending) + len(suggested)
        if total:
            console.print(f"[green]{total} task(s) now on the Kanban board for review.[/green]")
            console.print(
                "[dim]Open the activity feed (/feed) and go to the Kanban tab to review.[/dim]"
            )
        else:
            console.print("[dim]No new tasks suggested this cycle.[/dim]")


def _get_next_outreach(orchestrator: Orchestrator) -> dict | None:
    """Return the highest-priority pending outreach request, or None."""
    if not orchestrator.task_queue:
        return None
    try:
        items = orchestrator.task_queue.get_pending_outreach()
        return items[0] if items else None
    except Exception:
        return None


def _show_pending_approvals(orchestrator: Orchestrator) -> None:
    """Display all pending approval requests (transactions, etc.)."""
    if not orchestrator.task_queue:
        console.print("[red]Task queue not initialized.[/red]")
        return
    approvals = orchestrator.task_queue.get_pending_approvals()
    if not approvals:
        console.print("[dim]No pending approvals.[/dim]")
        return
    console.print("\n[bold]Pending Approvals[/bold]\n")
    table = Table(show_header=True)
    table.add_column("ID", style="bold cyan")
    table.add_column("Agent")
    table.add_column("Type")
    table.add_column("Description")
    table.add_column("Created")
    for a in approvals:
        table.add_row(
            a["approval_id"],
            a["agent_id"],
            a.get("approval_type", "?"),
            a["description"][:60],
            a.get("created_at", "")[:16],
        )
    console.print(table)
    console.print("[dim]Use /approve <id> or /deny <id> to resolve.[/dim]")


async def _handle_approve(orchestrator: Orchestrator, approval_id: str, *, approve: bool) -> None:
    """Approve or deny a pending approval and execute if approved."""
    if not orchestrator.task_queue:
        console.print("[red]Task queue not initialized.[/red]")
        return
    record = orchestrator.task_queue.resolve_approval(approval_id, approved=approve)
    if not record:
        console.print(f"[red]No pending approval found with ID '{approval_id}'.[/red]")
        return

    action = "Approved" if approve else "Denied"
    console.print(
        f"[{'green' if approve else 'red'}]{action}:[/{'green' if approve else 'red'}] {record['description']}"
    )

    if approve and record.get("approval_type") == "transaction":
        console.print("[dim]Executing transaction...[/dim]")
        try:
            result = await orchestrator.execute_approved_transaction(record)
            console.print(Panel(result, title="Transaction Result", border_style="green"))
        except Exception as e:
            console.print(f"[red]Transaction execution failed:[/red] {e}")


def _show_sync_suggestions(orchestrator: Orchestrator) -> None:
    """Show agent sync suggestions based on pending work."""
    if not orchestrator.task_queue:
        return
    try:
        suggestions = orchestrator.task_queue.get_sync_suggestions()
        if not suggestions:
            return
        console.print("\n[bold yellow]Sync Suggestions[/bold yellow]")
        for s in suggestions:
            priority_color = "red" if s["priority"] == "high" else "yellow"
            console.print(
                f"  [{priority_color}]{s['priority'].upper()}[/{priority_color}] "
                f"[cyan]{s['agent_id']}[/cyan] — {s['reason']}"
            )
        console.print(
            "[dim]Use /agent <id> to sync with a specific agent, or check the Kanban board in the browser.[/dim]\n"
        )
    except Exception:
        pass


def _show_daily_briefing(orchestrator: Orchestrator) -> None:
    """Display Charles's daily briefing if one was generated this session.

    The briefing is generated asynchronously during initialize(). By the
    time the chat loop starts, it may or may not be ready. If not, skip —
    the user will see the result in Charles's documents or next session.
    """
    briefing = getattr(orchestrator, "_daily_briefing", None)

    if briefing and len(briefing.strip()) > 20:
        from rich.markdown import Markdown as RichMarkdown

        console.print()
        console.print(
            Panel(
                RichMarkdown(briefing[:2000]),
                title="[bold cyan]Daily Briefing (Charles)[/bold cyan]",
                border_style="cyan",
                padding=(0, 1),
            )
        )


def _show_models(orchestrator: Orchestrator) -> None:
    """Display available LLM models, provider status, and agent assignments."""
    console.print("\n[bold]LLM Model Configuration[/bold]\n")

    # ── Provider Status ──────────────────────────────
    router = getattr(orchestrator, "model_router", None)

    status_table = Table(title="Provider Status", show_lines=True)
    status_table.add_column("Provider", style="bold")
    status_table.add_column("Status")
    status_table.add_column("Default Model")
    status_table.add_column("Fast Model")

    from vastian.config import ANTHROPIC_MODELS, OPENAI_MODELS

    openai_ok = router and router.has_provider("openai") if router else bool(orchestrator.llm)
    anthropic_ok = router and router.has_provider("anthropic") if router else False

    status_table.add_row(
        "OpenAI",
        "[green]Available[/green]" if openai_ok else "[red]Not configured[/red]",
        OPENAI_MODELS["default"],
        OPENAI_MODELS["fast"],
    )
    status_table.add_row(
        "Anthropic",
        "[green]Available[/green]" if anthropic_ok else "[red]Not configured[/red]",
        ANTHROPIC_MODELS["default"],
        ANTHROPIC_MODELS["fast"],
    )

    primary = router._primary if router else "unknown"
    status_table.add_row(
        "[bold cyan]Primary[/bold cyan]",
        f"[bold cyan]{primary}[/bold cyan]" if primary else "[red]None[/red]",
        "",
        "",
    )
    console.print(status_table)

    # ── Agent Model Assignments ──────────────────────
    console.print()
    agent_table = Table(title="Agent Model Assignments", show_lines=True)
    agent_table.add_column("Agent", style="bold")
    agent_table.add_column("Provider")
    agent_table.add_column("Model")
    agent_table.add_column("Override?")

    for config in orchestrator.registry.all_configs:
        prov_override = getattr(config, "llm_provider_override", None)
        model_override = config.model_override

        if prov_override:
            provider_display = f"[yellow]{prov_override}[/yellow]"
            model_display = model_override or "(provider default)"
            override_display = "[yellow]Yes[/yellow]"
        else:
            provider_display = primary or "default"
            model_display = model_override or "(default)"
            override_display = "No"

        agent_table.add_row(
            f"{config.sigil} {config.name}" if config.sigil else config.name,
            provider_display,
            model_display,
            override_display,
        )

    console.print(agent_table)

    # ── Detection Info ───────────────────────────────
    console.print(
        "\n[dim]Model detection: Set VASTIAN_LLM_PROVIDER=auto (default) to auto-detect "
        "from API keys.\nPer-agent overrides use llm_provider_override + model_override "
        "in agent YAML.[/dim]\n"
    )


def _show_metrics() -> None:
    """Display all simulation variables and their associated metrics."""
    console.print("\n[bold]Vastian Simulation Framework — V1 Variables[/bold]\n")

    table = Table(show_header=True, border_style="dim", title="MVP Simulation Variables (16)")
    table.add_column("Category", style="cyan", no_wrap=True)
    table.add_column("Variable", style="bold")
    table.add_column("Associated Metrics")
    table.add_column("Formula")

    # Core Life Optimization
    table.add_row(
        "Core Life",
        "Purpose Clarity",
        "Alignment Index, Legacy Contribution",
        "(Aligned / Total Actions) x 100",
    )
    table.add_row(
        "Core Life", "Focus Intensity", "Momentum Score", "(Active Goals / Total Goals) x 100"
    )
    table.add_row(
        "Core Life",
        "Emotional Resilience",
        "Emotional Flow, Resilience Recovery",
        "(Positive States / Total) x 100",
    )
    table.add_row(
        "Core Life", "Persona Harmony", "Persona Harmony Score", "(Harmonized Traits / Total) x 100"
    )

    # Relational
    table.add_row(
        "Relational",
        "Relational Magnetism",
        "Relational Impact, Boundary Effect.",
        "(Meaningful / Total Interact.) x 100",
    )

    # Productivity
    table.add_row(
        "Productivity",
        "Energy Alignment",
        "Energy Utilization Efficiency",
        "(Meaningful Energy / Total) x 100",
    )
    table.add_row(
        "Productivity",
        "Gratitude Frequency",
        "Fulfillment Ratio, Gratitude Depth",
        "(Fulfilling Days / Total) x 100",
    )
    table.add_row(
        "Productivity",
        "Creative Output",
        "Creative Productivity Rate",
        "(Projects Done / Opportunities) x 100",
    )

    # Growth
    table.add_row(
        "Growth",
        "Intellectual Expansion",
        "Learning Velocity, Skill Expansion",
        "(Skills Gained / Opportunities) x 100",
    )
    table.add_row(
        "Growth", "Self-Acceptance", "Self-Reflection Frequency", "(Reflective Days / Total) x 100"
    )

    # Well-Being
    table.add_row(
        "Well-Being",
        "Mind-Body Integration",
        "Inner Peace Quotient",
        "(Balanced States / Total) x 100",
    )
    table.add_row(
        "Well-Being", "Life Tempo Score", "Life Tempo Optimization", "(Aligned Days / Total) x 100"
    )
    table.add_row("Well-Being", "Time Optimization", "Life Tempo Optimization", "(see Life Tempo)")

    # Behavioral
    table.add_row(
        "Behavioral",
        "Courage Quotient",
        "Decision Confidence Ratio",
        "(Confident Decisions / Total) x 100",
    )
    table.add_row(
        "Behavioral", "Decision Agility", "Decision Confidence Ratio", "(see Courage Quotient)"
    )
    table.add_row(
        "Behavioral",
        "Autonomy Score",
        "Accountability Score",
        "(Commitments Fulfilled / Total) x 100",
    )

    console.print(table)

    console.print(
        "\n[dim]Post-Launch (7): Vision Execution Ratio, Spontaneity Activation, "
        "Social Contribution, Optimism Frequency, Empathy Resonance, "
        "Cognitive Strain, Vitality Quotient[/dim]"
    )
    console.print(
        "\n[dim]Use /simulate <scenario> to run a what-if simulation | "
        "/assess <variables> to score metrics[/dim]"
    )


def _show_tools(agent_id: str, orchestrator: Orchestrator) -> None:
    """Display the special tools available to an agent."""
    config = orchestrator.registry.get_config(agent_id)
    if config is None:
        console.print(f"[red]Agent not found: {agent_id}[/red]")
        return

    tool_names = get_tool_names_for_agent(agent_id)

    console.print(f"\n[bold]{config.display_name}[/bold] — Available Tools\n")

    table = Table(show_header=True, border_style="dim")
    table.add_column("Tool", style="cyan", no_wrap=True)
    table.add_column("Description")
    table.add_column("Special?", justify="center")

    # Base tools that every agent has
    base_tool_names = {"delegate_to_agent", "read_document"}

    for name in tool_names:
        desc = TOOL_DESCRIPTIONS.get(name, "—")
        is_special = "★" if name not in base_tool_names else ""
        table.add_row(name, desc, is_special)

    console.print(table)
    console.print("\n[dim]★ = Special tool unique to this agent's role[/dim]")


@app.command()
def council(
    topic: str = typer.Argument(..., help="Topic for the council meeting."),
    council_type: str = typer.Option(
        "ad_hoc",
        "--type",
        "-t",
        help="Council type: strategic_review, operational_sync, scenario_workshop, full_council, ad_hoc",
    ),
    members: str = typer.Option(
        None, "--members", "-m", help="Comma-separated agent IDs to include."
    ),
    verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
    """Convene a Mentor Council meeting on a specific topic."""
    _setup_logging("DEBUG" if verbose else "WARNING")

    member_list = [m.strip() for m in members.split(",")] if members else None

    console.print(
        Panel(
            f"[bold yellow]Convening {council_type.replace('_', ' ').title()} Council[/bold yellow]\n"
            f"Topic: {topic}",
            border_style="yellow",
        )
    )

    asyncio.run(_run_council(topic, council_type, member_list))


async def _run_council(topic: str, council_type: str, members: list[str] | None) -> None:
    settings = get_settings()
    orchestrator = Orchestrator(settings)
    await orchestrator.initialize(session_only=True)

    result = await orchestrator.convene_council(
        topic=topic,
        council_type=council_type,
        member_ids=members,
    )

    if "error" in result:
        console.print(f"[red]{result['error']}[/red]")
        return

    console.print(
        Panel(
            f"[bold green]Council Complete[/bold green]\n"
            f"Rounds: {result.get('rounds_completed', 0)} | "
            f"Contributions: {result.get('total_contributions', 0)}",
            border_style="green",
        )
    )

    if result.get("synthesis"):
        console.print(Markdown(result["synthesis"]))


@app.command()
def agents(
    verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
    """List all agents in the Vastian Network."""
    _setup_logging("DEBUG" if verbose else "WARNING")
    settings = get_settings()
    orchestrator = Orchestrator(settings)

    # Load configs without full initialization (no LLM needed)
    orchestrator.registry.load_from_directory(
        settings.agents_dir,
        template_vars={"user_name": settings.user_name},
    )

    table = Table(title="Vastian Network Agents", show_header=True)
    table.add_column("ID", style="cyan")
    table.add_column("Sigil")
    table.add_column("Name", style="bold")
    table.add_column("Title")
    table.add_column("Reports To")
    table.add_column("Teams")

    for config in orchestrator.registry.all_configs:
        table.add_row(
            config.agent_id,
            config.sigil,
            config.name,
            config.title,
            config.reports_to or "—",
            ", ".join(config.teams) if config.teams else "—",
        )

    console.print(table)


@app.command()
def status() -> None:
    """Show the current status of the Vastian Network."""
    settings = get_settings()
    console.print(
        Panel.fit(
            f"[bold cyan]Vastian Network Configuration[/bold cyan]\n\n"
            f"LLM Provider: {settings.llm_provider.value}\n"
            f"Default Model: {settings.default_model}\n"
            f"Agents Dir: {settings.agents_dir}\n"
            f"Knowledge Store: {settings.chroma_persist_dir}\n"
            f"Database: {settings.sqlite_path}\n"
            f"Documents: {settings.docs_dir}",
            border_style="cyan",
        )
    )


@app.command()
def init() -> None:
    """Initialize the Vastian Network data directories and default documents.

    Installs seed templates (config, state, saved_prompts) to the data root.
    Uses VASTIAN_DATA_ROOT (e.g. C:/Users/Owner/Documents/myvastian-user-data).
    Skips files that already exist so init is safe to re-run.
    """
    settings = get_settings()

    from vastian.seed import (
        install_config_seed,
        install_data_seed,
        install_saved_prompts_seed,
        install_state_seed,
    )
    from vastian.storage.data_root import get_config_dir, get_data_root, get_saved_prompts_dir

    # Resolve data root (VASTIAN_DATA_ROOT, data_root.txt, or project root)
    data_root = get_data_root(Path.cwd())
    config_dir = get_config_dir(Path.cwd())
    saved_prompts_dir = get_saved_prompts_dir(Path.cwd())

    console.print(f"\n[bold]Vastian Init[/bold] — data root: [cyan]{data_root}[/cyan]\n")

    # Core dirs (data, docs, logs, workspace) + data/ seed (chromadb, DBs init on first use)
    data_dir = settings.sqlite_path.parent
    install_data_seed(data_dir)
    for d in [
        data_dir,
        settings.chroma_persist_dir,
        settings.docs_dir,
        settings.log_dir,
        data_root / "workspace",
    ]:
        d.mkdir(parents=True, exist_ok=True)
        console.print(f"  [green]✓[/green] {d}")

    # Config defaults (ui-preferences, automation, sessions, access-tiers, boards)
    install_config_seed(config_dir)
    console.print("  [green]✓[/green] config/ (defaults installed)")

    # Full state suite (kanban, sessions, delegations, relay dirs, etc.)
    install_state_seed(settings.state_dir)
    console.print("  [green]✓[/green] state/ (full suite)")

    # saved_prompts (inbox, intake, complete + README, intro.example)
    install_saved_prompts_seed(saved_prompts_dir)
    console.print("  [green]✓[/green] saved_prompts/ (inbox, intake, complete)")

    # Create agent document directories
    orchestrator = Orchestrator(settings)
    orchestrator.registry.load_from_directory(
        settings.agents_dir,
        template_vars={"user_name": settings.user_name},
    )

    for config in orchestrator.registry.all_configs:
        agent_docs_dir = settings.docs_dir / config.agent_id
        agent_docs_dir.mkdir(parents=True, exist_ok=True)

        # Create placeholder documents
        for doc_name in config.owned_documents:
            doc_path = agent_docs_dir / doc_name
            if not doc_path.exists():
                doc_path.write_text(
                    f"---\nauthor: {config.agent_id}\n---\n\n"
                    f"# {doc_name.replace('.md', '').replace('-', ' ').title()}\n\n"
                    f"*Owned by {config.name} ({config.title})*\n\n"
                    f"---\n\n> This document is pending initial content.\n",
                    encoding="utf-8",
                )
                console.print(f"  [green]✓[/green] Created {config.agent_id}/{doc_name}")

    # Copy shared reference docs (config-options-overview for agent config proposals)
    project_root = Path(__file__).resolve().parents[2]  # src/vastian/cli.py -> project root
    vastian_docs = project_root / "vastian-docs"
    shared_dir = settings.docs_dir / "shared"
    shared_dir.mkdir(parents=True, exist_ok=True)
    ref_src = vastian_docs / "config-options-overview.md"
    ref_dst = shared_dir / "config-options-overview.md"
    if ref_src.exists() and (
        not ref_dst.exists() or ref_src.stat().st_mtime > ref_dst.stat().st_mtime
    ):
        ref_dst.write_text(ref_src.read_text(encoding="utf-8"), encoding="utf-8")
        console.print("  [green]✓[/green] Updated docs/shared/config-options-overview.md")

    console.print("\n[bold green]Vastian Network initialized![/bold green]")


migrate_app = typer.Typer(help="One-time migration commands (data-root, sqlite).")
app.add_typer(migrate_app, name="migrate")


@migrate_app.command("sqlite")
def migrate_sqlite() -> None:
    """Migrate existing SQLite data to git-tracked JSON files in state/.

    Safe to run multiple times — records are merged by ID, duplicates skipped.
    Run this once after upgrading to the FileStore-based architecture.
    """
    from vastian.storage.migrate import migrate as run_migration

    settings = get_settings()
    state_dir = settings.state_dir
    sqlite_path = settings.sqlite_path
    tasks_db_path = sqlite_path.parent / "tasks.db"

    console.print(
        Panel(
            "[bold cyan]Migrating SQLite → JSON[/bold cyan]\n"
            f"[dim]Source: {sqlite_path} + {tasks_db_path}\n"
            f"Target: {state_dir}/[/dim]",
            border_style="cyan",
        )
    )

    if not sqlite_path.exists() and not tasks_db_path.exists():
        console.print("[yellow]No existing databases found. Nothing to migrate.[/yellow]")
        console.print(f"[dim]Checked: {sqlite_path}, {tasks_db_path}[/dim]")
        return

    counts = run_migration(
        sqlite_path=sqlite_path,
        tasks_db_path=tasks_db_path,
        state_dir=state_dir,
    )

    total = sum(counts.values())
    if total:
        console.print(f"\n[bold green]Migrated {total} records:[/bold green]")
        for table, count in sorted(counts.items()):
            if count:
                console.print(f"  [green]✓[/green] {table}: {count} records → state/")
    else:
        console.print("[yellow]No records found to migrate.[/yellow]")

    console.print(
        "\n[dim]The state/ directory is now git-tracked. Commit it to share "
        "across machines.\nLocal SQLite databases in data/ are still used for "
        "ephemeral data (notifications, bg tasks).[/dim]"
    )


@migrate_app.command("data-root")
def migrate_data_root(
    force: bool = typer.Option(
        False, "--force", "-f", help="Overwrite existing files in data root."
    ),
) -> None:
    """Copy docs/, state/, saved_prompts/, workspace/, config/, data/ from project to configured data root.

    Run after `vastian config data-root <path>`. One-time seed so the new location has your data.
    If project dirs are empty or missing, installs seed templates (config, state, saved_prompts, data).
    Skips if target already has content (use --force to overwrite).
    """
    import shutil

    from vastian.seed import (
        install_config_seed,
        install_data_seed,
        install_saved_prompts_seed,
        install_state_seed,
    )
    from vastian.storage.data_root import get_config_dir, get_data_root

    project_root = Path.cwd()
    dest_root = get_data_root(project_root).resolve()
    if dest_root == project_root.resolve():
        console.print("[dim]Data root is project root. Nothing to migrate.[/dim]")
        return

    console.print(f"[dim]Data root: {dest_root}[/dim]\n")
    total = 0
    # Copy or seed each category (data/ = SQLite + ChromaDB)
    for d in ["docs", "state", "saved_prompts", "workspace", "config", "data"]:
        src = project_root / d
        target = dest_root / d
        if target.exists() and list(target.rglob("*")) and not force:
            console.print(
                f"[dim]Skipping {d}/ (already has content). Use --force to overwrite.[/dim]"
            )
            continue
        target.mkdir(parents=True, exist_ok=True)
        if src.exists() and any(src.rglob("*")):
            for f in src.rglob("*"):
                if f.is_file():
                    rel = f.relative_to(src)
                    out = target / rel
                    out.parent.mkdir(parents=True, exist_ok=True)
                    try:
                        shutil.copy2(f, out)
                        total += 1
                    except OSError as e:
                        console.print(f"[red]Failed to copy {rel}: {e}[/red]")
        else:
            # Seed from template when project dir is empty or missing
            if d == "config":
                install_config_seed(get_config_dir(project_root))
                total += 1
            elif d == "state":
                install_state_seed(target)
                total += 1
            elif d == "saved_prompts":
                install_saved_prompts_seed(target)
                total += 1
            elif d == "data":
                install_data_seed(target)
                total += 1
            # docs, workspace: leave empty (init creates agent docs separately)
    if total > 0:
        console.print(f"[green]Migrated/seeded {total} items to {dest_root}[/green]")
    else:
        console.print("[dim]No files copied (targets may already exist).[/dim]")


@app.command(name="migrate-to-storage")
def migrate_to_storage() -> None:
    """Upload state/, docs/, saved_prompts/, workspace/ to your configured storage provider.

    One-time migration to seed the cloud before switching to cloud-first. Requires:
    - Provider in config/backup/settings.yaml (google_drive, dropbox, box, tresorit)
    - OAuth done: vastian setup gdrive (or dropbox/box/tresorit)
    """
    from vastian.bootstrap import register_all
    from vastian.tasks.backup import create_backup_manager

    register_all()
    project_root = Path.cwd()
    mgr = create_backup_manager(project_root)

    if mgr.provider is None:
        console.print(
            "[red]No storage provider configured.[/red]\n"
            "Set provider in config/backup/settings.yaml and run: "
            "[cyan]vastian setup gdrive[/cyan] (or dropbox/box/tresorit)"
        )
        raise typer.Exit(1)

    console.print("[dim]Scanning state/, docs/, saved_prompts/, workspace/...[/dim]")
    files = mgr.scan()
    console.print(f"[dim]Found {len(files)} files.[/dim]")

    if not files:
        console.print(
            "[yellow]No files to migrate.[/yellow] Ensure state/, docs/, saved_prompts/ exist."
        )
        return

    async def _upload():
        return await mgr.upload(files, force_full=True)

    result = asyncio.run(_upload())
    uploaded = result.get("uploaded", 0)
    skipped = result.get("skipped", 0)
    errors = result.get("errors", [])

    for e in errors:
        console.print(f"[red]Error:[/red] {e.get('file', '?')} — {e.get('error', 'unknown')}")

    if uploaded > 0 or skipped > 0:
        console.print(f"[green]Migration complete:[/green] {uploaded} uploaded, {skipped} skipped.")
    if result.get("message"):
        console.print(f"[dim]{result['message']}[/dim]")


# ── Kanban ───────────────────────────────────────────────

kanban_app = typer.Typer(help="Kanban board management (reset for testing).")
app.add_typer(kanban_app, name="kanban")


@kanban_app.command("reset")
def kanban_reset(
    force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt."),
) -> None:
    """Clear all Kanban cards (overwrites state/kanban.json with []).

    Use for testing. Requires confirmation unless --force. Logs count of cleared cards.
    """
    from vastian.config import get_settings

    settings = get_settings()
    kanban_path = settings.state_dir / "kanban.json"
    if not kanban_path.exists():
        console.print("[dim]No kanban file found. Nothing to reset.[/dim]")
        return
    count = 0
    try:
        data = json.loads(kanban_path.read_text(encoding="utf-8"))
        count = len(data) if isinstance(data, list) else 0
    except (json.JSONDecodeError, OSError):
        pass
    if not force:
        confirm = typer.confirm(
            f"Clear all {count} Kanban card(s)? This cannot be undone.",
            default=False,
        )
        if not confirm:
            console.print("[dim]Aborted.[/dim]")
            raise typer.Exit(0)
    kanban_path.write_text("[]", encoding="utf-8")
    console.print(f"[green]Cleared {count} Kanban card(s).[/green] state/kanban.json is now empty.")


# ── Config (data root, etc.) ─────────────────────────────

config_app = typer.Typer(help="View or change configuration (data root, etc.).")
app.add_typer(config_app, name="config")


@config_app.command("mcp-key")
def config_mcp_key(
    generate: bool = typer.Option(False, "--generate", "-g", help="Generate a new secure token."),
    set_value: bool = typer.Option(False, "--set", help="Append to .env (creates if missing)."),
) -> None:
    """Show VASTIAN_MCP_KEY or generate a new one.

    MCP clients (Zo, Cursor) use Bearer token auth when connecting to Vastian.
    Run with --generate to create a token, then add to .env as VASTIAN_MCP_KEY=...

    Examples:
      vastian config mcp-key --generate
      vastian config mcp-key --generate --set   # generate and append to .env
    """
    import secrets
    from pathlib import Path

    env_path = Path.cwd() / ".env"
    if generate:
        token = secrets.token_urlsafe(32)
        console.print(f"[bold]Generated token:[/bold] [dim]{token}[/dim]")
        console.print(
            "[dim]Add to .env: VASTIAN_MCP_KEY=<token>[/dim]\n"
            "[dim]Or run: vastian config mcp-key --generate --set[/dim]"
        )
        if set_value:
            line = f"\n# Vastian MCP Bearer token (generated)\nVASTIAN_MCP_KEY={token}\n"
            if env_path.exists():
                content = env_path.read_text(encoding="utf-8")
                if "VASTIAN_MCP_KEY=" in content:
                    import re

                    content = re.sub(r"VASTIAN_MCP_KEY=.*", f"VASTIAN_MCP_KEY={token}", content)
                    env_path.write_text(content, encoding="utf-8")
                else:
                    env_path.write_text(content + line, encoding="utf-8")
            else:
                env_path.write_text(line.lstrip(), encoding="utf-8")
            console.print(f"[green]Appended to {env_path}[/green]")
        return
    key = os.environ.get("VASTIAN_MCP_KEY", "").strip()
    if key:
        console.print(f"[bold]VASTIAN_MCP_KEY:[/bold] [dim]{key[:8]}...[/dim] [dim](set)[/dim]")
    else:
        console.print("[dim]VASTIAN_MCP_KEY not set (open access for local dev)[/dim]")
        console.print("[dim]Run: vastian config mcp-key --generate[/dim]")


@config_app.command("data-root")
def config_data_root(
    path: str | None = typer.Argument(None, help="New data root path. Omit to show current."),
    clear: bool = typer.Option(
        False, "--clear", help="Clear override; revert to platform default."
    ),
) -> None:
    """Get or set VASTIAN_DATA_ROOT (where docs/, state/, config/, saved_prompts/, workspace/ live).

    User data always lives in a data folder (never in the repo). Default: platform user dir
    (e.g. %LOCALAPPDATA%\\vastian on Windows). Override via path or --clear to revert to default.
    """
    from vastian.storage.data_root import clear_data_root_override, get_data_root, set_data_root

    project_root = Path.cwd()
    if clear:
        ok, err = clear_data_root_override(project_root)
        if not ok:
            console.print(f"[red]Could not update .env or config files: {err}[/red]")
            raise typer.Exit(1)
        console.print("[green]Data root override cleared.[/green] Using platform default.")
        return
    if path is None:
        current = get_data_root(project_root)
        console.print(f"[bold]Data root:[/bold] {current}")
        override_file = project_root / "config" / "local" / "data_root.txt"
        if override_file.exists():
            console.print("[dim](Set via config/local/data_root.txt)[/dim]")
        elif os.environ.get("VASTIAN_DATA_ROOT"):
            console.print("[dim](Set via VASTIAN_DATA_ROOT in .env)[/dim]")
        return
    p = Path(path)
    if not p.is_absolute():
        p = (project_root / p).resolve()
    ok, err = set_data_root(project_root, str(p))
    if not ok:
        console.print(f"[red]Could not update .env or config/local/data_root.txt: {err}[/red]")
        raise typer.Exit(1)
    console.print(f"[green]Data root set to:[/green] {p}")
    console.print("[dim]Restart the CLI or backup panel for changes to take effect.[/dim]")


# ── Setup (OAuth for backup providers) ─────────────────

setup_app = typer.Typer(help="Configure OAuth for backup storage providers (e.g. Google Drive).")
app.add_typer(setup_app, name="setup")


@setup_app.command("gdrive")
def setup_gdrive() -> None:
    """Complete Google Drive OAuth2 and save tokens to config/local for backup.

    Requires GOOGLE_DRIVE_CLIENT_ID and GOOGLE_DRIVE_CLIENT_SECRET in .env.
    Create an OAuth 2.0 Client ID (Desktop) at https://console.cloud.google.com/apis/credentials
    and enable the Google Drive API. Then run this command; a browser will open to sign in.
    """
    import json
    from pathlib import Path

    from vastian.bootstrap import register_all
    from vastian.config import get_settings

    register_all()
    settings = get_settings()
    config_dir = Path(settings.local_config_dir).resolve()
    config_dir.mkdir(parents=True, exist_ok=True)

    client_id = (settings.google_drive_client_id or "").strip()
    client_secret = (settings.google_drive_client_secret or "").strip()
    if not client_id or not client_secret:
        console.print("[red]Missing Google Drive credentials in .env.[/red]")
        console.print(
            "Add [cyan]GOOGLE_DRIVE_CLIENT_ID[/cyan] and [cyan]GOOGLE_DRIVE_CLIENT_SECRET[/cyan] "
            "to your [cyan].env[/cyan] file.\n"
            "Create an OAuth 2.0 Client ID (Desktop app) at:\n"
            "  [link=https://console.cloud.google.com/apis/credentials]"
            "https://console.cloud.google.com/apis/credentials[/link]\n"
            "Enable the Google Drive API for your project, then copy Client ID and Client Secret."
        )
        raise SystemExit(1)

    creds_file = config_dir / "gdrive_credentials.json"
    client_secrets = {
        "installed": {
            "client_id": client_id,
            "client_secret": client_secret,
            "redirect_uris": ["http://localhost"],
            "auth_uri": "https://accounts.google.com/o/oauth2/auth",
            "token_uri": "https://oauth2.googleapis.com/token",
        }
    }
    creds_file.write_text(json.dumps(client_secrets, indent=2), encoding="utf-8")

    try:
        from google_auth_oauthlib.flow import InstalledAppFlow
    except ImportError:
        console.print(
            "[red]Google auth not installed.[/red] Run: "
            "[cyan]pip install google-api-python-client google-auth-oauthlib[/cyan]"
        )
        raise SystemExit(1) from None

    scopes = ["https://www.googleapis.com/auth/drive.file"]
    flow = InstalledAppFlow.from_client_secrets_file(str(creds_file), scopes)
    console.print("[dim]Opening browser for Google sign-in…[/dim]")
    creds = flow.run_local_server(port=0)
    token_file = config_dir / "gdrive_token.json"
    token_file.write_text(creds.to_json(), encoding="utf-8")
    console.print(f"[green]Google Drive connected. Token saved to {token_file}[/green]")
    console.print(
        "[dim]You can use the Backup Panel or backup tasks with provider google_drive.[/dim]"
    )


@setup_app.command("dropbox")
def setup_dropbox() -> None:
    """Complete Dropbox OAuth2 and save tokens to config/local for backup.

    Requires DROPBOX_APP_KEY in .env, or config/local/dropbox_app_key.txt.
    Create an app at https://www.dropbox.com/developers/apps (Scoped, Full Dropbox).
    A browser will open; after authorizing, paste the code shown back into the CLI.
    """
    import json
    import webbrowser
    from pathlib import Path

    from vastian.bootstrap import register_all
    from vastian.config import get_settings

    register_all()
    settings = get_settings()
    config_dir = Path(settings.local_config_dir).resolve()
    config_dir.mkdir(parents=True, exist_ok=True)

    if (settings.dropbox_access_token or "").strip():
        console.print(
            "[green]Dropbox already configured via DROPBOX_ACCESS_TOKEN in .env.[/green]\n"
            "[dim]No setup needed. Use the Backup Panel or backup tasks with provider dropbox.[/dim]"
        )
        return

    app_key = (settings.dropbox_app_key or "").strip()
    app_secret = (settings.dropbox_app_secret or "").strip() or None
    app_key_file = config_dir / "dropbox_app_key.txt"
    if not app_key and app_key_file.exists():
        app_key = app_key_file.read_text(encoding="utf-8").strip()
    if not app_key:
        console.print("[red]Missing Dropbox app key.[/red]")
        console.print(
            "Add [cyan]DROPBOX_APP_KEY[/cyan] to your [cyan].env[/cyan] file, or create\n"
            f"[cyan]{app_key_file}[/cyan] with your app key.\n"
            "Create an app at [link=https://www.dropbox.com/developers/apps]"
            "https://www.dropbox.com/developers/apps[/link]\n"
            "(Scoped access, Full Dropbox — no app secret needed with PKCE)"
        )
        raise SystemExit(1)

    try:
        import dropbox
    except ImportError:
        console.print("[red]Dropbox SDK not installed.[/red] Run: [cyan]pip install dropbox[/cyan]")
        raise SystemExit(1) from None

    flow = dropbox.DropboxOAuth2FlowNoRedirect(app_key, consumer_secret=app_secret, use_pkce=True)
    auth_url = flow.start()
    console.print("[dim]Opening browser for Dropbox authorization…[/dim]")
    webbrowser.open(auth_url)
    console.print(
        "\n[yellow]After authorizing, Dropbox will show an authorization code.[/yellow]\n"
        "[dim]Copy the code and paste it below (or paste the full redirect URL).[/dim]\n"
    )
    code = typer.prompt("Authorization code")
    code = code.strip()
    if not code:
        console.print("[red]No code provided.[/red]")
        raise SystemExit(1)
    # Extract code from full redirect URL if user pasted that
    if "code=" in code:
        for part in code.split("?")[-1].split("&"):
            if part.startswith("code="):
                code = part.split("=", 1)[1]
                break
    try:
        result = flow.finish(code)
    except Exception as e:
        console.print(f"[red]Dropbox authorization failed: {e}[/red]")
        raise SystemExit(1) from e
    token_data = {
        "access_token": result.access_token,
        "refresh_token": getattr(result, "refresh_token", None),
        "account_id": getattr(result, "account_id", None),
    }
    token_file = config_dir / "dropbox_token.json"
    token_file.write_text(json.dumps(token_data, indent=2), encoding="utf-8")
    console.print(f"[green]Dropbox connected. Token saved to {token_file}[/green]")
    console.print("[dim]You can use the Backup Panel or backup tasks with provider dropbox.[/dim]")


@setup_app.command("box")
def setup_box() -> None:
    """Complete Box OAuth2 and save tokens to config/local for backup.

    Create an app at https://app.box.com/developers/console. Save client_id and
    client_secret to config/local/box_config.json, then run this command.
    """
    import json
    import webbrowser
    from pathlib import Path

    from vastian.bootstrap import register_all

    register_all()
    settings = get_settings()
    config_dir = Path(settings.local_config_dir).resolve()
    config_dir.mkdir(parents=True, exist_ok=True)

    config_path = config_dir / "box_config.json"
    if not config_path.exists():
        console.print("[red]Box config not found.[/red]")
        console.print(
            "Create a Box app at [link=https://app.box.com/developers/console]"
            "https://app.box.com/developers/console[/link]\n"
            f"Save [cyan]client_id[/cyan] and [cyan]client_secret[/cyan] to:\n"
            f"  [cyan]{config_path}[/cyan]\n"
            'Format: {"client_id": "...", "client_secret": "..."}'
        )
        raise SystemExit(1)

    token_path = config_dir / "box_token.json"
    if token_path.exists():
        console.print(
            "[green]Box already configured.[/green]\n"
            "[dim]Delete config/local/box_token.json to re-authenticate.[/dim]"
        )
        return

    try:
        from boxsdk import Client, OAuth2  # noqa: F401
    except ImportError:
        console.print("[red]Box SDK not installed.[/red] Run: [cyan]pip install boxsdk[/cyan]")
        raise SystemExit(1) from None

    config_data = json.loads(config_path.read_text(encoding="utf-8"))
    client_id = config_data.get("client_id", "").strip()
    client_secret = config_data.get("client_secret", "").strip()
    if not client_id or not client_secret:
        console.print("[red]box_config.json must contain client_id and client_secret.[/red]")
        raise SystemExit(1)

    oauth = OAuth2(client_id=client_id, client_secret=client_secret)
    auth_url, csrf_token = oauth.get_authorization_url("https://localhost")
    console.print("[dim]Opening browser for Box authorization…[/dim]")
    webbrowser.open(auth_url)
    console.print("\n[yellow]After authorizing, paste the authorization code below.[/yellow]\n")
    code = typer.prompt("Authorization code")
    code = code.strip()
    if not code:
        console.print("[red]No code provided.[/red]")
        raise SystemExit(1)

    access_token, refresh_token = oauth.authenticate(code)
    token_data = {"access_token": access_token, "refresh_token": refresh_token}
    token_path.write_text(json.dumps(token_data, indent=2), encoding="utf-8")
    console.print(f"[green]Box connected. Token saved to {token_path}[/green]")
    console.print("[dim]You can use the Backup Panel or backup tasks with provider box.[/dim]")


@setup_app.command("tresorit")
def setup_tresorit() -> None:
    """Configure Tresorit WebDAV credentials for backup (zero-knowledge encrypted storage).

    Tresorit uses WebDAV — enable it in your account settings and generate an
    app-specific password. No OAuth; credentials stored in config/local/tresorit_token.json.
    """
    from pathlib import Path

    from vastian.bootstrap import register_all

    register_all()
    settings = get_settings()
    config_dir = Path(settings.local_config_dir).resolve()
    config_dir.mkdir(parents=True, exist_ok=True)

    token_path = config_dir / "tresorit_token.json"
    if token_path.exists():
        console.print(
            "[green]Tresorit already configured.[/green]\n"
            "[dim]Delete config/local/tresorit_token.json to re-configure.[/dim]"
        )
        return

    console.print(
        "[dim]Tresorit uses WebDAV. Enable it in your account settings and create\n"
        "an app-specific password for WebDAV access.[/dim]\n"
    )
    webdav_url = typer.prompt("WebDAV URL", default="https://webdav.tresorit.com")
    username = typer.prompt("Username (your Tresorit email)")
    password = typer.prompt("Password (WebDAV app password)", hide_input=True)

    if not username or not password:
        console.print("[red]Username and password are required.[/red]")
        raise SystemExit(1)

    token_data = {
        "webdav_url": webdav_url.strip(),
        "username": username.strip(),
        "password": password,
    }
    token_path.write_text(json.dumps(token_data, indent=2), encoding="utf-8")
    console.print(f"[green]Tresorit configured. Credentials saved to {token_path}[/green]")
    console.print("[dim]You can use the Backup Panel or backup tasks with provider tresorit.[/dim]")


@setup_app.command("plaid")
def setup_plaid(
    public_token: str | None = typer.Option(
        None, "--public-token", "-t", help="Plaid Link public_token to exchange"
    ),
) -> None:
    """Configure Plaid for multi-bank connectivity (additive to Mercury).

    Requires PLAID_CLIENT_ID and PLAID_SECRET in .env. Get them at https://dashboard.plaid.com
    Use sandbox for testing. After running Plaid Link (browser) to connect a bank, pass
    the public_token here to exchange it for an access_token and save to config.

    Example: vastian setup plaid --public-token access-sandbox-xxx
    """
    import os
    from pathlib import Path

    import yaml

    from vastian.bootstrap import register_all

    register_all()
    settings = get_settings()
    config_dir = Path(settings.local_config_dir).resolve()
    config_dir.mkdir(parents=True, exist_ok=True)
    cfg_path = config_dir / "plaid_credentials.yaml"

    client_id = os.environ.get("PLAID_CLIENT_ID", "").strip()
    secret = os.environ.get("PLAID_SECRET", "").strip()
    env_name = os.environ.get("PLAID_ENV", "sandbox")

    if cfg_path.exists():
        try:
            data = yaml.safe_load(cfg_path.read_text(encoding="utf-8")) or {}
            client_id = client_id or data.get("client_id", "")
            secret = secret or data.get("secret", "")
            env_name = data.get("environment", env_name)
        except Exception:
            pass

    if not client_id or not secret:
        console.print(
            "[red]Missing Plaid credentials.[/red] Add to .env:\n"
            "  [cyan]PLAID_CLIENT_ID[/cyan]=your_client_id\n"
            "  [cyan]PLAID_SECRET[/cyan]=your_secret\n"
            "  [cyan]PLAID_ENV[/cyan]=sandbox (or development, production)\n\n"
            "Get keys at [link=https://dashboard.plaid.com]https://dashboard.plaid.com[/link]"
        )
        raise SystemExit(1)

    items: list[dict] = []
    if cfg_path.exists():
        try:
            data = yaml.safe_load(cfg_path.read_text(encoding="utf-8")) or {}
            items = list(data.get("items", []))
        except Exception:
            pass

    if public_token:
        try:
            import plaid
            from plaid.api import plaid_api
            from plaid.model.item_public_token_exchange_request import (
                ItemPublicTokenExchangeRequest,
            )

            env_map = {
                "sandbox": plaid.Environment.Sandbox,
                "development": plaid.Environment.Development,
                "production": plaid.Environment.Production,
            }
            configuration = plaid.Configuration(
                host=env_map.get(env_name.lower(), plaid.Environment.Sandbox),
                api_key={"clientId": client_id, "secret": secret},
            )
            api_client = plaid.ApiClient(configuration)
            client = plaid_api.PlaidApi(api_client)
            req = ItemPublicTokenExchangeRequest(public_token=public_token)
            resp = client.item_public_token_exchange(req)
            if hasattr(resp, "to_dict"):
                resp = resp.to_dict()
            access_token = (
                resp.get("access_token")
                if isinstance(resp, dict)
                else getattr(resp, "access_token", None)
            )
            if access_token:
                items.append({"access_token": access_token, "institution_name": "Link"})
                console.print("[green]Plaid Link connected. Access token saved.[/green]")
            else:
                console.print("[red]Exchange failed: no access_token in response.[/red]")
                raise SystemExit(1)
        except ImportError:
            console.print(
                "[red]plaid-python not installed.[/red] Run: [cyan]pip install vastian-network[plaid][/cyan]"
            )
            raise SystemExit(1) from None
        except Exception as e:
            console.print(f"[red]Plaid exchange failed: {e}[/red]")
            raise SystemExit(1) from e
    else:
        console.print(
            "[dim]To add a bank account:\n"
            "1. Run Plaid Link (e.g. from https://github.com/plaid/quickstart) to connect a bank\n"
            "2. Copy the public_token returned by Link\n"
            "3. Run: vastian setup plaid --public-token <token>[/dim]"
        )

    cfg = {
        "client_id": client_id,
        "secret": secret,
        "environment": env_name,
        "items": items,
    }
    cfg_path.write_text(
        yaml.dump(cfg, default_flow_style=False, allow_unicode=True), encoding="utf-8"
    )
    console.print(f"[green]Plaid config saved to {cfg_path}[/green]")


# ── Giga (outbox and plan export) ─────────────────

giga_app = typer.Typer(help="Giga outbox and plan export.")
app.add_typer(giga_app, name="giga")


@app.command("mcp-serve")
def mcp_serve(
    port: int = typer.Option(4550, "--port", "-p", help="Port for the MCP server."),
    host: str = typer.Option("127.0.0.1", "--host", help="Host to bind."),
) -> None:
    """Start the Vastian MCP server — external AI tools connect at http://localhost:4550/mcp."""
    from vastian.mcp.server import run_mcp_server

    console.print(f"[dim]MCP server starting on http://{host}:{port}/mcp[/dim]")
    console.print("[dim]Auth: Bearer token from VASTIAN_MCP_KEY (leave empty for dev)[/dim]")
    run_mcp_server(port=port, host=host)


@app.command("cloud-serve")
def cloud_serve(
    port: int = typer.Option(4545, "--port", "-p", help="Port for the cloud API."),
    host: str = typer.Option("0.0.0.0", "--host", "-h", help="Host to bind (0.0.0.0 for Zo)."),
    inbox_path: str = typer.Option(
        None,
        "--inbox",
        help="Inbox directory (default: VASTIAN_ZO_INBOX_PATH or /home/workspace/vastian-inbox)",
    ),
    outbox_path: str = typer.Option(
        None,
        "--outbox",
        help="Outbox directory (default: VASTIAN_ZO_OUTBOX_PATH or /home/workspace/vastian-outbox)",
    ),
    interval: int = typer.Option(30, "--interval", "-i", help="Poll interval in seconds."),
) -> None:
    """
    Start the Vastian cloud worker + MCP + control API for Zo deployment.

    Serves: /api/health, /mcp, /api/worker/status, /api/worker/control, /
    Worker starts paused; use the web UI or API to start/stop.
    """
    import os

    inbox = inbox_path or os.environ.get("VASTIAN_ZO_INBOX_PATH", "/home/workspace/vastian-inbox")
    outbox = outbox_path or os.environ.get(
        "VASTIAN_ZO_OUTBOX_PATH", "/home/workspace/vastian-outbox"
    )

    _setup_logging("INFO")
    settings = get_settings()
    _ensure_data_root_seeded(settings)

    import uvicorn

    from vastian.server.cloud_app import _cloud_worker_loop, create_cloud_app

    app = create_cloud_app()

    async def _run() -> None:
        worker_task = asyncio.create_task(_cloud_worker_loop(inbox, outbox, interval))
        config = uvicorn.Config(
            app,
            host=host,
            port=port,
            log_level="info",
        )
        server = uvicorn.Server(config)
        try:
            await server.serve()
        finally:
            worker_task.cancel()
            with contextlib.suppress(asyncio.CancelledError):
                await worker_task

    base = f"http://{host}:{port}" if host != "0.0.0.0" else f"http://127.0.0.1:{port}"
    console.print(f"[dim]Cloud worker starting on {base}[/dim]")
    console.print(f"[dim]Inbox: {inbox} | Outbox: {outbox} | Poll: {interval}s[/dim]")
    console.print("[dim]Control: GET / or POST /api/worker/control with action=start|stop[/dim]")
    asyncio.run(_run())


@app.command("cloud-status")
def cloud_status(
    url: str = typer.Option(
        None,
        "--url",
        "-u",
        help="Cloud worker URL (default: VASTIAN_CLOUD_URL or https://engine.vastianconnect.com)",
    ),
) -> None:
    """
    Check cloud worker health and status. Use after deploying to Zo.
    """
    import json
    import os
    import urllib.request

    base = url or os.environ.get("VASTIAN_CLOUD_URL", "https://engine.vastianconnect.com")
    base = base.rstrip("/")
    # Cloudflare blocks Python's default User-Agent — use a real one
    headers = {"User-Agent": "vastian-cli/1.0"}
    try:
        req = urllib.request.Request(f"{base}/api/health", headers=headers)
        with urllib.request.urlopen(req, timeout=10) as r:
            health = json.loads(r.read().decode())
        console.print(
            f"[green]Health:[/green] {health.get('status', '?')} v{health.get('version', '?')}"
        )
    except Exception as e:
        console.print(f"[red]Health check failed:[/red] {e}")
        raise typer.Exit(1) from e
    try:
        req = urllib.request.Request(f"{base}/api/worker/status", headers=headers)
        with urllib.request.urlopen(req, timeout=10) as r:
            status = json.loads(r.read().decode())
        running = status.get("running", False)
        inbox = status.get("inbox_watcher", "unknown")
        last = status.get("last_poll") or "never"
        console.print(f"[green]Inbox watcher:[/green] {inbox}")
        console.print(
            f"[green]Background ops:[/green] {'running' if running else 'paused'} | last poll: {last}"
        )
    except Exception as e:
        console.print(f"[red]Status check failed:[/red] {e}")
        raise typer.Exit(1) from e


@giga_app.command("push")
def giga_push_cmd(
    mind: str = typer.Option("vastian-network", "--mind", "-m", help="Target Giga mind."),
) -> None:
    """Push outbox plans and neurons to Giga via native MCP.

    Reads state/giga-outbox/plans/ and neurons/, calls Giga MCP tools,
    moves success to state/giga-outbox/sent/.
    """
    import asyncio

    from vastian.mcp.giga_sync import giga_push

    async def _run() -> None:
        plans_pushed, neurons_pushed, errors = await giga_push()
        if errors:
            for e in errors:
                console.print(f"[red]{e}[/red]")
        total = plans_pushed + neurons_pushed
        if total:
            console.print(
                f"[green]Pushed {plans_pushed} plan(s), {neurons_pushed} neuron(s) to Giga.[/green]"
            )
        elif not errors:
            console.print("[dim]Giga outbox is empty — nothing to push.[/dim]")

    asyncio.run(_run())


@giga_app.command("pull")
def giga_pull_cmd(
    mind: str = typer.Option("vastian-network", "--mind", "-m", help="Target Giga mind to pull."),
) -> None:
    """Pull plans and neurons from Giga into local state/memory-inbox."""
    import asyncio

    from vastian.mcp.giga_sync import giga_pull

    async def _run() -> None:
        total, errors = await giga_pull(minds=[mind])
        if errors:
            for e in errors:
                console.print(f"[red]{e}[/red]")
        if total:
            console.print(f"[green]Pulled {total} item(s) from Giga mind '{mind}'.[/green]")

    asyncio.run(_run())


@giga_app.command("export-plans")
def giga_export_plans(
    plans_dir: Path = typer.Option(  # noqa: B008
        None,
        "--plans-dir",
        "-d",
        path_type=Path,
        help="Directory containing Cursor .plan.md files (default: .cursor/plans in project or home).",
    ),
    outbox_dir: Path = typer.Option(  # noqa: B008
        None,
        "--outbox",
        "-o",
        path_type=Path,
        help="Outbox plans directory (default: state/giga-outbox/plans).",
    ),
) -> None:
    """Export Cursor plans (ai-ecosystem-consolidation, phase-mercury) to Giga outbox as JSON.

    Reads .plan.md files, strips YAML frontmatter, and writes payloads to state/giga-outbox/plans/
    so /giga-sync lists them and they can be pushed to Giga via MCP.
    """
    import json
    from datetime import datetime

    project_root = Path(__file__).resolve().parents[2]
    if outbox_dir is None:
        outbox_dir = project_root / "state" / "giga-outbox" / "plans"
    outbox_dir.mkdir(parents=True, exist_ok=True)

    if plans_dir is None:
        for candidate in [project_root / ".cursor" / "plans", Path.home() / ".cursor" / "plans"]:
            if candidate.exists():
                plans_dir = candidate
                break
        if plans_dir is None:
            console.print(
                "[red]No plans directory found. Use --plans-dir or create .cursor/plans in project or home.[/red]"
            )
            raise typer.Exit(1)

    def strip_frontmatter(text: str) -> str:
        lines = text.splitlines()
        sep_count = 0
        start = 0
        for i, line in enumerate(lines):
            if line.strip() == "---":
                sep_count += 1
                if sep_count == 2:
                    start = i + 1
                    break
        return "\n".join(lines[start:]) if start else text

    exports = [
        ("ai-ecosystem-consolidation", "ai_ecosystem_consolidation"),
        ("phase-mercury-implementation", "phase_mercury_implementation"),
    ]
    created = []
    for key_name, stem_prefix in exports:
        matches = list(plans_dir.glob(f"*{stem_prefix}*.plan.md"))
        if not matches:
            console.print(
                f"[yellow]No plan file matching *{stem_prefix}*.plan.md in {plans_dir}[/yellow]"
            )
            continue
        plan_path = matches[0]
        raw = plan_path.read_text(encoding="utf-8")
        content = strip_frontmatter(raw)
        payload = {
            "key_name": key_name,
            "content": content,
            "mind": "vastian-network",
            "created_by": "cursor",
            "created_at": datetime.now(UTC).isoformat(),
        }
        outpath = outbox_dir / f"{key_name}.json"
        outpath.write_text(json.dumps(payload, indent=2), encoding="utf-8")
        created.append(key_name)
        console.print(f"[green]Exported[/green] {plan_path.name} -> {outpath.name}")

    if created:
        console.print(
            f"\n[bold]{len(created)} plan(s) in outbox.[/bold] Run [cyan]/giga-sync[/cyan] in the CLI to list them, then push to Giga via MCP."
        )
    else:
        console.print("[yellow]No plans were exported.[/yellow]")
        raise typer.Exit(1)


@app.command(name="backup-panel")
def backup_panel(
    port: int = typer.Option(4547, "--port", "-p", help="Port for the backup panel server."),
    open_browser: bool = typer.Option(
        False,
        "--open/--no-open",
        help="Open the panel in your browser (default: no; use the link in the terminal).",
    ),
    tier: str = typer.Option(
        "open",
        "--tier",
        "-t",
        help="User access tier (open/standard/elevated/admin). Gates schedule and other features.",
    ),
) -> None:
    """Start the Backup Panel — browser UI for local-first backups (provider, schedule, restore)."""
    import time
    from pathlib import Path

    from vastian.bootstrap import register_all
    from vastian.tasks.backup_panel import BackupPanel

    # Ensure storage plugins (gdrive, dropbox, box, tresorit) are registered
    register_all()

    project_root = Path.cwd()
    panel = BackupPanel(port=port, project_root=project_root, user_tier=tier)
    url = panel.start()
    console.print(
        Panel(
            f"[bold cyan]Backup Panel[/bold cyan]\n\n"
            f"Open in browser: [link={url}]{url}[/link]\n\n"
            f"[dim]Provider selection, Back Up Now, schedule, restore, config snapshots.[/dim]",
            border_style="cyan",
        )
    )
    if open_browser:
        _open_feed_browser(url)
    console.print("[dim]Press Ctrl+C to stop the server.[/dim]\n")
    try:
        while True:
            time.sleep(3600)
    except KeyboardInterrupt:
        panel.stop()
        console.print("[dim]Backup panel stopped.[/dim]")


@app.command()
def reset(
    confirm: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
) -> None:
    """Reset all personal data for a clean public-ready state.

    Clears:
    - All state/*.json files (reset to empty arrays)
    - All docs/{agent}/ content (replaced with placeholders)
    - saved_prompts/complete/ and saved_prompts/intake/ contents
    - data/ directory (SQLite + ChromaDB)

    Keeps the directory structure and .gitkeep files intact.
    Use this to prepare the main branch for public release.
    """
    import shutil

    settings = get_settings()

    if not confirm:
        console.print(
            Panel(
                "[bold red]WARNING: This will delete ALL personal data![/bold red]\n\n"
                "This resets:\n"
                "  - All state/*.json files (council sessions, delegations, etc.)\n"
                "  - All docs/ content (agent documents)\n"
                "  - saved_prompts/complete/ and intake/ contents\n"
                "  - data/ directory (databases)\n\n"
                "[dim]Use --yes to skip this prompt.[/dim]",
                border_style="red",
            )
        )
        answer = Prompt.ask("Are you sure?", choices=["yes", "no"], default="no")
        if answer != "yes":
            console.print("[dim]Aborted.[/dim]")
            return

    # 1. Reset state/*.json files to empty arrays
    state_dir = settings.state_dir
    if state_dir.exists():
        for json_file in state_dir.glob("*.json"):
            json_file.write_text("[]\n", encoding="utf-8")
            console.print(f"  [yellow]↻[/yellow] Reset {json_file.name}")

    # 2. Reset docs/ — remove content, recreate placeholders
    docs_dir = settings.docs_dir
    if docs_dir.exists():
        # Keep FEATURES.md at root
        features = docs_dir / "FEATURES.md"
        features_content = features.read_text(encoding="utf-8") if features.exists() else None

        # Load agent configs to recreate proper placeholders
        orchestrator = Orchestrator(settings)
        orchestrator.registry.load_from_directory(
            settings.agents_dir,
            template_vars={"user_name": settings.user_name},
        )

        for config in orchestrator.registry.all_configs:
            agent_dir = docs_dir / config.agent_id
            if agent_dir.exists():
                # Remove all files in agent doc dir
                for f in agent_dir.glob("*"):
                    f.unlink()
                console.print(f"  [yellow]↻[/yellow] Cleared docs/{config.agent_id}/")

            # Recreate placeholders
            agent_dir.mkdir(parents=True, exist_ok=True)
            for doc_name in config.owned_documents:
                doc_path = agent_dir / doc_name
                doc_path.write_text(
                    f"---\nauthor: {config.agent_id}\n---\n\n"
                    f"# {doc_name.replace('.md', '').replace('-', ' ').title()}\n\n"
                    f"*Owned by {config.name} ({config.title})*\n\n"
                    f"---\n\n> This document is pending initial content.\n",
                    encoding="utf-8",
                )

        # Restore FEATURES.md
        if features_content:
            features.write_text(features_content, encoding="utf-8")

    # 3. Clear saved_prompts/complete/ and intake/ (in data root)
    from vastian.storage.data_root import get_saved_prompts_dir

    prompts_dir = get_saved_prompts_dir(Path.cwd())
    for subdir_name in ("complete", "intake"):
        subdir = prompts_dir / subdir_name
        if subdir.exists():
            for f in subdir.glob("*"):
                if f.name != ".gitkeep":
                    f.unlink()
            console.print(f"  [yellow]↻[/yellow] Cleared saved_prompts/{subdir_name}/")

    # Remove intro.txt (personal)
    intro = prompts_dir / "intro.txt"
    if intro.exists():
        intro.unlink()
        console.print("  [yellow]↻[/yellow] Removed saved_prompts/intro.txt")

    # 4. Remove data/ directory
    data_dir = settings.sqlite_path.parent
    if data_dir.exists():
        shutil.rmtree(data_dir, ignore_errors=True)
        console.print(f"  [yellow]↻[/yellow] Removed {data_dir}/")

    console.print("\n[bold green]Reset complete![/bold green]")
    console.print(
        "[dim]Run 'vastian init' to recreate directory structure, then 'vastian onboard' with a new intro.txt.[/dim]"
    )


@app.command(name="memory-pull")
def memory_pull(
    mind: str = typer.Argument(..., help="Mind/namespace to pull from (e.g., 'work')."),
    offline: bool = typer.Option(
        False, "--offline", help="Skip agent distribution, write raw context only."
    ),
    verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging."),
) -> None:
    """Pull external context from a Giga mind and distribute briefs to subscribers.

    Pipeline: sync inbox JSON -> merge into raw context doc -> distribute tailored briefs.

    Example: vastian memory-pull work
    """
    if verbose:
        logging.basicConfig(level=logging.DEBUG, format="%(name)s | %(message)s")

    from vastian.tasks.memory_inbox import InboxProcessor

    console.print(f"\n[bold cyan]Memory Pull[/bold cyan] — mind: [bold]{mind}[/bold]\n")

    # Step 1: Process inbox JSON into raw context
    processor = InboxProcessor()
    result = processor.process_inbox(mind)

    if result.get("raw_doc") is None:
        console.print(f"  [dim]No inbox items found for mind '{mind}'.[/dim]")
        console.print(
            f"  [dim]Drop JSON files into state/memory-inbox/{mind}/plans/ or neurons/ first.[/dim]"
        )
        return

    console.print(
        f"  [green]✓[/green] Processed {result['plans']} plans + {result['neurons']} neurons"
    )
    console.print(f"  [green]✓[/green] Raw context: {result['raw_doc']}")

    if offline:
        console.print("\n  [dim]Offline mode — skipping agent distribution.[/dim]")
        # Still write raw copies to each recipient in offline mode
        raw_text = Path(result["raw_doc"]).read_text(encoding="utf-8")
        briefs = processor._distribute_offline(
            mind, processor.subscriptions.get(mind, {}).get("recipients", []), raw_text
        )
        if briefs:
            console.print(f"  [green]✓[/green] Wrote {len(briefs)} offline briefs")
            for b in briefs:
                console.print(f"    [dim]{b}[/dim]")
        return

    # Step 2: Distribute via orchestrator + Charles
    settings = get_settings()
    orchestrator = Orchestrator(settings)

    async def _run() -> None:
        await orchestrator.initialize(session_only=True)
        processor.orchestrator = orchestrator  # Must set BEFORE distribute()
        briefs = await processor.distribute(mind, result.pop("raw_text", None))
        if briefs:
            console.print(f"  [green]✓[/green] Distributed {len(briefs)} tailored briefs:")
            for b in briefs:
                console.print(f"    [dim]{b}[/dim]")
        else:
            console.print("  [yellow]![/yellow] No briefs written (check subscription config)")

    try:
        asyncio.run(_run())
    except KeyboardInterrupt:
        console.print("\n[dim]Interrupted.[/dim]")


@app.command(name="memory-status")
def memory_status() -> None:
    """Show status of all configured memory inbox subscriptions."""
    from vastian.tasks.memory_inbox import InboxProcessor

    processor = InboxProcessor()
    status = processor.get_status()

    if not status["minds"]:
        console.print("[dim]No memory subscriptions configured.[/dim]")
        console.print("[dim]Edit config/memory-subscriptions.yaml to add subscriptions.[/dim]")
        return

    console.print("\n[bold cyan]Memory Inbox Status[/bold cyan]\n")

    table = Table(show_header=True, header_style="bold cyan")
    table.add_column("Mind")
    table.add_column("Pending Plans")
    table.add_column("Pending Neurons")
    table.add_column("Raw Doc")
    table.add_column("Recipients")
    table.add_column("Distributor")
    table.add_column("Auto Sync")

    for mind_name, info in status["minds"].items():
        raw_status = "[green]✓[/green]" if info["raw_doc_exists"] else "[dim]—[/dim]"
        table.add_row(
            mind_name,
            str(info["pending_plans"]),
            str(info["pending_neurons"]),
            raw_status,
            ", ".join(info["recipients"]),
            info["distributor"],
            "[green]yes[/green]" if info["auto_sync"] else "[dim]no[/dim]",
        )

    console.print(table)
    console.print()


async def _zo_inbox_worker_loop(inbox_path: str, outbox_path: str, interval: int) -> None:
    """Zo-native worker: poll inbox dir, process through Charles, write results to outbox."""
    from pathlib import Path

    inbox_dir = Path(inbox_path)
    outbox_dir = Path(outbox_path)
    inbox_dir.mkdir(parents=True, exist_ok=True)
    outbox_dir.mkdir(parents=True, exist_ok=True)

    settings = get_settings()
    orchestrator = Orchestrator(settings)
    try:
        await orchestrator.initialize()
    except Exception as e:
        console.print(f"[red]Initialization error:[/red] {e}")
        return

    processed: set[str] = set()
    try:
        while True:
            for f in inbox_dir.glob("*.txt"):
                if f.name in processed:
                    continue
                try:
                    content = f.read_text(encoding="utf-8").strip()
                except Exception:
                    continue
                if len(content) < 10:
                    continue
                processed.add(f.name)
                console.print(f"[dim]Processing: {f.name}[/dim]")
                try:
                    # Run inbox pipeline (Charles triage → delegation)
                    from vastian.storage.data_root import get_data_root
                    from vastian.tasks.inbox import InboxWatcher

                    watcher = InboxWatcher(orchestrator, get_data_root(settings.agents_dir.parent))
                    import tempfile

                    tmp_intake = Path(tempfile.mkdtemp()) / "intake"
                    tmp_complete = Path(tempfile.mkdtemp()) / "complete"
                    tmp_intake.mkdir(parents=True, exist_ok=True)
                    tmp_complete.mkdir(parents=True, exist_ok=True)
                    intake_file = tmp_intake / f.name
                    intake_file.write_text(content, encoding="utf-8")
                    watcher.intake_dir = tmp_intake
                    watcher.complete_dir = tmp_complete
                    await watcher._execute_inbox_task(
                        "zo-inbox",
                        f.name,
                        content,
                        intake_file,
                    )
                    # Write summary to outbox
                    from datetime import datetime

                    ts = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
                    out_name = f"processed-{ts}-{f.name}"
                    outbox_dir.mkdir(parents=True, exist_ok=True)
                    (outbox_dir / out_name).write_text(
                        f"Processed: {f.name}\n\nContent length: {len(content)} chars.",
                        encoding="utf-8",
                    )
                    # Move or remove from inbox
                    with contextlib.suppress(Exception):
                        f.unlink()
                except Exception as e:
                    console.print(f"[red]Error processing {f.name}: {e}[/red]")
                    processed.discard(f.name)
            await asyncio.sleep(interval)
    except KeyboardInterrupt:
        console.print("\n[dim]Zo-inbox worker shutting down...[/dim]")
    finally:
        if orchestrator.task_queue:
            orchestrator.task_queue.close()


@app.command()
def worker(
    mode: str = typer.Argument("default", help="Worker mode: default or zo-inbox"),
    verbose: bool = typer.Option(False, "--verbose", "-v"),
    inbox_path: str = typer.Option(None, "--inbox"),
    outbox_path: str = typer.Option(None, "--outbox"),
    interval: int = typer.Option(30, "--interval", "-i"),
) -> None:
    """
    Run the Vastian background worker.

    Modes:
      default    — Inbox, kanban, feed (use when desktop is running)
      zo-inbox   — Zo-native: poll vastian-inbox, delegate, write to vastian-outbox (run ON Zo)

    Example: vastian worker zo-inbox
    """
    import os

    if mode == "zo-inbox":
        inbox = inbox_path or os.environ.get(
            "VASTIAN_ZO_INBOX_PATH", "/home/workspace/vastian-inbox"
        )
        outbox = outbox_path or os.environ.get(
            "VASTIAN_ZO_OUTBOX_PATH", "/home/workspace/vastian-outbox"
        )
        _setup_logging("DEBUG" if verbose else "INFO")
        console.print(
            Panel.fit(
                f"[bold cyan]Vastian Zo-Inbox Worker[/bold cyan]\n"
                f"[dim]Inbox: {inbox} | Outbox: {outbox} | Poll: {interval}s[/dim]\n"
                f"[dim]Press Ctrl+C to stop.[/dim]",
                border_style="cyan",
            )
        )
        asyncio.run(_zo_inbox_worker_loop(inbox, outbox, interval))
    else:
        _setup_logging("DEBUG" if verbose else "INFO")
        console.print(
            Panel.fit(
                "[bold cyan]Vastian Network — Background Worker[/bold cyan]\n"
                "[dim]Processing inbox files, kanban tasks, and background delegations.[/dim]\n"
                "[dim]Press Ctrl+C to stop.[/dim]",
                border_style="cyan",
            )
        )
        asyncio.run(_worker_loop(verbose))


async def _worker_loop(verbose: bool) -> None:
    """Background worker loop — no interactive chat."""
    settings = get_settings()
    orchestrator = Orchestrator(settings)

    try:
        await orchestrator.initialize()
    except Exception as e:
        console.print(f"[red]Initialization error:[/red] {e}")
        return

    # Start notification feed
    feed_url = ""
    try:
        from vastian.tasks.feed import NotificationFeed

        feed = NotificationFeed(
            db_path=settings.sqlite_path.parent / "tasks.db",
            port=4545,
            state_dir=settings.state_dir,
            user_tier=orchestrator.user_tier,
        )
        feed_url = feed.start()
        if feed_url:
            console.print(
                f"[green]Activity feed + Kanban:[/green] [link={feed_url}]{feed_url}[/link]"
            )

            # Wire up callbacks so browser actions trigger orchestrator
            async def _on_force_start(task_id: str) -> None:
                await orchestrator._execute_background_task(task_id)

            async def _on_kanban_approved(kanban_id: str) -> None:
                # Immediately kick off execution instead of waiting for polling
                if orchestrator.task_queue:
                    tasks = orchestrator.task_queue.get_kanban_tasks(status="approved")
                    for task in tasks:
                        if task["kanban_id"] == kanban_id:
                            orchestrator.task_queue.start_kanban_task(kanban_id)
                            orchestrator.task_queue.add_notification(
                                task["suggested_by"],
                                f"Kanban task started: {task['title']}",
                                "running",
                            )
                            asyncio.create_task(orchestrator._execute_kanban_task(task))
                            break

            feed.on_force_start = _on_force_start
            feed.on_kanban_approved = _on_kanban_approved
    except Exception:
        pass

    # Show status
    if orchestrator.task_queue:
        pending = orchestrator.task_queue.get_pending()
        running = orchestrator.task_queue.get_running()
        kanban = orchestrator.task_queue.get_kanban_tasks(status="approved")
        console.print(
            f"\n[dim]Pending: {len(pending)} | Running: {len(running)} | "
            f"Kanban approved: {len(kanban)}[/dim]"
        )

    console.print(
        "[green]Worker is running. Background tasks will be processed automatically.[/green]"
    )
    console.print(
        "[dim]Inbox folder: saved_prompts/inbox/ — drop .txt files for processing (cloud upload available at premium tier)[/dim]"
    )
    console.print(
        "[dim]Kanban board: approve tasks in the browser at the activity feed URL[/dim]\n"
    )

    try:
        # Keep alive — the orchestrator's async tasks run in this event loop
        while True:
            await asyncio.sleep(60)
            # Periodic status update
            if orchestrator.task_queue:
                running = orchestrator.task_queue.get_running()
                if running:
                    console.print(f"[dim]{len(running)} task(s) running...[/dim]")
    except KeyboardInterrupt:
        console.print("\n[dim]Worker shutting down...[/dim]")
    finally:
        if orchestrator.task_queue:
            orchestrator.task_queue.close()
        console.print("[dim]Worker stopped. Goodbye![/dim]")


@app.command()
def improve(
    verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging."),
) -> None:
    """
    Run Orion's operations improvement scan.

    Orion analyzes the codebase and suggests improvements on the Kanban board.
    You approve which ones to execute via the browser. Henry handles architecture.
    """
    _setup_logging("DEBUG" if verbose else "WARNING")
    console.print("[cyan]Running Orion's improvement scan...[/cyan]")
    asyncio.run(_run_improve())


async def _run_improve() -> None:
    settings = get_settings()
    orchestrator = Orchestrator(settings)
    await orchestrator.initialize(session_only=True)

    await orchestrator.run_orion_improvement_cycle()

    if orchestrator.task_queue:
        kanban = orchestrator.task_queue.get_kanban_tasks(status="suggested")
        if kanban:
            console.print(
                f"\n[green]Orion suggested {len(kanban)} task(s) on the Kanban board.[/green]"
            )
            console.print(
                "[dim]Open the activity feed in your browser and go to the Kanban tab to review.[/dim]"
            )
        else:
            console.print("[dim]No new improvements suggested this cycle.[/dim]")
        orchestrator.task_queue.close()


# ══════════════════════════════════════════════════════════════════════
# Presentation command group — Cursor presents TO the network
# ══════════════════════════════════════════════════════════════════════

presentation_app = typer.Typer(
    name="presentation",
    help="Cursor Session Protocol — present proposals to the Vastian Network.",
)
app.add_typer(presentation_app, name="presentation")


@presentation_app.command("start")
def presentation_start(
    topic: str = typer.Argument(..., help="Topic of the presentation."),
    brief: str = typer.Option(None, "--brief", "-b", help="Path to a brief document (.md)."),
    agents: str = typer.Option(
        None, "--agents", "-a", help="Comma-separated agent IDs, or 'all' (default)."
    ),
    presenter: str = typer.Option("Cursor AI", "--presenter", "-p", help="Presenter display name."),
    verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
    """Start a presentation — Cursor presents to the network, agents react."""
    _setup_logging("DEBUG" if verbose else "WARNING")

    agent_list = [a.strip() for a in agents.split(",")] if agents else None

    # Load brief content if provided
    brief_content = ""
    if brief:
        brief_path = Path(brief)
        if brief_path.exists():
            brief_content = brief_path.read_text(encoding="utf-8")
        else:
            console.print(f"[red]Brief file not found: {brief}[/red]")
            raise typer.Exit(1)

    console.print(
        Panel(
            f"[bold cyan]Starting Presentation[/bold cyan]\n"
            f"Topic: {topic}\n"
            f"Presenter: {presenter}\n"
            f"Brief: {brief or 'none'}",
            border_style="cyan",
        )
    )

    asyncio.run(_run_presentation_start(topic, brief_content, brief, agent_list, presenter))


async def _run_presentation_start(
    topic: str,
    brief_content: str,
    brief_path: str | None,
    agent_ids: list[str] | None,
    presenter_name: str,
) -> None:
    settings = get_settings()
    orchestrator = Orchestrator(settings)
    try:
        await orchestrator.initialize(session_only=True)
    except Exception as e:
        console.print(f"[dim]Init warning (non-fatal): {e}[/dim]")

    from vastian.sessions.engine import SessionEngine

    engine = SessionEngine(
        registry=orchestrator.registry,
        bus=orchestrator.bus,
        knowledge=orchestrator.knowledge,
        state_dir=settings.state_dir,
    )

    # Progress callback to show real-time output
    async def on_progress(event: str, agent_name: str, content: str) -> None:
        if event == "start":
            console.print(f"\n[dim]{content}[/dim]")
        elif event == "thinking":
            console.print(f"[dim]{content}[/dim]")
        elif event == "contribution":
            console.print(
                Panel(
                    Markdown(content[:2000]),
                    title=f"[bold]{agent_name}[/bold]",
                    border_style="green",
                )
            )

    engine.progress_callback = on_progress

    try:
        session = await engine.start_presentation(
            topic=topic,
            brief_content=brief_content,
            brief_path=brief_path,
            agent_ids=agent_ids,
            presenter_name=presenter_name,
            orchestrator=orchestrator,
        )

        console.print(
            Panel(
                f"[bold green]Presentation Started[/bold green]\n"
                f"Session ID: [bold]{session.id}[/bold]\n"
                f"Agents: {len(session.agent_ids)} | Round: {session.current_round}\n\n"
                f"[dim]Use these commands to continue:[/dim]\n"
                f'  vastian presentation say {session.id} "your message"\n'
                f'  vastian presentation ask {session.id} <agent_id> "question"\n'
                f"  vastian presentation discuss {session.id}\n"
                f"  vastian presentation wrap {session.id}",
                border_style="green",
            )
        )
    except Exception as e:
        console.print(f"[red]Error: {e}[/red]")
    finally:
        if orchestrator.task_queue:
            orchestrator.task_queue.close()


@presentation_app.command("say")
def presentation_say(
    session_id: str = typer.Argument(..., help="Session ID."),
    message: str = typer.Argument(..., help="Message to broadcast to all agents."),
    verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
    """Broadcast a follow-up message to all agents in the presentation."""
    _setup_logging("DEBUG" if verbose else "WARNING")
    console.print(f"[cyan]Broadcasting to session {session_id}...[/cyan]")
    asyncio.run(_run_presentation_say(session_id, message))


async def _run_presentation_say(session_id: str, message: str) -> None:
    settings = get_settings()
    orchestrator = Orchestrator(settings)
    await orchestrator.initialize(session_only=True)

    from vastian.sessions.engine import SessionEngine

    engine = SessionEngine(
        registry=orchestrator.registry,
        bus=orchestrator.bus,
        knowledge=orchestrator.knowledge,
        state_dir=settings.state_dir,
    )

    async def on_progress(event: str, agent_name: str, content: str) -> None:
        if event == "thinking":
            console.print(f"[dim]{content}[/dim]")
        elif event == "contribution":
            console.print(
                Panel(
                    Markdown(content[:2000]),
                    title=f"[bold]{agent_name}[/bold]",
                    border_style="green",
                )
            )

    engine.progress_callback = on_progress

    try:
        session = await engine.presentation_say(
            session_id=session_id,
            message=message,
            orchestrator=orchestrator,
        )
        console.print(f"\n[green]Round {session.current_round} complete.[/green]")

        # Auto-analyze the round
        console.print("[dim]Running post-round analysis...[/dim]")
        try:
            analysis = await engine.analyze_round(
                session_id=session_id,
                orchestrator=orchestrator,
            )
            if "error" not in analysis:
                _display_round_analysis(analysis)
        except Exception as ae:
            console.print(f"[dim]Analysis skipped: {ae}[/dim]")
    except Exception as e:
        console.print(f"[red]Error: {e}[/red]")
    finally:
        if orchestrator.task_queue:
            orchestrator.task_queue.close()


@presentation_app.command("ask")
def presentation_ask(
    session_id: str = typer.Argument(..., help="Session ID."),
    agent_id: str = typer.Argument(..., help="Agent ID to address."),
    message: str = typer.Argument(..., help="Question for the specific agent."),
    verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
    """Ask a specific agent a direct question in the presentation."""
    _setup_logging("DEBUG" if verbose else "WARNING")
    console.print(f"[cyan]Asking {agent_id} in session {session_id}...[/cyan]")
    asyncio.run(_run_presentation_ask(session_id, agent_id, message))


async def _run_presentation_ask(session_id: str, agent_id: str, message: str) -> None:
    settings = get_settings()
    orchestrator = Orchestrator(settings)
    await orchestrator.initialize(session_only=True)

    from vastian.sessions.engine import SessionEngine

    engine = SessionEngine(
        registry=orchestrator.registry,
        bus=orchestrator.bus,
        knowledge=orchestrator.knowledge,
        state_dir=settings.state_dir,
    )

    async def on_progress(event: str, agent_name: str, content: str) -> None:
        if event in ("thinking", "ask"):
            console.print(f"[dim]{content}[/dim]")
        elif event == "contribution":
            console.print(
                Panel(
                    Markdown(content[:2000]),
                    title=f"[bold]{agent_name}[/bold]",
                    border_style="green",
                )
            )

    engine.progress_callback = on_progress

    try:
        session = await engine.presentation_ask(
            session_id=session_id,
            agent_id=agent_id,
            message=message,
            orchestrator=orchestrator,
        )
        console.print(f"\n[green]Round {session.current_round} complete.[/green]")

        # Auto-analyze the round
        console.print("[dim]Running post-round analysis...[/dim]")
        try:
            analysis = await engine.analyze_round(
                session_id=session_id,
                orchestrator=orchestrator,
            )
            if "error" not in analysis:
                _display_round_analysis(analysis)
        except Exception as ae:
            console.print(f"[dim]Analysis skipped: {ae}[/dim]")
    except Exception as e:
        console.print(f"[red]Error: {e}[/red]")
    finally:
        if orchestrator.task_queue:
            orchestrator.task_queue.close()


@presentation_app.command("discuss")
def presentation_discuss(
    session_id: str = typer.Argument(..., help="Session ID."),
    verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
    """Open the floor for agents to discuss among themselves."""
    _setup_logging("DEBUG" if verbose else "WARNING")
    console.print(f"[cyan]Opening discussion in session {session_id}...[/cyan]")
    asyncio.run(_run_session_discuss(session_id))


@presentation_app.command("wrap")
def presentation_wrap(
    session_id: str = typer.Argument(..., help="Session ID."),
    verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
    """Conclude the presentation and generate a summary."""
    _setup_logging("DEBUG" if verbose else "WARNING")
    console.print(f"[cyan]Wrapping session {session_id}...[/cyan]")
    asyncio.run(_run_session_wrap(session_id))


@presentation_app.command("feedback")
def presentation_feedback(
    session_id: str = typer.Argument(..., help="Session ID."),
    feedback: str = typer.Argument(..., help="Your feedback for the agents."),
    verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
    """Record human feedback — agents will see this in the next round context."""
    _setup_logging("DEBUG" if verbose else "WARNING")
    asyncio.run(_run_presentation_feedback(session_id, feedback))


async def _run_presentation_feedback(session_id: str, feedback: str) -> None:
    settings = get_settings()
    orchestrator = Orchestrator(settings)
    await orchestrator.initialize(session_only=True)

    from vastian.sessions.engine import SessionEngine

    engine = SessionEngine(
        registry=orchestrator.registry,
        bus=orchestrator.bus,
        knowledge=orchestrator.knowledge,
        state_dir=settings.state_dir,
    )

    try:
        await engine.add_human_feedback(session_id, feedback)
        console.print(
            Panel(
                f"[bold green]Feedback Recorded[/bold green]\n\n"
                f"Session: {session_id}\n"
                f"Feedback: {feedback}\n\n"
                f"[dim]Agents will see this in their next round context.[/dim]",
                border_style="green",
            )
        )
    except Exception as e:
        console.print(f"[red]Error: {e}[/red]")
    finally:
        if orchestrator.task_queue:
            orchestrator.task_queue.close()


@presentation_app.command("analyze")
def presentation_analyze(
    session_id: str = typer.Argument(..., help="Session ID."),
    round_num: int = typer.Option(
        None, "--round", "-r", help="Round to analyze (default: latest)."
    ),
    verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
    """Analyze a round's responses — sentiment, consensus, concerns."""
    _setup_logging("DEBUG" if verbose else "WARNING")
    console.print(f"[cyan]Analyzing session {session_id}...[/cyan]")
    asyncio.run(_run_presentation_analyze(session_id, round_num))


async def _run_presentation_analyze(session_id: str, round_num: int | None) -> None:
    settings = get_settings()
    orchestrator = Orchestrator(settings)
    await orchestrator.initialize(session_only=True)

    from vastian.sessions.engine import SessionEngine

    engine = SessionEngine(
        registry=orchestrator.registry,
        bus=orchestrator.bus,
        knowledge=orchestrator.knowledge,
        state_dir=settings.state_dir,
    )

    try:
        analysis = await engine.analyze_round(
            session_id=session_id,
            round_number=round_num,
            orchestrator=orchestrator,
        )

        if "error" in analysis:
            console.print(f"[red]Analysis error: {analysis['error']}[/red]")
            return

        _display_round_analysis(analysis)
    except Exception as e:
        console.print(f"[red]Error: {e}[/red]")
    finally:
        if orchestrator.task_queue:
            orchestrator.task_queue.close()


def _display_round_analysis(analysis: dict) -> None:
    """Display the post-round analysis dashboard."""
    from rich.table import Table

    round_num = analysis.get("round_number", "?")
    consensus = analysis.get("consensus_level", "?")

    # Sentiment table
    table = Table(title=f"Round {round_num} Analysis", border_style="cyan")
    table.add_column("Agent", style="bold")
    table.add_column("Sentiment")
    table.add_column("Confidence")
    table.add_column("Key Concern", max_width=50)

    sentiment_colors = {
        "supportive": "green",
        "cautious": "yellow",
        "concerned": "red",
        "opposed": "bold red",
        "misaligned": "magenta",
    }

    per_agent = analysis.get("per_agent_sentiment", {})
    for agent_id, info in per_agent.items():
        sentiment = info.get("sentiment", "?")
        color = sentiment_colors.get(sentiment, "white")
        table.add_row(
            agent_id,
            f"[{color}]{sentiment}[/{color}]",
            f"{info.get('confidence', '?')}%",
            info.get("key_concern", ""),
        )

    console.print(table)

    # Consensus
    console.print(f"\n[bold]Overall Consensus: {consensus}%[/bold]")

    # Top concerns
    concerns = analysis.get("top_concerns", [])
    if concerns:
        console.print("\n[bold yellow]Top Concerns:[/bold yellow]")
        for c in concerns:
            sev = c.get("severity", "?")
            sev_color = {"high": "red", "medium": "yellow", "low": "dim"}.get(sev, "white")
            console.print(
                f"  [{sev_color}][{sev.upper()}][/{sev_color}] "
                f"[bold]{c.get('agent', '?')}[/bold]: {c.get('concern', '')}"
            )

    # Suggested rebuttals
    rebuttals = analysis.get("suggested_rebuttals", [])
    if rebuttals:
        console.print("\n[bold cyan]Suggested Rebuttals:[/bold cyan]")
        for r in rebuttals:
            console.print(
                f"  -> [bold]{r.get('agent', '?')}[/bold] ({r.get('topic', '')}): "
                f"{r.get('suggested_response', '')}"
            )

    human_count = len(concerns)
    console.print(f"\n[dim]Concerns requiring human input: {human_count}[/dim]")


@presentation_app.command("status")
def presentation_status(
    session_id: str = typer.Argument(None, help="Session ID. Omit to list all presentations."),
    verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
    """Show presentation status or list all presentations."""
    _setup_logging("DEBUG" if verbose else "WARNING")
    if session_id:
        asyncio.run(_run_session_status(session_id))
    else:
        asyncio.run(_run_list_sessions("presentation"))


# ══════════════════════════════════════════════════════════════════════
# Request command group — Network agents request FROM Cursor
# ══════════════════════════════════════════════════════════════════════

request_app = typer.Typer(
    name="request",
    help="Cursor Session Protocol — view and respond to network requests.",
)
app.add_typer(request_app, name="request")


@request_app.command("list")
def request_list(
    verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
    """List pending requests from the network."""
    _setup_logging("DEBUG" if verbose else "WARNING")
    asyncio.run(_run_list_requests())


async def _run_list_requests() -> None:
    settings = get_settings()
    from vastian.agents.registry import AgentRegistry
    from vastian.bus import MessageBus
    from vastian.sessions.engine import SessionEngine

    registry = AgentRegistry()
    bus = MessageBus()
    engine = SessionEngine(
        registry=registry,
        bus=bus,
        state_dir=settings.state_dir,
    )

    pending = engine.get_pending_requests()
    if not pending:
        console.print("[dim]No pending requests from the network.[/dim]")
        return

    table = Table(title="Pending Requests from the Network")
    table.add_column("ID", style="bold")
    table.add_column("Requested By", style="cyan")
    table.add_column("Topic")
    table.add_column("Rounds", justify="right")
    table.add_column("Created")

    for req in pending:
        table.add_row(
            req["id"],
            req["requested_by"],
            req["topic"],
            str(req["rounds"]),
            str(req["created_at"])[:16],
        )

    console.print(table)


@request_app.command("respond")
def request_respond(
    session_id: str = typer.Argument(..., help="Request session ID."),
    message: str = typer.Argument(..., help="Your response to the request."),
    no_notify: bool = typer.Option(False, "--no-notify", help="Don't notify session agents."),
    verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
    """Respond to a network request."""
    _setup_logging("DEBUG" if verbose else "WARNING")
    console.print(f"[cyan]Responding to request {session_id}...[/cyan]")
    asyncio.run(_run_request_respond(session_id, message, not no_notify))


async def _run_request_respond(session_id: str, message: str, notify: bool) -> None:
    settings = get_settings()
    orchestrator = Orchestrator(settings)
    await orchestrator.initialize(session_only=True)

    from vastian.sessions.engine import SessionEngine

    engine = SessionEngine(
        registry=orchestrator.registry,
        bus=orchestrator.bus,
        knowledge=orchestrator.knowledge,
        state_dir=settings.state_dir,
    )

    async def on_progress(event: str, agent_name: str, content: str) -> None:
        if event == "thinking":
            console.print(f"[dim]{content}[/dim]")
        elif event == "contribution":
            console.print(
                Panel(
                    Markdown(content[:2000]),
                    title=f"[bold]{agent_name}[/bold]",
                    border_style="green",
                )
            )

    engine.progress_callback = on_progress

    try:
        session = await engine.respond_to_request(
            session_id=session_id,
            message=message,
            notify_agents=notify,
            orchestrator=orchestrator,
        )
        console.print(f"\n[green]Response recorded — Round {session.current_round}.[/green]")
    except Exception as e:
        console.print(f"[red]Error: {e}[/red]")
    finally:
        if orchestrator.task_queue:
            orchestrator.task_queue.close()


@request_app.command("discuss")
def request_discuss(
    session_id: str = typer.Argument(..., help="Request session ID."),
    verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
    """Open the floor for agents to discuss a request."""
    _setup_logging("DEBUG" if verbose else "WARNING")
    console.print(f"[cyan]Opening discussion in request {session_id}...[/cyan]")
    asyncio.run(_run_session_discuss(session_id))


@request_app.command("wrap")
def request_wrap(
    session_id: str = typer.Argument(..., help="Request session ID."),
    verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
    """Resolve a request and generate a summary."""
    _setup_logging("DEBUG" if verbose else "WARNING")
    console.print(f"[cyan]Wrapping request {session_id}...[/cyan]")
    asyncio.run(_run_session_wrap(session_id))


@request_app.command("status")
def request_status(
    session_id: str = typer.Argument(None, help="Session ID. Omit to list all requests."),
    verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
    """Show request status or list all requests."""
    _setup_logging("DEBUG" if verbose else "WARNING")
    if session_id:
        asyncio.run(_run_session_status(session_id))
    else:
        asyncio.run(_run_list_sessions("request"))


# ══════════════════════════════════════════════════════════════════════
# Shared session helpers (used by both presentation and request)
# ══════════════════════════════════════════════════════════════════════


async def _run_session_discuss(session_id: str) -> None:
    settings = get_settings()
    orchestrator = Orchestrator(settings)
    await orchestrator.initialize(session_only=True)

    from vastian.sessions.engine import SessionEngine

    engine = SessionEngine(
        registry=orchestrator.registry,
        bus=orchestrator.bus,
        knowledge=orchestrator.knowledge,
        state_dir=settings.state_dir,
    )

    async def on_progress(event: str, agent_name: str, content: str) -> None:
        if event in ("discuss", "thinking"):
            console.print(f"[dim]{content}[/dim]")
        elif event == "contribution":
            console.print(
                Panel(
                    Markdown(content[:2000]),
                    title=f"[bold]{agent_name}[/bold]",
                    border_style="green",
                )
            )

    engine.progress_callback = on_progress

    try:
        session = await engine.discuss(
            session_id=session_id,
            orchestrator=orchestrator,
        )
        console.print(f"\n[green]Discussion round {session.current_round} complete.[/green]")
    except Exception as e:
        console.print(f"[red]Error: {e}[/red]")
    finally:
        if orchestrator.task_queue:
            orchestrator.task_queue.close()


async def _run_session_wrap(session_id: str) -> None:
    settings = get_settings()
    orchestrator = Orchestrator(settings)
    await orchestrator.initialize(session_only=True)

    from vastian.sessions.engine import SessionEngine

    engine = SessionEngine(
        registry=orchestrator.registry,
        bus=orchestrator.bus,
        knowledge=orchestrator.knowledge,
        state_dir=settings.state_dir,
    )

    async def on_progress(event: str, agent_name: str, content: str) -> None:
        if event == "wrap":
            console.print(f"[dim]{content}[/dim]")

    engine.progress_callback = on_progress

    try:
        session = await engine.wrap(
            session_id=session_id,
            orchestrator=orchestrator,
        )

        console.print(
            Panel(
                f"[bold green]Session Wrapped[/bold green]\n"
                f"ID: {session.id} | Type: {session.direction_label}\n"
                f"Rounds: {session.current_round} | Messages: {len(session.all_messages)}",
                border_style="green",
            )
        )

        if session.summary:
            console.print(Markdown(session.summary))

        if session.action_items:
            console.print("\n[bold yellow]Action Items:[/bold yellow]")
            for item in session.action_items:
                console.print(f"  - [{item.get('assigned_to', '?')}] {item.get('description', '')}")

    except Exception as e:
        console.print(f"[red]Error: {e}[/red]")
    finally:
        if orchestrator.task_queue:
            orchestrator.task_queue.close()


async def _run_session_status(session_id: str) -> None:
    settings = get_settings()
    from vastian.agents.registry import AgentRegistry
    from vastian.bus import MessageBus
    from vastian.sessions.engine import SessionEngine

    registry = AgentRegistry()
    bus = MessageBus()
    engine = SessionEngine(
        registry=registry,
        bus=bus,
        state_dir=settings.state_dir,
    )

    try:
        status = engine.get_status(session_id)
        table = Table(title=f"Session: {session_id}")
        for key, value in status.items():
            table.add_row(str(key), str(value))
        console.print(table)
    except Exception as e:
        console.print(f"[red]Error: {e}[/red]")


async def _run_list_sessions(direction: str) -> None:
    settings = get_settings()
    from vastian.agents.registry import AgentRegistry
    from vastian.bus import MessageBus
    from vastian.sessions.engine import SessionEngine

    registry = AgentRegistry()
    bus = MessageBus()
    engine = SessionEngine(
        registry=registry,
        bus=bus,
        state_dir=settings.state_dir,
    )

    sessions = engine.list_sessions(direction=direction)
    if not sessions:
        console.print(f"[dim]No {direction} sessions found.[/dim]")
        return

    label = "Presentations" if direction == "presentation" else "Requests"
    table = Table(title=label)
    table.add_column("ID", style="bold")
    table.add_column("Topic")
    table.add_column("Phase")
    table.add_column("Rounds", justify="right")
    table.add_column("Created")

    for s in sessions:
        table.add_row(
            s["id"],
            s["topic"],
            s["phase"],
            str(s["rounds"]),
            str(s["created_at"])[:16],
        )

    console.print(table)


if __name__ == "__main__":
    app()
