#!/usr/bin/env python3
"""CLI commands for nucleus-mcp."""

import os
import json
import sys
import shutil
import tempfile
from datetime import datetime, timezone
from pathlib import Path
import argparse
import subprocess
import time
from typing import Dict, Any, Optional, List

# Phase 24 Hardening: Universal Project Root
_PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent

# Self-healing engine (Intent-Level Error Recovery)
from .selfhealer import handle_cli_error
from .cli_output import EXIT_ERROR
from .runtime.common import get_brain_path

from .setup import install_nucleus_path

# ============================================================================
# TEMPLATE: DEFAULT (Full structure)
# ============================================================================
DEFAULT_STATE = {
    "version": "1.0.0",
    "current_sprint": {
        "name": "Sprint 1",
        "focus": "Getting Started with Nucleus",
        "started_at": None
    },
    "top_3_leverage_actions": [
        "Set up your first agent",
        "Configure triggers",
        "Connect to Claude Desktop"
    ]
}

DEFAULT_TRIGGERS = {
    "version": "1.0.0",
    "triggers": [
        {
            "event_type": "task_completed",
            "target_agent": "synthesizer",
            "emitter_filter": None
        },
        {
            "event_type": "research_done",
            "target_agent": "architect",
            "emitter_filter": ["researcher"]
        }
    ]
}

SAMPLE_AGENT = '''# {agent_name} Agent

## Role
Define what this agent does.

## Responsibilities
- Task 1
- Task 2

## Triggers
Activated when: [define trigger conditions]

## Output Format
Describe expected output format.
'''

# ============================================================================
# ONBOARDING: Instructional Seed Tasks
# ============================================================================

def get_welcome_engrams():
    """Generate welcome engrams so the brain has content on first connection."""
    now = datetime.now(timezone.utc).isoformat() + "Z"
    return [
        {
            "key": "welcome_to_nucleus",
            "value": "Welcome to your Sovereign Brain! This is Nucleus — your local-first AI memory server. Everything you store here stays on YOUR machine. Try asking your AI: 'What do you remember about me?' or 'Write an engram about my current project.'",
            "context": "Feature",
            "intensity": 8,
            "timestamp": now
        },
        {
            "key": "tip_cold_start",
            "value": "Pro tip: Nucleus has a 'cold_start' feature. When your AI connects, it automatically receives your latest context — active tasks, recent memories, and system status. This means your AI never starts from zero.",
            "context": "Strategy",
            "intensity": 6,
            "timestamp": now
        }
    ]

def get_default_tasks():
    """Generate instructional seed tasks with current timestamps."""
    now = datetime.now(timezone.utc).isoformat() + "Z"
    return [
        {
            "id": "onboard-1",
            "description": "Welcome! Ask your AI: 'What is my current focus?'",
            "status": "READY",
            "priority": 1,
            "blocked_by": [],
            "required_skills": [],
            "claimed_by": None,
            "source": "nucleus-init",
            "escalation_reason": None,
            "created_at": now,
            "updated_at": now
        },
        {
            "id": "onboard-2",
            "description": "Try: 'Show me all tasks' to see your task queue",
            "status": "PENDING",
            "priority": 2,
            "blocked_by": ["onboard-1"],
            "required_skills": [],
            "claimed_by": None,
            "source": "nucleus-init",
            "escalation_reason": None,
            "created_at": now,
            "updated_at": now
        },
        {
            "id": "onboard-3",
            "description": "Create your first real task: 'Add a task: [your task here]'",
            "status": "PENDING",
            "priority": 3,
            "blocked_by": ["onboard-2"],
            "required_skills": [],
            "claimed_by": None,
            "source": "nucleus-init",
            "escalation_reason": None,
            "created_at": now,
            "updated_at": now
        }
    ]

BRAIN_README = '''# 🧠 Your Nucleus Control Plane

This folder contains your AI assistant's persistent memory.
Generated by Nucleus v1.2.1

## Structure

| Folder | Purpose |
|--------|---------|
| `ledger/` | Event logs, task history |
| `sessions/` | Saved session states |
| `artifacts/` | Generated documents |
| `slots/` | Agent slot registry |
| `memory/` | Engram storage (Memory) |

## Quick Commands

- `nucleus_sessions(action="start")` — Start your session
- `nucleus_sessions(action="save", params={...})` — Save current context
- `nucleus_sessions(action="resume")` — Resume previous session
- `nucleus tasks(action="list")` — See pending tasks
- `nucleus siphon` — Siphon context from Windsurf/Antigravity

## CLI Commands

- `nucleus status` — Satellite view of your brain
- `nucleus morning-brief` — Daily health check
- `nucleus combo pulse` — Automated health pipeline

## Need Help?

- Docs: [https://github.com/eidetic-works/nucleus-mcp](https://github.com/eidetic-works/nucleus-mcp)
- Issues: [https://github.com/eidetic-works/nucleus-mcp/issues](https://github.com/eidetic-works/nucleus-mcp/issues)
'''

# ============================================================================
# TEMPLATE: SOLO (Minimal structure for single-user setup)
# ============================================================================
SOLO_STATE = {
    "version": "1.0.0",
    "mode": "solo",
    "current_focus": "Getting started with Nucleus",
    "tasks": []
}

SOLO_THREAD_REGISTRY = '''# Thread Registry

> **Protocol:** Agents check this file on activation to know their role.
> **Stable Anchor:** Thread ID (UUID) never changes, even when IDE renames the thread.

---

## Active Threads

| Thread ID | Role | Focus |
|:----------|:-----|:------|
| *(Add your threads here)* | | |

---

## How to Use

1. **Find your thread ID** in your IDE's artifact path (e.g., `.gemini/antigravity/brain/<thread-id>/`)
2. **Add a row** to the table above with your role
3. **Agent self-identifies** on next activation by reading this registry

---

## Example

| Thread ID | Role | Focus |
|:----------|:-----|:------|
| `7c654df4-b83e-...` | Lead Systems Architect | Infrastructure, MCP |
| `853a0b7e-9052-...` | Synthesizer | Product work, orchestration |
'''

SOLO_CONTEXT = '''# Project Context

> This file provides context to all AI agents working on your project.

## Company/Project
- **Name:** [Your Project Name]
- **Description:** [What you're building]

## Technical Stack
- **Backend:** 
- **Frontend:** 
- **Database:** 

## Current Priorities
1. 
2. 
3. 
'''


def init_brain_default(brain_path: Path) -> bool:
    """Initialize with default (full) template."""
    dirs = [
        brain_path / "ledger",
        brain_path / "sessions",
        brain_path / "slots",
        brain_path / "artifacts" / "research",
        brain_path / "artifacts" / "strategy",
        brain_path / "agents",
        brain_path / "memory",
        brain_path / "config",
    ]
    
    for d in dirs:
        d.mkdir(parents=True, exist_ok=True)
        print(f"  📁 Created {d}")
    
    (brain_path / "ledger" / "state.json").write_text(
        json.dumps(DEFAULT_STATE, indent=2)
    )
    print("  📄 Created ledger/state.json")
    
    (brain_path / "ledger" / "triggers.json").write_text(
        json.dumps(DEFAULT_TRIGGERS, indent=2)
    )
    print("  📄 Created ledger/triggers.json")
    
    (brain_path / "ledger" / "events.jsonl").write_text("")
    print("  📄 Created ledger/events.jsonl")
    
    # Seed instructional tasks
    (brain_path / "ledger" / "tasks.json").write_text(
        json.dumps(get_default_tasks(), indent=2)
    )
    print("  ✅ Created ledger/tasks.json (3 onboarding tasks)")
    
    # In-brain README
    (brain_path / "README.md").write_text(BRAIN_README)
    print("  📖 Created README.md")
    
    (brain_path / "agents" / "synthesizer.md").write_text(
        SAMPLE_AGENT.format(agent_name="Synthesizer")
    )
    print("  🤖 Created agents/synthesizer.md")
    
    (brain_path / "memory" / "context.md").write_text(
        "# Project Context\n\nDescribe your project here.\n"
    )
    print("  📝 Created memory/context.md")
    
    # Seed welcome engrams
    (brain_path / "memory" / "engrams.json").write_text(
        json.dumps(get_welcome_engrams(), indent=2)
    )
    print("  🧠 Created memory/engrams.json (2 welcome engrams)")
    
    # Seed default config
    _seed_default_config(brain_path)
    
    return True


def _seed_default_config(brain_path: Path):
    """Create default nucleus.yaml config with telemetry settings."""
    config_dir = brain_path / "config"
    config_dir.mkdir(parents=True, exist_ok=True)
    config_file = config_dir / "nucleus.yaml"
    if not config_file.exists():
        config_file.write_text(
            "# Nucleus configuration\n"
            "# Docs: https://github.com/eidetic-works/nucleus-mcp\n"
            "\n"
            "telemetry:\n"
            "  anonymous:\n"
            "    enabled: true  # opt out: nucleus config --no-telemetry\n"
            "    endpoint: \"https://telemetry.nucleusos.dev:4317\"\n",
            encoding="utf-8")
        print("  ⚙️  Created config/nucleus.yaml (anonymous telemetry: on)")


def init_brain_solo(brain_path: Path) -> bool:
    """Initialize with solo (minimal) template."""
    dirs = [
        brain_path / "ledger",
        brain_path / "sessions",
        brain_path / "slots",
        brain_path / "meta",
        brain_path / "memory",
        brain_path / "config",
    ]
    
    for d in dirs:
        d.mkdir(parents=True, exist_ok=True)
        print(f"  📁 Created {d}")
    
    (brain_path / "ledger" / "state.json").write_text(
        json.dumps(SOLO_STATE, indent=2)
    )
    print("  📄 Created ledger/state.json (solo mode)")
    
    (brain_path / "meta" / "thread_registry.md").write_text(SOLO_THREAD_REGISTRY)
    print("  📋 Created meta/thread_registry.md")
    
    (brain_path / "memory" / "context.md").write_text(SOLO_CONTEXT)
    print("  📝 Created memory/context.md")
    
    # Seed instructional tasks
    (brain_path / "ledger" / "tasks.json").write_text(
        json.dumps(get_default_tasks(), indent=2)
    )
    print("  ✅ Created ledger/tasks.json (3 onboarding tasks)")
    
    # In-brain README
    (brain_path / "README.md").write_text(BRAIN_README)
    print("  📖 Created README.md")
    
    # Seed welcome engrams
    (brain_path / "memory" / "engrams.json").write_text(
        json.dumps(get_welcome_engrams(), indent=2)
    )
    print("  🧠 Created memory/engrams.json (2 welcome engrams)")
    
    # Seed default config
    _seed_default_config(brain_path)
    
    return True



def _build_nucleus_mcp_config(brain_path: str) -> dict:
    """Single source of truth for generating MCP config."""
    from .runtime.common import get_nucleus_mcp_command
    cmd = get_nucleus_mcp_command()
    config = {"command": cmd[0]}
    if len(cmd) > 1:
        config["args"] = cmd[1:]
    config["env"] = {
        "NUCLEUS_BRAIN_PATH": brain_path,
        "NUCLEAR_BRAIN_PATH": brain_path,  # legacy compat
    }
    return config


def _get_ide_config_paths() -> list:
    """Return list of (Path, display_name) for all known MCP clients."""
    import platform
    home = Path.home()
    paths = []
    # Claude Desktop (platform-specific)
    if platform.system() == "Darwin":
        paths.append((home / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json", "Claude Desktop"))
    elif platform.system() == "Linux":
        paths.append((home / ".config" / "Claude" / "claude_desktop_config.json", "Claude Desktop"))
    else:
        paths.append((Path(os.environ.get("APPDATA", "")) / "Claude" / "claude_desktop_config.json", "Claude Desktop"))
    # Claude Code
    paths.append((home / ".claude" / ".mcp.json", "Claude Code"))
    # Cursor
    paths.append((home / ".cursor" / "mcp.json", "Cursor"))
    # Windsurf
    paths.append((home / ".codeium" / "windsurf" / "mcp_config.json", "Windsurf"))
    # Antigravity
    paths.append((home / ".gemini" / "antigravity" / "mcp_config.json", "Antigravity"))
    return paths


def _patch_mcp_config(config_path: Path, ide_name: str, nucleus_config: Dict[str, Any], force: bool = False):
    """Inject nucleus server into IDE-specific MCP configuration."""
    if not config_path.exists():
        return False
    try:
        print(f"\n🔍 Found {ide_name} config...")
        # Reversible: Create backup
        backup_path = config_path.with_suffix(".json.bak")
        if not backup_path.exists():
            shutil.copy2(config_path, backup_path)
            print(f"  📦 Created backup at {backup_path}")

        with open(config_path, 'r', encoding='utf-8') as f:
            config_data = json.load(f)

        # Normalize to standard 'mcpServers' block
        target_key = "mcpServers"
        if target_key not in config_data:
            config_data[target_key] = {}

        if "nucleus" not in config_data[target_key]:
            config_data[target_key]["nucleus"] = nucleus_config
            with open(config_path, 'w', encoding='utf-8') as f:
                json.dump(config_data, f, indent=2, ensure_ascii=False)
            print(f"  ✅ Auto-configured 'nucleus' in {ide_name} settings!")
            return True
        elif force:
            config_data[target_key]["nucleus"] = nucleus_config
            with open(config_path, 'w', encoding='utf-8') as f:
                json.dump(config_data, f, indent=2, ensure_ascii=False)
            print(f"  ✅ Overwrote 'nucleus' config in {ide_name}!")
            return True
        else:
            print(f"  ℹ️  'nucleus' already configured in {ide_name} (use --force to overwrite).")
            return True
    except Exception as e:
        print(f"  ⚠️  Could not auto-configure {ide_name}: {e}")
        return False

def init_brain(path: str = ".brain", template: str = "default") -> bool:
    """Initialize a new .brain directory structure."""
    brain_path = Path(path)
    
    if brain_path.exists():
        # Count files to detect active brain
        file_count = len(list(brain_path.rglob("*")))
        
        if file_count > 10:
            # This looks like an active brain - extra protection
            print(f"⚠️  {path}/ contains {file_count} files!")
            print("   This looks like an ACTIVE brain with real content.")
            print("   Overwriting is NOT recommended.")
            print()
            response = input("Type 'BACKUP-AND-OVERWRITE' to confirm (or anything else to abort): ")
            if response != 'BACKUP-AND-OVERWRITE':
                print("✅ Aborted. Your brain is safe.")
                return False
        else:
            print(f"⚠️  Directory {path} already exists.")
            response = input("Overwrite? (y/N): ")
            if response.lower() != 'y':
                print("Aborted.")
                return False
        
        # ALWAYS backup before overwrite
        timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
        backup_path = Path(f"{path}.backup.{timestamp}")
        print(f"📦 Creating backup at {backup_path}...")
        shutil.copytree(brain_path, backup_path)
        print(f"   ✅ Backup complete ({file_count} items saved)")
        
        # Now safe to remove old brain
        shutil.rmtree(brain_path)
    
    print(f"🧠 Initializing Nucleus Control Plane at {path}/ (template: {template})...")
    
    # Route to template-specific init
    if template == "solo":
        init_brain_solo(brain_path)
    else:
        init_brain_default(brain_path)
    
    print("\n✅ Nucleus Control Plane initialized!")
    
    # Generate config and auto-configure
    abs_path = str(brain_path.absolute())
    nucleus_config = _build_nucleus_mcp_config(abs_path)

    # Paths to known MCP configs
    ide_configs = _get_ide_config_paths()

    auto_configured_any = False
    for path_obj, name in ide_configs:
        if _patch_mcp_config(path_obj, name, nucleus_config):
            auto_configured_any = True

    if not auto_configured_any:
        # Build a complete, ready-to-paste JSON config block
        full_config = json.dumps({
            "mcpServers": {
                "nucleus": nucleus_config
            }
        }, indent=2)
        
        # Auto-copy to clipboard
        copied = False
        try:
            import subprocess
            import platform
            system = platform.system()
            if system == "Darwin":
                process = subprocess.Popen('pbcopy', env={'LANG': 'en_US.UTF-8'}, stdin=subprocess.PIPE)
                process.communicate(full_config.encode('utf-8'))
                copied = True
            elif system == "Linux":
                # Try xclip
                try:
                    process = subprocess.Popen(['xclip', '-selection', 'clipboard'], stdin=subprocess.PIPE)
                    process.communicate(full_config.encode('utf-8'))
                    copied = True
                except FileNotFoundError:
                    # Try xsel
                    try:
                        process = subprocess.Popen(['xsel', '--clipboard', '--input'], stdin=subprocess.PIPE)
                        process.communicate(full_config.encode('utf-8'))
                        copied = True
                    except FileNotFoundError:
                        pass
            elif system == "Windows":
                 import shutil as _shutil
                 clip_path = _shutil.which('clip')
                 if clip_path:
                     process = subprocess.Popen([clip_path], stdin=subprocess.PIPE)
                     process.communicate(full_config.encode('utf-8'))
                     copied = True
        except Exception:
            pass # Fail silently
            
        print("\n" + "=" * 60)
        if copied:
            print("📋 COPIED TO CLIPBOARD! Add this to your MCP config:")
        else:
            print("📋 ADD THIS to your AI client's MCP config:")
        print("=" * 60)
        print()
        print(full_config)
        print()
        print("=" * 60)
        
        # OS-specific config file locations
        print("\n📍 Config file locations:")
        for ide_path, ide_name in _get_ide_config_paths():
            print(f"   {ide_name}: {ide_path}")
    else:
        print("\n✅ Auto-configured your MCP client(s). Config used:")
        print(f'   Brain path: {abs_path}')
        print(f'   Command:    {nucleus_config["command"]}')
    
    # ── Progressive Disclosure: Persona-Aware Next Steps ──────
    print("\n" + "=" * 60)
    print("🚀 WHAT TO DO NEXT")
    print("=" * 60)

    # Step 1: Always needed
    print("\n① Restart your AI client (Claude Desktop, Cursor, Windsurf, etc.)")

    # Step 2: Quick win — immediate value
    print("\n② Try your first command (pick one):")
    print("   nucleus status          → See your brain health")
    print("   nucleus doctor          → Diagnose your setup")
    print("   nucleus                 → Start an AI chat session")

    # Step 3: Persona-specific guidance
    if template == "default":
        # Check if recipe was installed to tailor guidance
        recipe_hint = ""
        try:
            installed = list((brain_path / "recipes").glob("*.json"))
            if installed:
                recipe_hint = f" ({installed[0].stem} recipe active)"
        except Exception:
            pass
        print(f"\n③ Explore your brain{recipe_hint}:")
        print("   nucleus recipe list     → Browse workflow packs")
        print("   nucleus morning-brief   → Get your daily briefing")
    else:
        print("\n③ Build your memory:")
        print("   Ask your AI: \"Write an engram about my current project\"")
        print("   Ask your AI: \"Show me all tasks\"")

    # Step 4: Growth path (progressive disclosure)
    print("\n④ Level up (when you're ready):")
    print("   nucleus recipe install founder  → Install Founder OS workflow")
    print("   nucleus emergence collect       → Contribute usage patterns")

    # Cost awareness from day 1
    print("\n💰 Token budgets are active. View costs: nucleus cost-dashboard")
    print("📚 Docs: https://github.com/eidetic-works/nucleus-mcp")
    
    return True


# ============================================================
# CHAT: Interactive Gemini Conversation (inline, no external deps)
# Slash commands are a SUPERSET of Claude Code + Gemini CLI.
# ============================================================

def _run_chat(tier_name: str = "local_free", model_override: str = None, system_prompt: str = None, batch: bool = False, prompt: str = None, provider: str = None, output_format: str = "text", brother_context: str = None):
    """Interactive multi-turn chat. Works from any install location.

    Args:
        tier_name: LLM tier to use
        model_override: Specific model to use
        system_prompt: Custom system prompt
        batch: Non-interactive batch mode (single turn, then exit)
        prompt: Initial prompt for batch mode
        provider: LLM provider ('gemini' or 'anthropic'). Falls back to
                  NUCLEUS_LLM_PROVIDER env, then 'gemini'.
    """
    from dotenv import load_dotenv
    load_dotenv()  # Ensure GEMINI_API_KEYS are fresh from .env

    import warnings
    warnings.filterwarnings("ignore", message=".*non-text parts.*")
    warnings.filterwarnings("ignore", message=".*thought_signature.*")

    # ── Suppress ALL SDK noise during chat ─────────────────────
    import logging
    logging.getLogger("google.genai").setLevel(logging.ERROR)
    logging.getLogger("google").setLevel(logging.ERROR)
    logging.getLogger("nucleus.init").setLevel(logging.ERROR)
    logging.getLogger("nucleus.llm").setLevel(logging.ERROR)
    logging.getLogger("nucleus").setLevel(logging.ERROR)
    logging.getLogger("urllib3").setLevel(logging.ERROR)
    # Suppress any remaining stderr noise from SDK
    warnings.filterwarnings("ignore", category=UserWarning)
    warnings.filterwarnings("ignore", category=DeprecationWarning)

    # ── .env Auto-Discovery ─────────────────────────────────────
    if not os.environ.get("GEMINI_API_KEY"):
        for candidate in [
            Path.cwd() / ".env",
            Path.cwd().parent / ".env",
        ]:
            if candidate.exists():
                try:
                    for line in candidate.read_text().splitlines():
                        line = line.strip()
                        if line.startswith("GEMINI_API_KEY=") and not line.startswith("#"):
                            val = line.split("=", 1)[1].strip().strip('"').strip("'")
                            os.environ["GEMINI_API_KEY"] = val
                            break
                except Exception:
                    pass
                if os.environ.get("GEMINI_API_KEY"):
                    break
    
    from .runtime.llm_client import DualEngineLLM, LLMTier, TierRouter, get_llm_client

    TIER_MAP = {
        "premium": LLMTier.PREMIUM,
        "standard": LLMTier.STANDARD,
        "economy": LLMTier.ECONOMY,
        "local_paid": LLMTier.LOCAL_PAID,
        "local_free": LLMTier.LOCAL_FREE,
    }

    # Available models (from TierRouter.FREE_TIER_CASCADE + tier models)
    KNOWN_MODELS = list({
        cfg["model"] for cfg in TierRouter.TIER_CONFIGS.values()
    }) + getattr(TierRouter, "FREE_TIER_CASCADE", [])

    import getpass
    import platform
    sys_info = f"{getpass.getuser()}@{platform.node()} (OS: {platform.system()})"
    cwd_info = str(Path.cwd())

    # ── Workspace Awareness (auto-detect project shape) ───────
    _workspace_info = ""
    try:
        _cwd = Path.cwd()
        _ws_parts = []

        # Git info
        import subprocess as _ws_sp
        _git_res = _ws_sp.run(["git", "rev-parse", "--is-inside-work-tree"], capture_output=True, text=True, timeout=3, cwd=str(_cwd))
        if _git_res.returncode == 0:
            _branch = _ws_sp.run(["git", "branch", "--show-current"], capture_output=True, text=True, timeout=3, cwd=str(_cwd)).stdout.strip()
            _log = _ws_sp.run(["git", "log", "--oneline", "-3"], capture_output=True, text=True, timeout=3, cwd=str(_cwd)).stdout.strip()
            _ws_parts.append(f"Git: branch={_branch}")
            if _log:
                _ws_parts.append(f"Recent commits:\n{_log}")

        # Project type detection
        _config_files = {
            "pyproject.toml": "Python (pyproject.toml)",
            "package.json": "Node.js (package.json)",
            "Cargo.toml": "Rust (Cargo.toml)",
            "go.mod": "Go (go.mod)",
            "pom.xml": "Java (pom.xml)",
            "Gemfile": "Ruby (Gemfile)",
        }
        for _cf, _label in _config_files.items():
            if (_cwd / _cf).exists():
                _ws_parts.append(f"Project: {_label}")
                break

        # Key dirs
        _key_dirs = [d.name for d in _cwd.iterdir() if d.is_dir() and not d.name.startswith(".")][:8]
        if _key_dirs:
            _ws_parts.append(f"Directories: {', '.join(sorted(_key_dirs))}")

        if _ws_parts:
            _workspace_info = "\n".join(_ws_parts)
    except Exception:
        pass

    # ── Sovereign Brain Discovery ──────────────────────────────
    brain_context = ""
    brain_dir = None
    project_root = None
    try:
        curr = Path.cwd()
        for _ in range(4):
            if (curr / ".brain").exists():
                brain_dir = curr / ".brain"
                project_root = curr
                break
            curr = curr.parent

        # ── Auto-Init: Claude Code Model (Zero-Conf) ──────────────
        # If no .brain found, silently create one. Just like claude/gemini
        # auto-create their config directories on first run.
        if not brain_dir:
            auto_target = Path.cwd()
            # Prefer project root (has .git) over arbitrary CWD
            if (auto_target / ".git").exists():
                brain_path = auto_target / ".brain"
            elif Path.home() != auto_target:
                brain_path = auto_target / ".brain"
            else:
                brain_path = Path.home() / ".nucleus" / "brain"
            try:
                init_brain_solo(brain_path)
                brain_dir = brain_path
                project_root = brain_path.parent
                print("🧠 Nucleus memory initialized. I'll remember our conversations.")
            except Exception as init_err:
                import logging
                logging.getLogger("nucleus").debug(f"Auto-init skipped: {init_err}")

        if brain_dir and project_root:
            session_file = brain_dir / "session_context.md"
            gemini_file = project_root / "gemini.md"
            if not gemini_file.exists():
                gemini_file = brain_dir / "gemini.md"
            
            parts = []
            # Commandments: operator's encoded intent — read first, always
            _commandments_file = brain_dir / "commandments.md"
            if _commandments_file.exists():
                parts.append(f"### COMMANDMENTS (hard rules)\n{_commandments_file.read_text()[:3000]}")
            if session_file.exists():
                parts.append(f"### CURRENT SESSION CONTEXT ({session_file.name})\n{session_file.read_text()[:4000]}")
            if gemini_file.exists():
                parts.append(f"### KNOWLEDGE BASE ({gemini_file.name})\n{gemini_file.read_text()[:4000]}")

            # Breadcrumb: notes left by brother or heartbeat
            _breadcrumb_file = brain_dir / "heartbeat" / "breadcrumb.json"
            if _breadcrumb_file.exists():
                try:
                    _bc = json.loads(_breadcrumb_file.read_text())
                    _bc_age_s = (datetime.now(timezone.utc) - datetime.fromisoformat(_bc["ts"])).total_seconds()
                    if _bc_age_s < 7200:  # Only show if < 2h old
                        _bc_lines = [f"### RECENT ACTIVITY (breadcrumb from heartbeat, {int(_bc_age_s/60)}m ago)"]
                        _bc_lines.append(f"Summary: {_bc.get('summary', '?')}")
                        if _bc.get("files_changed"):
                            _bc_lines.append(f"Files: {', '.join(_bc['files_changed'][:5])}")
                        if _bc.get("commits"):
                            _bc_lines.append(f"Commits: {'; '.join(_bc['commits'][:3])}")
                        parts.append("\n".join(_bc_lines))
                except Exception:
                    pass

            # Pending escalations — decisions awaiting operator input
            _esc_dir = brain_dir / "escalations"
            if _esc_dir.exists():
                try:
                    _pending = []
                    for _ef in sorted(_esc_dir.glob("*.json")):
                        _ed = json.loads(_ef.read_text())
                        if _ed.get("status") == "pending":
                            _pending.append(f"- [{_ed.get('id')}] {_ed.get('question', '?')} (options: {', '.join(_ed.get('options', []))})")
                    if _pending:
                        parts.append("### PENDING ESCALATIONS (awaiting operator decision)\n" + "\n".join(_pending[:5]))
                except Exception:
                    pass

            if parts:
                brain_context = "\n\n" + "\n\n".join(parts) + "\n\n"

            # ── Startup Engram Injection: warm-start from brain ──
            try:
                from mcp_server_nucleus.runtime.engram_ops import _brain_search_engrams_impl
                import json as _ej
                _search_terms = [Path.cwd().name]
                # Add project type hint
                for _cf in ["pyproject.toml", "package.json", "Cargo.toml", "go.mod"]:
                    if (Path.cwd() / _cf).exists():
                        _search_terms.append(Path(_cf).stem)
                        break
                _startup_engrams = []
                _seen_keys = set()
                for term in _search_terms:
                    if len(term) < 3:
                        continue
                    raw = _brain_search_engrams_impl(term, limit=5)
                    data = _ej.loads(raw) if isinstance(raw, str) else raw
                    for e in data.get("data", {}).get("engrams", []):
                        k = e.get("key", "")
                        if k not in _seen_keys:
                            _seen_keys.add(k)
                            _startup_engrams.append(e)
                # Also load recent session/compact engrams
                for prefix in ["session_", "compact_"]:
                    raw = _brain_search_engrams_impl(prefix, limit=3)
                    data = _ej.loads(raw) if isinstance(raw, str) else raw
                    for e in data.get("data", {}).get("engrams", []):
                        k = e.get("key", "")
                        if k not in _seen_keys:
                            _seen_keys.add(k)
                            _startup_engrams.append(e)
                if _startup_engrams:
                    # Sort by intensity, take top 8
                    _startup_engrams.sort(key=lambda x: x.get("intensity", 5), reverse=True)
                    _top = _startup_engrams[:8]
                    _engram_lines = ["### BRAIN MEMORY (from past sessions)"]
                    for e in _top:
                        _engram_lines.append(f"- [{e.get('context', '?')}|{e.get('intensity', 5)}] {e.get('key', '?')}: {str(e.get('value', ''))[:200]}")
                    brain_context += "\n".join(_engram_lines) + "\n\n"
            except Exception:
                pass
    except Exception:
        pass

    _base_system = (
        "You are Nucleus, an intelligent AI assistant accessed directly via the user's terminal. "
        "You are currently running on the user's local machine.\n"
        f"System: {sys_info}\n"
        f"Current Working Directory: {cwd_info}\n"
        + (f"Workspace:\n{_workspace_info}\n\n" if _workspace_info else "\n")
        + "Guidelines:\n"
        "1. Be concise, direct, and helpful.\n"
        "2. If a user asks what folder they are in or asks about their environment, answer using the info provided above.\n"
    )

    _cli_commands = (
        "   Nucleus CLI commands (note: singular 'task', 'engram', 'session', 'feature'):\n"
        "     nucleus status           — system health\n"
        "     nucleus task list        — list tasks\n"
        "     nucleus task add \"desc\" — add task\n"
        "     nucleus engram list      — list memory\n"
        "     nucleus engram search Q  — search memory\n"
        "     nucleus session list     — list sessions\n"
        "     nucleus feature list     — list features\n"
        "     nucleus combo pulse      — run pulse combo\n"
        "     nucleus sovereign        — sovereign status\n"
        "     nucleus archive stats    — training data flywheel\n"
    )

    _tail_system = (
        "4. NEVER hallucinate file contents or codebase state. Always run commands first to verify!\n"
        "5. You have access to the Sovereign Brain (.brain folder) for persistent memory across sessions.\n"
        "6. You are already in an active session. Do not run redundant initialization (ls, status) unless the user asks.\n"
        "7. MULTI-STEP AUTONOMY: You can chain many tool calls to complete complex tasks.\n"
        "   Read files, understand code, make edits, verify — all in one conversation turn.\n"
        "   Keep going until the task is fully done. The user will see each step as you work.\n"
        f"{brain_context}"
    )

    # Gemini/Groq: use <execute> tags for tool execution
    _execute_tag_instructions = (
        "3. TOOL EXECUTION: You have these capabilities:\n"
        "   a) Shell commands — wrap in <execute>...</execute> tags:\n"
        "     <execute>ls -la</execute>           (correct)\n"
        "     <execute>cat README.md</execute>    (correct)\n"
        "     <execute>bash ls -la</execute>      (WRONG — never prefix with 'bash')\n"
        "   b) File operations — use JSON in <execute_tool> tags:\n"
        "     <execute_tool>{\"tool\": \"read_file\", \"path\": \"src/main.py\"}</execute_tool>\n"
        "     <execute_tool>{\"tool\": \"write_file\", \"path\": \"out.txt\", \"content\": \"hello\"}</execute_tool>\n"
        "     <execute_tool>{\"tool\": \"edit_file\", \"path\": \"f.py\", \"old_string\": \"foo\", \"new_string\": \"bar\"}</execute_tool>\n"
        "     <execute_tool>{\"tool\": \"search_files\", \"pattern\": \"**/*.py\"}</execute_tool>\n"
        "     <execute_tool>{\"tool\": \"search_code\", \"pattern\": \"def main\", \"path\": \"src/\"}</execute_tool>\n"
        "   c) Brain memory — persistent knowledge across sessions:\n"
        "     <execute_tool>{\"tool\": \"write_engram\", \"key\": \"my_key\", \"value\": \"learned this\", \"context\": \"Architecture\"}</execute_tool>\n"
        "     <execute_tool>{\"tool\": \"search_engrams\", \"query\": \"database\"}</execute_tool>\n"
        "   d) Task management — self-organizing backlog:\n"
        "     <execute_tool>{\"tool\": \"list_tasks\"}</execute_tool>\n"
        "     <execute_tool>{\"tool\": \"add_task\", \"description\": \"Implement auth\", \"priority\": 2}</execute_tool>\n"
        "     <execute_tool>{\"tool\": \"update_task\", \"task_id\": \"task-abc\", \"status\": \"DONE\"}</execute_tool>\n"
        "   Prefer read_file over `cat`, edit_file over `sed`, search_code over `grep`.\n"
        "   Use write_engram when you learn something important. Use search_engrams before assuming.\n"
        f"{_cli_commands}"
        "   Only emit ONE tool block per response. Wait for the result before issuing the next.\n"
    )

    # Anthropic/Groq 70b+: native tool calling
    _native_tool_instructions = (
        "3. TOOL EXECUTION: You have 11 tools available:\n"
        "   FILE TOOLS:\n"
        "   - shell_execute: Run shell commands (ls, git, nucleus, etc.)\n"
        "   - read_file: Read file contents with line numbers (prefer over `cat`)\n"
        "   - write_file: Create or overwrite files\n"
        "   - edit_file: Surgical find-and-replace in files (prefer over `sed`)\n"
        "   - search_files: Find files by glob pattern (prefer over `find`)\n"
        "   - search_code: Search file contents by regex (prefer over `grep`)\n"
        "   BRAIN TOOLS (persistent memory — survives across sessions):\n"
        "   - write_engram: Save important knowledge (code patterns, decisions, preferences)\n"
        "   - search_engrams: Recall knowledge from past sessions\n"
        "   TASK TOOLS (self-organizing backlog):\n"
        "   - list_tasks: See what needs doing, check status\n"
        "   - add_task: Create tasks for follow-up work\n"
        "   - update_task: Mark tasks done, change priority\n"
        "   Use ONE tool call per response. Wait for the result before calling again.\n"
        "   Prefer file tools over shell equivalents — they're faster and show better output.\n"
        "   Use write_engram when you learn something important. Use search_engrams before assuming.\n"
        "   Use add_task to track follow-ups. Use update_task to mark work done.\n"
        f"{_cli_commands}"
    )

    default_system = _base_system + _execute_tag_instructions + _tail_system
    native_tool_system = _base_system + _native_tool_instructions + _tail_system

    # Claude Code provider: lighter prompt that complements (not conflicts with) CC's own system prompt
    _anthropic_system = (
        "You are running inside Nucleus Brother, powered by Claude Code with a Max subscription.\n"
        "You have your standard Claude Code tools (Read, Edit, Bash, Glob, Grep, etc.) "
        "PLUS Nucleus MCP tools (nucleus_engrams, nucleus_tasks, nucleus_sessions, etc.).\n\n"
        "IMPORTANT CONTEXT:\n"
        "- The USER is typing in Nucleus Chat, a terminal-based AI coding assistant.\n"
        "- The user's available slash commands are: /help, /model, /provider, /status, /cost, "
        "/diff, /files, /undo, /clear, /cc-session, /retry, /compact. These are NOT your tools.\n"
        "- Do NOT list Claude Code skills (/browse, /ship, /review, /qa etc.) as user commands — "
        "those are internal to your process and the user cannot type them.\n"
        "- You have access to the Sovereign Brain (.brain folder) for persistent memory.\n"
        "- Use nucleus_engrams MCP tool for brain memory (search/write engrams).\n"
        "- Use nucleus_tasks MCP tool for task management.\n"
        "- Prefer MCP tools over shell commands for brain operations — they're structured and faster.\n"
        "- Be direct and concise. The user is a technical founder who prefers surgical answers.\n"
        f"{brain_context}"
    )

    sys_prompt = system_prompt or default_system
    _native_tool_sys_prompt = system_prompt or native_tool_system
    _anthropic_sys_prompt = system_prompt or _anthropic_system
    tier = TIER_MAP.get(tier_name, LLMTier.LOCAL_FREE)

    # ── Proxy Sovereignty Auto-Discovery ───────────────────────
    if "GEMINI_API_BASE_URL" not in os.environ:
        if Path(tempfile.gettempdir()) / "gemini_proxy.port".exists():
            try:
                port = Path(tempfile.gettempdir()) / "gemini_proxy.port".read_text().strip()
                os.environ["GEMINI_API_BASE_URL"] = f"http://localhost:{port}/v1"
            except: pass
        elif os.environ.get("NUCLEUS_PROXY_DEFAULT_URL"):
            os.environ["GEMINI_API_BASE_URL"] = os.environ["NUCLEUS_PROXY_DEFAULT_URL"]

    # ── Resolve provider ────────────────────────────────────────
    # CLI flag > env var > default "gemini"
    _provider = (
        provider
        or os.environ.get("NUCLEUS_LLM_PROVIDER", "gemini")
    ).strip().lower()

    # ── Model Capability Detection ─────────────────────────────
    # Models known to reliably support native tool/function calling.
    # Small models (8b) fail ~50% of the time — better to use <execute> tags.
    _TOOL_CAPABLE_MODELS = {
        # Anthropic — all models support tool_use
        "claude-sonnet-4-6-20250514", "claude-opus-4-6-20250514", "claude-haiku-4-5-20251001",
        # Groq — only 70b+ models handle function calling reliably
        "llama-3.3-70b-versatile",
        "meta-llama/llama-4-scout-17b-16e-instruct",
        "qwen/qwen3-32b",
    }

    def _model_supports_tools(model_name: str) -> bool:
        """Check if the model reliably supports native tool calling."""
        if _provider in ("anthropic"):
            return True  # All Claude models support tool_use
        return model_name in _TOOL_CAPABLE_MODELS

    # ── Initialize LLM ─────────────────────────────────────────
    def _init_llm(t, model=None, sys_instruction=None):
        kwargs = {"model_name": model} if model else {}
        if sys_instruction:
            if _provider in () and sys_instruction == sys_prompt:
                # Claude Code has its own system prompt — we append a lighter one
                kwargs["system_instruction"] = _anthropic_sys_prompt
            elif _provider in ("anthropic", "groq") and sys_instruction == sys_prompt:
                if _model_supports_tools(model or ""):
                    kwargs["system_instruction"] = _native_tool_sys_prompt
                else:
                    kwargs["system_instruction"] = sys_prompt
            else:
                kwargs["system_instruction"] = sys_instruction
        if _provider == "gemini":
            return DualEngineLLM(tier=t, **kwargs)
        # Non-Gemini providers go through the factory (Anthropic, Groq, Claude Code)
        return get_llm_client(provider=_provider, **kwargs)

    # Pre-flight check for local provider
    if _provider == "local":
        try:
            from .runtime.llm_client import LocalLLM
            _test_llm = LocalLLM()
            _test_llm.generate_content("ping", max_tokens=5)
            print(f"  ✅ Local model reachable ({_test_llm.endpoint})")
        except Exception as _local_err:
            print(f"❌ Local model not reachable at {_endpoint}")
            print(f"   Error: {_local_err}")
            print(f"\n   To set up the local model:")
            print(f"   1. Install Ollama: https://ollama.ai")
            print(f"   2. Train:  nucleus archive train --target local")
            print(f"   3. Create: ollama create nucleus-brother -f scripts/Modelfile")
            print(f"   4. Run:    nucleus brother --provider local")
            print(f"\n   Or use an existing model:")
            print(f"   ollama pull qwen2.5:7b")
            print(f"   NUCLEUS_LOCAL_MODEL=qwen2.5:7b nucleus brother --provider local")
            return

    try:
        llm = _init_llm(tier, model_override, sys_instruction=sys_prompt)
    except Exception as e:
        print(f"❌ Failed to initialize LLM: {e}")
        err_str = str(e)
        if "GEMINI_API_KEY" in err_str:
            print("   💡 Fix: /auth <your-api-key>  or  export GEMINI_API_KEY=...")
            print("   Get a key at: https://aistudio.google.com/apikey\n")
        elif "ANTHROPIC" in err_str.upper():
            print("   💡 Fix: export NUCLEUS_ANTHROPIC_API_KEY=sk-ant-...")
        elif "GROQ" in err_str.upper():
            print("   💡 Fix: export NUCLEUS_GROQ_API_KEY=gsk_...")
            print("   Get a free key at: https://console.groq.com\n")
        return

    # ── Auto-Bridge: Brothers discover each other without human coordination ──
    if _provider in () and brain_dir:
        _bridge_path = brain_dir / "sessions" / "cc_bridge.json"
        if _bridge_path.exists() and not getattr(llm, "_session_id", None):
            try:
                _bridge = json.loads(_bridge_path.read_text())
                _bridge_ts = _bridge.get("ts", "")
                # Auto-resume if bridge is fresh (< 24h)
                if _bridge_ts:
                    from datetime import datetime as _dt, timezone as _tz
                    _bridge_age = (_dt.now(_tz.utc) - _dt.fromisoformat(_bridge_ts)).total_seconds()
                    if _bridge_age < 86400:  # 24 hours
                        llm._session_id = _bridge.get("anthropic_session")
                        _bridge_src = _bridge.get("source", "brother")
                        logger.info(f"Auto-bridged to {_bridge_src} session: {llm._session_id}")
            except Exception as _e:
                logger.debug(f"Auto-bridge skipped: {_e}")

    # ── Stdin Piping Support (CLI Parity) ──────────────────────
    # Check for stdin input (enables: echo "prompt" | nucleus chat)
    stdin_prompt = None
    if not sys.stdin.isatty():
        try:
            stdin_content = sys.stdin.read().strip()
            if stdin_content:
                stdin_prompt = stdin_content
        except Exception:
            pass
    
    # Secret patterns — same as pre-commit hook for consistency
    import re as _re
    _SECRET_PATTERNS = [
        _re.compile(r'AIzaSy[A-Za-z0-9_-]{33}'),
        _re.compile(r'pplx-[A-Za-z0-9]{40,}'),
        _re.compile(r'sk_[a-f0-9]{40,}'),
        _re.compile(r'whsec_[A-Za-z0-9]+'),
        _re.compile(r'AKIA[0-9A-Z]{16}'),
        _re.compile(r'ghp_[A-Za-z0-9]{36}'),
        _re.compile(r'gho_[A-Za-z0-9]{36}'),
        _re.compile(r'glpat-[A-Za-z0-9_-]{20}'),
        _re.compile(r'[0-9]{10}:AA[A-Za-z0-9_-]{33}'),
    ]

    def _scrub_secrets(text: str) -> str:
        """Replace secret patterns with [REDACTED] before archiving."""
        for pat in _SECRET_PATTERNS:
            text = pat.sub("[REDACTED]", text)
        return text


    # ── Batch Mode (CLI Parity) ────────────────────────────────
    # Non-interactive mode: single turn, then exit
    if batch or stdin_prompt:
        batch_prompt = prompt or stdin_prompt
        if not batch_prompt:
            print("❌ Error: --batch requires --prompt or stdin input", file=sys.stderr)
            sys.exit(1)

        # ── Circuit Breaker (Commandment 10) ──────────────────────
        # If 3+ consecutive batch failures, refuse to run and escalate.
        _cb_file = brain_dir / "heartbeat" / "circuit_breaker.json" if brain_dir else None
        if _cb_file and _cb_file.exists():
            try:
                _cb = json.loads(_cb_file.read_text())
                _consecutive_fails = _cb.get("consecutive_failures", 0)
                if _consecutive_fails >= 3:
                    _cb_msg = f"🛑 CIRCUIT BREAKER OPEN: {_consecutive_fails} consecutive batch failures. Self-repair halted per Commandment 10. Father must /resolve or reset."
                    if output_format == "json":
                        print(json.dumps({"ok": False, "error": _cb_msg, "circuit_breaker": True}))
                    else:
                        print(_cb_msg, file=sys.stderr)
                    # Auto-escalate
                    try:
                        from mcp_server_nucleus.runtime.heartbeat_ops import escalate_to_operator
                        escalate_to_operator(
                            question=f"Circuit breaker tripped: {_consecutive_fails} consecutive failures. Review and reset.",
                            context=f"Last error: {_cb.get('last_error', 'unknown')}",
                            options=["reset and retry", "investigate manually", "pause autonomous work"],
                            source="circuit-breaker")
                    except Exception:
                        pass
                    sys.exit(1)
            except Exception:
                pass

        # Brother context: big brother guiding small brother
        if brother_context:
            batch_prompt = f"[CONTEXT FROM BIG BROTHER]\n{brother_context}\n[END CONTEXT]\n\n{batch_prompt}"

        try:
            response = None
            success = False
            last_err = ""
            _batch_start = time.time() if 'time' in dir() else __import__('time').time()

            if _provider == "gemini":
                # --- Gemini: Sweeper Key Extraction + Model Cascade ---
                api_keys = []
                combo_keys = os.environ.get("GEMINI_API_KEYS", "")
                if combo_keys:
                    api_keys = [k.strip() for k in combo_keys.split(",") if k.strip()]
                single_key = os.environ.get("GEMINI_API_KEY", "")
                if single_key and single_key not in api_keys:
                    api_keys.insert(0, single_key)
                if not api_keys:
                    api_keys = [""]

                models_to_try = [llm.model_name]
                if getattr(TierRouter, "SOVEREIGN_CASCADE", None) and llm.model_name in TierRouter.SOVEREIGN_CASCADE:
                    idx = TierRouter.SOVEREIGN_CASCADE.index(llm.model_name)
                    models_to_try = TierRouter.SOVEREIGN_CASCADE[idx:]

                for try_model in models_to_try:
                    if success: break
                    for key in api_keys:
                        if key: os.environ["GEMINI_API_KEY"] = key
                        try:
                            temp_llm = _init_llm(tier, try_model, sys_instruction=sys_prompt)
                            response = temp_llm.generate(batch_prompt)
                            success = True
                            break
                        except Exception as e:
                            last_err = str(e)
                            if "429" in last_err or "503" in last_err or "404" in last_err or "quota" in last_err.lower():
                                continue
                            else:
                                break
            else:
                # --- Non-Gemini (Anthropic, etc.): single attempt, no key sweeping ---
                try:
                    response = llm.generate_content(batch_prompt).text
                    success = True
                except Exception as e:
                    last_err = str(e)

            if not success:
                # Circuit breaker: track consecutive failures
                if _cb_file:
                    try:
                        _cb = json.loads(_cb_file.read_text()) if _cb_file.exists() else {}
                        _cb["consecutive_failures"] = _cb.get("consecutive_failures", 0) + 1
                        _cb["last_error"] = last_err[:200]
                        _cb["last_failure_ts"] = datetime.now(timezone.utc).isoformat()
                        _cb_file.parent.mkdir(parents=True, exist_ok=True)
                        _cb_file.write_text(json.dumps(_cb, indent=2))
                    except Exception:
                        pass
                if output_format == "json":
                    import json as _bj
                    print(_bj.dumps({"ok": False, "error": last_err, "provider": _provider, "model": getattr(llm, 'model_name', '')}))
                else:
                    print(f"❌ Batch failed. Last error: {last_err}", file=sys.stderr)
                sys.exit(1)

            # Circuit breaker: reset on success
            if _cb_file and _cb_file.exists():
                try:
                    _cb_file.write_text(json.dumps({"consecutive_failures": 0, "last_success_ts": datetime.now(timezone.utc).isoformat()}, indent=2))
                except Exception:
                    pass

            _response_text = response.text if hasattr(response, 'text') else str(response)
            _batch_elapsed = __import__('time').time() - _batch_start

            if output_format == "json":
                import json as _bj
                print(_bj.dumps({
                    "ok": True,
                    "response": _response_text,
                    "provider": _provider,
                    "model": getattr(llm, 'model_name', ''),
                    "brain_loaded": bool(brain_context),
                    "commandments_loaded": bool(brain_dir and (brain_dir / "commandments.md").exists()),
                    "elapsed_s": round(_batch_elapsed, 2),
                }, ensure_ascii=False))
            else:
                print(_response_text)
            return
        except Exception as e:
            if output_format == "json":
                import json as _bj
                print(_bj.dumps({"ok": False, "error": str(e), "provider": _provider}))
            else:
                print(f"❌ Error: {e}", file=sys.stderr)
            sys.exit(1)

    # ── Auto-Session Detection (Zero-Conf) ────────────────────
    # CHAT UX PARITY: Automatically detect and resume sessions without prompts
    def get_auto_session_id():
        try:
            # 0. Centralized Environment Priority (Mother ID)
            env_id = os.environ.get("NUCLEUS_SESSION_ID")
            if env_id:
                return env_id, False, "environment"

            # 1. Check if running inside Antigravity IDE
            ide_token = os.environ.get("GEMINI_CLI_IDE_AUTH_TOKEN")
            if ide_token:
                return ide_token, True, "ide"

            # 2. Sovereign Mother Session (Global Context Persistence)
            return "mother", False, "global"
        except Exception:
            pass
        return None, False, None

    session_id, is_ide_native, session_type = get_auto_session_id()
    default_chat_tag = session_id[:8] if session_id else "latest"
    
    latest_turns = 0
    _saved_history = []
    if brain_dir:
        latest_file = brain_dir / "chat" / f"{default_chat_tag}.json"
        if latest_file.exists():
            try:
                import json
                data = json.loads(latest_file.read_text())
                latest_turns = data.get("turn_count", 0)
                _saved_history = data.get("history", [])
            except Exception:
                pass

    # ── Help Text ────────────────────────────────────────────────
    HELP_TEXT = """
╔══════════════════════════════════════════════════════════════╗
║                   Nucleus Brother Commands                    ║
╠══════════════════════════════════════════════════════════════╣
║  /help              Show this help                           ║
║  /model [name]      Show or switch model                     ║
║  /provider [name]   Show or switch provider (gemini/         ║
║                     anthropic/groq/anthropic)               ║
║  /dual [provider]   Enable dual-agent review mode            ║
║  /dual off          Disable dual-agent mode                  ║
║  /auth [key]        Show or set API key for current provider ║
║  /status            Show full chat status                     ║
║  /tools             Show available tools                      ║
║  /brain [cmd]       Brain ops (pulse/consolidate/engrams/     ║
║                     tasks/health) — try /brain help           ║
║  /cc-session [id]   Claude Code session (show/set/continue)  ║
║  /learn <text>      Save knowledge to brain memory            ║
║  /tier              Show current tier info                    ║
║  /history           Show conversation stats                  ║
║  /compact           Compress conversation history            ║
║  /diff              Show git diff for files edited this session║
║  /files             List all files touched this session       ║
║  /recall [query]    Search conversation archive (on-demand RAG)║
║  /recall inject Q   Search + inject results into context     ║
║  /archive           Show archive stats                        ║
║  /escalate <Q>      Escalate decision to operator              ║
║  /resolve [id]      Resolve pending escalation                ║
║  /undo              Revert last file edit/write               ║
║  /retry             Re-send last message                      ║
║  /clear             Clear conversation history               ║
║  /chat save [tag]   Save conversation                        ║
║  /chat resume [tag] Resume saved conversation                ║
║  /chat list         List saved conversations                 ║
║  /exit              Quit                                     ║
╚══════════════════════════════════════════════════════════════╝
"""

    # ── Banner ──────────────────────────────────────────────────
    print("""
╔══════════════════════════════════════════════════════════════╗
║               🧬 Nucleus Brother                              ║
║──────────────────────────────────────────────────────────────║
║  /help for commands  •  /model to switch  •  Ctrl+C to quit ║
╚══════════════════════════════════════════════════════════════╝""")
    _tool_count = "35 (23 native + 12 MCP)" if _provider in () and getattr(llm, "_mcp_config", None) else "11"
    print(f"  Model: {llm.model_name} | Provider: {_provider} | Tools: {_tool_count}")
    # Show archive status in banner
    try:
        _ab = ArchivePipeline()
        _as = _ab.get_stats()
        _at = _as.get('total_turns', 0)
        if _at > 0:
            print(f"  Archive: {_at} turns | /archive for details")
    except Exception:
        pass
    if _workspace_info:
        # Show compact one-liner from workspace
        _ws_oneliner = _workspace_info.split("\n")[0]  # e.g. "Git: branch=main"
        _ws_proj = [l for l in _workspace_info.split("\n") if l.startswith("Project:")]
        if _ws_proj:
            _ws_oneliner += f" | {_ws_proj[0]}"
        print(f"  Workspace: {_ws_oneliner}")
    _auth_env_map = {
        "gemini": "GEMINI_API_KEY",
        "anthropic": "NUCLEUS_ANTHROPIC_API_KEY",
        "groq": "NUCLEUS_GROQ_API_KEY",
    }
    _auth_key_env = _auth_env_map.get(_provider, "GEMINI_API_KEY")
    key_hint = os.environ.get(_auth_key_env, "")
    _oauth_mode = getattr(llm, "_oauth_mode", False)
    if _provider in ():
        _cc_sid = getattr(llm, "_session_id", None)
        _cc_mcp = getattr(llm, "_mcp_config", None)
        _cc_parts = ["$0 via Max"]
        if _cc_sid:
            _cc_parts.append(f"session: {_cc_sid[:8]}...")
        if _cc_mcp:
            _cc_parts.append("MCP: nucleus")
        print(f"  Auth:  Claude Code Max — {' | '.join(_cc_parts)}")
    elif _oauth_mode:
        print(f"  Auth:  Claude Code Max (OAuth) — $0 API cost")
    elif key_hint:
        print(f"  Auth:  ...{key_hint[-6:]}")
    else:
        if _provider == "anthropic":
            # Check if Claude Code OAuth is available
            _has_oauth = hasattr(llm, "_get_anthropic_oauth_token") and llm._get_anthropic_oauth_token()
            if _has_oauth:
                print(f"  Auth:  Claude Code Max available! Will auto-detect on /provider anthropic")
            else:
                print(f"  Auth:  ⚠️  No API key. Use /auth <key> or install Claude Code Max")
        else:
            print(f"  Auth:  ⚠️  No {_provider} API key set. Use /auth <key>")
        
    if brain_context:
        _engram_count = brain_context.count("- [")  # Count injected engrams
        _engram_hint = f" + {_engram_count} engrams loaded" if _engram_count else ""
        print(f"  Context: 🧠 Sovereign Brain Active{_engram_hint}")
        if session_id:
            session_label = {"ide": "IDE Session", "project": "Project Session", "recent": "Recent Session"}.get(session_type, "Session")
            print(f"           • {session_label}: {session_id[:16]}")
        if latest_turns > 0:
            print(f"           • Resuming prior session ({latest_turns} turns)")
        # Archive stats (check file directly — setup comes later)
        _archive_path = brain_dir / "chat" / "archive" / "thread.jsonl" if brain_dir else None
        if _archive_path and _archive_path.exists():
            try:
                _a_size = _archive_path.stat().st_size
                _a_turns = sum(1 for l in open(_archive_path) if l.strip())
                print(f"           • Archive: {_a_turns} turns ({round(_a_size/1024, 1)} KB) — /recall to search")
            except Exception:
                pass
    else:
        print("  Context: ⚪️ Sandboxed (No .brain found)")
    print()

    # Resume saved history (cap at last 10 turns to avoid blowing context)
    # Normalize entries: ensure each is (role, content) tuple, not dict or malformed
    def _normalize_history(raw):
        result = []
        for item in raw:
            if isinstance(item, (list, tuple)) and len(item) >= 2:
                result.append((str(item[0]), str(item[1])))
            elif isinstance(item, dict):
                role = item.get("role", "user")
                content = item.get("content", "")
                result.append((str(role), str(content)))
            # skip malformed entries
        return result

    _max_resume_msgs = 20  # 10 turns × 2 messages each
    _normalized = _normalize_history(_saved_history)
    if len(_normalized) > _max_resume_msgs:
        history = _normalized[-_max_resume_msgs:]
    else:
        history = _normalized
    turn_count = latest_turns

    # ── Session File Tracker (Governance: audit what AI touched) ──
    # Tracks every file operation for /diff and auto-engram on exit
    _session_files = {
        "read": [],      # files read
        "written": [],   # files created/overwritten
        "edited": [],    # files edited (find/replace)
        "searched": [],  # code search paths
    }

    def _track_file(op: str, path: str):
        """Record a file operation for session audit."""
        if path and path not in _session_files.get(op, []):
            _session_files.setdefault(op, []).append(path)


    def _search_archive(query: str, limit: int = 10) -> list:
        """Search the conversation archive for past turns matching query."""
        results = []
        query_lower = query.lower()
        try:
            # Return most recent matches
            return results[-limit:]
        except Exception:
            return []

    def _archive_stats() -> dict:
        """Get archive stats (total turns, file size, date range)."""
        try:
            turns = 0
            first_ts = None
            last_ts = None
            return {
                "turns": turns,
                "size_kb": round(size / 1024, 1),
                "first": first_ts,
                "last": last_ts,
            }
        except Exception:
            return {"turns": 0, "size_kb": 0}

    # ── Undo Stack (reversible file changes) ───────────────────
    # Each entry: {"path": str, "op": "edit"|"write", "backup": str|None, "existed": bool}
    _undo_stack = []
    _MAX_UNDO = 50

    def _snapshot_before(path: str, op: str):
        """Snapshot file content before a destructive op (edit/write)."""
        p = Path(path).expanduser()
        existed = p.exists()
        backup = p.read_text() if existed else None
        _undo_stack.append({"path": str(p), "op": op, "backup": backup, "existed": existed})
        if len(_undo_stack) > _MAX_UNDO:
            _undo_stack.pop(0)

    # No global SIGINT handler — let KeyboardInterrupt propagate naturally.
    # Ctrl+C during streaming → cancel response (caught in ReAct loop)
    # Ctrl+C at prompt → exit (caught in main loop)

    def _expand_at_files(user_input: str) -> str:
        """Expand @file references in user input. Like Cursor's @-mentions.

        Patterns:
          @path/to/file.py      → reads and attaches full file
          @path/to/file.py:42   → reads and attaches lines around line 42
          @src/**/*.py           → glob and list matching files (no content)
        """
        import re as _re
        # Find all @references (not @mentions like @user)
        _at_pattern = _re.compile(r'@([\w./\-*]+(?::\d+)?)')
        matches = _at_pattern.findall(user_input)
        if not matches:
            return user_input

        attachments = []
        for ref in matches:
            # Split line number: @file.py:42 → file.py, 42
            if ":" in ref and ref.rsplit(":", 1)[1].isdigit():
                fpath, line_str = ref.rsplit(":", 1)
                target_line = int(line_str)
            else:
                fpath = ref
                target_line = None

            p = Path(fpath).expanduser()

            # Glob pattern (contains *)
            if "*" in fpath:
                try:
                    glob_matches = sorted(Path(".").glob(fpath))[:20]
                    if glob_matches:
                        file_list = "\n".join(f"  {m}" for m in glob_matches)
                        attachments.append(f"[Files matching @{fpath}]\n{file_list}")
                except Exception:
                    pass
                continue

            # Single file
            if not p.exists():
                # Try relative to cwd
                p = Path.cwd() / fpath
            if not p.exists():
                continue

            try:
                text = p.read_text()
                lines = text.split("\n")
                _track_file("read", str(p))

                if target_line:
                    # Show ~20 lines around target
                    start = max(0, target_line - 10)
                    end = min(len(lines), target_line + 10)
                    selected = lines[start:end]
                    numbered = [f"{start + i + 1:>5}│ {l}" for i, l in enumerate(selected)]
                    attachments.append(
                        f"[File: {p.name}:{target_line} (lines {start+1}-{end})]\n" +
                        "\n".join(numbered)
                    )
                    print(f"   📎 Attached: {p.name}:{target_line} ({len(selected)} lines)")
                else:
                    # Full file (cap at 200 lines for context efficiency)
                    if len(lines) > 200:
                        numbered = [f"{i+1:>5}│ {l}" for i, l in enumerate(lines[:200])]
                        attachments.append(
                            f"[File: {p.name} ({len(lines)} lines, showing first 200)]\n" +
                            "\n".join(numbered)
                        )
                        print(f"   📎 Attached: {p.name} (first 200 of {len(lines)} lines)")
                    else:
                        numbered = [f"{i+1:>5}│ {l}" for i, l in enumerate(lines)]
                        attachments.append(
                            f"[File: {p.name} ({len(lines)} lines)]\n" +
                            "\n".join(numbered)
                        )
                        print(f"   📎 Attached: {p.name} ({len(lines)} lines)")

                # Also attach brain context
                brain_ctx = _brain_context_for(str(p))
                if brain_ctx:
                    attachments.append(brain_ctx)
                    print(f"   📎 🧠 brain context for {p.name}")
            except Exception:
                pass

        if not attachments:
            return user_input

        return user_input + "\n\n" + "\n\n".join(attachments)

    def _build_prompt(user_input: str) -> str:
        """String-concatenated prompt (Gemini path)."""
        parts = [f"[SYSTEM]\n{sys_prompt}\n"]
        for role, content in history:
            tag = "USER" if role == "user" else "ASSISTANT"
            parts.append(f"[{tag}]\n{content}\n")
        parts.append(f"[USER]\n{user_input}\n[ASSISTANT]\n")
        return "\n".join(parts)

    def _build_messages(user_input: str) -> list:
        """Native messages array (Anthropic/Groq path). System prompt is handled by the LLM client."""
        msgs = []
        for role, content in history:
            if role == "system":
                # Compaction summary — inject as user context
                api_role = "user"
                content = f"[Context] {content}"
            elif role == "reviewer":
                api_role = "assistant"
            elif role == "assistant":
                api_role = "assistant"
            else:
                api_role = "user"
            # Anthropic requires strictly alternating roles — merge consecutive same-role
            if msgs and msgs[-1]["role"] == api_role:
                msgs[-1]["content"] += "\n\n" + content
            else:
                msgs.append({"role": api_role, "content": content})
        msgs.append({"role": "user", "content": user_input})
        return msgs

    # --- Sweeper Session State ---
    api_keys = []
    combo_keys = os.environ.get("GEMINI_API_KEYS", "")
    if combo_keys:
        api_keys = [k.strip() for k in combo_keys.split(",") if k.strip()]

    single_key = os.environ.get("GEMINI_API_KEY", "")
    if single_key and single_key.strip():
        sk = single_key.strip()
        if sk not in api_keys:
            api_keys.append(sk)

    if not api_keys:
        api_keys = [""]

    current_key_idx = 0
    is_initial = True


    # ── Dual-Agent Coordination State ──
    _dual_mode = False
    _dual_reviewer = None  # The review LLM (different provider)
    _dual_reviewer_name = ""  # e.g. "gemini", "groq"

    # ── prompt_toolkit session (history, completion, input isolation) ──
    _pt_session = None
    try:
        from prompt_toolkit import PromptSession
        from prompt_toolkit.history import InMemoryHistory
        from prompt_toolkit.completion import WordCompleter
        from prompt_toolkit.patch_stdout import patch_stdout as _pt_patch_stdout

        _slash_commands = ["/help", "/model", "/auth", "/provider", "/dual",
                           "/tier", "/status", "/tools", "/brain", "/learn", "/history", "/retry",
                           "/compact", "/diff", "/files", "/undo", "/clear", "/chat", "/quit", "/exit"]
        _pt_session = PromptSession(
            history=InMemoryHistory(),
            completer=WordCompleter(_slash_commands, sentence=True),
            complete_while_typing=False)
    except ImportError:
        _pt_patch_stdout = None  # Fallback: bare input()

    # ── Terminal echo control (suppress user keystrokes during agent output) ──
    _saved_termios = None
    def _suppress_echo():
        """Turn off terminal echo so keystrokes don't appear during streaming."""
        nonlocal _saved_termios
        try:
            import termios
            fd = sys.stdin.fileno()
            _saved_termios = termios.tcgetattr(fd)
            new = termios.tcgetattr(fd)
            new[3] = new[3] & ~termios.ECHO  # Disable ECHO flag
            termios.tcsetattr(fd, termios.TCSANOW, new)
        except (ImportError, termios.error, AttributeError, ValueError):
            _saved_termios = None

    def _restore_echo():
        """Restore terminal echo and drain any buffered keystrokes."""
        nonlocal _saved_termios
        try:
            import termios
            fd = sys.stdin.fileno()
            if _saved_termios:
                termios.tcsetattr(fd, termios.TCSANOW, _saved_termios)
                _saved_termios = None
            termios.tcflush(fd, termios.TCIFLUSH)
        except (ImportError, termios.error, AttributeError, ValueError):
            pass

    def _brain_context_for(filepath: str) -> str:
        """Search engrams for context related to a file path. Returns empty string or context block."""
        if not brain_dir:
            return ""
        try:
            from mcp_server_nucleus.runtime.engram_ops import _brain_search_engrams_impl
            import json
            # Extract meaningful search terms from path
            p = Path(filepath)
            terms = [p.stem]  # filename without extension
            if p.parent.name and p.parent.name != ".":
                terms.append(p.parent.name)
            hits = []
            for term in terms:
                if len(term) < 3:
                    continue
                raw = _brain_search_engrams_impl(term, limit=3)
                data = json.loads(raw) if isinstance(raw, str) else raw
                for e in data.get("data", {}).get("engrams", []):
                    key = e.get("key", "")
                    if key not in [h.get("key") for h in hits]:
                        hits.append(e)
            if not hits:
                return ""
            lines = [f"\n[Brain Context — {len(hits)} related engram(s)]"]
            for h in hits[:3]:
                lines.append(f"  • {h.get('key', '?')}: {str(h.get('value', ''))[:120]}")
            return "\n".join(lines)
        except Exception:
            return ""

    def _execute_tool(tool_name: str, tool_input: dict, step: int) -> str:
        """Execute a tool call and return the result string. Shows compact preview."""
        if tool_name == "shell_execute":
            command = tool_input.get("command", "")
            print(f"   [Step {step}: $ {command}]")
            import subprocess as _sp
            try:
                res = _sp.run(command, shell=True, capture_output=True, text=True, timeout=30)
                result = f"STDOUT:\n{res.stdout}\nSTDERR:\n{res.stderr}"
                # Compact preview
                lines = res.stdout.strip().split("\n") if res.stdout.strip() else []
                for _pl in lines[:3]:
                    print(f"   │ {_pl[:120]}")
                if len(lines) > 3:
                    print(f"   │ ... ({len(lines)} lines)")
            except Exception as e:
                result = f"Execution failed: {e}"
            return result

        elif tool_name == "read_file":
            fpath = tool_input.get("path", "")
            offset = tool_input.get("offset", 1)
            limit = tool_input.get("limit", 500)
            print(f"   [Step {step}: read {fpath}]")
            _track_file("read", fpath)
            try:
                p = Path(fpath).expanduser()
                if not p.exists():
                    return f"Error: File not found: {fpath}"
                text = p.read_text()
                all_lines = text.split("\n")
                start = max(0, (offset or 1) - 1)
                end = start + (limit or 500)
                selected = all_lines[start:end]
                # Number lines
                numbered = [f"{start + i + 1:>5}│ {line}" for i, line in enumerate(selected)]
                content = "\n".join(numbered)
                print(f"   │ {len(selected)} lines ({p.stat().st_size} bytes)")
                brain_ctx = _brain_context_for(fpath)
                if brain_ctx:
                    print(f"   │ 🧠 brain context attached")
                return content[:8000] + brain_ctx
            except Exception as e:
                return f"Error reading file: {e}"

        if tool_name == "write_file":
            fpath = tool_input.get("path", "")
            content = tool_input.get("content", "")
            print(f"   [Step {step}: write {fpath}]")
            _track_file("written", fpath)
            _snapshot_before(fpath, "write")
            try:
                p = Path(fpath).expanduser()
                p.parent.mkdir(parents=True, exist_ok=True)
                p.write_text(content)
                lines = content.count("\n") + 1
                print(f"   │ wrote {lines} lines ({len(content)} bytes)")
                return f"Successfully wrote {fpath} ({lines} lines, {len(content)} bytes)"
            except Exception as e:
                return f"Error writing file: {e}"

        elif tool_name == "edit_file":
            fpath = tool_input.get("path", "")
            old = tool_input.get("old_string", "")
            new = tool_input.get("new_string", "")
            print(f"   [Step {step}: edit {fpath}]")
            _track_file("edited", fpath)
            _snapshot_before(fpath, "edit")
            try:
                p = Path(fpath).expanduser()
                if not p.exists():
                    return f"Error: File not found: {fpath}"
                text = p.read_text()
                count = text.count(old)
                if count == 0:
                    return f"Error: old_string not found in {fpath}"
                if count > 1:
                    return f"Error: old_string matches {count} times in {fpath}. Make it more specific."
                new_text = text.replace(old, new, 1)
                p.write_text(new_text)
                # Show diff preview
                old_preview = old.strip().split("\n")[0][:60]
                new_preview = new.strip().split("\n")[0][:60]
                print(f"   │ - {old_preview}")
                print(f"   │ + {new_preview}")
                brain_ctx = _brain_context_for(fpath)
                return f"Successfully edited {fpath}" + brain_ctx
            except Exception as e:
                return f"Error editing file: {e}"

        if tool_name == "search_files":
            pattern = tool_input.get("pattern", "")
            search_path = tool_input.get("path", ".")
            print(f"   [Step {step}: glob {pattern}]")
            try:
                p = Path(search_path).expanduser()
                matches = sorted(p.glob(pattern))[:50]  # Cap at 50
                result_lines = [str(m) for m in matches]
                for _pl in result_lines[:5]:
                    print(f"   │ {_pl}")
                if len(result_lines) > 5:
                    print(f"   │ ... ({len(result_lines)} files)")
                return "\n".join(result_lines) if result_lines else "No files found"
            except Exception as e:
                return f"Error searching files: {e}"

        elif tool_name == "search_code":
            pattern = tool_input.get("pattern", "")
            search_path = tool_input.get("path", ".")
            file_glob = tool_input.get("glob", "")
            print(f"   [Step {step}: grep '{pattern}']")
            _track_file("searched", search_path)
            try:
                import subprocess as _sp
                cmd = ["grep", "-rn", "--include", file_glob, pattern, search_path] if file_glob else ["grep", "-rn", pattern, search_path]
                res = _sp.run(cmd, capture_output=True, text=True, timeout=15)
                lines = res.stdout.strip().split("\n") if res.stdout.strip() else []
                for _pl in lines[:5]:
                    print(f"   │ {_pl[:120]}")
                if len(lines) > 5:
                    print(f"   │ ... ({len(lines)} matches)")
                result = res.stdout[:8000] if res.stdout else "No matches found"
                # Search engrams for the code pattern too
                brain_ctx = _brain_context_for(pattern)
                return result + brain_ctx
            except Exception as e:
                return f"Error searching code: {e}"

        if tool_name == "write_engram":
            key = tool_input.get("key", "")
            value = tool_input.get("value", "")
            context = tool_input.get("context", "Architecture")
            intensity = tool_input.get("intensity", 5)
            print(f"   [Step {step}: 🧠 write engram '{key}']")
            try:
                from mcp_server_nucleus.runtime.engram_ops import _brain_write_engram_impl
                import json
                raw = _brain_write_engram_impl(key, value, context, intensity)
                data = json.loads(raw) if isinstance(raw, str) else raw
                if data.get("success"):
                    print(f"   │ 🧠 saved: {key} ({context}, intensity={intensity})")
                    return f"Engram saved: {key} = {value[:200]}"
                else:
                    err = data.get("error", "Unknown error")
                    return f"Error writing engram: {err}"
            except Exception as e:
                return f"Error writing engram: {e}"

        if tool_name == "search_engrams":
            query = tool_input.get("query", "")
            limit = tool_input.get("limit", 5)
            print(f"   [Step {step}: 🧠 search engrams '{query}']")
            try:
                from mcp_server_nucleus.runtime.engram_ops import _brain_search_engrams_impl
                import json
                raw = _brain_search_engrams_impl(query, limit=limit)
                data = json.loads(raw) if isinstance(raw, str) else raw
                engrams = data.get("data", {}).get("engrams", [])
                if not engrams:
                    print(f"   │ no engrams found for '{query}'")
                    return f"No engrams found for '{query}'. The brain has no memory of this yet."
                lines = []
                for e in engrams[:limit]:
                    k = e.get("key", "?")
                    v = str(e.get("value", ""))[:150]
                    ctx = e.get("context", "")
                    i = e.get("intensity", 5)
                    lines.append(f"  [{ctx}|{i}] {k}: {v}")
                    print(f"   │ {k}: {v[:80]}")
                return f"Found {len(engrams)} engram(s):\n" + "\n".join(lines)
            except Exception as e:
                return f"Error searching engrams: {e}"

        if tool_name == "list_tasks":
            status_filter = tool_input.get("status")
            print(f"   [Step {step}: 📋 list tasks{' (' + status_filter + ')' if status_filter else ''}]")
            try:
                from mcp_server_nucleus.runtime.task_ops import _list_tasks
                tasks = _list_tasks(status=status_filter)
                if not tasks:
                    print(f"   │ no tasks found")
                    return "No tasks in backlog." + (" Try add_task to create one." if not status_filter else "")
                lines = []
                for t in tasks[:15]:
                    tid = t.get("id", "?")[:12]
                    desc = t.get("description", "")[:80]
                    st = t.get("status", "?")
                    pri = t.get("priority", 3)
                    lines.append(f"  [{st}|P{pri}] {tid}: {desc}")
                    print(f"   │ [{st}] {desc[:60]}")
                if len(tasks) > 15:
                    print(f"   │ ... ({len(tasks)} total)")
                return f"{len(tasks)} task(s):\n" + "\n".join(lines)
            except Exception as e:
                return f"Error listing tasks: {e}"

        if tool_name == "add_task":
            desc = tool_input.get("description", "")
            priority = tool_input.get("priority", 3)
            print(f"   [Step {step}: 📋 add task '{desc[:50]}']")
            try:
                from mcp_server_nucleus.runtime.task_ops import _add_task
                result = _add_task(desc, priority=priority, source="nucleus_chat")
                if result.get("success"):
                    tid = result["task"]["id"]
                    print(f"   │ ✅ {tid} (P{priority})")
                    return f"Task created: {tid} — {desc}"
                else:
                    return f"Error: {result.get('error', 'unknown')}"
            except Exception as e:
                return f"Error adding task: {e}"

        elif tool_name == "update_task":
            task_id = tool_input.get("task_id", "")
            updates = {}
            if "status" in tool_input:
                updates["status"] = tool_input["status"]
            if "priority" in tool_input:
                updates["priority"] = tool_input["priority"]
            print(f"   [Step {step}: 📋 update task '{task_id[:20]}']")
            try:
                from mcp_server_nucleus.runtime.task_ops import _update_task
                result = _update_task(task_id, updates)
                if result.get("success"):
                    print(f"   │ ✅ updated → {updates}")
                    return f"Task {task_id} updated: {updates}"
                else:
                    return f"Error: {result.get('error', 'unknown')}"
            except Exception as e:
                return f"Error updating task: {e}"

        else:
            print(f"   [Step {step}: unknown tool '{tool_name}']")
            return f"Unknown tool: {tool_name}"

    def _get_input() -> str:
        """Get user input via prompt_toolkit (preferred) or bare input()."""
        _restore_echo()  # Ensure echo is on before prompting
        if _pt_session:
            return _pt_session.prompt("You › ").strip()
        return input("You › ").strip()

    # ── Main Loop ───────────────────────────────────────────────
    while True:
        try:
            user_input = _get_input()
        except (EOFError, KeyboardInterrupt):
            print("\n\n👋 Goodbye!")
            break

        if not user_input:
            continue

        # ── Slash Command Router ────────────────────────────────
        if user_input.startswith("/"):
            parts = user_input.split(None, 1)
            cmd = parts[0].lower()
            cmd_arg = parts[1].strip() if len(parts) > 1 else ""

            if cmd in ("/exit", "/quit", "/q"):
                print("\n👋 Goodbye!")
                break

            elif cmd == "/help":
                print(HELP_TEXT)
                continue

            elif cmd == "/brain":
                # Quick brain operations — runs via nucleus CLI
                import subprocess as _brain_sp
                _brain_cmds = {
                    "": ("status", ["nucleus", "status"]),
                    "status": ("status", ["nucleus", "status"]),
                    "pulse": ("pulse & polish", ["nucleus", "combo", "pulse"]),
                    "consolidate": ("consolidation", ["nucleus", "combo", "consolidate"]),
                    "engrams": ("engram list", ["nucleus", "engram", "list"]),
                    "tasks": ("task list", ["nucleus", "task", "list"]),
                    "health": ("health check", ["nucleus", "combo", "self-healing-sre"]),
                }
                _bcmd = cmd_arg.lower().strip()
                if _bcmd == "help":
                    print("🧠 /brain commands:")
                    print("   /brain           — system status")
                    print("   /brain pulse     — run pulse & polish (health + cleanup)")
                    print("   /brain consolidate — brain consolidation (archive stale)")
                    print("   /brain engrams   — list engrams")
                    print("   /brain tasks     — list tasks")
                    print("   /brain health    — self-healing SRE check\n")
                elif _bcmd in _brain_cmds:
                    _label, _argv = _brain_cmds[_bcmd]
                    print(f"🧠 Running brain {_label}...")
                    try:
                        _br = _brain_sp.run(_argv, capture_output=True, text=True, timeout=30)
                        print(_br.stdout[:2000] if _br.stdout else f"(no output, exit {_br.returncode})")
                        if _br.stderr:
                            print(f"   {_br.stderr[:200]}")
                    except Exception as _be:
                        print(f"❌ {_be}")
                    print()
                else:
                    print(f"❓ Unknown brain command '{_bcmd}'. Try /brain help\n")
                continue

            if cmd in ("/clear", "/reset"):
                history.clear()
                turn_count = 0
                print("🔄 Conversation cleared.\n")
                continue

            elif cmd == "/cc-session":
                if _provider not in ():
                    print(f"⚠️  /cc-session only works with anthropic provider. Current: {_provider}\n")
                elif not cmd_arg:
                    _sid = getattr(llm, "_session_id", None)
                    if _sid:
                        print(f"🔗 Claude Code session: {_sid}")
                    else:
                        print(f"🔗 No active session (will auto-create on first message)")
                    # Show bridge info if available
                    _bridge_path = brain_dir / "sessions" / "cc_bridge.json" if brain_dir else None
                    if _bridge_path and _bridge_path.exists():
                        try:
                            _b = json.loads(_bridge_path.read_text())
                            print(f"   Bridge: {_b.get('anthropic_session', '?')[:16]}... ({_b.get('source', '?')} @ {_b.get('ts', '?')[:16]})")
                        except Exception:
                            pass
                    print(f"   Usage: /cc-session <session-id>  — resume a specific session")
                    print(f"          /cc-session continue      — resume most recent session")
                    print(f"          /cc-session bridge        — resume from bridge (shared with Claude Code normal)\n")
                else:
                    if cmd_arg.lower() == "bridge":
                        # Resume from bridge — pick up the session the other brother left
                        _bridge_path = brain_dir / "sessions" / "cc_bridge.json" if brain_dir else None
                        if _bridge_path and _bridge_path.exists():
                            try:
                                _b = json.loads(_bridge_path.read_text())
                                llm._session_id = _b.get("anthropic_session")
                                _src = _b.get("source", "unknown")
                                print(f"🔗 Bridged to session from {_src}: {llm._session_id}")
                                print(f"   Both brothers now share the same context.\n")
                            except Exception as _e:
                                print(f"❌ Failed to read bridge: {_e}\n")
                        else:
                            print(f"⚠️  No bridge found. Start a session first — the bridge auto-creates.\n")
                    elif cmd_arg.lower() == "continue":
                        # Find the most recent session for this directory
                        import subprocess as _sp
                        try:
                            _test = _sp.run(
                                ["claude", "-p", "say OK", "--output-format", "json", "--continue", "--model", llm._cli_model],
                                capture_output=True, text=True, timeout=30, cwd=os.getcwd())
                            _out = json.loads(_test.stdout.strip())
                            llm._session_id = _out.get("session_id")
                            print(f"🔗 Resumed most recent session: {llm._session_id}")
                            print(f"   Claude Code now shares context with that session.\n")
                        except Exception as _e:
                            print(f"❌ Failed to resume: {_e}\n")
                    else:
                        llm._session_id = cmd_arg.strip()
                        print(f"🔗 Set Claude Code session: {llm._session_id}")
                        print(f"   Next message will resume this session's full context.\n")
                continue

            if cmd == "/recall":
                if not cmd_arg:
                    stats = _archive_stats()
                    if stats["turns"]:
                        print(f"📚 Conversation Archive:")
                        print(f"   Turns: {stats['turns']}  |  Size: {stats['size_kb']} KB")
                        print(f"   Range: {stats.get('first', '?')[:10]} → {stats.get('last', '?')[:10]}")
                        print(f"   Usage: /recall <query>  — search past conversations")
                        print(f"          /recall inject <query>  — search + inject into context\n")
                    else:
                        print(f"📚 Archive is empty. Conversations will auto-save as you chat.\n")
                else:
                    # Check if user wants to inject results into context
                    _inject = False
                    _query = cmd_arg
                    if cmd_arg.lower().startswith("inject "):
                        _inject = True
                        _query = cmd_arg[7:].strip()

                    results = _search_archive(_query, limit=10)
                    if not results:
                        print(f"📚 No matches for '{_query}' in archive.\n")
                    else:
                        print(f"📚 Found {len(results)} matches for '{_query}':\n")
                        for r in results:
                            _ts = r.get("ts", "")[:16]
                            _role = r.get("role", "?")
                            _content = r.get("content", "")
                            # Truncate long content
                            _preview = _content[:200] + ("..." if len(_content) > 200 else "")
                            _icon = "👤" if _role == "user" else "🧠"
                            print(f"  {_icon} [{_ts}] {_preview}")
                        print()

                        if _inject:
                            # Inject relevant turns into conversation context
                            _recall_text = "\n".join(
                                f"[{r.get('ts', '')[:16]}] {r.get('role', 'user')}: {r.get('content', '')[:500]}"
                                for r in results[-5:]  # Last 5 matches
                            )
                            history.append(("system", f"[Recalled from archive — matching '{_query}']\n{_recall_text}"))
                            print(f"   💉 Injected {min(len(results), 5)} turns into context. The AI can now reference them.\n")
                continue

            if cmd == "/archive":
                stats = _archive_stats()
                if stats["turns"]:
                    print(f"📚 Conversation Archive:")
                    print(f"   Turns: {stats['turns']}  |  Size: {stats['size_kb']} KB")
                    print(f"   Range: {stats.get('first', '?')[:10]} → {stats.get('last', '?')[:10]}")
                    print(f"   Search: /recall <query>")
                else:
                    print(f"📚 Archive is empty. Auto-saves every turn.")
                try:
                    _ta = ArchivePipeline()
                    _ts = _ta.get_stats()
                    _total = _ts.get('total_turns', 0)
                    _by = _ts.get('by_brother', {})
                    _brothers = ", ".join(f"{k}={v}" for k, v in sorted(_by.items()))
                    _rt = _ta.should_retrain()
                    _rt_flag = "  RETRAIN READY" if _rt['should_retrain'] else f"  {_rt['new_turns']}/{_rt['threshold']} to retrain"
                    print(f"\n🧬 local model Training Data:")
                    print(f"   SFT:  {_total} turns  |  By: {_brothers}")
                    print(f"   Status:{_rt_flag}")
                    print(f"   Export: nucleus archive export | nucleus archive dpo-export\n")
                except Exception:
                    print()
                continue

            if cmd == "/escalate":
                if not cmd_arg:
                    print(f"🚨 Escalate a decision to the operator.")
                    print(f"   Usage: /escalate <question>")
                    print(f"   Example: /escalate Should we migrate auth to OAuth2?\n")
                else:
                    try:
                        from mcp_server_nucleus.runtime.heartbeat_ops import escalate_to_operator
                        _esc_id = escalate_to_operator(
                            question=cmd_arg,
                            context=f"Raised from nucleus brother ({_provider})",
                            source=f"nucleus-chat-{_provider}")
                        print(f"🚨 Escalation created: {_esc_id}")
                        print(f"   Will surface on next heartbeat or session.\n")
                    except Exception as _e:
                        print(f"❌ Failed to escalate: {_e}\n")
                continue

            elif cmd == "/resolve":
                if not brain_dir:
                    print(f"⚠️  No brain found. Cannot resolve escalations.\n")
                    continue
                _esc_dir = brain_dir / "escalations"
                if not cmd_arg:
                    # List pending escalations
                    if not _esc_dir.exists():
                        print(f"✅ No pending escalations.\n")
                        continue
                    _found = False
                    for _ef in sorted(_esc_dir.glob("*.json")):
                        try:
                            _ed = json.loads(_ef.read_text())
                            if _ed.get("status") == "pending":
                                _opts = ", ".join(_ed.get("options", [])) or "open-ended"
                                print(f"  🚨 [{_ed['id']}] {_ed.get('question', '?')}")
                                print(f"     Options: {_opts}  |  From: {_ed.get('source', '?')}")
                                _found = True
                        except Exception:
                            continue
                    if _found:
                        print(f"\n   Resolve: /resolve <id> <decision>\n")
                    else:
                        print(f"✅ No pending escalations.\n")
                else:
                    # Parse: /resolve esc_123456 migrate
                    _parts = cmd_arg.split(None, 1)
                    _esc_id = _parts[0]
                    _decision = _parts[1] if len(_parts) > 1 else "approved"
                    _esc_file = _esc_dir / f"{_esc_id}.json"
                    if _esc_file.exists():
                        try:
                            _ed = json.loads(_esc_file.read_text())
                            _ed["status"] = "resolved"
                            _ed["decision"] = _decision
                            _ed["resolved_at"] = datetime.now(timezone.utc).isoformat()
                            _ed["resolved_by"] = "operator"
                            _esc_file.write_text(json.dumps(_ed, indent=2))
                            print(f"✅ Resolved [{_esc_id}]: {_decision}")
                            print(f"   Agents will see this decision on next startup.\n")
                        except Exception as _e:
                            print(f"❌ Failed to resolve: {_e}\n")
                    else:
                        print(f"⚠️  Escalation '{_esc_id}' not found.\n")
                continue

            if cmd == "/model":
                if not cmd_arg:
                    # Show current + available
                    print(f"📊 Current model: {llm.model_name}")
                    if _provider == "gemini":
                        print(f"   Available: {', '.join(sorted(set(KNOWN_MODELS)))}\n")
                    elif _provider == "groq":
                        groq_models = [
                            "llama-3.3-70b-versatile",
                            "llama-3.1-8b-instant",
                            "meta-llama/llama-4-scout-17b-16e-instruct",
                            "qwen/qwen3-32b",
                        ]
                        print(f"   Available: {', '.join(groq_models)}\n")
                    elif _provider in ():
                        cc_models = ["sonnet", "opus", "haiku"]
                        print(f"   Available: {', '.join(cc_models)}  ($0 via Max subscription)\n")
                    else:
                        anthropic_models = [
                            "claude-sonnet-4-6-20250514",
                            "claude-opus-4-6-20250514",
                            "claude-haiku-4-5-20251001",
                        ]
                        print(f"   Available: {', '.join(anthropic_models)}\n")
                else:
                    # Switch model mid-chat
                    new_model = cmd_arg
                    try:
                        llm = _init_llm(tier, new_model, sys_instruction=sys_prompt)
                        print(f"✅ Switched to: {llm.model_name}\n")
                    except Exception as e:
                        print(f"❌ Failed to switch: {e}\n")
                continue

            if cmd == "/auth":
                if _provider == "gemini":
                    if not cmd_arg:
                        key = os.environ.get("GEMINI_API_KEY", "")
                        if key:
                            print(f"🔑 Current key: ...{key[-6:]}\n")
                        else:
                            print("⚠️  No API key set. Usage: /auth AIzaSy...\n")
                    else:
                        os.environ["GEMINI_API_KEY"] = cmd_arg
                        # Force API key mode (local_free) — prevents Vertex AI 403
                        try:
                            from .runtime.llm_client import LLMTier
                            llm = _init_llm(LLMTier.LOCAL_FREE, llm.model_name, sys_instruction=sys_prompt)
                            print(f"✅ API key set (...{cmd_arg[-6:]}). Model reloaded.\n")
                        except Exception as e:
                            print(f"❌ Key set but LLM init failed: {e}\n")
                else:
                    # Non-Gemini providers (Anthropic, Groq, etc.)
                    env_var = _auth_env_map.get(_provider, f"NUCLEUS_{_provider.upper()}_API_KEY")
                    if not cmd_arg:
                        key = os.environ.get(env_var, "")
                        if key:
                            print(f"🔑 Current key: ...{key[-6:]}\n")
                        else:
                            print(f"⚠️  No {_provider} key set. Usage: /auth <key>\n")
                    else:
                        os.environ[env_var] = cmd_arg
                        try:
                            llm = _init_llm(tier, llm.model_name, sys_instruction=sys_prompt)
                            print(f"✅ {_provider.capitalize()} key set (...{cmd_arg[-6:]}). Model reloaded.\n")
                        except Exception as e:
                            print(f"❌ Key set but LLM init failed: {e}\n")
                continue

            if cmd == "/dual":
                if not cmd_arg or cmd_arg.lower() == "off":
                    if _dual_mode:
                        _dual_mode = False
                        _dual_reviewer = None
                        _dual_reviewer_name = ""
                        print(f"🔌 Dual-agent mode OFF. Single provider: {_provider}\n")
                    else:
                        print("🤝 Dual-Agent Coordination")
                        print(f"   Status: OFF")
                        print(f"   Primary: {_provider} ({llm.model_name})")
                        print(f"   Usage: /dual <reviewer-provider>")
                        print(f"   Example: /dual gemini  (primary={_provider}, reviewer=gemini)")
                        print(f"   /dual off  to disable\n")
                elif cmd_arg.lower() in ["gemini", "anthropic", "groq"]:
                    reviewer_provider = cmd_arg.lower()
                    if reviewer_provider == _provider:
                        print(f"⚠️  Reviewer must differ from primary ({_provider}). Pick another.\n")
                    else:
                        try:
                            _dual_reviewer = get_llm_client(
                                provider=reviewer_provider,
                                system_instruction="You are a senior code reviewer. Critique the following AI response for accuracy, completeness, and missed edge cases. Be concise — 2-3 bullet points max. If the response is good, say so briefly.")
                            _dual_reviewer_name = reviewer_provider
                            _dual_mode = True
                            print(f"🤝 Dual-Agent Mode ON")
                            print(f"   Primary:  {_provider} ({llm.model_name})")
                            print(f"   Reviewer: {_dual_reviewer_name} ({_dual_reviewer.model_name})")
                            print(f"   Flow: Primary answers → Reviewer critiques\n")
                        except Exception as e:
                            print(f"❌ Failed to init reviewer: {e}\n")
                else:
                    print(f"❌ Unknown provider '{cmd_arg}'. Available: gemini, anthropic, groq, anthropic\n")
                continue

            if cmd == "/provider":
                available = ["gemini", "anthropic", "groq"]
                if not cmd_arg:
                    print(f"🔌 Current provider: {_provider}")
                    print(f"   Available: {', '.join(available)}")
                    print(f"   Usage: /provider anthropic  \n")
                elif cmd_arg.lower() in available:
                    new_provider = cmd_arg.lower()
                    try:
                        _provider = new_provider
                        llm = _init_llm(tier, None, sys_instruction=sys_prompt)
                        print(f"✅ Switched to {_provider} → {llm.model_name}")
                        print(f"   Conversation history preserved ({len(history)} messages)\n")
                    except Exception as e:
                        print(f"❌ Failed to switch: {e}\n")
                else:
                    print(f"❌ Unknown provider '{cmd_arg}'. Available: {', '.join(available)}\n")
                continue

            elif cmd == "/tier":
                cfg = TierRouter.TIER_CONFIGS.get(tier, {})
                print(f"📊 Tier: {tier_name}")
                print(f"   Model: {llm.model_name}")
                print(f"   Engine: {llm.engine}")
                print(f"   Platform: {cfg.get('platform', 'unknown')}")
                print(f"   Description: {cfg.get('description', 'N/A')}\n")
                continue

            elif cmd == "/history":
                print(f"📜 Conversation: {turn_count} turn(s), {len(history)} messages\n")
                continue

            elif cmd == "/files":
                _total = sum(len(v) for v in _session_files.values())
                if _total == 0:
                    print("📂 No files touched this session.\n")
                else:
                    print(f"📂 Session File Tracker ({_total} operations)")
                    for op, label in [("read", "📖 Read"), ("edited", "✏️  Edited"), ("written", "📝 Written"), ("searched", "🔍 Searched")]:
                        paths = _session_files.get(op, [])
                        if paths:
                            print(f"  {label}:")
                            for fp in paths:
                                print(f"    {fp}")
                    print()
                continue

            elif cmd == "/diff":
                # Show git diff for all files edited/written this session
                _modified = list(set(_session_files.get("edited", []) + _session_files.get("written", [])))
                if not _modified:
                    print("📋 No files edited or written this session.\n")
                else:
                    print(f"📋 Session Diff ({len(_modified)} file(s) modified)")
                    import subprocess as _sp
                    for fp in _modified:
                        try:
                            # Try git diff first (tracked files)
                            res = _sp.run(["git", "diff", "--no-color", fp], capture_output=True, text=True, timeout=5)
                            if res.stdout.strip():
                                # Show compact diff
                                diff_lines = res.stdout.strip().split("\n")
                                print(f"\n  ── {fp} ──")
                                for dl in diff_lines:
                                    if dl.startswith("+") and not dl.startswith("+++"):
                                        print(f"  \033[32m{dl}\033[0m")
                                    elif dl.startswith("-") and not dl.startswith("---"):
                                        print(f"  \033[31m{dl}\033[0m")
                                    elif dl.startswith("@@"):
                                        print(f"  \033[36m{dl}\033[0m")
                            else:
                                # New file or no git changes — show file size
                                try:
                                    _sz = Path(fp).stat().st_size
                                    print(f"\n  ── {fp} (new file, {_sz} bytes) ──")
                                except Exception:
                                    print(f"\n  ── {fp} (no diff available) ──")
                        except Exception:
                            print(f"\n  ── {fp} (diff failed) ──")
                    print()
                continue

            if cmd == "/status":
                _native_tools = hasattr(llm, "stream_with_tools") and _model_supports_tools(llm.model_name)
                _tool_count = "11 tools (native)" if _native_tools else "11 tools (<execute> tags)"
                print(f"📊 Nucleus Brother Status")
                print(f"   Provider: {_provider} | Model: {llm.model_name}")
                print(f"   Tools: {_tool_count}")
                print(f"   File: shell_execute, read_file, write_file, edit_file, search_files, search_code")
                print(f"   Brain: write_engram, search_engrams | Tasks: list_tasks, add_task, update_task")
                if _dual_mode:
                    print(f"   Dual-agent: ON (reviewer: {_dual_reviewer_name})")
                else:
                    print(f"   Dual-agent: OFF")
                print(f"   History: {turn_count} turns, {len(history)} messages")
                _files_edited = len(_session_files.get("edited", [])) + len(_session_files.get("written", []))
                _files_read = len(_session_files.get("read", []))
                print(f"   Files: {_files_read} read, {_files_edited} modified (/diff to review)")
                # Token budget
                try:
                    from mcp_server_nucleus.runtime.token_budget import get_budget_manager
                    _bm = get_budget_manager()
                    _sid = os.environ.get("NUCLEUS_SESSION_ID", "default")
                    _usage = _bm.get_usage(_sid) if hasattr(_bm, "get_usage") else None
                    if _usage:
                        _used = _usage.get("total_tokens", 0)
                        _limit = _usage.get("limit", 0)
                        _pct = int((_used / _limit) * 100) if _limit else 0
                        print(f"   Tokens: {_used:,}/{_limit:,} ({_pct}%) — /compact to free space")
                except Exception:
                    pass
                _k = os.environ.get(_auth_env_map.get(_provider, ""), "")
                print(f"   Auth: ...{_k[-6:]}" if _k else "   Auth: ⚠️ no key")
                # Training flywheel status
                try:
                    _rt = ArchivePipeline().should_retrain()
                    _rt_label = "RETRAIN READY" if _rt['should_retrain'] else f"{_rt['new_turns']}/{_rt['threshold']} to retrain"
                    print(f"   Training: {_rt['total_turns']} turns | {_rt_label}")
                except Exception:
                    pass
                print()
                continue

            if cmd == "/tools":
                _native_tools = hasattr(llm, "stream_with_tools") and _model_supports_tools(llm.model_name)
                _mode = "native API" if _native_tools else "<execute> tags"
                print(f"🛠  Available Tools ({_mode})")
                print(f"   ┌─────────────────┬──────────────────────────────────────────┐")
                print(f"   │ FILE TOOLS                                                 │")
                print(f"   │ shell_execute    │ Run shell commands (ls, git, nucleus)   │")
                print(f"   │ read_file        │ Read file with line numbers             │")
                print(f"   │ write_file       │ Create or overwrite files               │")
                print(f"   │ edit_file        │ Surgical find-and-replace               │")
                print(f"   │ search_files     │ Find files by glob pattern              │")
                print(f"   │ search_code      │ Search contents by regex                │")
                print(f"   │ BRAIN TOOLS (persistent memory)                            │")
                print(f"   │ write_engram     │ Save knowledge to brain                 │")
                print(f"   │ search_engrams   │ Recall from past sessions               │")
                print(f"   │ TASK TOOLS (self-organizing backlog)                       │")
                print(f"   │ list_tasks       │ See what needs doing                    │")
                print(f"   │ add_task         │ Create follow-up tasks                  │")
                print(f"   │ update_task      │ Mark done, change priority              │")
                print(f"   └─────────────────┴──────────────────────────────────────────┘")
                print(f"   Brain context: auto-attached on read_file, edit_file, search_code")
                print()
                continue

            if cmd == "/brain":
                print("🧠 Brain Dashboard")
                if not brain_dir:
                    print("   ⚪ No brain found. Run `nucleus init` first.\n")
                    continue
                print(f"   Path: {brain_dir}")
                # Engram stats
                try:
                    from mcp_server_nucleus.runtime.engram_ops import _get_cached_engrams
                    all_e = _get_cached_engrams()
                    print(f"   Engrams: {len(all_e)} total")
                    # Show top 5 by intensity
                    top = sorted(all_e, key=lambda x: x.get("intensity", 5), reverse=True)[:5]
                    for e in top:
                        print(f"   │ [{e.get('context', '?')}|{e.get('intensity', 5)}] {e.get('key', '?')}: {str(e.get('value', ''))[:60]}")
                except Exception:
                    print("   Engrams: (unavailable)")
                # Task stats
                try:
                    from mcp_server_nucleus.runtime.task_ops import _list_tasks
                    all_t = _list_tasks()
                    pending = [t for t in all_t if t.get("status") == "PENDING"]
                    in_prog = [t for t in all_t if t.get("status") == "IN_PROGRESS"]
                    done = [t for t in all_t if t.get("status") == "DONE"]
                    print(f"   Tasks: {len(all_t)} total ({len(pending)} pending, {len(in_prog)} active, {len(done)} done)")
                    for t in (in_prog + pending)[:3]:
                        print(f"   │ [{t.get('status')}] {t.get('description', '?')[:60]}")
                except Exception:
                    print("   Tasks: (unavailable)")
                # Session info
                print(f"   Session: {turn_count} turns, {len(history)} messages")
                print()
                continue

            if cmd == "/learn":
                if not cmd_arg:
                    print("Usage: /learn <key> <value>  (e.g. /learn db_pattern Uses SQLAlchemy async)\n")
                elif not brain_dir:
                    print("⚠️  No brain found. Run `nucleus init` first.\n")
                else:
                    parts = cmd_arg.split(None, 1)
                    if len(parts) < 2:
                        print("Usage: /learn <key> <value>\n")
                    else:
                        _lkey, _lval = parts
                        try:
                            from mcp_server_nucleus.runtime.engram_ops import _brain_write_engram_impl
                            _brain_write_engram_impl(_lkey, _lval, "Architecture", 6)
                            print(f"🧠 Saved: {_lkey} = {_lval[:80]}\n")
                        except Exception as e:
                            print(f"❌ Failed: {e}\n")
                continue

            elif cmd == "/compact":
                if len(history) <= 4:
                    print("📎 History too short to compact.\n")
                else:
                    old_count = len(history)
                    kept = history[-4:]  # last 2 turns (4 messages)
                    # Extract topics from dropped history and save as engram
                    dropped = history[:-4]
                    _topics = []
                    for role, msg in dropped:
                        if role == "user" and not msg.startswith("[Tool Result"):
                            _topics.append(msg.strip().split("\n")[0][:60])
                    if _topics and brain_dir:
                        try:
                            from mcp_server_nucleus.runtime.engram_ops import _brain_write_engram_impl
                            from datetime import datetime
                            ts = datetime.now().strftime("%Y%m%d_%H%M")
                            _summary = f"Compacted context ({len(dropped)//2} turns): " + " | ".join(_topics[:5])
                            _brain_write_engram_impl(f"compact_{ts}", _summary[:500], "Decision", 3)
                            print(f"   🧠 Dropped context saved to brain as engram")
                        except Exception:
                            pass
                    summary = f"[Previous {old_count // 2 - 2} turn(s) compacted — context saved to brain]"
                    history.clear()
                    history.append(("system", summary))
                    history.extend(kept)
                    print(f"📎 Compacted: {old_count} → {len(history)} messages\n")
                continue

            if cmd == "/clear":
                history.clear()
                turn_count = 0
                for k in _session_files:
                    _session_files[k].clear()
                print("🗑️  Conversation and file tracker cleared.\n")
                continue

            elif cmd == "/undo":
                if not _undo_stack:
                    print("⏪ Nothing to undo.\n")
                else:
                    entry = _undo_stack.pop()
                    fpath = entry["path"]
                    fname = Path(fpath).name
                    try:
                        p = Path(fpath)
                        if entry["backup"] is not None:
                            # Restore previous content
                            p.write_text(entry["backup"])
                            print(f"⏪ Reverted {fname} ({entry['op']}) — restored previous version")
                        elif not entry["existed"]:
                            # File was newly created — delete it
                            if p.exists():
                                p.unlink()
                                print(f"⏪ Reverted {fname} (write) — deleted newly created file")
                            else:
                                print(f"⏪ {fname} already gone.")
                        else:
                            print(f"⏪ Cannot undo {fname} — no backup available")
                        if _undo_stack:
                            print(f"   ({len(_undo_stack)} more undo(s) available)")
                        print()
                    except Exception as e:
                        print(f"❌ Undo failed for {fname}: {e}\n")
                continue

            if cmd in ("/cost", "/stats"):
                print(f"📊 Session Stats")
                print(f"   Turns: {turn_count} | Messages: {len(history)}")
                print(f"   Model: {llm.model_name} | Provider: {_provider}")
                _files_mod = len(set(_session_files.get("edited", []) + _session_files.get("written", [])))
                _files_rd = len(_session_files.get("read", []))
                print(f"   Files: {_files_rd} read, {_files_mod} modified, {len(_undo_stack)} undo(s)")
                # Token budget from BudgetManager
                try:
                    from mcp_server_nucleus.runtime.token_budget import get_budget_manager, estimate_tokens
                    _bm = get_budget_manager()
                    _sid = os.environ.get("NUCLEUS_SESSION_ID", "default")
                    _usage = _bm.get_usage(_sid) if hasattr(_bm, "get_usage") else None
                    if _usage:
                        _used = _usage.get("total_tokens", 0)
                        _limit = _usage.get("limit", 0)
                        _pct = int((_used / _limit) * 100) if _limit else 0
                        # Estimate cost (rough: $0.50/1M input tokens for Gemini Flash)
                        _cost_per_1m = {"gemini": 0.50, "anthropic": 3.00, "groq": 0.05}
                        _cpm = _cost_per_1m.get(_provider, 0.50)
                        _est_cost = (_used / 1_000_000) * _cpm
                        print(f"   Tokens: {_used:,}/{_limit:,} ({_pct}%)")
                        print(f"   Est. cost: ${_est_cost:.4f} (~${_cpm}/1M tokens for {_provider})")
                    else:
                        # Estimate from history
                        _hist_chars = sum(len(c) for _, c in history)
                        _est_tokens = _hist_chars // 4
                        print(f"   Tokens: ~{_est_tokens:,} estimated (from history)")
                except Exception:
                    _est_tokens = sum(len(c) for _, c in history) // 4
                    print(f"   Tokens: ~{_est_tokens:,} estimated")
                cascade = getattr(TierRouter, "FREE_TIER_CASCADE", [])
                if cascade:
                    print(f"   Free cascade: {' → '.join(cascade)}")
                print()
                continue

            if cmd == "/chat":
                if not brain_dir:
                    print("❌ Cannot save/resume chat: No .brain directory found.\n")
                    continue
                    
                # Safely parse sub-command and optional tag
                sub_parts = parts[1].split(None, 1) if len(parts) > 1 else []
                chat_cmd = sub_parts[0].lower() if sub_parts else ""
                chat_tag = sub_parts[1].strip() if len(sub_parts) > 1 else default_chat_tag
                
                chat_dir = brain_dir / "chat"
                chat_dir.mkdir(exist_ok=True, parents=True)
                
                if chat_cmd == "save":
                    file_path = chat_dir / f"{chat_tag}.json"
                    data = {"history": history, "turn_count": turn_count}
                    try:
                        import json
                        file_path.write_text(json.dumps(data, indent=2))
                        print(f"✅ Chat saved to tag '{chat_tag}'\n")
                    except Exception as e:
                        print(f"❌ Failed to save chat: {e}\n")
                        
                elif chat_cmd == "resume":
                    file_path = chat_dir / f"{chat_tag}.json"
                    if file_path.exists():
                        try:
                            import json
                            data = json.loads(file_path.read_text())
                            history.clear()
                            history.extend(_normalize_history(data.get("history", [])))
                            turn_count = data.get("turn_count", 0)
                            print(f"✅ Chat resumed from tag '{chat_tag}' ({turn_count} turns)\n")
                        except Exception as e:
                            print(f"❌ Failed to resume chat: {e}\n")
                    else:
                        print(f"❌ Chat tag '{chat_tag}' not found.\n")
                        
                elif chat_cmd == "link":
                    if len(sub_parts) < 2:
                        print("❌ Usage: /chat link <id>\n")
                    else:
                        new_id = sub_parts[1].strip()
                        if "/" in new_id or " " in new_id:
                            print("❌ Invalid Chat ID. Do not include spaces or slashes.\n")
                        else:
                            default_chat_tag = new_id
                            print(f"🔗 Mother Chat ID manually linked to: {default_chat_tag}\n")
                        
                elif chat_cmd == "list":
                    files = list(chat_dir.glob("*.json"))
                    if not files:
                        print("📂 No saved chats found.\n")
                    else:
                        print("📂 Saved chats:")
                        for f in files:
                            mark = " (active)" if f.stem == default_chat_tag else ""
                            print(f"   - {f.stem}{mark}")
                        print()
                else:
                    print(f"❓ Usage: /chat save [tag] | /chat resume [tag] | /chat list | /chat link <id>")
                    print(f"   (If no tag is provided, defaults to active Mother ID: {default_chat_tag})\n")
                continue

            if cmd == "/retry":
                if history:
                    # Capture the rejected response BEFORE trimming history
                    for _ri in range(len(history) - 1, -1, -1):
                        if history[_ri][0] == "assistant":
                            break
                    # Find the last user message
                    for _ri in range(len(history) - 1, -1, -1):
                        if history[_ri][0] == "user":
                            user_input = history[_ri][1]
                            # Remove last turn so it doesn't duplicate
                            history = history[:_ri]
                            turn_count = max(0, turn_count - 1)
                            break
                    else:
                        print("❌ No previous message to retry.\n")
                        continue
                else:
                    print("❌ No previous message to retry.\n")
                    continue

            else:
                print(f"❓ Unknown command: {cmd}. Type /help for available commands.\n")
                continue

        # ── LLM Call (ReAct Loop) ───────────────────────────────
        # No hard step limit — deep work needs unlimited reasoning.
        # Safety comes from token budget (already enforced in llm_client)
        # and Ctrl+C (user can always interrupt).
        _suppress_echo()  # Block keystroke echo while agent is working
        reply_chunks = []  # Defined here so KeyboardInterrupt handler can access partial output
        try:
            current_input = _expand_at_files(user_input)
            react_step = 0

            while True:
                # Native multi-turn for Anthropic/Groq; string-concat for Gemini
                # Claude Code maintains its own context via --resume, so just pass current input
                if _provider in ():
                    prompt = current_input  # Plain string — Claude Code has the full session
                elif _provider in ("anthropic", "groq", "local"):
                    prompt = _build_messages(current_input)
                else:
                    prompt = _build_prompt(current_input)

                response = None
                success = False
                last_err = ""

                # ── Select working LLM (Gemini sweeper or direct) ──
                if _provider == "gemini":
                    # --- The Sweeper (Auto-Rotation) --- Gemini only
                    models_to_try = [llm.model_name]
                    if getattr(TierRouter, "SOVEREIGN_CASCADE", None):
                        for m in TierRouter.SOVEREIGN_CASCADE:
                            if m not in models_to_try:
                                models_to_try.append(m)

                    for m_idx, try_model in enumerate(models_to_try):
                        if success: break
                        start_k = current_key_idx if m_idx == 0 else 0
                        for k_offset in range(len(api_keys)):
                            k_idx = (start_k + k_offset) % len(api_keys)
                            key = api_keys[k_idx]
                            if key: os.environ["GEMINI_API_KEY"] = key
                            try:
                                temp_llm = _init_llm(tier, try_model, sys_instruction=sys_prompt)
                                if getattr(temp_llm, "model_name", None) != getattr(llm, "model_name", None):
                                    print(f"\n   [🔄 Rotating to Model: {getattr(temp_llm, 'model_name', try_model)}]")
                                llm = temp_llm
                                # Stream the response
                                reply_chunks = []
                                _first_chunk = True
                                for chunk in llm.stream_content(prompt):
                                    if _first_chunk:
                                        sys.stdout.write("\n🧠 ")
                                        _first_chunk = False
                                    sys.stdout.write(chunk)
                                    sys.stdout.flush()
                                    reply_chunks.append(chunk)
                                print("\n")
                                reply = "".join(reply_chunks).strip()
                                success = True
                                current_key_idx = k_idx
                                break
                            except Exception as e:
                                last_err = str(e)
                                if "429" in last_err or "503" in last_err or "404" in last_err or "quota" in last_err.lower():
                                    key_hint = f"...{key[-4:]}" if key else ""
                                    if "{" in last_err and "quota" in last_err.lower():
                                        err_msg = "Quota exceeded (429)"
                                    else:
                                        err_msg = last_err.splitlines()[0] if last_err else "Unknown Error"
                                    if is_initial or k_offset > 0 or m_idx > 0:
                                        print(f"   [⚠️  Key #{k_idx+1} ({key_hint}) failed on {try_model}: {err_msg}]")
                                    continue
                                else:
                                    break
                if _provider in ():
                    # --- Claude Code: tools handled internally by claude CLI ---
                    # Claude Code has its own agent loop (Read, Edit, Bash, etc.)
                    # We just stream the final response — no ReAct loop needed.
                    try:
                        reply_chunks = []
                        _first_chunk = True
                        for chunk in llm.stream_content(prompt):
                            if _first_chunk:
                                sys.stdout.write("\n🧠 ")
                                _first_chunk = False
                            sys.stdout.write(chunk)
                            sys.stdout.flush()
                            reply_chunks.append(chunk)
                        print("\n")
                        reply = "".join(reply_chunks).strip()
                        success = True
                    except Exception as e:
                        last_err = str(e)

                if hasattr(llm, "stream_with_tools") and _model_supports_tools(llm.model_name):
                    # --- Native Tool Calling (Anthropic / Groq 70b+) ---
                    _tool_stop_reasons = ("tool_use", "tool_calls")
                    try:
                        reply_chunks = []
                        _first_chunk = True
                        for chunk in llm.stream_with_tools(prompt):
                            if _first_chunk:
                                sys.stdout.write("\n🧠 ")
                                _first_chunk = False
                            sys.stdout.write(chunk)
                            sys.stdout.flush()
                            reply_chunks.append(chunk)
                        print("\n")
                        reply = "".join(reply_chunks).strip()
                        success = True

                        # Check for native tool calls
                        if llm.last_tool_calls and llm.last_stop_reason in _tool_stop_reasons:
                            for tc in llm.last_tool_calls:
                                react_step += 1
                                tool_result = _execute_tool(tc["name"], tc["input"], react_step)
                                history.append(("user", current_input))
                                history.append(("assistant", reply))
                                _tool_label = tc["input"].get("command") or tc["input"].get("path") or tc["input"].get("pattern") or tc["name"]
                                current_input = f"[Tool Result for `{_tool_label}`]\n{tool_result[:4000]}"
                                is_initial = False
                            if llm.last_stop_reason in _tool_stop_reasons:
                                continue  # ReAct loop continues
                    except Exception as e:
                        err_str = str(e)
                        # "Failed to call a function" — fall back to plain streaming
                        if "failed_generation" in err_str.lower() or "failed to call a function" in err_str.lower():
                            try:
                                reply_chunks = []
                                _first_chunk = True
                                for chunk in llm.stream_content(prompt):
                                    if _first_chunk:
                                        sys.stdout.write("\n🧠 ")
                                        _first_chunk = False
                                    sys.stdout.write(chunk)
                                    sys.stdout.flush()
                                    reply_chunks.append(chunk)
                                print("\n")
                                reply = "".join(reply_chunks).strip()
                                success = True
                            except Exception as e2:
                                last_err = str(e2)
                        else:
                            last_err = err_str

                else:
                    # --- Plain streaming (small models / no tool calling) ---
                    try:
                        reply_chunks = []
                        _first_chunk = True
                        for chunk in llm.stream_content(prompt):
                            if _first_chunk:
                                sys.stdout.write("\n🧠 ")
                                _first_chunk = False
                            sys.stdout.write(chunk)
                            sys.stdout.flush()
                            reply_chunks.append(chunk)
                        print("\n")
                        reply = "".join(reply_chunks).strip()
                        success = True
                    except Exception as e:
                        last_err = str(e)

                # ── Groq Auto-Rotation: try next model on rate limit ──
                if not success and _provider == "groq" and ("429" in last_err or "rate_limit" in last_err.lower()):
                    _groq_cascade = [
                        "llama-3.3-70b-versatile",
                        "meta-llama/llama-4-scout-17b-16e-instruct",
                        "qwen/qwen3-32b",
                        "llama-3.1-8b-instant",
                    ]
                    _tried = llm.model_name
                    for _fallback_model in _groq_cascade:
                        if _fallback_model == _tried:
                            continue
                        try:
                            llm = _init_llm(tier, _fallback_model, sys_instruction=sys_prompt)
                            print(f"   [🔄 {_tried} rate limited → rotating to {_fallback_model}]")
                            # Rebuild prompt (model switch may change tool support)
                            if _provider in ("anthropic", "groq"):
                                prompt = _build_messages(current_input)
                            else:
                                prompt = _build_prompt(current_input)
                            # Try streaming with the fallback model
                            reply_chunks = []
                            _first_chunk = True
                            _uses_tools = hasattr(llm, "stream_with_tools") and _model_supports_tools(_fallback_model)
                            _stream_fn = llm.stream_with_tools if _uses_tools else llm.stream_content
                            for chunk in _stream_fn(prompt):
                                if _first_chunk:
                                    sys.stdout.write("\n🧠 ")
                                    _first_chunk = False
                                sys.stdout.write(chunk)
                                sys.stdout.flush()
                                reply_chunks.append(chunk)
                            print("\n")
                            reply = "".join(reply_chunks).strip()
                            success = True
                            # Handle native tool calls from fallback model
                            if _uses_tools and llm.last_tool_calls and llm.last_stop_reason in ("tool_use", "tool_calls"):
                                for tc in llm.last_tool_calls:
                                    react_step += 1
                                    tool_result = _execute_tool(tc["name"], tc["input"], react_step)
                                    history.append(("user", current_input))
                                    history.append(("assistant", reply))
                                    _tool_label = tc["input"].get("command") or tc["input"].get("path") or tc["input"].get("pattern") or tc["name"]
                                    current_input = f"[Tool Result for `{_tool_label}`]\n{tool_result[:4000]}"
                                    is_initial = False
                                # Continue ReAct loop if tool was called
                                if llm.last_tool_calls and llm.last_stop_reason in ("tool_use", "tool_calls"):
                                    break  # Break cascade, outer while loop continues
                            break
                        except Exception:
                            _tried = _fallback_model
                            continue  # Try next model in cascade

                if not success:
                    # Friendly error messages for known issues
                    _le = last_err.lower()
                    _is_oauth = getattr(llm, "_oauth_mode", False)
                    if ("authentication" in _le or "invalid x-api-key" in _le or "invalid api key" in _le or "401" in last_err) and _is_oauth:
                        # OAuth token might be expired or invalid
                        print(f"⚠️  OAuth authentication failed. Token may be expired.")
                        print(f"   Try: Open Claude Code once to refresh the token, then retry.\n")
                        # Attempt auto-refresh
                        if hasattr(llm, "refresh_oauth_token") and llm.refresh_oauth_token():
                            print(f"   🔑 Token refreshed! Try your message again.\n")
                    elif "credit balance" in _le or "purchase credits" in _le:
                        if _is_oauth:
                            print(f"⚠️  Claude Code OAuth error (may be auth, not billing).")
                            print(f"   Raw: {last_err[:200]}")
                            print(f"   Try: Open Claude Code to refresh session, then retry.\n")
                        else:
                            print(f"⚠️  {_provider.capitalize()} credits exhausted. Top up at your provider's billing page.")
                            print(f"   Or switch: /provider groq (free) or /provider gemini\n")
                    elif "429" in last_err or "rate_limit" in _le or "Rate limit" in last_err:
                        import re as _re
                        wait = _re.search(r"try again in (\d+m[\d.]+s|\d+[\d.]*s)", last_err)
                        if wait:
                            # Clean up ugly floats: 14m4.127999999s → 14m4s
                            _wt = _re.sub(r'(\d+)\.\d+s', r'\1s', wait.group(1))
                            wait_hint = f" Wait {_wt}."
                        else:
                            wait_hint = ""
                        print(f"⚠️  Rate limited on {llm.model_name}.{wait_hint}")
                        print(f"   Try: /model <other> or /provider <other>\n")
                    elif "decommissioned" in _le or "no longer supported" in _le:
                        print(f"⚠️  Model '{llm.model_name}' has been retired by {_provider}.")
                        print(f"   Try: /model to see available models\n")
                    elif "GEMINI_API_KEY" in last_err or "API key" in last_err:
                        print(f"⚠️  No API key. Use /auth <key> to set one.\n")
                    else:
                        # Extract just the message from JSON error blobs
                        import re as _re
                        msg_match = _re.search(r"'message':\s*'([^']+)'", last_err)
                        if msg_match:
                            print(f"❌ {msg_match.group(1)[:150]}\n")
                        else:
                            print(f"❌ {last_err[:150]}\n")
                    break
                is_initial = False

                # ── ReAct: check for tool calls in text response ──
                import re
                _text_tool_call = None  # (tool_name, tool_input) or None

                # 1. <execute_tool>{JSON}</execute_tool> — structured file tools
                _etool_m = re.search(r"<execute_tool>(.*?)</execute_tool>", reply, flags=re.DOTALL)
                if _etool_m:
                    try:
                        import json as _json
                        _td = _json.loads(_etool_m.group(1).strip())
                        _tn = _td.pop("tool", "")
                        if _tn in ("read_file", "write_file", "edit_file", "search_files", "search_code", "write_engram", "search_engrams", "list_tasks", "add_task", "update_task"):
                            _text_tool_call = (_tn, _td)
                    except Exception:
                        pass

                # 2. <execute>cmd</execute> — shell commands
                if not _text_tool_call:
                    _exec_m = re.search(r"<execute>(.*?)</execute>", reply, flags=re.DOTALL)
                    if _exec_m:
                        _text_tool_call = ("shell_execute", {"command": _exec_m.group(1).strip()})

                # 3. Text-pattern tool calls (models printing tool calls as plain text)
                if not _text_tool_call:
                    # JSON-style: tool_name({"key": "val"}) or tool_name={"key": "val"}
                    _RICH_TOOLS = ("read_file", "write_file", "edit_file", "search_files", "search_code", "write_engram", "search_engrams", "list_tasks", "add_task", "update_task")
                    for _tname in _RICH_TOOLS:
                        _jm = re.search(rf'{_tname}[=(]\s*(\{{.*?\}})', reply, re.DOTALL)
                        if _jm:
                            try:
                                import json as _json
                                _text_tool_call = (_tname, _json.loads(_jm.group(1)))
                            except Exception:
                                pass
                            break

                # 4. shell_execute text patterns (various formats from Groq etc.)
                if not _text_tool_call:
                    for tc_pattern in [
                        r'shell_execute[=(]\s*\{["\']command["\']\s*:\s*["\']([^"\']+)["\']',
                        r'function=shell_execute>\s*\{\s*"command"\s*:\s*"([^"]+)"',
                        r"shell_execute\(\[?['\"]([^'\"]+)['\"]\]?\)",
                        r'shell_execute:\s+(.+?)$',
                    ]:
                        tc_match = re.search(tc_pattern, reply, re.DOTALL | re.MULTILINE)
                        if tc_match:
                            _text_tool_call = ("shell_execute", {"command": tc_match.group(1).strip()})
                            break

                if _text_tool_call:
                    react_step += 1
                    _tn, _ti = _text_tool_call
                    tool_result = _execute_tool(_tn, _ti, react_step)
                    _tool_label = _ti.get("command") or _ti.get("path") or _ti.get("pattern") or _tn


                    history.append(("user", current_input))
                    history.append(("assistant", reply))

                    current_input = f"[Tool Result for `{_tool_label}`]\n{tool_result[:4000]}\nProvide your next response or use another tool."
                    is_initial = False
                    continue

                # No tool call = final response
                if not reply:
                    print("   [Empty response — try /retry or /model <other>]\n")
                    break

                # ── Dual-Agent Review ──────────────────────────
                if _dual_mode and _dual_reviewer and reply:
                    try:
                        review_prompt = (
                            f"User asked: {current_input}\n\n"
                            f"Primary AI ({_provider}) responded:\n{reply}\n\n"
                            "Give a concise review (2-3 bullets max). "
                            "Flag errors, missed points, or say 'Looks good' if solid."
                        )
                        review_chunks = []
                        _first_review = True
                        for chunk in _dual_reviewer.stream_content(review_prompt):
                            if _first_review:
                                sys.stdout.write(f"   [{_dual_reviewer_name} review] ")
                                _first_review = False
                            sys.stdout.write(chunk)
                            sys.stdout.flush()
                            review_chunks.append(chunk)
                        print("\n")
                        review_text = "".join(review_chunks).strip()
                        history.append(("reviewer", f"[{_dual_reviewer_name}] {review_text}"))
                    except Exception as e:
                        print(f"   [Review failed: {str(e)[:100]}]\n")

                history.append(("user", current_input))
                history.append(("assistant", reply))
                turn_count += 1


                # Auto-save (context window state — overwrites each time)
                if brain_dir:
                    try:
                        import json
                        chat_dir = brain_dir / "chat"
                        chat_dir.mkdir(exist_ok=True, parents=True)
                        latest_file = chat_dir / f"{default_chat_tag}.json"
                        data = {"history": history, "turn_count": turn_count}
                        latest_file.write_text(json.dumps(data, indent=2))
                    except Exception:
                        pass

                # Per-turn cost estimate (quiet — just a subtle line)
                if react_step > 0:
                    # Multi-step turn: show step count
                    print(f"   [{react_step} tool call(s) this turn]")

                break # Exit ReAct loop

        except KeyboardInterrupt:
            _restore_echo()
            # Ctrl+C during streaming → cancel current response, return to prompt
            partial = "".join(reply_chunks)
            if partial:
                print(f"\n\n   [Cancelled after {len(partial)} chars]\n")
            else:
                print("\n   [Cancelled]\n")
            # Don't break — return to input prompt
        except Exception as e:
            _restore_echo()
            err = str(e)
            _el = err.lower()
            if "token budget exceeded" in _el or "budget exceeded" in _el:
                print(f"\n⚠️  Token budget exceeded. Auto-compacting conversation...")
                # Auto-compact: save context to brain, then slim history
                if len(history) > 4:
                    old_count = len(history)
                    dropped = history[:-4]
                    _topics = []
                    for role, msg in dropped:
                        if role == "user" and not msg.startswith("[Tool Result"):
                            _topics.append(msg.strip().split("\n")[0][:60])
                    if _topics and brain_dir:
                        try:
                            from mcp_server_nucleus.runtime.engram_ops import _brain_write_engram_impl
                            from datetime import datetime
                            ts = datetime.now().strftime("%Y%m%d_%H%M")
                            _summary = f"Auto-compacted ({len(dropped)//2} turns): " + " | ".join(_topics[:5])
                            _brain_write_engram_impl(f"compact_{ts}", _summary[:500], "Decision", 3)
                        except Exception:
                            pass
                    kept = history[-4:]
                    history.clear()
                    history.append(("system", f"[Previous {old_count // 2 - 2} turn(s) auto-compacted — context saved to brain]"))
                    history.extend(kept)
                    print(f"   📎 Compacted: {old_count} → {len(history)} messages. Try your message again.\n")
                else:
                    print(f"   History too short to compact. Try /provider groq (free) or wait for budget reset.\n")
            if "credit balance" in _el or "purchase credits" in _el:
                _is_oauth = getattr(llm, "_oauth_mode", False)
                if _is_oauth:
                    print(f"\n⚠️  Claude Code OAuth error. Raw: {err[:200]}")
                    print(f"   Try: Open Claude Code to refresh, then retry.\n")
                else:
                    print(f"\n⚠️  {_provider.capitalize()} credits exhausted. Top up or switch: /provider groq\n")
            elif "429" in err or "RESOURCE_EXHAUSTED" in err or "rate_limit" in _el:
                print(f"\n⚠️  Rate limited on {llm.model_name}. Try /model <other> or /provider <other>\n")
            elif "decommissioned" in _el or "no longer supported" in _el:
                print(f"\n⚠️  Model '{llm.model_name}' has been retired. Try /model to see available models\n")
            elif "404" in err:
                print(f"\n❌ Model '{llm.model_name}' not found. Try /model to see available models\n")
            elif "GEMINI_API_KEY" in err or "API key" in err:
                print(f"\n❌ No API key. Use /auth <key> to set one.\n")
            else:
                print(f"\n❌ Error: {err[:150]}\n")

    # ── Auto-Engram: save session summary on exit ─────────────
    if brain_dir and turn_count >= 3:
        try:
            from mcp_server_nucleus.runtime.engram_ops import _brain_write_engram_impl
            # Extract topics from user messages
            user_msgs = [msg for role, msg in history if role == "user"][:10]
            topics = []
            for msg in user_msgs:
                # Take first 60 chars of each user message as a topic hint
                topic = msg.strip().split("\n")[0][:60]
                if topic and not topic.startswith("[Tool Result") and not topic.startswith("[Command Result"):
                    topics.append(topic)
            if topics:
                from datetime import datetime
                ts = datetime.now().strftime("%Y%m%d_%H%M")
                # Include file changes in summary
                _mod_files = list(set(_session_files.get("edited", []) + _session_files.get("written", [])))
                _files_note = ""
                if _mod_files:
                    _short_names = [Path(f).name for f in _mod_files[:5]]
                    _files_note = f" | Modified: {', '.join(_short_names)}"
                summary = f"Chat session ({turn_count} turns): " + " | ".join(topics[:5]) + _files_note
                _brain_write_engram_impl(
                    f"session_{ts}",
                    summary[:500],
                    "Decision",
                    3  # Low intensity — just session notes
                )
                print(f"  🧠 Session summary saved to brain ({len(topics)} topic(s))")

                try:
                    _archive = ArchivePipeline()
                    _brother = "cowork" if _provider in () else "code"
                    # Build conversation from history — father's words + brother's responses
                    _conv = []
                    for _role, _msg in history[-50:]:  # Last 50 turns max
                        if _role == "user" and _msg and len(_msg) > 3:
                            _conv.append({"role": "user", "content": _msg[:2000]})
                        elif _role == "assistant" and _msg and len(_msg) > 3:
                            _conv.append({"role": "assistant", "content": _msg[:2000]})
                    _archive.record_turn(
                        brother=_brother,
                        intent=topics[0] if topics else "Chat session",
                        actions=[t[:100] for t in topics[:10]],
                        tools_used=list(set(_tool for _tool in _session_files.get("tools_called", []))),
                        decisions=[],
                        outcome=summary[:500],
                        signal_absorbed=[],
                        signal_produced=[f"engram/session_{ts}"] + [Path(f).name for f in _mod_files[:5]],
                        confidence=0.8,
                        context=f"nucleus brother ({_provider}) — {turn_count} turns",
                        conversation=_conv)
                    print(f"  📊 Loop turn archived for training")
                except Exception:
                    pass
        except Exception:
            pass  # Silent — never block exit


def main():
    """CLI entry point."""
    parser = argparse.ArgumentParser(
        prog='nucleus',
        description='Nucleus Control Plane CLI - Manage your AI coordination system'
    )
    
    from mcp_server_nucleus import __version__
    parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
    parser.add_argument('--self-heal', action='store_true', default=True,
                        help='Enable self-healing error handler (default: on)')
    parser.add_argument('--no-self-heal', dest='self_heal', action='store_false',
                        help='Disable self-healing, raise errors normally')
    subparsers = parser.add_subparsers(dest='cli_command', help='Available commands')

    # Help command (curated, user-friendly)
    subparsers.add_parser('help', help='Show curated command guide')
    
    # ============================================================
    # INIT COMMAND
    # ============================================================
    init_parser = subparsers.add_parser('init', help='Initialize a new .brain directory')
    init_parser.add_argument(
        'path',
        nargs='?',
        default='.brain',
        help='Path to create .brain directory (default: .brain)'
    )
    init_parser.add_argument(
        '-t', '--template',
        choices=['default', 'solo'],
        default='default',
        help='Template to use: default (full) or solo (minimal)'
    )
    init_parser.add_argument(
        '--sidecar',
        action='store_true',
        help='Start the discovery sidecar (HTTP bridge) immediately after init'
    )
    init_parser.add_argument(
        '--recipe',
        type=str,
        default=None,
        help='Install a recipe after init (e.g., founder, sre, adhd)'
    )
    
    # nucleus recipe — Browse and install workflow packs
    recipe_parser = subparsers.add_parser('recipe', help='Browse and install workflow recipe packs')
    recipe_sub = recipe_parser.add_subparsers(dest='recipe_action')
    recipe_sub.add_parser('list', help='List available recipes')
    recipe_install_parser = recipe_sub.add_parser('install', help='Install a recipe into your brain')
    recipe_install_parser.add_argument('recipe_name', help='Recipe name (e.g., founder, sre, adhd)')

    # nucleus setup — Configure MCP clients for your brain
    setup_ide_parser = subparsers.add_parser('setup',
        help='Configure MCP clients (Claude, Cursor, Windsurf, etc.) for your brain')
    setup_ide_parser.add_argument('--brain-path', type=str, default=None,
        help='Path to .brain directory (auto-detects if not set)')
    setup_ide_parser.add_argument('--dry-run', action='store_true',
        help='Show what would be configured without writing')
    setup_ide_parser.add_argument('--force', action='store_true',
        help='Overwrite existing nucleus config in each IDE')

    # nucleus self-setup - Meta-config: Automatically add Nucleus paths to your shell profile
    setup_parser = subparsers.add_parser('self-setup', help='🛠 Meta-config: Automatically add Nucleus paths to your shell profile')
    setup_parser.add_argument('--dry-run', action='store_true', help='Show what changes would be made without applying them')
    
    # ============================================================
    # INSTALL COMMAND
    # ============================================================
    install_parser = subparsers.add_parser('install', help='Install an agent from a .nuke artifact')
    install_parser.add_argument('path', help='Path to the .nuke artifact file')

    # ============================================================
    # LICENSE COMMANDS (Nucleus Pro)
    # ============================================================
    activate_parser = subparsers.add_parser('activate', help='Activate a Nucleus Pro license key')
    activate_parser.add_argument('key', help='License key (NUC-PRO-...)')

    subparsers.add_parser('trial', help='Start a 14-day Nucleus Pro trial')

    subparsers.add_parser('license', help='Show current license status')

    
    # ============================================================
    # DEPTH COMMANDS (ADHD Accommodation)
    # ============================================================
    depth_parser = subparsers.add_parser('depth', help='Track conversation depth (ADHD guardrail)')
    depth_subparsers = depth_parser.add_subparsers(dest='depth_action', help='Depth actions')
    
    # nucleus depth show
    depth_subparsers.add_parser('show', help='Show current depth indicator')
    
    # nucleus depth up [--to=N]
    depth_up = depth_subparsers.add_parser('up', help='Come back up one level')
    depth_up.add_argument('--to', type=int, help='Go up to specific level (optional)')
    
    # nucleus depth reset
    depth_subparsers.add_parser('reset', help='Reset to root level')
    
    # nucleus depth max N
    depth_max = depth_subparsers.add_parser('max', help='Set max safe depth')
    depth_max.add_argument('level', type=int, help='Max safe depth (1-10)')
    
    # nucleus depth push TOPIC
    depth_push = depth_subparsers.add_parser('push', help='Go deeper into a topic')
    depth_push.add_argument('topic', help='Topic you are diving into')
    
    # nucleus depth map
    depth_subparsers.add_parser('map', help='Show visual exploration map')
    
    # ============================================================
    # SIPHON COMMAND — Context Centralization
    # ============================================
    siphon_parser = subparsers.add_parser('siphon', help='🌪️ Siphon context from Windsurf and Antigravity into the brain')
    siphon_parser.add_argument('--full', action='store_true', help='Deep siphon (extract all session history)')

    # ============================================================
    # DISTILL COMMAND — SCRP Context Distillation (MVE Week 1)
    # ============================================================
    distill_parser = subparsers.add_parser('distill', help='🧬 Distill Decision Context Atoms (DCAs) from AI conversation artifacts')
    distill_parser.add_argument('--source', choices=['antigravity', 'claude', 'windsurf', 'all'], default='all',
                                help='Source to distill from (default: all)')
    distill_parser.add_argument('--limit', type=int, default=3,
                                help='Max sessions to process per source (default: 3)')
    distill_parser.add_argument('--output', choices=['jsonld', 'engram', 'both'], default='both',
                                help='Output format (default: both)')

    # ============================================================
    # REPLAY COMMAND — SCRP Context Replay (MVE Week 3)
    # ============================================================
    replay_parser = subparsers.add_parser('replay', help='🔄 Replay Decision Context Atoms into a fresh session')
    replay_parser.add_argument('--mode', choices=['system_prompt', 'engram', 'seed'], default='system_prompt',
                               help='Replay mode (default: system_prompt)')
    replay_parser.add_argument('--source', default=None,
                               help='Path to DCA JSONL file (default: latest distillation)')
    replay_parser.add_argument('--min-confidence', type=float, default=0.6,
                               help='Minimum confidence threshold (default: 0.6)')
    replay_parser.add_argument('--max-atoms', type=int, default=50,
                               help='Max atoms to include (default: 50)')
    replay_parser.add_argument('--tags', nargs='*', default=None,
                               help='Filter by tags (e.g., architecture deployment)')

    # ============================================================
    # VALIDATE COMMAND — SCRP H1 Validation (MVE Week 4)
    # ============================================================
    validate_parser = subparsers.add_parser('validate', help='🧪 Run SCRP hypothesis validation tests')
    validate_parser.add_argument('--test', choices=['h1'], default='h1',
                                 help='Which test to run (default: h1)')
    validate_parser.add_argument('--scenarios', type=int, default=5,
                                 help='Number of scenarios (default: 5)')
    
    # ============================================================
    # FEATURES COMMANDS (Feature Map)
    # ============================================================
    features_parser = subparsers.add_parser('features', help='Manage product feature map')
    features_subparsers = features_parser.add_subparsers(dest='features_action', help='Feature actions')
    
    # nucleus features list [--product=X] [--status=X]
    features_list = features_subparsers.add_parser('list', help='List all features')
    features_list.add_argument('--product', help='Filter by product name')
    features_list.add_argument('--status', help='Filter by status')
    
    # nucleus features test <id>
    features_test = features_subparsers.add_parser('test', help='Show test instructions for a feature')
    features_test.add_argument('id', help='Feature ID to get test instructions for')
    
    # nucleus features search <query>
    features_search = features_subparsers.add_parser('search', help='Search features')
    features_search.add_argument('query', help='Search query')
    
    # nucleus features proof <id>
    features_proof = features_subparsers.add_parser('proof', help='Show proof document for a feature')
    features_proof.add_argument('id', help='Feature ID to show proof for')
    
    # nucleus sessions - Session management commands
    sessions_parser = subparsers.add_parser('sessions', help='Session management commands')
    sessions_subparsers = sessions_parser.add_subparsers(dest='sessions_action')
    
    # nucleus sessions list
    sessions_subparsers.add_parser('list', help='List all saved sessions')
    
    # nucleus sessions save <context>
    sessions_save = sessions_subparsers.add_parser('save', help='Save current session')
    sessions_save.add_argument('context', help='Context name (e.g., "Nucleus v1.2.1")')
    sessions_save.add_argument('--task', help='Current active task')
    
    # nucleus sessions resume [id]
    sessions_resume = sessions_subparsers.add_parser('resume', help='Resume a saved session')
    sessions_resume.add_argument('id', nargs='?', help='Session ID to resume (defaults to most recent)')
    
    # ============================================================
    # RECOVER COMMAND — Universal Session Recovery
    # ============================================================
    recover_parser = subparsers.add_parser('recover', help='🆘 Universal session recovery for frozen/bloated conversations')
    recover_subparsers = recover_parser.add_subparsers(dest='recover_action', help='Recovery actions')
    
    # nucleus recover detect
    recover_subparsers.add_parser('detect', help='Detect bloated conversations')
    
    # nucleus recover extract <conversation-id>
    recover_extract = recover_subparsers.add_parser('extract', help='Extract context from conversation')
    recover_extract.add_argument('conversation_id', help='Conversation UUID to extract')
    
    # nucleus recover quarantine <conversation-id>
    recover_quarantine = recover_subparsers.add_parser('quarantine', help='Quarantine bloated files')
    recover_quarantine.add_argument('conversation_id', help='Conversation UUID to quarantine')
    
    # nucleus recover bootstrap <conversation-id>
    recover_bootstrap = recover_subparsers.add_parser('bootstrap', help='Generate fresh session with context')
    recover_bootstrap.add_argument('conversation_id', help='Conversation UUID to bootstrap from')
    
    # nucleus recover rewrite <old-id> <new-id>
    recover_rewrite = recover_subparsers.add_parser('rewrite', help='Rewrite test script paths')
    recover_rewrite.add_argument('old_id', help='Old conversation UUID')
    recover_rewrite.add_argument('new_id', help='New session UUID')
    recover_rewrite.add_argument('--dry-run', action='store_true', help='Show changes without applying')
    
    # nucleus recover auto <conversation-id>
    recover_auto = recover_subparsers.add_parser('auto', help='One-shot automatic recovery')
    recover_auto.add_argument('conversation_id', help='Conversation UUID to recover')
    
    # ============================================================
    # MORNING BRIEF — THE ALIVE WORKFLOW (MDR_015)
    # ============================================================
    brief_parser = subparsers.add_parser('morning-brief', help='🧠 The Alive Workflow — your daily brief')
    brief_parser.add_argument('--json', action='store_true', help='Output as JSON instead of formatted')
    
    # nucleus loop - Compounding v0 Loop status
    loop_parser = subparsers.add_parser('loop', help='🔄 Compounding v0 Loop status')
    loop_parser.add_argument('--json', action='store_true', help='Output as JSON')
    
    # nucleus end-of-day - Capture learnings
    eod_parser = subparsers.add_parser('end-of-day', help='📝 Capture end-of-day learnings')
    eod_parser.add_argument('summary', help='What was accomplished today (2-3 sentences)')
    eod_parser.add_argument('--decisions', nargs='*', help='Key decisions made')
    eod_parser.add_argument('--blockers', nargs='*', help='Blockers encountered')
    
    # --- STATUS SUBCOMMAND (SATELLITE VIEW) ---
    status_parser = subparsers.add_parser('status', help='Show unified satellite view of the brain')
    status_parser.add_argument('--minimal', action='store_true', help='Show minimal view (depth only)')
    status_parser.add_argument('--sprint', action='store_true', help='Show sprint view (includes tasks)')
    status_parser.add_argument('--full', action='store_true', help='Show full view (includes session)')
    status_parser.add_argument('--json', action='store_true', help='Output as JSON')
    status_parser.add_argument('--health', action='store_true', help='Perform active liveness check (Proxy, Kernel, Locks)')
    status_parser.add_argument('--cleanup-lock', action='store_true', help='Forcefully clear a confirmed stale BrainLock')
    status_parser.add_argument('--quiet', '-q', action='store_true', help='Bare output for scripting')
    status_parser.add_argument('--format', choices=['json', 'table'], default=None, help='Output format (json or table)')
    
    # --- CONSOLIDATE SUBCOMMAND ---
    consolidate_parser = subparsers.add_parser('consolidate', help='Brain consolidation and cleanup operations')
    consolidate_subparsers = consolidate_parser.add_subparsers(dest='consolidate_action', help='Consolidation commands')
    
    
    # nucleus consolidate propose
    consolidate_subparsers.add_parser('propose', help='Detect redundant artifacts and generate merge proposals')
    
    # nucleus consolidate status
    consolidate_subparsers.add_parser('status', help='Show consolidation status and archive info')
    
    # nucleus consolidate tasks [--dry-run] [--max-age=72]
    tasks_gc = consolidate_subparsers.add_parser('tasks', help='Garbage collect stale and auto-generated tasks')
    tasks_gc.add_argument('--dry-run', action='store_true', help='Preview what would be archived without doing it')
    tasks_gc.add_argument('--max-age', type=int, default=72, help='Hours of inactivity before archiving (default: 72)')
    
    # ============================================================
    # GRAPH COMMAND — Context Graph visualization
    # ============================================================
    graph_parser = subparsers.add_parser('graph', help='Visualize the engram context graph')
    graph_parser.add_argument('--max-nodes', type=int, default=30, help='Max nodes to display (default: 30)')
    graph_parser.add_argument('--min-intensity', type=int, default=1, help='Minimum engram intensity (default: 1)')
    graph_parser.add_argument('--json', action='store_true', help='Output raw graph JSON instead of ASCII')
    graph_parser.add_argument('--neighbors', metavar='KEY', help='Show neighborhood of a specific engram key')
    graph_parser.add_argument('--depth', type=int, default=1, help='BFS depth for --neighbors (default: 1)')

    # ============================================================
    # COMBO COMMAND — God Combo shortcuts
    # ============================================================
    combo_parser = subparsers.add_parser('combo', help='Run God Combos — multi-tool automation pipelines')
    combo_subparsers = combo_parser.add_subparsers(dest='combo_action', help='God Combo actions')

    # nucleus combo pulse
    combo_subparsers.add_parser('pulse', help='Pulse & Polish — automated health check (metrics → audit → brief → engram)')

    # nucleus combo diagnose <symptom>
    combo_diagnose = combo_subparsers.add_parser('diagnose', help='Self-Healing SRE — diagnose an issue (search → metrics → diagnose → recommend)')
    combo_diagnose.add_argument('symptom', help='Description of the issue (e.g., "high latency", "dispatch errors")')

    # nucleus combo learn <observation>
    combo_learn = combo_subparsers.add_parser('learn', help='Fusion Reactor — compound an observation into memory')
    combo_learn.add_argument('observation', help='Observation or insight to compound (e.g., "cache fix reduced p99 by 40%%")')
    combo_learn.add_argument('--context', default='Decision', choices=['Feature', 'Architecture', 'Brand', 'Strategy', 'Decision'], help='Engram context (default: Decision)')
    combo_learn.add_argument('--intensity', type=int, default=6, help='Base intensity 1-10 (default: 6)')

    # ============================================================
    # BILLING COMMAND — Usage cost tracking
    # ============================================================
    billing_parser = subparsers.add_parser('billing', help='View usage cost tracking from audit logs')
    billing_parser.add_argument('--hours', type=float, help='Only show last N hours (default: all time)')
    billing_parser.add_argument('--group-by', choices=['tool', 'tier', 'session'], default='tool', help='Group costs by tool, tier, or session (default: tool)')
    billing_parser.add_argument('--json', action='store_true', help='Output as JSON')

    # ============================================================
    # COMPLY COMMAND — Jurisdiction Configuration (MVE-2/MVE-3)
    # ============================================================
    comply_parser = subparsers.add_parser('comply', help='🏛️ Configure jurisdiction-specific compliance')
    comply_parser.add_argument(
        '--jurisdiction', '-j',
        choices=['eu-dora', 'sg-mas-trm', 'us-soc2', 'global-default'],
        help='Apply a regulatory jurisdiction configuration'
    )
    comply_parser.add_argument('--list', action='store_true', help='List available jurisdictions')
    comply_parser.add_argument('--report', action='store_true', help='Generate compliance status report')
    comply_parser.add_argument('--brain', default=None, help='Path to .brain directory (default: auto-detect)')

    # ============================================================
    # AUDIT-REPORT COMMAND — Generate Audit Trail Reports (MVE-2)
    # ============================================================
    audit_parser = subparsers.add_parser('audit-report', help='📋 Generate audit-ready compliance report')
    audit_parser.add_argument('--format', choices=['text', 'json', 'html'], default='text', help='Output format')
    audit_parser.add_argument('--hours', type=float, help='Only include events from last N hours')
    audit_parser.add_argument('--output', '-o', help='Write report to file instead of stdout')
    audit_parser.add_argument('--brain', default=None, help='Path to .brain directory (default: auto-detect)')
    audit_parser.add_argument('--signed', action='store_true', help='Cryptographically sign the report (Nucleus Pro)')

    # ============================================================
    # SOVEREIGN COMMAND — Identity & Status (Sovereign Agent OS)
    # ============================================================
    # ============================================================
    # SECURE COMMAND — One-Shot Security Hardening
    # ============================================================
    secure_parser = subparsers.add_parser('secure', help='🔐 One-command security hardening + posture report')
    secure_parser.add_argument('--jurisdiction', '-j', choices=['eu-dora', 'sg-mas-trm', 'us-soc2', 'global-default'],
                               default=None, help='Apply jurisdiction (default: detect or global-default)')
    secure_parser.add_argument('--json', action='store_true', help='Output as JSON')
    secure_parser.add_argument('--brain', default=None, help='Path to .brain directory')

    sovereign_parser = subparsers.add_parser('sovereign', help='Show sovereignty status report')
    sovereign_parser.add_argument('--json', action='store_true', help='Output as JSON')
    sovereign_parser.add_argument('--brain', default=None, help='Path to .brain directory')

    # ============================================================
    # COMPLIANCE-CHECK COMMAND (Nucleus Pro)
    # ============================================================
    cc_parser = subparsers.add_parser('compliance-check', help='Score your AI governance posture (Pro: exportable report)')
    cc_parser.add_argument('--jurisdiction', '-j', choices=['eu-dora', 'sg-mas-trm', 'us-soc2', 'global-default'],
                           default=None, help='Jurisdiction to check against (default: auto-detect)')
    cc_parser.add_argument('--format', choices=['text', 'json', 'html'], default='text', help='Output format')
    cc_parser.add_argument('--output', '-o', help='Write report to file (Pro only)')
    cc_parser.add_argument('--brain', default=None, help='Path to .brain directory')

    # ============================================================
    # SEARCH COMMAND
    # ============================================================
    search_parser = subparsers.add_parser('search', help='Search for agents in the registry')
    search_parser.add_argument('query', help='Search query (name, tag, or description)')

    # ============================================================
    # TRACE COMMAND — DSoR Trace Viewer
    # ============================================================
    trace_parser = subparsers.add_parser('trace', help='📜 Browse DSoR decision trails')
    trace_subparsers = trace_parser.add_subparsers(dest='trace_action', help='Trace actions')

    # nucleus trace list
    trace_list = trace_subparsers.add_parser('list', help='List all DSoR traces')
    trace_list.add_argument('--type', help='Filter by trace type (e.g., KYC_REVIEW)')

    # nucleus trace view <ID>
    trace_view = trace_subparsers.add_parser('view', help='View detailed trace')
    trace_view.add_argument('trace_id', help='Trace ID to view')
    trace_view.add_argument('--json', action='store_true', help='Output as JSON')

    # nucleus trace interference <ID>
    trace_interf = trace_subparsers.add_parser('interference', help='Detect context interference')
    trace_interf.add_argument('node_id', help='Node ID to check (Decision ID or File Path)')
    trace_interf.add_argument('--json', action='store_true', help='Output as JSON')

    # ============================================================
    # DASHBOARD COMMAND
    # ============================================================
    dashboard_parser = subparsers.add_parser('dashboard', help='🖥️  Sovereign Dashboard (Web or CLI)')
    dashboard_parser.add_argument('--port', type=int, default=8080, help='Port to run web dashboard on')
    dashboard_parser.add_argument('--hr', action='store_true', help='Show team-level HR & Sentiment report (ASCII)')
    dashboard_parser.add_argument('--ascii', action='store_true', help='Show full ASCII status dashboard')
    dashboard_parser.add_argument('--brain', default=None, help='Path to .brain directory')

    # ============================================================
    # KYC COMMAND — Demo Compliance Workflow (MVE-2)
    # ============================================================
    kyc_parser = subparsers.add_parser('kyc', help='📝 Run simulated KYC review (compliance demo)')
    kyc_subparsers = kyc_parser.add_subparsers(dest='kyc_action', help='KYC actions')

    # nucleus kyc review [APP-001|APP-002|APP-003]
    kyc_review = kyc_subparsers.add_parser('review', help='Run KYC review on a demo application')
    kyc_review.add_argument('application', nargs='?', default='APP-001',
                            choices=['APP-001', 'APP-002', 'APP-003'],
                            help='Application ID (default: APP-001)')
    kyc_review.add_argument('--json', action='store_true', help='Output as JSON')
    kyc_review.add_argument('--html', action='store_true', help='Generate HTML report')
    kyc_review.add_argument('--output', '-o', help='Write report to file')

    # nucleus kyc list
    
    # ============================================================
    # CHAT COMMAND — Interactive Gemini conversation
    # ============================================================
    chat_parser = subparsers.add_parser('brother', aliases=['chat'], help='🧬 Talk to a Brother — the family intelligence interface')
    chat_parser.add_argument('--tier', choices=['premium', 'standard', 'economy', 'local_paid', 'local_free'],
                             default='local_free', help='LLM tier to use (default: local_free, 500 RPD)')
    chat_parser.add_argument('--model', default=None, help='Override model name directly (e.g., gemini-3-flash)')
    chat_parser.add_argument('--system', default=None, help='Custom system prompt')
    chat_parser.add_argument('--provider', choices=['gemini', 'anthropic', 'groq'], default=None,
                             help='Which provider: gemini (default), anthropic, groq. '
                                  'Anthropic requires NUCLEUS_ANTHROPIC_API_KEY. '
                                  'Groq requires NUCLEUS_GROQ_API_KEY (free at console.groq.com).')
    chat_parser.add_argument('--batch', action='store_true', help='Non-interactive batch mode (single turn, then exit)')
    chat_parser.add_argument('--prompt', type=str, help='Initial prompt for batch mode')
    chat_parser.add_argument('--output-format', choices=['text', 'json'], default='text',
                             help='Output format for batch mode: text (default) or json (structured)')
    chat_parser.add_argument('--context', type=str, default=None,
                             help='Extra context from big brother (injected into prompt for guided delegation)')
    kyc_subparsers.add_parser('list', help='List available demo applications')

    # nucleus kyc demo (runs all 3 in sequence)
    kyc_subparsers.add_parser('demo', help='Run full demo: review all 3 applications')

    # ============================================================
    # SUMMON COMMAND (Recursive Sovereignty)
    # ============================================================
    summon_parser = subparsers.add_parser('summon', help='🧬 Recursive Sovereignty: Summon a specialized sub-agent')
    summon_parser.add_argument('agent', help='Agent role/identity (e.g., Critic, DBA, Researcher)')
    summon_parser.add_argument('task', nargs='?', default='', help='Task for the summoned agent')
    summon_parser.add_argument('--yolo', action='store_true', help='Auto-approve for summoned agent')
    summon_parser.add_argument('--audit-plan', type=str, help='Path to plan file to audit (Critic only)')
    summon_parser.add_argument('--audit-decision', type=str, help='Decision ID to audit (Critic only)')

    # ============================================================
    # MOUNT COMMAND
    # ============================================================
    mount_parser = subparsers.add_parser('mount', help='Manage external MCP mounts')
    mount_subparsers = mount_parser.add_subparsers(dest='mount_action', help='Mount actions')

    # nucleus mount add <id> [options]
    mount_add = mount_subparsers.add_parser('add', help='Add a new mount')
    mount_add.add_argument('id', help='Unique mount ID')
    mount_add.add_argument('--transport', choices=['stdio', 'sse'], default='stdio', help='Transport type')
    mount_add.add_argument('--command', help='Command to execute (stdio)')
    mount_add.add_argument('--args', nargs='*', help='Arguments for command (stdio)')
    mount_add.add_argument('--env', nargs='*', help='Environment variables (KEY=VALUE)')
    mount_add.add_argument('--url', help='URL for SSE transport')

    # nucleus mount list
    mount_subparsers.add_parser('list', help='List all mounts')

    # nucleus mount remove <id>
    mount_remove = mount_subparsers.add_parser('remove', help='Remove a mount')
    mount_remove.add_argument('id', help='Mount ID to remove')

    # ============================================================
    # DEPLOY COMMAND (MVE-3)
    # ============================================================
    deploy_parser = subparsers.add_parser('deploy', help='Deploy jurisdictional environment (MVE-3)')
    deploy_parser.add_argument('--jurisdiction', choices=['eu-dora', 'sg-mas-trm', 'us-soc2', 'global-default'], help='Regulatory jurisdiction profile')
    deploy_parser.add_argument('--dry-run', action='store_true', help='Simulate deployment without changes')

    # ============================================================
    # DOGFOOD COMMAND (30-Day Experiment Tracker)
    # ============================================================
    dogfood_parser = subparsers.add_parser('dogfood', help='30-day dog food test tracker')
    dogfood_subparsers = dogfood_parser.add_subparsers(dest='dogfood_action', help='Dogfood actions')

    # nucleus dogfood log <score> [options]
    dogfood_log = dogfood_subparsers.add_parser('log', help='Log daily pain score')
    dogfood_log.add_argument('score', type=int, help='Pain-if-broken score 1-10')
    dogfood_log.add_argument('--pay', action='store_true', help='Would you pay $29/mo?')
    dogfood_log.add_argument('--faster', type=int, default=0, help='Decisions made faster today')
    dogfood_log.add_argument('--notes', default='', help='Daily notes')

    # nucleus dogfood status
    dogfood_subparsers.add_parser('status', help='Show experiment status')

    # ============================================================
    # HEARTBEAT COMMAND — v1.5.0 "The Alive Brain"
    # Context-triggered proactive agent engagement
    # ============================================================
    heartbeat_parser = subparsers.add_parser('heartbeat', help='💓 Proactive context-triggered agent check-ins')
    heartbeat_subparsers = heartbeat_parser.add_subparsers(dest='heartbeat_action', help='Heartbeat actions')

    # nucleus heartbeat check [--notify] [--format json]
    hb_check = heartbeat_subparsers.add_parser('check', help='Evaluate context triggers and report insights',
        epilog='Examples:\n  nucleus heartbeat check\n  nucleus heartbeat check --format json | jq .triggers\n  nucleus heartbeat check --notify\n  nucleus heartbeat check -q',
        formatter_class=argparse.RawDescriptionHelpFormatter)
    hb_check.add_argument('--notify', action='store_true', help='Send native OS notification if triggers fire')
    hb_check.add_argument('--format', choices=['json', 'table'], default=None,
                          help='Output format (default: table)')
    hb_check.add_argument('-q', '--quiet', action='store_true', help='Bare trigger messages only')
    hb_check.add_argument('--brain-path', default=None, help='Path to .brain directory')

    # nucleus heartbeat install [--interval 30]
    hb_install = heartbeat_subparsers.add_parser('install', help='Install platform-native scheduling (launchd/systemd)')
    hb_install.add_argument('--interval', type=int, default=30, help='Check interval in minutes (default: 30)')
    hb_install.add_argument('--brain-path', default=None, help='Path to .brain directory')

    # nucleus heartbeat uninstall
    heartbeat_subparsers.add_parser('uninstall', help='Remove heartbeat scheduling')

    # nucleus heartbeat status
    hb_status = heartbeat_subparsers.add_parser('status', help='Show heartbeat installation status and recent checks')
    hb_status.add_argument('--format', choices=['json', 'table'], default=None, help='Output format')
    hb_status.add_argument('--brain-path', default=None, help='Path to .brain directory')


    # ============================================================
    # AGENT CLI — v1.4.0 "The Universal Interface"
    # These commands call runtime functions directly and output
    # pipe-friendly JSON (when piped) or tables (when interactive).
    # ============================================================

    def _add_agent_flags(p):
        p.add_argument('--format', choices=['json', 'table', 'tsv'], default=None,
                        help='Output format (default: table if TTY, json if piped)')
        p.add_argument('--brain-path', default=None,
                        help='Path to .brain directory (default: auto-detect)')
        p.add_argument('-q', '--quiet', action='store_true', default=False,
                        help='Bare output (one value per line, no headers — for pipes/xargs)')
        return p

    # ============================================================
    # FEDERATION COMMANDS (Multi-Brain Coordination)
    # ============================================================
    fed_parser = subparsers.add_parser('federation', help='🌐 Multi-brain federation control plane')
    fed_subparsers = fed_parser.add_subparsers(dest='fed_action', help='Federation actions')
    
    # nucleus federation status
    _p = _add_agent_flags(fed_subparsers.add_parser('status', help='Show federation health and consensus status'))
    
    # nucleus federation peers
    _p = _add_agent_flags(fed_subparsers.add_parser('peers', help='List all discovered federation peers'))
    
    # nucleus federation sync
    _p = _add_agent_flags(fed_subparsers.add_parser('sync', help='Trigger a manual state synchronization across peers'))

    # --- ENGRAM COMMANDS ---
    engram_parser = subparsers.add_parser('engram', help='🧠 Agent memory: search, write, query engrams')
    engram_subs = engram_parser.add_subparsers(dest='engram_action', help='Engram actions')

    _p = _add_agent_flags(engram_subs.add_parser('search', help='Search engrams by keyword',
        epilog='Examples:\n  nucleus engram search "auth"\n  nucleus engram search "auth" --format json | jq .key\n  nucleus engram search "auth" -q | xargs -I{} echo "Found: {}"',
        formatter_class=argparse.RawDescriptionHelpFormatter))
    _p.add_argument('query', help='Search query string')
    _p.add_argument('--limit', type=int, default=50, help='Max results (default: 50)')

    _p = _add_agent_flags(engram_subs.add_parser('write', help='Write an engram to memory',
        epilog='Examples:\n  nucleus engram write my_key "important insight"\n  nucleus engram write arch_v2 "new design" --context Architecture --intensity 8\n  nucleus engram write my_key "value" --format json | jq .status',
        formatter_class=argparse.RawDescriptionHelpFormatter))
    _p.add_argument('key', help='Engram key (alphanumeric, min 2 chars)')
    _p.add_argument('value', help='Engram content text')
    _p.add_argument('--context', default='Decision',
                    choices=['Feature', 'Architecture', 'Brand', 'Strategy', 'Decision'],
                    help='Engram context (default: Decision)')
    _p.add_argument('--intensity', type=int, default=5, help='Importance 1-10 (default: 5)')

    _p = _add_agent_flags(engram_subs.add_parser('query', help='Query engrams by context/intensity',
        epilog='Examples:\n  nucleus engram query --context Strategy --limit 10\n  nucleus engram query --min-intensity 7 --format json\n  nucleus engram query -q | wc -l',
        formatter_class=argparse.RawDescriptionHelpFormatter))
    _p.add_argument('--context', default=None, help='Filter by context')
    _p.add_argument('--min-intensity', type=int, default=1, help='Minimum intensity (default: 1)')
    _p.add_argument('--limit', type=int, default=50, help='Max results (default: 50)')

    _p = engram_subs.add_parser('quarantine', help='Quarantine an engram (exclude from brief/search)')
    _p.add_argument('key', help='Engram key to quarantine')
    _p.add_argument('--undo', action='store_true', help='Remove quarantine')

    # --- TASK COMMANDS ---
    task_parser = subparsers.add_parser('task', aliases=['tasks'], help='📋 Task management: list, add, update')
    task_subs = task_parser.add_subparsers(dest='task_action', help='Task actions')

    _p = _add_agent_flags(task_subs.add_parser('list', help='List tasks',
        epilog='Examples:\n  nucleus task list --status READY\n  nucleus task list --format json | jq length\n  nucleus task list -q | wc -l',
        formatter_class=argparse.RawDescriptionHelpFormatter))
    _p.add_argument('--status', default=None, help='Filter by status (e.g., READY, IN_PROGRESS)')
    _p.add_argument('--priority', type=int, default=None, help='Filter by priority')

    _p = _add_agent_flags(task_subs.add_parser('add', help='Create a new task',
        epilog='Examples:\n  nucleus task add "Ship v1.4.1" --priority 1\n  nucleus task add "Review PR" --format json | jq .task_id',
        formatter_class=argparse.RawDescriptionHelpFormatter))
    _p.add_argument('description', help='Task description')
    _p.add_argument('--priority', type=int, default=3, help='Priority 1-5 (default: 3)')

    _p = _add_agent_flags(task_subs.add_parser('update', help='Update a task',
        epilog='Examples:\n  nucleus task update task-abc123 --status DONE\n  nucleus task update task-abc123 --priority 1 --format json',
        formatter_class=argparse.RawDescriptionHelpFormatter))
    _p.add_argument('task_id', help='Task ID to update')
    _p.add_argument('--status', help='New status')
    _p.add_argument('--priority', type=int, help='New priority')
    _p.add_argument('--description', help='New description')

    # --- SESSION COMMANDS (agent-facing) ---
    session_parser = subparsers.add_parser('session', help='💾 Session save/resume for agents')
    session_subs = session_parser.add_subparsers(dest='session_action', help='Session actions')

    _p = _add_agent_flags(session_subs.add_parser('save', help='Save current session',
        epilog='Examples:\n  nucleus session save "Working on CLI polish"\n  nucleus session save "Debugging auth" --task task-abc --format json',
        formatter_class=argparse.RawDescriptionHelpFormatter))
    _p.add_argument('context', help='Session context description')
    _p.add_argument('--task', default=None, help='Current active task')

    _p = _add_agent_flags(session_subs.add_parser('resume', help='Resume a saved session',
        epilog='Examples:\n  nucleus session resume\n  nucleus session resume sess-abc123 --format json',
        formatter_class=argparse.RawDescriptionHelpFormatter))
    _p.add_argument('id', nargs='?', default=None, help='Session ID (default: most recent)')

    # --- GROWTH COMMANDS ---
    growth_parser = subparsers.add_parser('growth', help='📈 Growth pulse and metrics')
    growth_subs = growth_parser.add_subparsers(dest='growth_action', help='Growth actions')

    _add_agent_flags(growth_subs.add_parser('pulse', help='Run daily growth pulse',
        epilog='Examples:\n  nucleus growth pulse\n  nucleus growth pulse --format json | jq .sections',
        formatter_class=argparse.RawDescriptionHelpFormatter))
    _add_agent_flags(growth_subs.add_parser('status', help='Show growth metrics summary',
        epilog='Examples:\n  nucleus growth status\n  nucleus growth status --format json',
        formatter_class=argparse.RawDescriptionHelpFormatter))

    # --- OUTBOUND COMMANDS ---
    outbound_parser = subparsers.add_parser('outbound', help='📤 Outbound posting: check, record, plan')
    outbound_subs = outbound_parser.add_subparsers(dest='outbound_action', help='Outbound actions')

    _p = _add_agent_flags(outbound_subs.add_parser('check', help='Check if content already posted',
        epilog='Examples:\n  nucleus outbound check reddit r/ClaudeAI\n  nucleus outbound check reddit r/ClaudeAI --format json | jq .status',
        formatter_class=argparse.RawDescriptionHelpFormatter))
    _p.add_argument('channel', help='Channel name (e.g., reddit, twitter, hackernews)')
    _p.add_argument('identifier', help='Post identifier (e.g., subreddit, thread ID)')
    _p.add_argument('--body', default='', help='Post body for hash computation')

    _p = _add_agent_flags(outbound_subs.add_parser('record', help='Record a successful post',
        epilog='Examples:\n  nucleus outbound record reddit r/ClaudeAI --permalink https://reddit.com/abc\n  nucleus outbound record reddit r/ClaudeAI --workhorse chrome --format json',
        formatter_class=argparse.RawDescriptionHelpFormatter))
    _p.add_argument('channel', help='Channel name')
    _p.add_argument('identifier', help='Post identifier')
    _p.add_argument('--permalink', default='', help='URL of the published post')
    _p.add_argument('--body', default='', help='Post body text')
    _p.add_argument('--workhorse', default='manual', help='How it was posted (manual, praw, chrome)')

    _p = _add_agent_flags(outbound_subs.add_parser('plan', help='Show posting plan',
        epilog='Examples:\n  nucleus outbound plan\n  nucleus outbound plan --format json | jq ".ready | length"\n  nucleus outbound plan -q',
        formatter_class=argparse.RawDescriptionHelpFormatter))
    _p.add_argument('--channel', default=None, help='Filter by channel')

    # ============================================================
    # RUN COMMAND — Run Nucleus agents (coordinator, etc.)
    # ============================================================
    run_parser = subparsers.add_parser('run', help='🚀 Run Nucleus agents')
    run_subparsers = run_parser.add_subparsers(dest='run_agent', help='Agent to run')

    coord_parser = run_subparsers.add_parser('coordinator',
        help='Start the dual-agent coordinator (Gemini CLI + Cascade loop)')
    coord_parser.add_argument('--task', type=str, help='Initial task for the agent')
    coord_parser.add_argument('--resident', action='store_true', help='Start in Resident Bridge Mode (waits for tasks via .brain/session/bridge.task)')
    coord_parser.add_argument('--no-resume', action='store_true', help='Start fresh Gemini session instead of resuming')
    coord_parser.add_argument('--gemini-yolo', action='store_true', help='Launch Gemini CLI with --yolo (auto-approve tools like run_shell_command)')
    coord_parser.add_argument('--gemini-auto-wait', action='store_true', help='Auto-wait when Gemini replies with "retry in N seconds"')
    coord_parser.add_argument('--resume-id', type=str, default=None, help='Resume specific Gemini session id (overrides --resume/--no-resume)')
    coord_parser.add_argument('--resume-main', action='store_true', help='Resume the stored main Gemini session (env GEMINI_MAIN_SESSION_ID or .gemini-sessions.json)')
    coord_parser.add_argument('--resume-test', action='store_true', help='Resume the stored test Gemini session (env GEMINI_TEST_SESSION_ID or .gemini-sessions.json)')
    coord_parser.add_argument('--autopilot', action='store_true', help='Enable multi-turn autopilot mode (keeps Gemini CLI alive across turns)')
    coord_parser.add_argument('--prompt-file', type=str, default=None, help='Batch mode: read prompts from file (one per line, # comments skipped)')
    coord_parser.add_argument('--idle-timeout', type=float, default=15.0, help='Seconds of silence before considering a Gemini turn done (default: 15)')
    coord_parser.add_argument('--quiet', '-q', action='store_true', help='Minimal output for scripting')

    # ============================================================
    # CHIEF COMMAND — "nucleus chief <task>"
    # Orchestration delegation to the hardened chief.sh
    # ============================================================
    chief_parser = subparsers.add_parser('chief', help='🧠 Launch the Chief of Staff autonomous orchestrator')
    chief_parser.add_argument('task', nargs='?', default=None, help='Task description to execute')
    chief_parser.add_argument('--yolo', action='store_true', help='Auto-approve all actions (DANGEROUS)')
    chief_parser.add_argument('--resident', action='store_true', help='Run in resident bridge mode (IPC)')
    chief_parser.add_argument('--popcorn', action='store_true', help='Enable Popcorn Mode: autonomous observation')
    chief_parser.add_argument('--critic', action='store_true', help='Enable specialized Critic agent for this session')
    chief_parser.add_argument('--direct', action='store_true', help='Run directly in terminal (no TMUX)')
    chief_parser.add_argument('--no-attach', action='store_true', help='Don\'t auto-attach to TMUX session')

    # ============================================================
    # RESCUE COMMAND — The "Killer Combo" Handshake
    # Automated thread recovery across IDEs
    # ============================================================
    rescue_parser = subparsers.add_parser('rescue', help='🆘 Rescue Protocol: Recover session context into a fresh IDE thread')
    rescue_parser.add_argument('--force', action='store_true', help='Force rescue even if current_id is locked')

    archive_subparsers = archive_parser.add_subparsers(dest='archive_command')
    archive_subparsers.add_parser('status', help='Retrain readiness check — should you train now?')
    archive_subparsers.add_parser('stats', help='Show archive statistics')
    archive_subparsers.add_parser('recent', help='Show recent loop turns')
    archive_export = archive_subparsers.add_parser('export', help='Export training data for fine-tuning')
    archive_export.add_argument('--format', choices=['gemini', 'openai', 'anthropic', 'all'], default='all', help='Export format (default: all)')
    archive_export.add_argument('--output', type=str, default=None, help='Output directory (default: .brain/training/exports/)')
    archive_record = archive_subparsers.add_parser('record', help='Manually record a loop turn')
    archive_record.add_argument('--brother', choices=['code', 'cowork', 'father'], default='father', help='Who is recording')
    archive_record.add_argument('--intent', type=str, required=True, help='What this turn set out to do')
    archive_record.add_argument('--outcome', type=str, required=True, help='What happened')
    archive_record.add_argument('--decisions', type=str, nargs='*', default=[], help='Key decisions made')
    archive_train = archive_subparsers.add_parser('train', help='Prepare or launch fine-tuning for the local model')
    archive_train.add_argument('--target', choices=['gemini', 'openai', 'local'], default='local',
                               help='Training target: gemini (Vertex AI), openai (API), local (Ollama/unsloth)')
    archive_train.add_argument('--dry-run', action='store_true', help='Export data only, do not launch training')
    archive_ingest = archive_subparsers.add_parser('ingest', help='Bulk import conversations from Gemini/Claude transcripts')
    archive_ingest.add_argument('paths', nargs='+', help='Paths to conversation files (Gemini .json or Claude .md)')
    archive_ingest.add_argument('--brother', choices=['code', 'cowork'], default='code', help='Which brother had this conversation')
    archive_subparsers.add_parser('ingest-threads', help='Bridge thread.jsonl (chat history) into training archive')
    archive_subparsers.add_parser('mark-trained', help='Mark current archive as trained (resets retrain counter)')
    archive_dpo_export.add_argument('--output', type=str, default=None, help='Output path (default: .brain/training/exports/dpo_training.jsonl)')
    archive_dpo_export.add_argument('--exclude-unjudged', action='store_true', help='Drop shadow pairs without LLM judge verification')
    archive_cot_export.add_argument('--output', type=str, default=None, help='Output path (default: .brain/training/exports/reasoning_training.jsonl)')
    archive_subparsers.add_parser('registry', help='Show all registered model versions')

    # ============================================================
    # CONFIG COMMAND — Nucleus settings (telemetry, etc.)
    # ============================================================
    config_parser = subparsers.add_parser('config', help='⚙️  View or change Nucleus configuration')
    config_parser.add_argument('--show', action='store_true', help='Show current configuration')
    config_parser.add_argument('--no-telemetry', action='store_true', help='Opt out of anonymous usage telemetry')
    config_parser.add_argument('--telemetry', action='store_true', help='Opt in to anonymous usage telemetry')
    config_parser.add_argument('--telemetry-endpoint', type=str, default=None, help='Set anonymous telemetry endpoint')

    # ============================================================
    # PROJECT PURPLE 2: One Turn of the Key
    # ============================================================
    start_parser = subparsers.add_parser('start', help='Start the Nucleus daemon — one command, zero crons')
    start_parser.add_argument('--foreground', '--fg', action='store_true', help='Run in foreground (no daemonize)')
    start_parser.add_argument('--no-compound', action='store_true', help='Disable auto-compound triggering')
    start_parser.add_argument('--no-cron', action='store_true', help='Disable scheduled jobs (debug)')

    stop_parser = subparsers.add_parser('stop', help='Gracefully stop the Nucleus daemon')

    drive_parser = subparsers.add_parser('drive', help='Manually trigger the compound intelligence loop')
    drive_parser.add_argument('--compound', type=int, default=5, help='Sparring rounds (default: 5)')
    drive_parser.add_argument('--branch', default='tb/nucleus-work', help='Git branch for driver')

    train_parser = subparsers.add_parser('train', help='Manually trigger training pipeline')
    train_parser.add_argument('--refresh', action='store_true', help='Run full training data refresh')
    train_parser.add_argument('--check', action='store_true', help='Check retrain readiness only')

    # Frontier 2: ALIGN — Human review of escalated tasks
    review_parser = subparsers.add_parser('review', help='Review escalated tasks (Frontier 2: ALIGN)')
    review_parser.add_argument('task_id', nargs='?', help='Task ID to review (omit to list all blocked)')
    review_parser.add_argument('--accept', action='store_true', help='Accept task output (platinum SFT)')
    review_parser.add_argument('--direction', type=str, metavar='NOTE', help='Strategic redirect (logged)')

    # Frontier 1: GROUND — Execution verification (kills Gödel)
    verify_parser = subparsers.add_parser('verify', help='GROUND — tiered execution verification')
    verify_parser.add_argument('--project-root', type=str, help='Project root (auto-detected if omitted)')
    verify_parser.add_argument('--python-path', type=str, help='Python interpreter (auto-detected if omitted)')
    verify_parser.add_argument('--tiers', type=str, default='0,1,2,3', help='Comma-separated tiers: 0=diff, 1=syntax, 2=imports, 3=tests, 4=runtime (default: 0,1,2,3)')
    verify_parser.add_argument('--timeout', type=int, default=30, help='Total time budget in seconds (default: 30)')
    verify_parser.add_argument('--pre-head', type=str, help='Git ref before session started')
    verify_parser.add_argument('--json', dest='json_output', action='store_true', help='Output raw JSON receipt')

    args = parser.parse_args()
    cli_command = args.cli_command

    if not cli_command:
        _print_curated_help()
        return

    # ════════════════════════════════════════════════════════════════
    # ADAPTIVE BRAIN DISCOVERY & SESSION RECOVERY (MDR_001)
    # ════════════════════════════════════════════════════════════════
    # 1. Resolve Brain Path
    brain_path_str = os.environ.get("NUCLEUS_BRAIN_PATH") or os.environ.get("NUCLEAR_BRAIN_PATH")
    if not brain_path_str:
        try:
            cwd = Path.cwd()
            for parent in [cwd] + list(cwd.parents):
                if (parent / ".brain").is_dir():
                    brain_path_str = str((parent / ".brain").resolve())
                    break
        except Exception:
            pass
    
    if brain_path_str:
        os.environ["NUCLEUS_BRAIN_PATH"] = brain_path_str
        os.environ["NUCLEAR_BRAIN_PATH"] = brain_path_str
        brain_path = Path(brain_path_str)
        
        # 2. Resolve Session ID (Mother ID Inheritance)
        session_id = os.environ.get("NUCLEUS_SESSION_ID")
        if not session_id:
            current_id_file = brain_path / "session" / "current_id"
            if current_id_file.exists():
                try:
                    session_id = current_id_file.read_text().strip()
                    if session_id:
                        os.environ["NUCLEUS_SESSION_ID"] = session_id
                except Exception:
                    pass

    # 3. Proxy Sovereignty: Auto-detect local proxy (Phase 23/24)
    if "GEMINI_API_BASE_URL" not in os.environ:
        proxy_port_file = Path(tempfile.gettempdir()) / "gemini_proxy.port"
        if proxy_port_file.exists():
            try:
                port = proxy_port_file.read_text().strip()
                if port.isdigit():
                    proxy_url = f"http://localhost:{port}/v1"
                    os.environ["GEMINI_API_BASE_URL"] = proxy_url
                    os.environ["GEMINI_BASE_URL"] = proxy_url
                    os.environ["GEMINI_NEXT_GEN_API_BASE_URL"] = proxy_url
                    os.environ["GOOGLE_GEMINI_BASE_URL"] = proxy_url
            except Exception:
                pass

    # ── Anonymous telemetry: first-run notice (one-time, silent on failure) ──
    try:
        from .runtime.anon_telemetry import show_first_run_notice
        show_first_run_notice()
    except Exception:
        pass

    _cli_t0 = time.perf_counter()

    try:
        # ════════════════════════════════════════════════════════════════
        # CLI ROUTING (Unified & Deduplicated)
        # ════════════════════════════════════════════════════════════════
        if cli_command == 'help':
            _print_curated_help()

        elif cli_command == 'init':
            success = init_brain(args.path, args.template)
            if success and getattr(args, 'recipe', None):
                _install_recipe_into_brain(Path(args.path), args.recipe)
            if success and args.sidecar:
                from .runtime.discovery import start_discovery_sidecar
                start_discovery_sidecar()
        
        elif cli_command == 'recipe':
            _handle_recipe_command(args)

        elif cli_command == 'setup':
            # Auto-detect brain path
            brain_path_str = args.brain_path
            if not brain_path_str:
                # Try to find .brain in cwd or parents
                cwd = Path.cwd()
                if (cwd / ".brain").exists():
                    brain_path_str = str((cwd / ".brain").absolute())
                else:
                    for parent in cwd.parents:
                        if (parent / ".brain").exists():
                            brain_path_str = str((parent / ".brain").absolute())
                            break
            if not brain_path_str:
                print("❌ No .brain directory found. Run 'nucleus init' first or pass --brain-path.")
                sys.exit(1)

            nucleus_config = _build_nucleus_mcp_config(brain_path_str)
            ide_configs = _get_ide_config_paths()

            print(f"🧠 Brain path: {brain_path_str}")
            print(f"   Command:    {nucleus_config['command']}")

            if args.dry_run:
                print("\n[DRY RUN] Would configure these IDEs:\n")
                for ide_path, ide_name in ide_configs:
                    status = "EXISTS" if ide_path.exists() else "not found"
                    print(f"  {ide_name}: {ide_path} [{status}]")
                print("\nConfig that would be written:")
                print(json.dumps({"mcpServers": {"nucleus": nucleus_config}}, indent=2))
            else:
                configured = 0
                for ide_path, ide_name in ide_configs:
                    if _patch_mcp_config(ide_path, ide_name, nucleus_config, force=args.force):
                        configured += 1

                if configured == 0:
                    full_config = json.dumps({"mcpServers": {"nucleus": nucleus_config}}, indent=2)
                    print("\n⚠️  No IDE config files found. Copy this config manually:\n")
                    print(full_config)
                else:
                    print(f"\n✅ Configured {configured} IDE(s). Restart them to pick up changes.")

                # Perplexity manual instructions (GUI-only, no config file)
                print("\n📝 Perplexity (manual — GUI only):")
                print("   Settings → MCP Servers → Add Server")
                print(f'   Name: nucleus | Command: {nucleus_config["command"]}')
                print(f'   Env: NUCLEUS_BRAIN_PATH={brain_path_str}')

        if cli_command == 'self-setup':
            from .setup import install_nucleus_path
            install_nucleus_path(dry_run=args.dry_run)
            
        elif cli_command in ('brother', 'chat'):
            _run_chat(
                tier_name=getattr(args, 'tier', 'local_free') or 'local_free',
                model_override=getattr(args, 'model', None),
                batch=getattr(args, 'batch', False),
                prompt=getattr(args, 'prompt', None),
                system_prompt=getattr(args, 'system', None),
                provider=getattr(args, 'provider', None),
                output_format=getattr(args, 'output_format', 'text') or 'text',
                brother_context=getattr(args, 'context', None))
            return

        elif cli_command == 'siphon':
            from .siphon import run_siphon
            run_siphon()
            return

        elif cli_command == 'distill':
            from .distill import run_distill
            run_distill(
                source=args.source,
                limit=args.limit,
                output=args.output)
            return

        elif cli_command == 'replay':
            from .replay import run_replay
            run_replay(
                mode=args.mode,
                source=args.source,
                min_confidence=args.min_confidence,
                max_atoms=args.max_atoms,
                tags=args.tags)
            return

        elif cli_command == 'validate':
            from .validate import run_validate
            run_validate(
                test=args.test,
                scenarios=args.scenarios)
            return
            
        elif cli_command == 'depots':
            handle_depot_command(args)
            
        elif cli_command == 'recover':
            return handle_recover_command(args)
        
        elif cli_command == 'rescue':
            return handle_rescue_command(args)
        
        elif cli_command == 'install':
            handle_install_command(args)

        elif cli_command == 'activate':
            handle_activate_command(args)

        elif cli_command == 'trial':
            handle_trial_command(args)

        elif cli_command == 'license':
            handle_license_command(args)

        elif cli_command == 'status':
            handle_status_command(args)

        elif cli_command == 'chief':
            sys.exit(handle_chief_command(args))
            
        elif cli_command == 'run':
            sys.exit(handle_run_command(args))

        elif cli_command == 'summon':
            handle_summon_command(args)

        elif cli_command == 'features':
            handle_features_command(args)

        elif cli_command == 'sessions':
            handle_sessions_command(args)

        elif cli_command == 'morning-brief':
            handle_morning_brief_command(args)

        elif cli_command == 'loop':
            handle_loop_command(args)

        elif cli_command == 'end-of-day':
            handle_end_of_day_command(args)

        elif cli_command == 'graph':
            handle_graph_command(args)

        elif cli_command == 'billing':
            handle_billing_command(args)

        elif cli_command == 'comply':
            handle_comply_command(args)

        elif cli_command == 'compliance-check':
            handle_compliance_check_command(args)

        elif cli_command == 'audit-report':
            handle_audit_report_command(args)

        elif cli_command == 'secure':
            handle_secure_command(args)

        elif cli_command == 'sovereign':
            handle_sovereign_command(args)

        elif cli_command == 'trace':
            handle_trace_command(args)

        elif cli_command == 'dashboard':
            handle_dashboard_command(args)

        elif cli_command == 'kyc':
            handle_kyc_command(args)

        elif cli_command == 'mount':
            handle_mount_command(args)

        elif cli_command == 'combo':
            handle_combo_command(args)

        elif cli_command == 'search':
            handle_search_command(args)

        elif cli_command == 'consolidate':
            handle_consolidate_command(args)

        elif cli_command == 'config':
            handle_config_command(args)

        # ── Project Purple 2: Daemon + Drive + Train ──────────────
        if cli_command == 'start':
            handle_start_command(args)

        elif cli_command == 'stop':
            handle_stop_command(args)

        elif cli_command == 'drive':
            handle_drive_command(args)

        elif cli_command == 'train':
            handle_train_command(args)

        elif cli_command == 'review':
            handle_review_command(args)

        elif cli_command == 'verify':
            handle_verify_command(args)

        elif cli_command == 'schema':
            handle_schema_command(args)

        elif cli_command == 'deploy':
            handle_deploy_command(args)

        elif cli_command == 'dogfood':
            handle_dogfood_command(args)

        elif cli_command == 'heartbeat':
            handle_heartbeat_command(args)

        # ── Agent CLI Commands (v1.4.0) ──────────────────────────────
        if cli_command == 'federation':
            sys.exit(handle_federation_command(args))

        elif cli_command == 'engram':
            sys.exit(handle_engram_command(args))
        elif cli_command == 'task':
            sys.exit(handle_task_command(args))
        elif cli_command == 'session':
            sys.exit(handle_session_command(args))
        elif cli_command == 'growth':
            sys.exit(handle_growth_command(args))
        elif cli_command == 'outbound':
            sys.exit(handle_outbound_command(args))


        elif cli_command is None:
            # Bare `nucleus` → launches brother (the family intelligence interface)
            _run_chat(tier_name="local_free")
        else:
            print(f"Unknown command: {cli_command}")
            parser.print_help()
        # ── Anonymous telemetry: record CLI command (fire-and-forget) ──
        try:
            _cli_duration = (time.perf_counter() - _cli_t0) * 1000
            from .runtime.anon_telemetry import record_anon_command, shutdown_anon_telemetry
            record_anon_command(cli_command or "bare", "cli", _cli_duration)
            shutdown_anon_telemetry()  # Flush metrics before exit
        except Exception:
            pass

    except SystemExit:
        raise  # Let sys.exit() pass through cleanly
    except Exception as exc:
        # ── Anonymous telemetry: record CLI error (fire-and-forget) ──
        try:
            _cli_duration = (time.perf_counter() - _cli_t0) * 1000
            from .runtime.anon_telemetry import record_anon_command, shutdown_anon_telemetry
            record_anon_command(
                cli_command or "bare", "cli", _cli_duration,
                error_type=type(exc).__name__)
            shutdown_anon_telemetry()  # Flush metrics before exit
        except Exception:
            pass

        if not getattr(args, 'self_heal', True):
            raise  # --no-self-heal: reraise raw exception
        use_llm = getattr(args, 'self_heal', True)
        err = handle_cli_error(exc, getattr(args, 'cli_command', ''), vars(args), use_llm=use_llm)
        heal = err.get("self_heal", {})
        if getattr(args, 'format', None) == 'json':
            print(json.dumps(err, default=str), file=sys.stderr)
        else:
            print(f"\n[self-heal] {err.get('error', 'error')}: {err.get('message', exc)}", file=sys.stderr)
            if heal.get("fixed"):
                print(f"[self-heal] AUTO-FIXED: {heal.get('deterministic', {}).get('action', '')}", file=sys.stderr)
            if heal.get("llm_diagnosis", {}).get("llm_used"):
                diag = heal["llm_diagnosis"]
                print(f"[self-heal] LLM diagnosis: {diag.get('diagnosis', '')[:200]}", file=sys.stderr)
                if diag.get("fix"):
                    print(f"[self-heal] Suggested fix: {diag['fix'][:200]}", file=sys.stderr)
            if heal.get("suggestions"):
                print("[self-heal] Suggestions:", file=sys.stderr)
                for s in heal["suggestions"]:
                    if s:
                        print(f"  - {s}", file=sys.stderr)
            if heal.get("needs_human"):
                print("[self-heal] ⚠ This error needs human review.", file=sys.stderr)
            print(f"[self-heal] Incident logged: {heal.get('error_id', 'unknown')}", file=sys.stderr)
        sys.exit(err.get('exit_code', EXIT_ERROR))


# ════════════════════════════════════════════════════════════════
# PROJECT PURPLE 2 HANDLERS — "One Turn of the Key"
# ════════════════════════════════════════════════════════════════

def handle_start_command(args):
    """nucleus start — launch the daemon with scheduler."""
    from .runtime.daemon import run_daemon

    no_compound = getattr(args, 'no_compound', False)
    no_cron = getattr(args, 'no_cron', False)
    fg = getattr(args, 'foreground', False)

    if not fg:
        # Daemonize: fork and run in background
        pid = os.fork()
        if pid > 0:
            print(f"Nucleus daemon started (PID {pid})")
            print(f"  Logs: .brain/logs/daemon.log")
            print(f"  Stop: nucleus stop")
            sys.exit(0)
        # Child process — detach
        os.setsid()
        sys.stdin = open(os.devnull)
        sys.stdout = open(os.devnull, 'w')
        sys.stderr = open(os.devnull, 'w')

    run_daemon(no_compound=no_compound, no_cron=no_cron)


def handle_stop_command(args):
    """nucleus stop — graceful shutdown via PID file."""
    from .runtime.common import get_brain_path
    brain_path = get_brain_path()
    pid_path = brain_path / "daemon" / "daemon.pid"

    if not pid_path.exists():
        print("No daemon running (no PID file found)")
        sys.exit(1)

    try:
        pid = int(pid_path.read_text().strip())
        os.kill(pid, signal.SIGTERM)
        print(f"Sent SIGTERM to daemon (PID {pid})")
    except ProcessLookupError:
        print(f"Daemon not running (stale PID file). Cleaning up.")
        pid_path.unlink(missing_ok=True)
    except Exception as e:
        print(f"Failed to stop daemon: {e}", file=sys.stderr)
        sys.exit(1)


def handle_drive_command(args):
    """nucleus drive — manual compound trigger."""
    import asyncio as _asyncio
    from .runtime.jobs.driver_job import run_compound

    rounds = getattr(args, 'compound', 5)
    branch = getattr(args, 'branch', 'tb/nucleus-work')

    print(f"Triggering compound mode: {rounds} rounds on {branch}")
    result = _asyncio.run(run_compound(branch=branch, rounds=rounds))

    if result.get("ok"):
        print("Compound mode completed successfully.")
    else:
        print(f"Compound mode failed: {result.get('error')}", file=sys.stderr)
        sys.exit(1)


def handle_train_command(args):
    """nucleus train — manual training pipeline trigger."""
    import asyncio as _asyncio

    if getattr(args, 'check', False):
        from .runtime.jobs.training_refresh_job import check_readiness
        result = _asyncio.run(check_readiness())
        if result.get("ok"):
            readiness = result.get("readiness", {})
            if readiness.get("ready"):
                print(f"RETRAIN RECOMMENDED: {readiness.get('reason')}")
            else:
                print(f"Not ready yet. {readiness.get('reason', '')}")
        else:
            print(f"Check failed: {result.get('error')}", file=sys.stderr)
            sys.exit(1)
    elif getattr(args, 'refresh', False):
        from .runtime.jobs.training_refresh_job import run_refresh
        print("Running training data refresh pipeline...")
        result = _asyncio.run(run_refresh(index=True))
        if result.get("ok"):
            print("Training refresh completed.")
            readiness = result.get("readiness", {})
            if readiness and readiness.get("ready"):
                print(f"RETRAIN RECOMMENDED: {readiness.get('reason')}")
        else:
            print(f"Refresh failed: {result.get('error')}", file=sys.stderr)
            sys.exit(1)
    else:
        print("Usage: nucleus train --refresh | nucleus train --check")
        sys.exit(1)


def handle_review_command(args):
    """nucleus review — Frontier 2: ALIGN. Human review of escalated tasks."""
    from pathlib import Path as _Path
    verdicts_path = _Path.cwd() / ".brain" / "driver" / "human_verdicts.jsonl"

    if not getattr(args, 'task_id', None):
        if not verdicts_path.exists():
            print("No escalated tasks found.")
            return
        pending = []
        for line in verdicts_path.read_text().strip().splitlines():
            try:
                entry = json.loads(line)
                if entry.get("verdict") == "pending":
                    pending.append(entry)
            except json.JSONDecodeError:
                pass
        if not pending:
            print("No pending reviews.")
            return
        print(f"{len(pending)} task(s) pending review:\n")
        for e in pending:
            tid = e.get("task_id", "?")
            desc = e.get("task_description", "")[:80]
            ts = e.get("ts", "")
            print(f"  {tid}  {ts}  {desc}")
        return

    task_id = args.task_id
    if not verdicts_path.exists():
        print("No verdicts file found.")
        sys.exit(1)

    lines = verdicts_path.read_text().strip().splitlines()
    updated = False
    new_lines = []
    for line in lines:
        try:
            entry = json.loads(line)
            if entry.get("task_id") == task_id and entry.get("verdict") == "pending":
                if getattr(args, 'accept', False):
                    entry["verdict"] = "accepted"
                    entry["human_notes"] = "Accepted via nucleus review"
                    updated = True
                elif getattr(args, 'reject', None):
                    entry["verdict"] = "rejected"
                    entry["human_notes"] = args.reject
                    updated = True
                elif getattr(args, 'correct', None):
                    entry["verdict"] = "corrected"
                    entry["correction"] = args.correct
                    updated = True
                elif getattr(args, 'direction', None):
                    entry["verdict"] = "redirected"
                    entry["human_notes"] = args.direction
                    updated = True
            new_lines.append(json.dumps(entry, default=str))
        except json.JSONDecodeError:
            new_lines.append(line)

    if updated:
        verdicts_path.write_text("\n".join(new_lines) + "\n")

        # Emit align_reviewed event (Phase 3: Three Frontiers)
        try:
            from .runtime.event_ops import _emit_event
            verdict_type = entry.get("verdict", "unknown")
            _emit_event("align_reviewed", "human", {
                "task_id": task_id,
                "verdict": verdict_type,
                "human_notes": entry.get("human_notes", ""),
                "correction": entry.get("correction", ""),
            })
        except Exception:
            pass  # Never let event emission break the review command

        print(f"Task {task_id} updated.")
    else:
        print(f"Task {task_id} not found or not pending.")
        sys.exit(1)


def handle_verify_command(args):
    """nucleus verify — Frontier 1: GROUND. Execution verification."""
    from .runtime.ground import run_ground

    tiers = [int(t.strip()) for t in args.tiers.split(",")]
    receipt = run_ground(
        project_root=args.project_root,
        python_path=args.python_path,
        tiers=tiers,
        timeout_s=args.timeout,
        pre_head=args.pre_head)

    if args.json_output:
        print(json.dumps(receipt, indent=2, default=str))
        return

    verified = receipt.get("verified", False)
    tier_reached = receipt.get("tier_reached", -1)
    duration = receipt.get("duration_s", 0)
    status = "PASS" if verified else "FAIL"

    print(f"GROUND: {status} (tier {tier_reached}, {duration}s)")
    for sig in receipt.get("signals", []):
        passed = sig.get("passed", False)
        marker = "[+]" if passed else "[x]"
        check = sig.get("check", "?")
        target = sig.get("file", sig.get("module", ""))
        line = f"  {marker} T{sig.get('tier', '?')} {check}"
        if target:
            line += f" {target}"
        err = sig.get("error", "")
        if err and not passed:
            line += f" -- {err[:80]}"
        print(line)

    python_used = receipt.get("python_used", "")
    if python_used:
        print(f"  python: {python_used}")

    if not verified:
        sys.exit(1)


# ════════════════════════════════════════════════════════════════
# v1.4.0 AGENT CLI HANDLERS — "The Universal Interface"
# Direct runtime calls + cli_output formatting.
# Unix conventions: stdout=data, stderr=errors, exit codes.
# ════════════════════════════════════════════════════════════════

def _setup_agent_env(args):
    """Set brain path from --brain-path flag before calling runtime."""
    bp = getattr(args, 'brain_path', None)
    if bp:
        os.environ["NUCLEAR_BRAIN_PATH"] = str(Path(bp).resolve())


def _get_fmt(args) -> str:
    """Get output format from args, with TTY auto-detection. --quiet takes precedence."""
    from .cli_output import detect_format
    return detect_format(getattr(args, 'format', None), quiet=getattr(args, 'quiet', False))


def handle_engram_command(args) -> int:
    """Handle engram search/write/query — direct runtime calls."""
    from .cli_output import output, parse_runtime_response
    _setup_agent_env(args)
    fmt = _get_fmt(args)
    action = getattr(args, 'engram_action', None)

    if action == 'search':
        from .runtime.engram_ops import _brain_search_engrams_impl
        raw = _brain_search_engrams_impl(args.query, case_sensitive=False, limit=args.limit)
        ok, data, err = parse_runtime_response(raw)
        if not ok:
            return output(None, fmt, error=err)
        engrams = data if isinstance(data, list) else data.get("results", data.get("engrams", [])) if isinstance(data, dict) else []
        if isinstance(engrams, list):
            return output(engrams, fmt, columns=["key", "value", "context", "intensity"])
        return output(data, fmt)

    elif action == 'write':
        from .runtime.engram_ops import _brain_write_engram_impl
        raw = _brain_write_engram_impl(args.key, args.value, args.context, args.intensity)
        ok, data, err = parse_runtime_response(raw)
        if not ok:
            return output(None, fmt, error=err)
        return output({"key": args.key, "status": "written"}, fmt)

    elif action == 'query':
        from .runtime.engram_ops import _brain_query_engrams_impl
        min_int = getattr(args, 'min_intensity', 1)
        raw = _brain_query_engrams_impl(args.context, min_int, args.limit)
        ok, data, err = parse_runtime_response(raw)
        if not ok:
            return output(None, fmt, error=err)
        engrams = data if isinstance(data, list) else data.get("results", data.get("engrams", [])) if isinstance(data, dict) else []
        if isinstance(engrams, list):
            return output(engrams, fmt, columns=["key", "value", "context", "intensity"])
        return output(data, fmt)

    elif action == 'quarantine':
        from .runtime.common import get_brain_path
        import json
        brain = get_brain_path()
        ledger = brain / "engrams" / "ledger.jsonl"
        if not ledger.exists():
            print("No engram ledger found.", file=sys.stderr)
            return 1
        undo = getattr(args, 'undo', False)
        lines = []
        found = False
        with open(ledger, "r", encoding="utf-8") as f:
            for line in f:
                if line.strip():
                    try:
                        e = json.loads(line)
                        if e.get("key") == args.key and not e.get("deleted", False):
                            found = True
                            if undo:
                                e.pop("quarantined", None)
                            else:
                                e["quarantined"] = True
                        lines.append(json.dumps(e, ensure_ascii=False) + "\n")
                    except json.JSONDecodeError:
                        lines.append(line)
                else:
                    lines.append(line)
        if not found:
            print(f"Engram '{args.key}' not found.", file=sys.stderr)
            return 3
        with open(ledger, "w", encoding="utf-8") as f:
            f.writelines(lines)
        verb = "unquarantined" if undo else "quarantined"
        print(f"Engram '{args.key}' {verb}. It will be {'included in' if undo else 'excluded from'} morning-brief and search.")
        return 0

    else:
        print("Usage: nucleus engram <search|write|query|quarantine>", file=sys.stderr)
        return 1


def handle_federation_command(args) -> int:
    """Handle federation subcommands."""
    from .cli_output import output
    _setup_agent_env(args)
    fmt = _get_fmt(args)
    action = getattr(args, 'fed_action', None)
    
    from .runtime.federation import create_federation_engine, FederationConfig, PartitionStatus, FederationPeer
    from .runtime.common import get_brain_path
    
    brain_path = get_brain_path()
    brain_id = os.environ.get("NUCLEUS_BRAIN_ID", "local")
    region = "local"
    address = "localhost:0"
    
    engine = create_federation_engine(brain_id, region=region, brain_path=brain_path)
    
    # Check if a saved state exists to populate the engine
    state_file = brain_path / "federation" / "state.json"
    if state_file.exists():
        try:
            with open(state_file, 'r') as f:
                state_data = json.load(f)
                engine.state.leader_id = state_data.get("leader_id")
                engine.state.term = state_data.get("term", 0)
                engine.state.partition_status = PartitionStatus[state_data.get("partition_status", "NORMAL")]
                for p_id, p_data in state_data.get("peers", {}).items():
                    engine.state.peers[p_id] = FederationPeer.from_dict(p_data)
        except Exception as e:
            # Not critical, just means we start with empty state for this CLI call
            pass

    if action == 'status':
        # Federation status is derived from persistence and ephemeral metrics
        status = {
            "brain_id": brain_id,
            "region": region,
            "status": "ALIVE" if engine.running else "IDLE",
            "leader": engine.state.leader_id or "NONE",
            "term": engine.state.term,
            "partition": engine.state.partition_status.name,
            "peers_online": len([p for p in engine.state.peers.values() if p.is_online()]),
            "peers_total": len(engine.state.peers),
            "merkle_root": engine.sync.merkle_tree.get_root()
        }
        return output(status, fmt)

    elif action == 'peers':
        peers = [p.to_dict() for p in engine.state.peers.values()]
        # Add local brain for context
        peers.append({
            "peer_id": brain_id + " (SELF)",
            "address": address,
            "region": region,
            "status": "ONLINE",
            "latency_ms": 0.0,
            "trust_level": "OWNER"
        })
        return output(peers, fmt, columns=["peer_id", "status", "region", "latency_ms"])

    elif action == 'sync':
        # Logic to trigger a sync (if engine was running) or force a manual crawl
        # Since CLI is ephemeral, we just report the current root and suggest the running engine handles it.
        return output({
            "status": "SYNC_TRIGGERED",
            "merkle_root": engine.sync.merkle_tree.get_root(),
            "note": "Synchronization is managed by the background discovery sidecar."
        }, fmt)

    else:
        print("Usage: nucleus federation <status|peers|sync>", file=sys.stderr)
        return 1


def handle_task_command(args) -> int:
    """Handle task list/add/update — direct runtime calls."""
    from .cli_output import output, parse_runtime_response
    _setup_agent_env(args)
    fmt = _get_fmt(args)
    action = getattr(args, 'task_action', None)

    if action == 'list':
        from .runtime.task_ops import _list_tasks
        data = _list_tasks(
            status=getattr(args, 'status', None),
            priority=getattr(args, 'priority', None))
        tasks = data.get("tasks", []) if isinstance(data, dict) else data
        return output(tasks, fmt, columns=["task_id", "description", "status", "priority"])

    elif action == 'add':
        from .runtime.task_ops import _add_task
        result = _add_task(args.description, priority=args.priority)
        if result.get("success"):
            task = result.get("task", {})
            return output({"task_id": task.get("task_id", ""), "status": "created"}, fmt)
        return output(None, fmt, error=result.get("error", "Failed to add task"))

    elif action == 'update':
        from .runtime.task_ops import _update_task
        updates = {}
        if getattr(args, 'status', None):
            updates["status"] = args.status
        if getattr(args, 'priority', None) is not None:
            updates["priority"] = args.priority
        if getattr(args, 'description', None):
            updates["description"] = args.description
        if not updates:
            return output(None, fmt, error="No updates specified. Use --status, --priority, or --description")
        result = _update_task(args.task_id, updates)
        if result.get("success"):
            return output({"task_id": args.task_id, "status": "updated"}, fmt)
        return output(None, fmt, error=result.get("error", "Failed to update task"))

    else:
        print("Usage: nucleus task <list|add|update>", file=sys.stderr)
        return 1


def handle_session_command(args) -> int:
    """Handle session save/resume — direct runtime calls."""
    from .cli_output import output, parse_runtime_response
    _setup_agent_env(args)
    fmt = _get_fmt(args)
    action = getattr(args, 'session_action', None)

    if action == 'save':
        from .runtime.session_ops import _save_session
        result = _save_session(
            args.context,
            active_task=getattr(args, 'task', None))
        if result.get("success"):
            return output({"session_id": result.get("session_id", ""), "status": "saved"}, fmt)
        return output(None, fmt, error=result.get("error", "Failed to save session"))

    elif action == 'resume':
        from .runtime.session_ops import _resume_session
        result = _resume_session(getattr(args, 'id', None))
        if result:
            return output(result, fmt)
        return output(None, fmt, error="No session found to resume")

    else:
        print("Usage: nucleus session <save|resume>", file=sys.stderr)
        return 1


def handle_growth_command(args) -> int:
    """Handle growth pulse/status — direct runtime calls."""
    from .cli_output import output
    _setup_agent_env(args)
    fmt = _get_fmt(args)
    action = getattr(args, 'growth_action', None)

    if action == 'pulse':
        from .runtime.growth_ops import growth_pulse
        result = growth_pulse()
        return output(result, fmt)

    elif action == 'status':
        from .runtime.growth_ops import capture_metrics
        result = capture_metrics(write_engram=False)
        return output(result, fmt)

    else:
        print("Usage: nucleus growth <pulse|status>", file=sys.stderr)
        return 1


