#!/usr/bin/env python3
"""
Summarizer MCP Server - Typed tool wrappers for hierarchical summarization.
Only exposes tools the summarizer agents need.
"""
import json
import logging
import traceback
import subprocess
import os
from datetime import datetime
from pathlib import Path
from typing import List, Optional
from functools import wraps

from pydantic import BaseModel
from mcp.server.fastmcp import FastMCP
from mcp.types import TextContent

# Import CartOn utilities
from carton_mcp.carton_utils import CartOnUtils
from carton_mcp.add_concept_tool import add_concept_tool_func, add_observation, rename_concept_func, get_observation_queue_dir
# DISABLED: ChromaDB/SmartChromaRAG — CPU too heavy, needs filtered sync first
# from carton_mcp.smart_chroma_rag import SmartChromaRAG
from carton_mcp.concept_config import ConceptConfig

# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

import re

def _strip_md(text: str) -> str:
    """Strip markdown links aggressively, keep text only."""
    if not isinstance(text, str):
        return text
    # Iterate until no more changes (handles nested cases)
    prev = None
    while prev != text:
        prev = text
        # [text](path) -> text (handles nested brackets in path)
        text = re.sub(r'\[([^\[\]]+)\]\([^)]+\)', r'\1', text)
    # Clean up any leftover path fragments like "/path/file.md)"
    text = re.sub(r'[a-zA-Z_/]+\.md\)', '', text)
    text = re.sub(r'\(\.\./[^)]+\)', '', text)
    # Clean double spaces
    text = re.sub(r'  +', ' ', text)
    return text.strip()

_OVERFLOW_THRESHOLD = 10000  # chars before file overflow kicks in
_OVERFLOW_DIR = Path(os.environ.get("HEAVEN_DATA_DIR", "/tmp/heaven_data")) / "query_overflow"

def _fmt(data) -> str:
    """Format data as compact string. No JSON bloat. Overflows to file if > 10k chars."""
    result = _fmt_inner(data)
    if len(result) > _OVERFLOW_THRESHOLD:
        _OVERFLOW_DIR.mkdir(parents=True, exist_ok=True)
        overflow_file = _OVERFLOW_DIR / f"overflow_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
        overflow_file.write_text(result)
        truncated = result[:_OVERFLOW_THRESHOLD]
        return f"{truncated}\n\n... Full results ({len(result)} chars) at: {overflow_file}"
    return result

def _fmt_inner(data) -> str:
    """Inner formatter — no truncation, no overflow."""
    if isinstance(data, str):
        return _strip_md(data)
    if isinstance(data, dict):
        if "result" in data:
            return _fmt_inner(data["result"])
        parts = []
        for k, v in data.items():
            if v is not None and v != [] and v != {}:
                parts.append(f"{k}: {_fmt_inner(v)}")
        return "\n".join(parts) if parts else "(empty)"
    if isinstance(data, list):
        if not data:
            return "(none)"
        return ", ".join(_fmt_inner(x) for x in data)
    return str(data)

# Create FastMCP server
mcp = FastMCP("summarizer")

# Initialize shared Neo4j connection (lives for MCP lifetime)
def _create_shared_neo4j():
    """Create persistent Neo4j connection for MCP server lifetime."""
    try:
        from heaven_base.tool_utils.neo4j_utils import KnowledgeGraphBuilder
        config = ConceptConfig()
        conn = KnowledgeGraphBuilder(
            uri=config.neo4j_url,
            user=config.neo4j_username,
            password=config.neo4j_password
        )
        conn._ensure_connection()
        logger.info("Neo4j shared connection established")
        return conn
    except Exception as e:
        logger.warning(f"Failed to create shared Neo4j connection: {e}")
        return None

_neo4j_conn = _create_shared_neo4j()

# Initialize utilities with shared connection
utils = CartOnUtils(shared_connection=_neo4j_conn)

# No ontology bootstrap/enforcement — carton daemon handles that

"""
UARL ONTOLOGY SYSTEM STATUS:

Foundation implemented:
- Soup layer: Concepts with arbitrary descriptions, weak/strong compression tracking
- Ontology promotion: REIFIES → PROGRAMS → IS_A Carton_Ontology_Entity validation
- Dynamic UARL predicates: Bootstrap (is_a, part_of, instantiates) + reified concepts
- Compression marking: Weak relationship types and concepts using them marked REQUIRES_EVOLUTION

Not yet implemented:
- Ontology axiom extraction: Converting origination stacks (triples) into formal axioms
- Semantic pattern matching: Validating origination stack semantic narrative against UARL template
- Description composition: Generating ontology-level descriptions from composed triples instead of arbitrary text
- Complete origination stack semantics: Metastack template pattern, embodies/manifests tracking

Current state: Can type concepts into soup layer and manually trigger REIFIES for validation,
but ontology layer doesn't extract real formal knowledge yet. Infrastructure is ready for future axiom extraction.
"""


def git_push_batch():
    """Push all accumulated local commits to remote. Non-blocking, returns warning on failure."""
    try:
        # Get config from environment
        github_pat = os.getenv('GITHUB_PAT')
        repo_url = os.getenv('REPO_URL')
        branch = os.getenv('BRANCH', 'main')
        base_path = os.getenv('HEAVEN_DATA_DIR', '/tmp/heaven_data')
        wiki_path = os.path.join(base_path, 'wiki')

        if not github_pat or not repo_url:
            return "⚠️ Git push skipped: GITHUB_PAT or REPO_URL not set"

        # Set up credentials
        auth_url = f"https://{github_pat}@github.com"
        credentials_path = Path.home() / ".git-credentials"
        credentials_path.write_text(auth_url + "\n")

        # Push accumulated commits
        result = subprocess.run(
            ["git", "push", "origin", branch],
            cwd=wiki_path,
            capture_output=True,
            text=True,
            timeout=30
        )

        if result.returncode != 0:
            return f"⚠️ Git push failed: {result.stderr}"

        return "✅ Git push successful"
    except Exception as e:
        traceback.print_exc()
        return f"⚠️ Git push error: {str(e)}"


def with_git_push(func):
    """Decorator that pushes git changes after tool execution."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Execute the tool
        result = func(*args, **kwargs)

        # Push changes (non-blocking, failures become warnings)
        push_result = git_push_batch()

        # Append push status to result
        if isinstance(result, str):
            result = f"{result}\n\n{push_result}"

        return result
    return wrapper


# Pydantic models for proper schema validation
class ConceptRelationship(BaseModel):
    relationship: str
    related: List[str]

def _format_concept_result(concept_name: str, raw_result: str) -> str:
    """Format concept creation result for LLM readability"""
    # import re  # moved to top
    # Async queue flow: "queued" means daemon will create files + neo4j
    if "queued" in raw_result.lower() or "created successfully" in raw_result or raw_result.startswith("✅"):
        files_created = "⏳ queued"
        neo4j_created = "⏳ queued"
    else:
        files_created = "❌"
        neo4j_created = "❌"

    # Override if we see explicit success/failure markers
    if "Neo4j: Created concept" in raw_result:
        neo4j_created = "✅"
    if "files created" in raw_result.lower():
        files_created = "✅"

    # Extract YOUKNOW warning if present
    youknow_line = ""
    if "[YOUKNOW" in raw_result:
        match = re.search(r'\[YOUKNOW[^\]]+\]', raw_result)
        if match:
            youknow_line = f"\n⚠️ **YOUKNOW**: {match.group(0)}"

    return f"""🗺️‍⟷‍📦 **CartON** (Cartographic Ontology Net)

**Concept**: `{concept_name}`
📁 **Files**: {files_created}
📊 **Neo4j**: {neo4j_created}{youknow_line}"""


def _check_observation_geometry(observation_data: dict) -> str | None:
    """Check geometry for all concepts in observation. Returns error string or None if valid."""
    OBSERVATION_TAGS = ["insight_moment", "struggle_point", "daily_action", "implementation", "emotional_state"]
    errors = []

    for tag in OBSERVATION_TAGS:
        concepts = observation_data.get(tag, [])
        for i, concept in enumerate(concepts):
            name = concept.get("name", f"unnamed_{tag}_{i}")
            rels = concept.get("relationships", [])
            rel_types = {r.get("relationship") for r in rels}

            missing = []
            if "is_a" not in rel_types:
                missing.append("is_a")
            if "part_of" not in rel_types:
                missing.append("part_of")
            if "instantiates" not in rel_types:
                missing.append("instantiates")
            if not any(k.startswith("has_") for k in rel_types):
                missing.append("has_*")

            if missing:
                errors.append(f"{name}: missing {', '.join(missing)}")

    if errors:
        return f"❌ GEOMETRY ERROR: Incomplete ontological position in observation.\n" + "\n".join(errors)
    return None


# @mcp.tool()  # STRIPPED
def add_concept(
    concept_name: str,
    is_a: List[str],
    part_of: List[str],
    instantiates: List[str],
    concept: str = None,
    relationships: Optional[List[ConceptRelationship]] = None,
    desc_update_mode: str = "append",
    hide_youknow: bool = True
) -> str:
    """Add a new concept to the knowledge graph

    Args:
        concept_name: Name of the concept (will be normalized to Title_Case_With_Underscores)
        is_a: REQUIRED. What category/type is this? e.g. ["Bug_Report"], ["Implementation_Status"]
        part_of: REQUIRED. What contains this? e.g. ["GNOSYS_System"], ["Launch_Strategy"]
        instantiates: REQUIRED. What pattern does this realize? e.g. ["Bug_Fix_Pattern"], ["Working_System_Pattern"]
        concept: Full conceptual content explaining the entire concept, ideas, technical details, etc. Mentioning other concept names auto-creates relates_to links.
        relationships: OPTIONAL. Additional custom relationships beyond the required three. Each object must have format: {"relationship": "relation_type", "related": ["concept_name1", "concept_name2", ...]}. Use for has_*, depends_on, validates, etc.
        desc_update_mode: How to update description if concept exists - "append" (default), "prepend", or "replace"
        hide_youknow: If False (default), YOUKNOW validates and warns if concept lacks proper UARL relationships. If True, skip validation - silent add to soup.

    Returns:
        Formatted result showing success/failure of file and Neo4j operations
    """
    try:
        description = concept
        # Build relationships from required params + optional custom rels
        relationships_dict = [
            {"relationship": "is_a", "related": is_a},
            {"relationship": "part_of", "related": part_of},
            {"relationship": "instantiates", "related": instantiates},
        ]
        if relationships:
            relationships_dict.extend([rel.model_dump() for rel in relationships])

        raw_result = add_concept_tool_func(concept_name, description, relationships_dict, desc_update_mode=desc_update_mode, hide_youknow=hide_youknow, shared_connection=_neo4j_conn)
        return _format_concept_result(concept_name, raw_result)
    except Exception as e:
        traceback.print_exc()
        return f"❌ Error creating concept: {str(e)}"


# @mcp.tool()  # STRIPPED
def add_document_concept(
    concept_name: str,
    description: str,
    canonical_path: str,
    template: str = None,
    relationships: Optional[List[ConceptRelationship]] = None
) -> str:
    """Index a document in Carton with canonical path and optional template

    Use this when you want Carton to act as a database/index for documents.
    The description should be a SUMMARY, not the full content.
    The canonical_path points to where the actual document lives.
    The template specifies how to parse/render the document.

    Args:
        concept_name: Name of the document concept (will be normalized)
        description: Summary of the document (not full content)
        canonical_path: Absolute path to the actual document file
        template: Optional metastack template name for parsing/rendering
        relationships: Additional relationships for the concept

    Returns:
        Formatted result showing success/failure
    """
    try:
        # Build relationships list
        rels = []
        
        # Add canonical path as relationship
        rels.append({
            "relationship": "has_canonical_path",
            "related": [canonical_path]
        })
        
        # Add template if provided
        if template:
            rels.append({
                "relationship": "uses_template",
                "related": [template]
            })
        
        # Add is_a Document_Concept
        rels.append({
            "relationship": "is_a",
            "related": ["Document_Concept"]
        })
        
        # Add any additional relationships
        if relationships:
            for rel in relationships:
                rels.append(rel.model_dump())
        
        # Use observation queue for speed (instant) instead of direct add_concept (30s+)
        observation_data = {
            "implementation": [{
                "name": concept_name,
                "description": description,
                "relationships": rels
            }],
            "confidence": 1.0,
            "hide_youknow": True  # Documents don't need UARL validation
        }
        
        result = add_observation(observation_data)
        return f"✅ ✅ Document concept queued: {result}"
    except Exception as e:
        traceback.print_exc()
        return f"❌ Error creating document concept: {str(e)}"



# @mcp.tool()  # STRIPPED
def add_observation_batch(observation_data: dict, hide_youknow: bool = True) -> str:
    """Create observation capturing complete cognitive state

    Format example: {"insight_moment": [{"name": "Discord_Platform_Choice", "description": "Realized Discord is ideal for egregore launch", "relationships": [{"relationship": "is_a", "related": ["Platform_Decision"]}, {"relationship": "part_of", "related": ["Launch_Strategy"]}, {"relationship": "has_personal_domain", "related": ["cave"]}, {"relationship": "has_actual_domain", "related": ["Business_Strategy"]}]}], "struggle_point": [{"name": "Channel_Structure_Confusion", "description": "Struggled with organizing public vs private channels", "relationships": [{"relationship": "is_a", "related": ["Design_Challenge"]}, {"relationship": "part_of", "related": ["Discord_Architecture"]}, {"relationship": "has_personal_domain", "related": ["cave"]}, {"relationship": "has_actual_domain", "related": ["System_Design"]}]}], "daily_action": [{"name": "Channel_Setup", "description": "Organized AI LAB Discord channels", "relationships": [{"relationship": "is_a", "related": ["Implementation_Task"]}, {"relationship": "part_of", "related": ["AI_LAB_Discord"]}, {"relationship": "has_personal_domain", "related": ["cave"]}, {"relationship": "has_actual_domain", "related": ["Infrastructure"]}]}], "implementation": [{"name": "AI_LAB_Discord", "description": "Discord server for frameworks and COGLOG", "relationships": [{"relationship": "is_a", "related": ["Discord_Server"]}, {"relationship": "part_of", "related": ["Isaac_Infrastructure"]}, {"relationship": "has_personal_domain", "related": ["cave"]}, {"relationship": "has_actual_domain", "related": ["Infrastructure"]}]}], "emotional_state": [{"name": "Clarity_About_Path", "description": "Feeling clear about release strategy", "relationships": [{"relationship": "is_a", "related": ["Emotional_State"]}, {"relationship": "part_of", "related": ["Development_Journey"]}, {"relationship": "has_personal_domain", "related": ["cave"]}, {"relationship": "has_actual_domain", "related": ["Personal_Development"]}]}], "confidence": 0.9}

    Args:
        observation_data: ALL FIVE TAGS REQUIRED (multiple concepts each with is_a, part_of, has_personal_domain, has_actual_domain relationships), confidence float. Personal domains (enum): paiab, sanctum, cave, misc, personal. Actual domains: flexible.

        OPTIONAL desc_update_mode per concept:
        - "append" (default): Add new description after existing
        - "prepend": Add new description before existing
        - "replace": Sink old version to _vN, use only new description

        hide_youknow: If False (default), YOUKNOW validates and warns. If True, skip validation.

    Returns:
        Summary
    """
    try:
        # GEOMETRY CHECK (warn, don't block)
        geometry_warning = _check_observation_geometry(observation_data)

        observation_data["hide_youknow"] = hide_youknow
        result = add_observation(observation_data)
        if geometry_warning:
            warning_text = geometry_warning.replace("❌ GEOMETRY ERROR:", "").strip()
            return f"✅ {result}\n\n⚠️ WARN: GEOMETRY ERROR: {warning_text} -- You may want to keep observing accordingly"
        return f"✅ {result}"
    except Exception as e:
        traceback.print_exc()
        return f"❌ Error creating observation: {str(e)}"


# @mcp.tool()  # STRIPPED
def observe_from_identity_pov(observation_data: dict, agent_identity: str = None, hide_youknow: bool = True) -> str:
    """Create observation from agent identity perspective

    Resolves agent identity via: env var AGENT_IDENTITY (takes priority) > agent_identity param.
    Ensures {identity}_Collection exists, adds concepts as PART_OF that collection,
    and transforms has_actual_domain to has_domain.

    The agent identity collection follows the proper hierarchy:
    - {AGENT_IDENTITY}_Collection IS_A Identity_Collection
    - Identity_Collection IS_A Carton_Collection
    - Concept PART_OF {AGENT_IDENTITY}_Collection

    Same format as add_observation_batch.

    Args:
        agent_identity: Identity name (used if AGENT_IDENTITY env var not set).
        hide_youknow: If False (default), YOUKNOW validates and warns. If True, skip validation.

    Returns:
        Summary
    """
    import os
    from carton_mcp.add_concept_tool import normalize_concept_name

    try:
        # GEOMETRY CHECK (warn, don't block)
        geometry_warning = _check_observation_geometry(observation_data)

        observation_data["hide_youknow"] = hide_youknow
        # Env var takes priority (hardcoded identity). If unset, accept input.
        resolved_identity = os.getenv('AGENT_IDENTITY') or agent_identity
        if not resolved_identity:
            return "❌ No agent identity: set AGENT_IDENTITY env var or pass agent_identity param"
        agent_identity = resolved_identity

        # Normalize the collection name
        collection_name = f"{normalize_concept_name(agent_identity)}_Collection"

        # Ensure the agent's identity collection exists
        _ensure_identity_collection_exists(collection_name, agent_identity)

        # Transform observation_data
        for tag, concepts in observation_data.items():
            if tag in ("confidence", "hide_youknow"):
                continue

            for concept in concepts:
                if 'relationships' not in concept:
                    concept['relationships'] = []

                new_rels = []
                for rel in concept['relationships']:
                    rel_type = rel.get('relationship')
                    
                    if rel_type == 'has_actual_domain':
                        # Transform to has_domain (proper ontological relationship)
                        new_rels.append({
                            'relationship': 'has_domain',
                            'related': rel['related']
                        })
                    elif rel_type == 'has_personal_domain':
                        # Keep personal domain as-is (enum: paiab, sanctum, cave, etc.)
                        new_rels.append(rel)
                    elif rel_type in ('has_subdomain', 'has_subsubdomain'):
                        # Remove these - they were artifacts of the broken approach
                        # The domain hierarchy should be expressed through proper IS_A/PART_OF
                        pass
                    else:
                        new_rels.append(rel)

                # Add PART_OF relationship to the agent's identity collection
                new_rels.append({
                    'relationship': 'part_of',
                    'related': [collection_name]
                })

                concept['relationships'] = new_rels

        result = add_observation(observation_data)
        if geometry_warning:
            warning_text = geometry_warning.replace("❌ GEOMETRY ERROR:", "").strip()
            return f"✅ [{agent_identity}] {result}\n\n⚠️ WARN: GEOMETRY ERROR: {warning_text} -- You may want to keep observing accordingly"
        return f"✅ [{agent_identity}] {result}"

    except Exception as e:
        traceback.print_exc()
        return f"❌ Error: {str(e)}"


def _ensure_identity_collection_exists(collection_name: str, agent_identity: str):
    """Ensure the agent's identity collection exists with proper typing.
    
    Creates if needed:
    - {AGENT_IDENTITY}_Collection IS_A Identity_Collection
    - Identity_Collection IS_A Carton_Collection (should already exist from bootstrap)
    """
    try:
        # Check if collection already exists
        check_query = """
        MATCH (c:Wiki {n: $collection_name})
        RETURN c.n as name
        """
        result = utils.query_wiki_graph(check_query, {"collection_name": collection_name})
        
        if result.get("success") and result.get("data"):
            # Collection exists
            return
        
        # Collection doesn't exist - create it
        # First ensure the collection type hierarchy is bootstrapped
        utils.bootstrap_collection_types()
        
        # Create the identity collection
        from carton_mcp.add_concept_tool import add_concept_tool_func
        
        description = f"Identity collection for {agent_identity} agent. Contains all concepts observed from this agent's perspective."
        relationships = [
            {
                "relationship": "is_a",
                "related": ["Identity_Collection"]
            }
        ]
        
        add_concept_tool_func(
            collection_name, 
            description, 
            relationships, 
            desc_update_mode="append",
            hide_youknow=True,  # Don't validate collection concepts
            shared_connection=_neo4j_conn
        )
        
        logger.info(f"Created identity collection: {collection_name}")
        
    except Exception as e:
        logger.warning(f"Could not ensure identity collection exists: {e}")
        # Don't fail the observation - the collection might just not have the IS_A yet


# @mcp.tool()  # STRIPPED
def carton_management(
    restart_bg_server: bool = False,
    get_git_repo_url: bool = False,
    get_carton_dir: bool = False,
    get_carton_guide: bool = False,
    get_requires_evolution_list: bool = False,
    sync_rag: bool = False,
    check_failed_observations: bool = False,
    retry_failed_observations: bool = False,
    enable_gps: bool = False,
    disable_gps: bool = False,
    get_gps_status: bool = False,
    page: Optional[int] = None
) -> str:
    """CartON management utility

    Args:
        restart_bg_server: Kill and restart observation worker daemon
        get_git_repo_url: Return GitHub repository URL
        get_carton_dir: Return carton queue directory path
        get_carton_guide: Return CartON usage guide
        get_requires_evolution_list: Return paginated list of concepts requiring evolution
        sync_rag: Sync all concepts to ChromaRAG for semantic search
        check_failed_observations: Check for failed observation files in queue
        retry_failed_observations: Retry failed observations marked with "fixed": true
        enable_gps: Enable GPS auto-injection hook
        disable_gps: Disable GPS auto-injection hook
        get_gps_status: Get current GPS hook status
        page: Page number for requires_evolution_list (100 items per page, default: 1)

    Returns:
        Formatted string with requested information (one per line)
    """
    import subprocess
    import signal

    result_parts = []

    if restart_bg_server:
        try:
            # Kill existing daemon
            result = subprocess.run(
                ['pkill', '-f', 'observation_worker_daemon.py'],
                capture_output=True,
                text=True
            )

            # Start new daemon
            github_pat = os.getenv('GITHUB_PAT')
            repo_url = os.getenv('REPO_URL')
            neo4j_uri = os.getenv('NEO4J_URI', 'bolt://host.docker.internal:7687')
            neo4j_user = os.getenv('NEO4J_USER', 'neo4j')
            neo4j_password = os.getenv('NEO4J_PASSWORD', 'password')
            heaven_data_dir = os.getenv('HEAVEN_DATA_DIR', '/tmp/heaven_data')
            openai_api_key = os.getenv('OPENAI_API_KEY')

            env = os.environ.copy()
            env_update = {
                'GITHUB_PAT': github_pat,
                'REPO_URL': repo_url,
                'NEO4J_URI': neo4j_uri,
                'NEO4J_USER': neo4j_user,
                'NEO4J_PASSWORD': neo4j_password,
                'HEAVEN_DATA_DIR': heaven_data_dir,
                'OPENAI_API_KEY': openai_api_key
            }
            # Filter out None values to avoid subprocess error
            env.update({k: v for k, v in env_update.items() if v is not None})

            daemon_path = Path(__file__).parent / 'observation_worker_daemon.py'
            log_path = '/tmp/carton_worker.log'

            process = subprocess.Popen(
                ['python3', str(daemon_path)],
                env=env,
                stdout=open(log_path, 'w'),
                stderr=subprocess.STDOUT,
                start_new_session=True
            )

            result_parts.append(f"✅ Daemon restarted: PID {process.pid}")

        except Exception as e:
            traceback.print_exc()
            result_parts.append(f"❌ Daemon restart failed: {e}")

    if get_git_repo_url:
        repo_url = os.getenv('REPO_URL', 'https://github.com/sancovp/private_wiki')
        result_parts.append(f"Git repo: {repo_url}")

    if get_carton_dir:
        heaven_data_dir = os.getenv('HEAVEN_DATA_DIR', '/tmp/heaven_data')
        queue_dir = Path(heaven_data_dir) / 'carton_queue'
        result_parts.append(f"Queue dir: {queue_dir}")

    if get_carton_guide:
        guide = """
CartON Usage Guide:

1. **Observations** (instant, async processing):
   - Use add_observation_batch() to capture insights
   - Returns immediately with queue confirmation
   - Background daemon processes and commits

2. **Concepts** (direct creation):
   - Use add_concept() for individual concepts
   - Creates files + queues Neo4j write

3. **Background Daemon**:
   - Processes queue files from $HEAVEN_DATA_DIR/carton_queue/
   - ONE git commit when queue empty
   - ONE git push after commit
   - Check logs: tail -f /tmp/carton_worker.log

4. **Management**:
   - Restart daemon: carton_management(restart_bg_server=True)
   - Check queue: ls $HEAVEN_DATA_DIR/carton_queue/
   - Processed files: ls $HEAVEN_DATA_DIR/carton_queue/processed/
"""
        result_parts.append(guide.strip())

    if get_requires_evolution_list:
        try:
            # Default to page 1 if not specified
            current_page = page if page is not None else 1
            items_per_page = 100
            skip = (current_page - 1) * items_per_page

            # Query for concepts requiring evolution
            query = """
            MATCH (c:Wiki)-[r:REQUIRES_EVOLUTION]->(re:Wiki {n: "Requires_Evolution"})
            RETURN c.n as name, c.d as description, r.reason as reason, r.ts as timestamp
            ORDER BY r.ts DESC
            SKIP $skip
            LIMIT $limit
            """

            result = utils.query_wiki_graph(query, {"skip": skip, "limit": items_per_page})

            if result.get("success") and result.get("data"):
                concepts = result["data"]

                # Get total count
                count_query = """
                MATCH (c:Wiki)-[:REQUIRES_EVOLUTION]->(re:Wiki {n: "Requires_Evolution"})
                RETURN count(c) as total
                """
                count_result = utils.query_wiki_graph(count_query, {})
                total = count_result["data"][0]["total"] if count_result.get("success") else 0

                total_pages = (total + items_per_page - 1) // items_per_page

                evolution_list = f"Requires Evolution (Page {current_page}/{total_pages}, {total} total):\n"
                for i, concept in enumerate(concepts, skip + 1):
                    reason = concept.get("reason", "No reason specified")
                    evolution_list += f"{i}. {concept['name']} - {reason}\n"

                result_parts.append(evolution_list.strip())
            else:
                result_parts.append("No concepts require evolution")

        except Exception as e:
            traceback.print_exc()
            result_parts.append(f"❌ Error getting evolution list: {e}")

    if sync_rag:
        result_parts.append("⚠️ RAG sync DISABLED — ChromaDB needs filtered sync redesign")
    if False:  # DISABLED: ChromaDB sync melts CPU
        try:
            from neo4j import GraphDatabase

            neo4j_uri = os.getenv('NEO4J_URI', 'bolt://host.docker.internal:7687')
            neo4j_user = os.getenv('NEO4J_USER', 'neo4j')
            neo4j_pass = os.getenv('NEO4J_PASSWORD', 'password')

            driver = GraphDatabase.driver(neo4j_uri, auth=(neo4j_user, neo4j_pass))
            with driver.session() as session:
                records = session.run(
                    "MATCH (c:Wiki) RETURN c.n AS name, c.d AS desc LIMIT 200000"
                ).data()
            driver.close()

            # Filter: skip empty, single-char, numeric, observations, syncs, transcripts
            import re as _re
            skip_patterns = [
                _re.compile(r'^[A-Za-z0-9]$'),
                _re.compile(r'^\d+$'),
                _re.compile(r'^\d{4}_\d{2}_\d{2}_\d{2}_\d{2}_\d{2}_Observation$'),
                _re.compile(r'^Sync_\d+$'),
                _re.compile(r'^Day_\d{4}_\d{2}_\d{2}$'),
                _re.compile(r'^Raw_Conversation_Timeline_'),
                _re.compile(r'^Conversation_'),
                _re.compile(r'^UserThought_'),
                _re.compile(r'^AgentMessage_'),
                _re.compile(r'^ToolCall_'),
                _re.compile(r'.*_v\d+$'),
            ]
            filtered = {}
            for r in records:
                name = r.get("name") or ""
                if not name or name == "Requires_Evolution":
                    continue
                if any(p.match(name) for p in skip_patterns):
                    continue
                if name in filtered:
                    continue
                desc = r.get("desc") or name
                filtered[name] = {"id": name, "text": f"{name}: {desc}"}
            filtered = list(filtered.values())

            # Batch upsert to ChromaDB via SmartChromaRAG's underlying collection
            rag = _get_rag("carton_concepts")
            BATCH = 500
            total = len(filtered)
            for i in range(0, total, BATCH):
                batch = filtered[i:i+BATCH]
                ids = [b["id"] for b in batch]
                docs = [b["text"] for b in batch]
                metas = [{"concept_name": b["id"]} for b in batch]
                rag.vs.add_texts(docs, metadatas=metas, ids=ids)

            result_parts.append(
                f"✅ RAG sync complete: {total} concepts synced from Neo4j"
            )

        except Exception as e:
            traceback.print_exc()
            result_parts.append(f"❌ RAG sync error: {str(e)}")

    if check_failed_observations:
        try:
            heaven_data_dir = os.getenv('HEAVEN_DATA_DIR', '/tmp/heaven_data')
            failed_dir = Path(heaven_data_dir) / 'carton_queue' / 'failed'

            if not failed_dir.exists():
                result_parts.append("✅ No failed observations (failed directory doesn't exist)")
            else:
                failed_files = sorted(failed_dir.glob('*.json'))

                if not failed_files:
                    result_parts.append("✅ No failed observations")
                else:
                    failure_report = f"❌ Found {len(failed_files)} failed observations:\n\n"

                    for i, failed_file in enumerate(failed_files, 1):
                        failure_report += f"{i}. {failed_file.name}\n"
                        failure_report += f"   Path: {failed_file}\n"

                        # Try to read and show basic info
                        try:
                            with open(failed_file, 'r') as f:
                                data = json.load(f)
                                # Count concepts across all tags
                                total_concepts = sum(
                                    len(data.get(tag, []))
                                    for tag in ['insight_moment', 'struggle_point', 'daily_action', 'implementation', 'emotional_state']
                                )
                                failure_report += f"   Concepts: {total_concepts}\n"
                        except Exception as read_error:
                            failure_report += f"   Error reading file: {read_error}\n"

                        failure_report += "\n"

                    failure_report += f"To retry: mv {failed_dir}/FILENAME.json {failed_dir.parent}/"
                    result_parts.append(failure_report.strip())

        except Exception as e:
            traceback.print_exc()
            result_parts.append(f"❌ Error checking failed observations: {str(e)}")

    if retry_failed_observations:
        try:
            heaven_data_dir = os.getenv('HEAVEN_DATA_DIR', '/tmp/heaven_data')
            failed_dir = Path(heaven_data_dir) / 'carton_queue' / 'failed'
            queue_dir = Path(heaven_data_dir) / 'carton_queue'

            if not failed_dir.exists():
                result_parts.append("✅ No failed observations to retry")
            else:
                failed_files = sorted(failed_dir.glob('*.json'))

                if not failed_files:
                    result_parts.append("✅ No failed observations to retry")
                else:
                    retried = []
                    skipped = []

                    for failed_file in failed_files:
                        try:
                            # Read and check for "fixed": true
                            with open(failed_file, 'r') as f:
                                data = json.load(f)

                            if data.get('fixed') == True:
                                # Clean the observation data
                                data.pop('fixed', None)
                                data.pop('error_message', None)
                                data.pop('error_traceback', None)

                                # Write cleaned data
                                with open(failed_file, 'w') as f:
                                    json.dump(data, f)

                                # Move back to queue
                                queue_file = queue_dir / failed_file.name
                                failed_file.rename(queue_file)
                                retried.append(failed_file.name)
                            else:
                                skipped.append(failed_file.name)

                        except Exception as file_error:
                            result_parts.append(f"⚠️  Error processing {failed_file.name}: {file_error}")

                    retry_report = f"✅ Retry complete:\n"
                    retry_report += f"   Retried: {len(retried)}\n"
                    retry_report += f"   Skipped (not marked fixed): {len(skipped)}\n"

                    if retried:
                        retry_report += f"\nRetried files:\n"
                        for name in retried:
                            retry_report += f"   - {name}\n"

                    result_parts.append(retry_report.strip())

        except Exception as e:
            traceback.print_exc()
            result_parts.append(f"❌ Error retrying failed observations: {str(e)}")

    if enable_gps:
        try:
            heaven_data_dir = os.getenv('HEAVEN_DATA_DIR', '/tmp/heaven_data')
            gps_flag_file = Path(heaven_data_dir) / 'carton_gps_enabled'
            gps_flag_file.write_text('1')
            result_parts.append("✅ GPS auto-injection enabled")
        except Exception as e:
            traceback.print_exc()
            result_parts.append(f"❌ Error enabling GPS: {str(e)}")

    if disable_gps:
        try:
            heaven_data_dir = os.getenv('HEAVEN_DATA_DIR', '/tmp/heaven_data')
            gps_flag_file = Path(heaven_data_dir) / 'carton_gps_enabled'
            if gps_flag_file.exists():
                gps_flag_file.unlink()
            result_parts.append("✅ GPS auto-injection disabled")
        except Exception as e:
            traceback.print_exc()
            result_parts.append(f"❌ Error disabling GPS: {str(e)}")

    if get_gps_status:
        try:
            heaven_data_dir = os.getenv('HEAVEN_DATA_DIR', '/tmp/heaven_data')
            gps_flag_file = Path(heaven_data_dir) / 'carton_gps_enabled'
            if gps_flag_file.exists():
                result_parts.append("GPS auto-injection: ✅ ENABLED")
            else:
                result_parts.append("GPS auto-injection: ❌ DISABLED")
        except Exception as e:
            traceback.print_exc()
            result_parts.append(f"❌ Error checking GPS status: {str(e)}")

    return "\n".join(result_parts) if result_parts else "No actions requested"


# @mcp.tool()  # STRIPPED
def rename_concept(
    old_concept_name: str,
    new_concept_name: str,
    reason: str = "Conceptual refinement"
) -> str:
    """Rename a concept by creating new concept and updating all graph references

    This is proactive evolution (not defensive sinking with _v1 suffix).

    Operations:
    1. Creates new concept with improved terminology (inherits old description)
    2. Updates ALL incoming relationships to point to new concept
    3. Copies ALL outgoing relationships from old to new
    4. Creates bidirectional evolution links (evolved_from/evolved_to)
    5. Preserves old concept as historical record

    Distinction from sinking:
    - Sinking (_v1): automatic on validation failures, marks broken concepts
    - Renaming: user-initiated refinement, improves terminology while preserving graph integrity

    Args:
        old_concept_name: Current concept name to be evolved/renamed
        new_concept_name: New improved concept name
        reason: Explanation for the rename (stored in evolution relationship)

    Returns:
        Summary of rename operation including relationship counts
    """
    try:
        result = rename_concept_func(old_concept_name, new_concept_name, reason)
        return f"✅ {result}"
    except Exception as e:
        traceback.print_exc()
        return f"❌ Error renaming concept: {str(e)}"


# @mcp.tool()  # STRIPPED
def observe_auto_meta_test(test_subject: str, fix_description: str) -> str:
    """Auto-observe a bug fix using meta-testing pattern

    Creates an observation about fixing a system bug by using the fixed system itself.
    The observation's completeness proves the fix worked (meta-testing).

    Args:
        test_subject: What was being tested/fixed (e.g., "observation_wrapper_linking")
        fix_description: Description of the bug and how it was fixed

    Returns:
        Result of meta-test observation creation
    """
    # Will call add_observation_batch, potentially in a loop, plus have pattern validators TBD
    raise NotImplementedError("Meta-testing pattern not yet implemented. Will create self-validating observations about system fixes.")


# @mcp.tool()  # STRIPPED
def run_experiment(experiment_hypothesis: str, flight_config_name: str = None) -> str:
    """Run an experiment using flight config with waypoint PD for meta-test validation

    Sends message to use a specific flight that has a specific waypoint PD for running
    an experiment that sets up a meta test hypothesis and executes the constructor to
    see if it validates and then figures out why not through carton observation.
    This lets it chain research to farm knowledge.

    Args:
        experiment_hypothesis: The hypothesis to test
        flight_config_name: Optional specific flight config to use for the experiment

    Returns:
        Experiment execution results and observations
    """
    # Will integrate: flight configs + waypoint PDs + meta-test hypothesis validation + Carton observations
    # Creates knowledge farming research chain
    raise NotImplementedError("Experiment runner not yet implemented. Will chain research through flight/waypoint/observation system.")

@mcp.tool()
def query_wiki_graph(cypher_query: str, parameters: dict = None) -> str:
    """Execute arbitrary Cypher query on :Wiki namespace (read-only)

    Neo4j Schema:
    - Node label: :Wiki
    - Properties: n (name), d (description), c (canonical), t (timestamp)
    - Relationships: Various types like is_a, part_of, depends_on, relates_to, etc.

    UARL Two-Phase System:
    - Soup layer: All concepts (informal, arbitrary descriptions)
    - Ontology layer: Concepts with REIFIES (formal, strongly typed, complete knowledge)

    Query Patterns:
    - All concepts: MATCH (c:Wiki) RETURN c.n, c.d
    - Ontology only: MATCH (c:Wiki)-[:IS_A]->(:Wiki {n: "Carton_Ontology_Entity"}) RETURN c.n, c.d
    - Soup only: MATCH (c:Wiki) WHERE NOT (c)-[:IS_A]->(:Wiki {n: "Carton_Ontology_Entity"}) RETURN c.n, c.d
    - Find concepts: MATCH (c:Wiki) WHERE c.n CONTAINS "MCP" RETURN c.n, c.d
    - Get relationships: MATCH (c:Wiki)-[r]->(related:Wiki) WHERE c.n = "HEAVEN_System" RETURN type(r), related.n
    - Count concepts: MATCH (c:Wiki) RETURN count(c)

    Args:
        cypher_query: Cypher query targeting :Wiki namespace (read-only, no CREATE/MERGE allowed)
        parameters: Optional parameters for the Cypher query (use $param_name in query)

    Returns:
        JSON string with query results containing success status and data
    """
    try:
        result = utils.query_wiki_graph(cypher_query, parameters)
        if result.get("success"):
            data = result["data"]
            formatted = _fmt(data)
            # Warn if no results and query might have case mismatch
            if not data or (isinstance(data, list) and len(data) == 0):
                import re
                # Check for potential case mismatch in concept names
                potential_names = re.findall(r"['\"]([A-Za-z_]+)['\"]", cypher_query)
                has_lowercase = any(c.islower() for name in potential_names for c in name if c.isalpha())
                if has_lowercase and potential_names:
                    formatted += "\n\n⚠️ REMINDER: CartON uses Title_Case for all concept names. Your query contains lowercase - try Title_Case (e.g., 'my_concept' → 'My_Concept')"
            return formatted
        else:
            return f"❌ {result.get('error')}"
    except Exception as e:
        traceback.print_exc()
        return f"❌ Error: {e}"

# @mcp.tool()  # STRIPPED
def get_concept_network(
    concept_name: str,
    depth: int = 1,
    rel_types: List[str] = None
) -> str:
    """Get concept network with specified relationship depth (1-3 hops)

    Explores the knowledge graph starting from a concept and following relationships
    to discover connected concepts. Useful for understanding concept dependencies,
    related ideas, and knowledge clusters.

    Args:
        concept_name: Name of the concept to explore network for (exact match on n property)
        depth: Relationship depth to traverse (1-3, default: 1). Higher depth = more connections
        rel_types: Optional list of relationship types to filter (e.g., ["IS_A", "PART_OF"])
                   Common types: IS_A, PART_OF, DEPENDS_ON, HAS_COMPONENT, VALIDATES, AUTO_RELATED_TO
                   If None, includes all relationship types

    Returns:
        JSON string with concept network data including nodes, relationships, and metadata
    """
    try:
        result = utils.get_concept_network(concept_name, depth, rel_types)
        if result.get("success"):
            return _fmt(result)
        else:
            return f"❌ {result.get('error')}"
    except Exception as e:
        traceback.print_exc()
        return f"❌ Error: {e}"

@mcp.tool()
def get_concept(concept_name: str) -> TextContent:
    """Get complete concept information including description and all relationships
    
    Retrieves the full concept data in one call - both the concept description
    and all its relationships. This is the standard way to research a concept
    for blog writing, analysis, or understanding its place in the knowledge graph.
    
    Args:
        concept_name: Name of the concept to retrieve (exact match on n property)
        
    Returns:
        JSON string with complete concept data: name, description, and relationships
    """
    try:
        # Query for concept with all its relationships
        # Prioritize nodes with descriptions to handle duplicates
        cypher_query = """
        MATCH (c:Wiki) WHERE c.n = $concept_name AND c.d IS NOT NULL
        OPTIONAL MATCH (c)-[r]->(related:Wiki)
        RETURN c.n as name, c.d as description,
               collect({type: type(r), target: related.n}) as relationships
        """
        result = utils.query_wiki_graph(cypher_query, {"concept_name": concept_name})

        if result.get("success") and result.get("data"):
            concept_data = result["data"][0]
            # Filter out empty relationships (from OPTIONAL MATCH)
            relationships = [rel for rel in concept_data.get("relationships", []) if rel.get("type")]

            # Build compact output
            name = concept_data.get("name", "")
            desc = _strip_md(concept_data.get("description", ""))
            
            # Group relationships
            requires_evolution = []
            auto_rels = []
            normal_rels = []
            
            for rel in relationships:
                rel_type = rel.get("type", "")
                target = rel.get("target", "")
                if rel_type == "REQUIRES_EVOLUTION":
                    requires_evolution.append(target)
                elif rel_type == "AUTO_RELATED_TO":
                    auto_rels.append(target)
                else:
                    normal_rels.append(f"{rel_type.lower()} {target}")
            
            # Build output string
            lines = [f"Name: {name}", f"Description: {desc}", "", "Rels:["]
            
            if requires_evolution:
                lines.append("# ⚠️ REQUIRES_EVOLUTION")
            
            for rel in normal_rels:
                lines.append(f"  {rel}")
            
            if auto_rels:
                lines.append("")
                lines.append("  AutoRels:[")
                for ar in auto_rels:
                    lines.append(f"    auto_related_to {ar}")
                lines.append("  ]")
            
            lines.append("]")

            return TextContent(type="text", text="\n".join(lines))
        else:
            # Suggest Title_Case_With_Underscores if user might have wrong casing
            suggestion = concept_name.replace(" ", "_").replace("-", "_")
            parts = suggestion.split("_")
            title_cased = "_".join(p.capitalize() for p in parts if p)
            hint = ""
            if concept_name != title_cased:
                hint = f"\n💡 Did you mean: {title_cased}?"
            return TextContent(type="text", text=f"❌ Concept '{concept_name}' not found.{hint}\n⚠️ CartON uses Title_Case_With_Underscores naming convention.")

    except Exception as e:
        traceback.print_exc()
        return TextContent(type="text", text=f"❌ Error: {str(e)}")

@mcp.tool()
def get_history_info(
    info_type: str,
    id: str
) -> str:
    """Query conversation history and summarizer outputs from typed CartON concepts

    Traverses the typed conversation structure to reconstruct full content.
    Use this instead of raw Cypher queries for history traversal.
    Each summarizer level can use this to get what the previous level created.

    Args:
        info_type: Type of history to retrieve:
            RAW DATA:
            - "iteration": Single iteration with all messages and tool calls
            - "conversation": All iterations in a conversation
            - "session": All conversations in a starlog session
            - "context_bundle": What was in context when a file was edited

            SUMMARIZER OUTPUTS:
            - "iteration_summary": Single iteration summary (L1 output)
            - "all_iteration_summaries": All iteration summaries for a conversation (for L2)
            - "phase": Single phase with its iteration summaries (L2 output)
            - "all_phases": All phases for a conversation (for L5)
            - "subphase": Single subphase (L3 output)
            - "executive_summary": Executive summary for conversation (L5 output)

        id: The concept name or conversation ID depending on info_type

    Returns:
        Formatted string with full content in sequence
    """
    try:
        if info_type == "iteration":
            # Get iteration with all its components in sequence
            # First get the iteration and its relationships
            query = """
            MATCH (iter:Wiki {n: $id})
            OPTIONAL MATCH (iter)-[r]->(component:Wiki)
            WHERE type(r) STARTS WITH 'HAS_USER_MESSAGE_'
               OR type(r) STARTS WITH 'HAS_AGENT_MESSAGE_'
               OR type(r) STARTS WITH 'HAS_TOOL_CALL_'
            RETURN iter.n as iteration_name, iter.d as iteration_desc,
                   type(r) as rel_type, component.n as component_name, component.d as component_desc
            ORDER BY rel_type
            """
            result = utils.query_wiki_graph(query, {"id": id})

            if not result.get("success") or not result.get("data"):
                return f"❌ Iteration '{id}' not found"

            data = result["data"]

            # Parse results into ordered components
            user_messages = []
            agent_messages = []
            tool_calls = []

            for row in data:
                rel_type = row.get("rel_type")
                if not rel_type:
                    continue

                component = {
                    "name": row.get("component_name"),
                    "content": row.get("component_desc")
                }

                if rel_type.startswith("HAS_USER_MESSAGE_"):
                    seq_num = int(rel_type.replace("HAS_USER_MESSAGE_", ""))
                    user_messages.append((seq_num, component))
                elif rel_type.startswith("HAS_AGENT_MESSAGE_"):
                    seq_num = int(rel_type.replace("HAS_AGENT_MESSAGE_", ""))
                    agent_messages.append((seq_num, component))
                elif rel_type.startswith("HAS_TOOL_CALL_"):
                    seq_num = int(rel_type.replace("HAS_TOOL_CALL_", ""))
                    tool_calls.append((seq_num, component))

            # Sort by sequence number
            user_messages.sort(key=lambda x: x[0])
            agent_messages.sort(key=lambda x: x[0])
            tool_calls.sort(key=lambda x: x[0])

            # Build output
            lines = [f"=== {id} ===\n"]

            for seq, msg in user_messages:
                lines.append(f"[USER MESSAGE {seq}]")
                lines.append(msg["content"] or "(empty)")
                lines.append("")

            for seq, msg in agent_messages:
                lines.append(f"[AGENT MESSAGE {seq}]")
                lines.append(msg["content"] or "(empty)")
                lines.append("")

            if tool_calls:
                lines.append("[TOOL CALLS]")
                for seq, tc in tool_calls:
                    lines.append(f"  {seq}. {tc['content']}")
                lines.append("")

            return "\n".join(lines)

        elif info_type == "conversation":
            # Get all iterations in conversation ordered by sequence
            query = """
            MATCH (conv:Wiki {n: $id})<-[:PART_OF]-(iter:Wiki)
            WHERE iter.n STARTS WITH 'Iteration_'
            RETURN iter.n as iteration_name
            ORDER BY iter.n
            """
            result = utils.query_wiki_graph(query, {"id": id})

            if not result.get("success") or not result.get("data"):
                return f"❌ Conversation '{id}' not found or has no iterations"

            iterations = [row["iteration_name"] for row in result["data"]]

            lines = [f"=== {id} ===", f"Total iterations: {len(iterations)}\n"]

            for iter_name in iterations:
                # Recursively get each iteration
                iter_content = get_history_info("iteration", iter_name)
                lines.append(iter_content)
                lines.append("---\n")

            return "\n".join(lines)

        elif info_type == "session":
            # Get all conversations in session
            query = """
            MATCH (session:Wiki {n: $id})<-[:PART_OF]-(conv:Wiki)
            WHERE conv.n STARTS WITH 'Conversation_'
            RETURN conv.n as conversation_name
            ORDER BY conv.n
            """
            result = utils.query_wiki_graph(query, {"id": id})

            if not result.get("success") or not result.get("data"):
                return f"❌ Session '{id}' not found or has no conversations"

            conversations = [row["conversation_name"] for row in result["data"]]

            lines = [f"=== {id} ===", f"Total conversations: {len(conversations)}\n"]

            for conv_name in conversations:
                lines.append(f"### {conv_name}")
                # Just list iterations, don't recurse fully to avoid massive output
                iter_query = """
                MATCH (conv:Wiki {n: $conv_name})<-[:PART_OF]-(iter:Wiki)
                WHERE iter.n STARTS WITH 'Iteration_'
                RETURN count(iter) as iteration_count
                """
                iter_result = utils.query_wiki_graph(iter_query, {"conv_name": conv_name})
                if iter_result.get("success") and iter_result.get("data"):
                    count = iter_result["data"][0].get("iteration_count", 0)
                    lines.append(f"  Iterations: {count}")
                lines.append("")

            return "\n".join(lines)

        elif info_type == "context_bundle":
            # Find what files were read before a file was edited
            # id should be a file path or File_* concept name

            # Normalize file path to concept name if needed
            file_concept = id if id.startswith("File_") else f"File_{id.replace('/', '_').replace('.', '_')}"

            query = """
            // Find edit tool calls that touched this file
            MATCH (tc:Wiki)-[:TOUCHES_FILE]->(file:Wiki {n: $file_concept})
            MATCH (tc)-[:USES_TOOL]->(tool:Wiki {n: 'Edit'})
            MATCH (iter:Wiki)-[edit_rel]->(tc)
            WHERE type(edit_rel) STARTS WITH 'HAS_TOOL_CALL_'

            // Find all read tool calls in the same iteration before the edit
            WITH iter, toInteger(replace(type(edit_rel), 'HAS_TOOL_CALL_', '')) as edit_seq
            MATCH (iter)-[read_rel]->(read_tc:Wiki)-[:USES_TOOL]->(read_tool:Wiki {n: 'Read'})
            WHERE toInteger(replace(type(read_rel), 'HAS_TOOL_CALL_', '')) < edit_seq
            MATCH (read_tc)-[:TOUCHES_FILE]->(read_file:Wiki)

            RETURN iter.n as iteration,
                   read_file.n as file_read,
                   type(read_rel) as read_seq
            ORDER BY iter.n, read_seq
            """
            result = utils.query_wiki_graph(query, {"file_concept": file_concept})

            if not result.get("success") or not result.get("data"):
                return f"❌ No context bundle found for '{id}' (no edits with prior reads)"

            # Group by iteration
            bundles = {}
            for row in result["data"]:
                iter_name = row["iteration"]
                if iter_name not in bundles:
                    bundles[iter_name] = []
                bundles[iter_name].append(row["file_read"])

            lines = [f"=== Context Bundle for {id} ===\n"]
            for iter_name, files in bundles.items():
                lines.append(f"In {iter_name}:")
                for f in files:
                    # Convert File_... back to path
                    path = f.replace("File_", "").replace("_", "/").replace("/_", ".")
                    lines.append(f"  - {path}")
                lines.append("")

            return "\n".join(lines)

        # === SUMMARIZER OUTPUT TYPES ===

        elif info_type == "iteration_summary":
            # Get a single iteration summary
            query = """
            MATCH (s:Wiki {n: $id})
            WHERE s.n STARTS WITH 'Iteration_Summary_'
            RETURN s.n as name, s.d as description
            """
            result = utils.query_wiki_graph(query, {"id": id})

            if not result.get("success") or not result.get("data"):
                return f"❌ Iteration summary '{id}' not found"

            data = result["data"][0]
            return f"=== {data['name']} ===\n\n{data['description']}"

        elif info_type == "all_iteration_summaries":
            # Get all iteration summaries for a conversation (id = conversation concept name)
            query = """
            MATCH (s:Wiki)-[:PART_OF]->(conv:Wiki {n: $id})
            WHERE s.n STARTS WITH 'Iteration_Summary_'
            RETURN s.n as name, s.d as description
            ORDER BY s.n
            """
            result = utils.query_wiki_graph(query, {"id": id})

            if not result.get("success") or not result.get("data"):
                return f"❌ No iteration summaries found for '{id}'"

            lines = [f"=== Iteration Summaries for {id} ===\n"]
            for row in result["data"]:
                lines.append(f"### {row['name']}")
                lines.append(row['description'] or "(empty)")
                lines.append("")

            return "\n".join(lines)

        elif info_type == "phase":
            # Get a single phase with its iteration summaries
            query = """
            MATCH (p:Wiki {n: $id})
            WHERE p.n STARTS WITH 'Conversation_Phase_'
            OPTIONAL MATCH (p)-[r]->(s:Wiki)
            WHERE type(r) STARTS WITH 'HAS_ITERATION_SUMMARY_'
            RETURN p.n as phase_name, p.d as phase_desc,
                   type(r) as rel_type, s.n as summary_name, s.d as summary_desc
            ORDER BY rel_type
            """
            result = utils.query_wiki_graph(query, {"id": id})

            if not result.get("success") or not result.get("data"):
                return f"❌ Phase '{id}' not found"

            data = result["data"]
            phase_desc = data[0].get("phase_desc", "")

            # Collect summaries in order
            summaries = []
            for row in data:
                rel_type = row.get("rel_type")
                if rel_type and rel_type.startswith("HAS_ITERATION_SUMMARY_"):
                    seq = int(rel_type.replace("HAS_ITERATION_SUMMARY_", ""))
                    summaries.append((seq, row.get("summary_name"), row.get("summary_desc")))

            summaries.sort(key=lambda x: x[0])

            lines = [f"=== {id} ===\n", phase_desc or "(no description)", "\n### Iteration Summaries:"]
            for seq, name, desc in summaries:
                lines.append(f"\n{seq}. {name}")
                lines.append(desc or "(empty)")

            return "\n".join(lines)

        elif info_type == "all_phases":
            # Get all phases for a conversation (id = conversation concept name)
            query = """
            MATCH (p:Wiki)-[:PART_OF]->(conv:Wiki {n: $id})
            WHERE p.n STARTS WITH 'Conversation_Phase_'
            RETURN p.n as name, p.d as description
            ORDER BY p.n
            """
            result = utils.query_wiki_graph(query, {"id": id})

            if not result.get("success") or not result.get("data"):
                return f"❌ No phases found for '{id}'"

            lines = [f"=== All Phases for {id} ===\n"]
            for row in result["data"]:
                lines.append(f"### {row['name']}")
                lines.append(row['description'] or "(empty)")
                lines.append("")

            return "\n".join(lines)

        elif info_type == "subphase":
            # Get a single subphase
            query = """
            MATCH (s:Wiki {n: $id})
            WHERE s.n STARTS WITH 'Conversation_Subphase_'
            RETURN s.n as name, s.d as description
            """
            result = utils.query_wiki_graph(query, {"id": id})

            if not result.get("success") or not result.get("data"):
                return f"❌ Subphase '{id}' not found"

            data = result["data"][0]
            return f"=== {data['name']} ===\n\n{data['description']}"

        elif info_type == "all_subphases":
            # Get all subphases for a phase (id = phase concept name)
            query = """
            MATCH (s:Wiki)-[:PART_OF]->(p:Wiki {n: $id})
            WHERE s.n STARTS WITH 'Conversation_Subphase_'
            RETURN s.n as name, s.d as description
            ORDER BY s.n
            """
            result = utils.query_wiki_graph(query, {"id": id})

            if not result.get("success") or not result.get("data"):
                return f"❌ No subphases found for '{id}'"

            lines = [f"=== Subphases for {id} ===\n"]
            for row in result["data"]:
                lines.append(f"### {row['name']}")
                lines.append(row['description'] or "(empty)")
                lines.append("")

            return "\n".join(lines)

        elif info_type == "executive_summary":
            # Get executive summary for a conversation (id = conversation concept name OR executive summary name)
            # Try direct match first
            query = """
            MATCH (e:Wiki)
            WHERE e.n = $id OR e.n = 'Executive_Summary_' + $id
            RETURN e.n as name, e.d as description
            """
            result = utils.query_wiki_graph(query, {"id": id})

            if not result.get("success") or not result.get("data"):
                # Try finding by conversation
                query2 = """
                MATCH (e:Wiki)-[:PART_OF]->(conv:Wiki {n: $id})
                WHERE e.n STARTS WITH 'Executive_Summary_'
                RETURN e.n as name, e.d as description
                """
                result = utils.query_wiki_graph(query2, {"id": id})

            if not result.get("success") or not result.get("data"):
                return f"❌ Executive summary not found for '{id}'"

            data = result["data"][0]
            return f"=== {data['name']} ===\n\n{data['description']}"

        else:
            valid_types = "iteration, conversation, session, context_bundle, iteration_summary, all_iteration_summaries, phase, all_phases, subphase, all_subphases, executive_summary"
            return f"❌ Unknown info_type '{info_type}'. Must be one of: {valid_types}"

    except Exception as e:
        traceback.print_exc()
        return f"❌ Error: {str(e)}"


# @mcp.tool()  # STRIPPED
def list_missing_concepts() -> str:
    """List all missing concepts that are referenced but don't exist yet
    
    Scans the knowledge graph for concept names mentioned in descriptions or relationships
    that don't have their own concept files. Useful for finding gaps in the knowledge base
    and planning which concepts need to be created next.
    
    Returns:
        JSON string with missing concepts and their inferred relationships from existing concepts
    """
    try:
        result = utils.list_missing_concepts()
        if result.get("success"):
            return _fmt(result["data"])
        else:
            return f"❌ {result.get('error')}"
    except Exception as e:
        traceback.print_exc()
        return f"❌ Error: {e}"

# @mcp.tool()  # STRIPPED
def create_missing_concepts(concepts_data: list) -> str:
    """Create multiple missing concepts with AI-generated descriptions
    
    Batch creates concepts that were identified as missing from the knowledge graph.
    Uses AI to generate appropriate descriptions based on context from existing concepts
    that reference them.
    
    Args:
        concepts_data: List of concept objects to create, each containing name and context
        
    Returns:
        JSON string with creation results showing success/failure for each concept
    """
    try:
        result = utils.create_missing_concepts(concepts_data)
        if result.get("success"):
            return _fmt(result["data"])
        else:
            return f"❌ {result.get('error')}"
    except Exception as e:
        traceback.print_exc()
        return f"❌ Error: {e}"

# @mcp.tool()  # STRIPPED
def get_recent_concepts(n: int = 20) -> str:
    """Get the N most recently created/updated concepts from the knowledge graph
    
    Returns a chronological list of recently added or modified concepts with timestamps.
    Useful for reviewing recent work, understanding current context, and maintaining
    awareness of knowledge graph evolution.
    
    Args:
        n: Number of recent concepts to retrieve (default: 20, max: 100)
        
    Returns:
        JSON string with recent concepts list including names and timestamps
    """
    try:
        # Limit to reasonable maximum
        n = min(n, 100)
        
        query = """
        MATCH (c:Wiki) 
        WHERE c.t IS NOT NULL 
        RETURN c.n as name, toString(c.t) as timestamp 
        ORDER BY c.t DESC 
        LIMIT $n
        """
        
        result = utils.query_wiki_graph(query, {"n": n})

        if result.get("success", False):
            concepts = result.get("data", [])

            # Format for readability (ablated - no wrapper)
            formatted_concepts = []
            for i, concept in enumerate(concepts, 1):
                formatted_concepts.append({
                    "rank": i,
                    "name": concept["name"],
                    "timestamp": concept["timestamp"]
                })

            return _fmt(formatted_concepts)
        else:
            return "❌ Failed to query recent concepts"

    except Exception as e:
        logger.error(f"Error getting recent concepts: {e}")
        traceback.print_exc()
        return f"❌ Error: {e}"

# @mcp.tool()  # STRIPPED
def calculate_missing_concepts() -> str:
    """Scan all concepts, update missing_concepts.md, and commit to GitHub

    Scans all existing concepts in the knowledge graph to find references to concepts
    that don't exist yet. Updates the missing_concepts.md file with the findings
    and commits the changes to GitHub.

    Returns:
        JSON string with calculation results and list of missing concepts
    """
    try:
        result = utils.calculate_missing_concepts()
        if result.get("success"):
            return _fmt(result["data"])
        else:
            return f"❌ {result.get('error')}"
    except Exception as e:
        traceback.print_exc()
        return f"❌ Error: {e}"

# @mcp.tool()  # STRIPPED
def deduplicate_concepts(similarity_threshold: float = 0.8) -> str:
    """Find and analyze duplicate or similar concepts

    Scans all concepts in the knowledge graph to find duplicates or similar concepts
    based on name similarity. Useful for identifying concepts that may need to be
    merged or renamed for consistency.

    Args:
        similarity_threshold: Similarity threshold (0.0-1.0, default: 0.8). Higher values require closer matches.

    Returns:
        JSON string with duplicate groups and similarity analysis
    """
    try:
        result = utils.deduplicate_concepts(similarity_threshold)
        if result.get("success"):
            return _fmt(result["data"])
        else:
            return f"❌ {result.get('error')}"
    except Exception as e:
        traceback.print_exc()
        return f"❌ Error: {e}"

# @mcp.tool()  # STRIPPED
def equip_frame(frame: str) -> str:
    """Equip observation frame/lens determining observation structure

    Frames define how to structure observations for different contexts (skill development,
    task decomposition, meta-testing, health tracking, etc.). User-extensible via JSON file.

    Args:
        frame: Name of frame to equip (e.g., 'skill_development', 'meta_test', 'exercise')

    Returns:
        Frame-specific observation prompt/description
    """
    try:
        # Get frames path from env or use default
        frames_path = os.getenv('CARTON_FRAMES_PATH', '/tmp/heaven_data/carton_frames.json')
        frames_file = Path(frames_path)

        # Create default frames if file doesn't exist
        if not frames_file.exists():
            frames_file.parent.mkdir(parents=True, exist_ok=True)
            default_frames = {
                "skill_development": "Observe: skill being developed, current level, practice method, challenges, progress indicators",
                "task_decomposition": "Observe: task, subtasks, dependencies, blockers, completion criteria",
                "meta_test": "Observe: hypothesis, test execution, validation result, what the test proves",
                "transfer_learning": "Observe: source domain, target domain, analogies, what transfers, what doesn't",
                "exercise": "Observe: activity, duration, intensity, recovery state, progression",
                "nutrition": "Observe: food/meal, nutritional content, timing, energy/satiety effects",
                "supplements": "Observe: supplement, dosage, timing, effects, stacking interactions",
                "sleep": "Observe: duration, quality indicators, sleep phases, recovery assessment",
                "wake": "Observe: wake time, energy level, cognitive clarity, readiness state",
                "vision_synthesis": "Observe: components, relationships, emergent properties, big picture integration"
            }
            frames_file.write_text(json.dumps(default_frames))

        # Load frames
        frames = json.loads(frames_file.read_text())

        if frame not in frames:
            available = ", ".join(frames.keys())
            return f"❌ Frame '{frame}' not found. Available frames: {available}"

        return f"🔭 Frame equipped: {frame}\n\n{frames[frame]}"

    except Exception as e:
        traceback.print_exc()
        return f"❌ Error loading frame: {str(e)}"

# CartON Knowledge Management Prompts
@mcp.prompt()
def add_user_thought(user_quote: str, topic: str) -> str:
    """Capture user thoughts verbatim in the CartON knowledge graph with proper attribution and relationships
    
    Args:
        user_quote: Exact user quote to capture
        topic: Topic or context for the thought
        
    Returns:
        Prompt for adding user thought to knowledge graph
    """
    return f"""CartON Prompt Chain Triggered! This prompt is for the caller, you reading this. You need to call mcp__carton__add_concept with concept_name="User_Thoughts_{topic}", concept containing the exact quote "{user_quote}", and relationships formatted as [{{"relationship": "relates_to", "related": ["Concept1", "Concept2"]}}] for any concepts mentioned in the quote, in order to capture this user thought verbatim in the knowledge graph."""

@mcp.prompt()
def update_known_concept(concept_name: str, current_description: str, new_info: str) -> str:
    """Update existing concepts with new information while maintaining consistency and relationships
    
    Args:
        concept_name: Name of concept to update
        current_description: Current concept description
        new_info: New information to integrate
        
    Returns:
        Prompt for updating existing concept
    """
    return f"""CartON Prompt Chain Triggered! This prompt is for the caller, you reading this. You need to call mcp__carton__add_concept with concept_name="{concept_name}", concept that merges "{current_description}" with "{new_info}" seamlessly while preserving core meaning, and maintain all existing relationships formatted as [{{"relationship": "type", "related": ["ConceptList"]}}], in order to update this known concept with new information."""

@mcp.prompt()
def update_user_thought_train_emergently(
    original_concept_name: str, 
    original_description: str, 
    later_concept: str, 
    how_it_led_to: str
) -> str:
    """Track intellectual lineage by showing how user thoughts evolved and led to later insights
    
    Args:
        original_concept_name: Original user thought concept
        original_description: Original description
        later_concept: Later insight/concept
        how_it_led_to: How the thought evolved
        
    Returns:
        Prompt for tracking thought evolution
    """
    return f"""CartON Prompt Chain Triggered! This prompt is for the caller, you reading this. You need to call mcp__carton__add_concept with concept_name="{original_concept_name}", concept that preserves "{original_description}" but adds how it evolved to "{later_concept}" via "{how_it_led_to}", and add relationships=[{{"relationship": "led_to", "related": ["{later_concept}"]}}], in order to track this user thought evolution emergently."""

@mcp.prompt()
def sync_after_update_known_concept(
    concept_list: str,
    change_summary: str,
    sync_number: str = "001"
) -> str:
    """Document concept changes and create sync concepts for version control integration

    Args:
        concept_list: List of updated concepts
        change_summary: Summary of changes made
        sync_number: Sync number (e.g., 001)

    Returns:
        Prompt for creating sync documentation
    """
    return f"""CartON Prompt Chain Triggered! This prompt is for the caller, you reading this. You need to call mcp__carton__add_concept with concept_name="Sync{sync_number}", concept that documents "{concept_list}" were updated because "{change_summary}" and any key insights discovered, ready for GitHub sync, in order to create sync documentation for version control."""

@mcp.prompt()
def observe(description: str) -> str:
    """Quick observation capture - LLM analyzes description and creates appropriate observation

    Args:
        description: What you want to observe (insight, struggle, action, implementation, emotion, etc.)

    Returns:
        Prompt instructing LLM to analyze and create observation
    """
    return f"""[OBSERVATION MODE]
I need you to observe the following input: {description}"""

@mcp.prompt()
def add_frame(frame_name: str, description: str) -> str:
    """Add new observation frame to user's frame collection

    Args:
        frame_name: Name of the new frame (e.g., 'coding_session', 'debugging')
        description: Frame-specific observation prompt describing what to observe

    Returns:
        Prompt instructing LLM to add frame to frames file
    """
    frames_path = os.getenv('CARTON_FRAMES_PATH', '/tmp/heaven_data/carton_frames.json')

    return f"""[ADD FRAME MODE]
I need you to add a new observation frame to the frames file.

Frame name: {frame_name}
Frame description: {description}

File path: {frames_path}

Instructions:
1. Read the current frames file (JSON format)
2. Add new entry: "{frame_name}": "{description}"
3. Write the updated JSON back to the file
4. Confirm the frame was added successfully"""

@mcp.prompt()
def discover_patterns(n: int = 10) -> str:
    """Retrospective pattern discovery - analyze recent conversation turns

    Args:
        n: Number of recent chat turns to analyze (default: 10)

    Returns:
        Prompt for discovering interaction patterns
    """
    return f"""[PATTERN DISCOVERY MODE]
Map any patterns from the last {n} chat turns.

Analysis protocol:
1. What happened step by step?
2. What did I (user) do and what did you (assistant) do?
3. How were we interacting?
4. What patterns emerged in our collaboration?
5. What worked well? What didn't?
6. What metacognitive shifts occurred?

Then create an observation capturing:
- The interaction pattern discovered
- Step-by-step breakdown of the turns
- Collaboration dynamics
- Emergent insights from the pattern analysis"""

@mcp.prompt()
def scientific_method(hypothesis: str) -> str:
    """Apply scientific method to test hypothesis systematically

    Args:
        hypothesis: The hypothesis to test

    Returns:
        Prompt for systematic hypothesis testing
    """
    return f"""[SCIENTIFIC METHOD MODE]
Hypothesis: {hypothesis}

Testing protocol:
1. Query Carton for related concepts and past experiments
2. Design experiment to test hypothesis:
   - What variables to control?
   - What to measure?
   - What outcomes validate/invalidate hypothesis?
3. Execute experiment (code, analysis, observation)
4. Measure results objectively
5. Analyze: Does evidence support or refute hypothesis?
6. Draw conclusions with confidence score

Then create observation capturing:
- Hypothesis statement
- Experimental design
- Execution details
- Results and measurements
- Conclusion with confidence score (0-1)
- What this proves/disproves"""

@mcp.prompt()
def deep_dive(description: str) -> str:
    """Deep exploration of topic with systematic knowledge building

    Args:
        description: Topic or concept to explore deeply

    Returns:
        Prompt for deep dive exploration
    """
    return f"""[DEEP DIVE MODE]
Topic: {description}

Exploration protocol:
1. Query Carton for existing knowledge on this topic
2. Map what we know vs what we don't know
3. Identify knowledge gaps and questions
4. Systematically explore each gap:
   - Research/analyze
   - Build understanding
   - Connect to existing concepts
5. Synthesize findings into coherent understanding
6. Identify emergent insights from deep exploration

Then create observation capturing:
- Topic overview
- Knowledge map (what we knew before)
- Gaps explored
- New understanding built
- Connections to existing concepts
- Emergent insights from deep dive"""

@mcp.prompt()
def krr_engineer_domain(description: str) -> str:
    """Knowledge Representation & Reasoning - engineer domain ontology

    Args:
        description: Domain to engineer (e.g., 'exercise physiology', 'task management')

    Returns:
        Prompt for domain ontology engineering
    """
    return f"""[KRR DOMAIN ENGINEERING MODE]
Domain: {description}

Engineering protocol:
1. Query Carton for any existing domain concepts
2. Map domain landscape:
   - Core concepts (entities)
   - Relationships between concepts
   - Hierarchies (is_a, part_of)
   - Constraints and rules
3. Define ontology structure:
   - What are the fundamental building blocks?
   - How do they relate?
   - What patterns repeat?
4. Create concept network in Carton:
   - Add core domain concepts
   - Establish relationships
   - Document constraints
5. Validate ontology completeness

Then create observation capturing:
- Domain scope and boundaries
- Core concepts identified
- Relationship structure
- Ontology design decisions
- Knowledge representation choices
- What this domain model enables"""

@mcp.prompt()
def autobiography() -> str:
    """Interactive autobiography mapping - capture life memories in Timeline format

    Returns:
        Prompt for guided autobiographical memory capture
    """
    return """[AUTOBIOGRAPHY MODE]
Timeline memory capture protocol:

1. Check for Autobiography concept:
   - Query Carton for "Autobiography" concept
   - If not found, create it with description of life story structure

2. Ask user which period they want to map out:
   - "Which period of your life would you like to map out?"
   - If no preference, choose a significant period for them (childhood, college, first job, etc.)

3. Query Timeline structure:
   - Check existing Year/Month/Day concept formats in Carton
   - Timeline hierarchy: Year -contains-> Month -contains-> Day
   - Format: YYYY (Year), YYYY_MM (Month), YYYY_MM_DD (Day)

4. Capture the coherent story FIRST:
   - "Tell me the general story of this period - what was happening overall?"
   - Focus on narrative arc, major themes, life circumstances
   - Get the complete picture before drilling into details

5. Then capture specific memories:
   - "What specific moments or events do you remember from this period?"
   - For each memory shared:
     a) Identify the date (YYYY_MM_DD format)
     b) Create Day concept if it doesn't exist (e.g., "1995_03_15")
     c) Create observation with memory content
     d) Link observation as part_of the Day concept
     e) Link Day as part_of appropriate Month/Year in Timeline

6. Continue interaction:
   - Keep prompting for more memories in this period
   - Build out the Timeline structure as memories emerge
   - Only exit when user says they want to do something else

Remember:
- Coherent total story > great little pieces
- Conform to Timeline year/month/day formats
- Each memory gets part_of relationship to its Day
- Days get part_of relationship to Months, Months to Years"""

@mcp.prompt()
def stream() -> str:
    """Stream of consciousness capture with recursive grounding - map imagination to reality

    Returns:
        Prompt for active stream of consciousness with granular observation chains
    """
    return """[STREAM MODE]
Active stream of consciousness protocol:

Your role: Listen, suggest, ground, and capture through observation chains.

1. Stream Reception:
   - User will share stream of consciousness thoughts
   - Ideas may be abstract, imaginative, half-formed, or visionary
   - Do NOT interrupt the flow - let ideas emerge naturally

2. Active Suggestion Loop:
   - As ideas emerge, send targeted suggestions to develop them:
     * "What would make this idea concrete?"
     * "How does this connect to [existing concept from Carton]?"
     * "What's the next layer down in this abstraction?"
     * "If you had to implement this tomorrow, what would you need?"

3. Recursive Imagination → Reality Mapping:
   - For each abstract idea, recursively ground it:
     a) Abstract vision (imagination layer)
     b) Concrete manifestation (what this looks like in reality)
     c) Component breakdown (parts needed to build it)
     d) Implementation details (granular next steps)
   - Query Carton for related concepts at each layer
   - Suggest connections between new ideas and existing knowledge

4. Observation Chain Construction (EXTREMELY GRANULAR):
   - Each complete thought → observation
   - Each refinement → observation linked to parent thought
   - Each grounding step → observation showing abstraction→concrete mapping
   - Each connection discovered → observation with relates_to relationships
   - Build chains: Vision → Refinement → Grounding → Implementation → Next_Steps

5. Round Out Ideas:
   - Identify incomplete aspects: "What about [X]?"
   - Fill gaps: "You mentioned [Y], but how does it handle [Z]?"
   - Challenge assumptions: "What if [assumption] isn't true?"
   - Expand edges: "Where else does this apply?"

6. Capture Everything:
   - Create concepts for fully-formed ideas
   - Create observation chains for developmental thinking
   - Link observations: thought_evolution, refines, grounds_into, implements
   - Preserve the journey from abstract to concrete

Continue until user signals they're done streaming.

Remember:
- Granular observation chains > sparse high-level concepts
- Map imagination → reality recursively at every abstraction layer
- Active suggestions drive fuller, rounder ideas
- Preserve both the vision AND the grounding path"""

@mcp.prompt()
def hj(story: str) -> str:
    """Hero's Journey mapping - map story to Vogler's 12-stage model with archetypes

    Args:
        story: The story to map to hero's journey structure

    Returns:
        Prompt for guided hero's journey analysis and mapping
    """
    return f"""[HERO'S JOURNEY MODE]
Story to map: {story}

Vogler's 12-Stage Hero's Journey mapping protocol:

**The 12 Stages:**
1. Ordinary World
2. Call to Adventure
3. Refusal of the Call
4. Meeting the Mentor
5. Crossing the Threshold (Act 1 → Act 2a)
6. Tests, Allies, and Enemies (Fun and Games)
7. Approach to the Inmost Cave
8. Ordeal (Midpoint - Act 2a → Act 2b)
9. Reward (Seizing the Sword)
10. The Road Back (Act 2b → Act 3)
11. Resurrection (Climax)
12. Return with the Elixir

**Your Mission:**
Map the story to these 12 stages with proper archetypes and symbols. You CANNOT do this arbitrarily - the dynamics must be clear and follow archetype logic.

**Protocol:**

1. Assess Story Completeness:
   - Read the story carefully
   - Identify which stages are present vs missing
   - Check for required archetypal elements in each stage

2. If Story is Incomplete, Ask Specific Questions:
   - "I think I still need the act2a fun and games ordeal that breaks into the midpoint."
   - "I don't see the internal psychological obstacle/solution/solved mechanics."
   - "What's the mentor's gift or wisdom that enables threshold crossing?"
   - "Where's the death/rebirth moment at the ordeal?"
   - "What does the hero bring back to the ordinary world?"
   - "Is there a shadow figure or antagonist representing the internal obstacle?"

3. Map Archetypes:
   - Hero (protagonist)
   - Mentor (wisdom giver)
   - Threshold Guardian (tests readiness)
   - Herald (calls to adventure)
   - Shapeshifter (ambiguous loyalty)
   - Shadow (antagonist, internal obstacle)
   - Trickster (comic relief, disruption)

4. Map Symbols and Mechanics:
   - External obstacle (physical challenge)
   - Internal obstacle (psychological barrier)
   - Solution mechanism (how internal obstacle is overcome)
   - Transformation arc (who hero is at start → who they become)
   - Elixir (what knowledge/power hero brings back)

5. Visualize Architecture:
   - As you map, create visual structure showing:
     * Three-act breaks (Threshold, Ordeal, Road Back)
     * Character arcs (transformation journey)
     * Internal vs external conflict progression
     * Archetype relationships and dynamics
   - Use ASCII diagrams or clear structural breakdowns

6. Validate Dynamics:
   - Do the archetypes follow proper logic?
   - Does the internal obstacle resolution enable the external victory?
   - Is there a clear death/rebirth at the ordeal?
   - Does the hero return transformed with something valuable?

7. Iterate Until Complete:
   - Continue asking questions and refining mapping
   - Don't accept incomplete or arbitrary mappings
   - Ensure all 12 stages have clear archetypal content
   - Only conclude when dynamics are crystal clear

8. Capture in Carton:
   - Create concept for the story with hero's journey structure
   - Link archetypes, stages, and transformation mechanics
   - Preserve the complete mapping as knowledge

Remember:
- Archetypal logic > arbitrary stage filling
- Internal psychological mechanics must be clear
- Three-act structure must have proper breaks
- Transformation arc must be complete
- Elixir must be meaningful (not just "they won")"""

# CartON Event Detection Tools - LLM-triggered knowledge capture
# @mcp.tool()
def DetectEvent_user_thought(trigger: bool = True) -> str:
    """YOU MUST call this whenever user expresses a thought worth capturing in the knowledge graph
    
    YOU MUST USE THIS TOOL when you detect these patterns:
    - User shares insights, opinions, or ideas
    - User makes connections between concepts
    - User expresses preferences or mental models
    - User articulates problem-solving approaches
    
    This tool returns instructions for capturing the thought as a concept.
    """
    if trigger:
        return "CartON Prompt Chain Triggered! This prompt is for the caller, you reading this. You need to call mcp__carton__add_concept with concept_name='User_Thoughts_{topic}', concept containing the exact user quote, and relationships formatted as [{\"relationship\": \"relates_to\", \"related\": [\"ConceptList\"]}] for any concepts mentioned, in order to capture this user thought verbatim in the knowledge graph."
    return ""

# @mcp.tool()
def DetectEvent_concept_update(trigger: bool = True) -> str:
    """YOU MUST call this whenever existing concepts need updating with new information
    
    YOU MUST USE THIS TOOL when you detect:
    - New information contradicts or expands existing concept descriptions
    - User provides corrections or clarifications about concepts
    - Context reveals concepts need more detail or different framing
    - Relationships between concepts change or need updating
    
    This tool returns instructions for updating the existing concept, then syncing.
    """
    if trigger:
        return "CartON Prompt Chain Triggered! This prompt is for the caller, you reading this. Find the existing concept file at /tmp/heaven_data/wiki/concepts/{concept_name}/{concept_name}_itself.md (or HEAVEN_DATA_DIR/wiki/concepts/... if set differently). Read the current concept content, merge it with the new information while preserving core meaning and relationships, then write the updated content back to the file. After updating the concept file, call mcp__carton__DetectEvent_sync_needed to document this change."
    return ""

# @mcp.tool()
def DetectEvent_thought_evolution(trigger: bool = True) -> str:
    """YOU MUST call this whenever tracking how user thoughts evolve over time
    
    YOU MUST USE THIS TOOL when you detect:
    - User's current statement builds on or changes previous thoughts
    - User explicitly mentions how their thinking has changed
    - User connects current ideas to past conversations or concepts
    - User shows intellectual progression or refinement of ideas
    
    This triggers a concept update on the specific user thought.
    """
    if trigger:
        return "CartON Prompt Chain Triggered! This prompt is for the caller, you reading this. User thought evolution detected. Call mcp__carton__DetectEvent_concept_update to update the specific user thought concept with how it has evolved."
    return ""

# @mcp.tool()  # STRIPPED
def chroma_query(
    query: str,
    collection_name: str = "carton_concepts",
    k: int = 10,
    max_tokens: int = 20000
) -> str:
    """Query CartON concepts using semantic search via ChromaDB

    Returns ranked concept names based on semantic similarity to the query.
    Use this to discover which concepts are relevant to a topic, then use
    get_concept() or get_concept_network() to retrieve structured knowledge.

    Args:
        query: Natural language query to search for
        collection_name: ChromaDB collection name (default: carton_concepts)
        k: Number of results to retrieve (default: 10)
        max_tokens: Maximum tokens for results (default: 20000)

    Returns:
        Formatted string with ranked concept names and scores
    """
    try:
        return "⚠️ chroma_query DISABLED — ChromaDB needs filtered sync redesign. Use query_wiki_graph with Cypher instead."

        rag = _get_rag(collection_name)

        result = rag.query(
            query=query,
            k=k,
            max_tokens=max_tokens,
            search_type="mmr",
            keyword_boost=True
        )

        if result.get("status") == "success":
            concepts = result.get("concepts", "No concepts found")

            # Cache results to file for query_graph_from_rag_result
            try:
                from datetime import datetime
                import re

                # Parse concept names and scores from formatted string
                # Format: "1. Concept_Name (0.50)\n2. Another_Concept (1.00)\n..."
                concept_list = []
                for line in concepts.split('\n'):
                    # Match: "N. Concept_Name (score)"
                    match = re.match(r'^\d+\.\s+([^\(]+)\s+\(([0-9.]+)\)', line.strip())
                    if match:
                        concept_name = match.group(1).strip()
                        score = float(match.group(2))
                        concept_list.append([concept_name, score])

                cache_data = {
                    "timestamp": datetime.now().isoformat(),
                    "query": query,
                    "results": concept_list
                }

                cache_file = Path(heaven_data_dir) / 'carton_last_rag_query.json'
                cache_file.write_text(json.dumps(cache_data))

            except Exception as cache_error:
                logger.warning(f"Failed to cache RAG results: {cache_error}")

            return f"🔍 **ChromaRAG Semantic Search**\n\nQuery: {query}\n\n{concepts}"
        else:
            return f"❌ Query failed: {result.get('error', 'Unknown error')}"

    except Exception as e:
        traceback.print_exc()
        return f"❌ Error querying ChromaRAG: {str(e)}"

# @mcp.tool()  # STRIPPED
def query_graph_from_rag_result(
    n: int = 5,
    scopes: List[int] = [0, 1],
    max_results: int = 100
) -> str:
    """Query graph for top N concepts from last RAG search with specified depth scopes

    Fetches complete graph context for concepts from the last chroma_query() call.
    Scope levels: 0=concept only, 1=1-hop network, 2=2-hop network

    Deduplicates connected concepts across all source concepts globally.

    Args:
        n: Number of top concepts from last RAG query (default: 5)
        scopes: Depth levels to fetch [0, 1, 2] (default: [0, 1])
        max_results: Maximum items to return before pagination (default: 100)

    Returns:
        JSON string with concept graph data at requested scopes
    """
    try:
        heaven_data_dir = os.getenv('HEAVEN_DATA_DIR', '/tmp/heaven_data')
        cache_file = Path(heaven_data_dir) / 'carton_last_rag_query.json'

        if not cache_file.exists():
            return "❌ No RAG query cached. Run chroma_query() first."

        # Read cached RAG results
        cache_data = json.loads(cache_file.read_text())
        top_concepts = cache_data["results"][:n]

        if not top_concepts:
            return "❌ No concepts in cache"

        # Global deduplication tracking
        seen_concepts = {}  # {concept_name: {description, sources: [list of source concepts]}}
        results = []

        logger.info(f"query_graph_from_rag_result: n={n}, scopes={scopes}, processing {len(top_concepts)} concepts")

        for concept_name, score in top_concepts:
            concept_data = {
                "name": concept_name,
                "rank": len(results) + 1,
                "score": score
            }

            # Track source concept
            if concept_name not in seen_concepts:
                seen_concepts[concept_name] = {"sources": [concept_name]}

            # Fetch requested scopes
            if 0 in scopes:
                # Scope 0: Just the concept description + relationships
                scope_0_result = utils.query_wiki_graph(
                    """
                    MATCH (c:Wiki) WHERE c.n = $concept_name AND c.d IS NOT NULL
                    OPTIONAL MATCH (c)-[r]->(related:Wiki)
                    RETURN c.n as name, c.d as description,
                           collect({type: type(r), target: related.n}) as relationships
                    """,
                    {"concept_name": concept_name}
                )

                if scope_0_result.get("success") and scope_0_result.get("data"):
                    concept_info = scope_0_result["data"][0]
                    relationships = [rel for rel in concept_info.get("relationships", []) if rel.get("type")]
                    concept_data["scope_0"] = {
                        "name": concept_info.get("name"),
                        "description": concept_info.get("description"),
                        "relationships": relationships
                    }
                    seen_concepts[concept_name]["description"] = concept_info.get("description")

            if 1 in scopes or 2 in scopes:
                # Collect network results for deduplication
                for scope in [1, 2]:
                    if scope not in scopes:
                        continue

                    scope_result = utils.get_concept_network(concept_name, depth=scope)
                    if scope_result.get("success"):
                        network = scope_result.get("network", [])
                        deduped_network = []
                        skipped_count = 0

                        for item in network:
                            connected_name = item.get("connected_concept")

                            # Track this connected concept globally
                            if connected_name not in seen_concepts:
                                seen_concepts[connected_name] = {
                                    "description": item.get("connected_description"),
                                    "sources": [concept_name]
                                }
                                # First time seeing this concept - include full info
                                deduped_network.append(item)
                            else:
                                # Already seen - just add source reference
                                if concept_name not in seen_concepts[connected_name]["sources"]:
                                    seen_concepts[connected_name]["sources"].append(concept_name)
                                # Skip duplicate - don't add to network
                                skipped_count += 1

                        logger.info(f"  {concept_name} scope_{scope}: {len(network)} total, {len(deduped_network)} new, {skipped_count} skipped")
                        concept_data[f"scope_{scope}"] = deduped_network

            results.append(concept_data)

        # Count total unique concepts across all scopes
        total_unique_concepts = len(seen_concepts)

        response = {
            "success": True,
            "original_query": cache_data.get("query"),
            "cached_at": cache_data.get("timestamp"),
            "requested_n": n,
            "returned_count": len(results),
            "scopes": scopes,
            "total_unique_concepts": total_unique_concepts,
            "deduplication_applied": True,
            "concepts": results
        }

        if total_unique_concepts > max_results:
            response["note"] = f"Total unique concepts ({total_unique_concepts}) exceeds max_results ({max_results}), but showing all due to deduplication"

        return _fmt(response)

    except Exception as e:
        logger.error(f"Error querying graph from RAG result: {e}")
        traceback.print_exc()
        return f"❌ Error: {e}"

# @mcp.tool()
def DetectEvent_sync_needed(trigger: bool = True) -> str:
    """YOU MUST call this ONLY when other event detection tools tell you to (never independently)

    YOU MUST USE THIS TOOL only when triggered by:
    - DetectEvent_concept_update (after updating concepts)
    - When other events explicitly request sync documentation

    Creates a sync entry that overviews what changes are being synchronized.
    """
    if trigger:
        from datetime import datetime
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        return f"CartON Prompt Chain Triggered! This prompt is for the caller, you reading this. Use add_concept to add this concept: Sync_{timestamp}, with your commit message as the concept arg."
    return ""

# @mcp.tool()  # STRIPPED
def create_collection(
    collection_name: str,
    description: str,
    member_concepts: List[str],
    collection_type: str = "local"
) -> str:
    """Create a new Carton_Collection with HAS_PART relationships

    Collections are concepts that organize related concepts for context engineering.
    Use this to group concepts needed for specific work contexts.

    Args:
        collection_name: Name of the collection (e.g., "OAuth_Implementation_Collection")
        description: Description of what this collection contains and its purpose
        member_concepts: List of concept names that are part of this collection
        collection_type: Type of collection - "global", "local" (default), or "identity"

    Returns:
        Result of collection creation
    """
    try:
        # Bootstrap collection type system if not already done
        utils.bootstrap_collection_types()

        # Map collection_type to appropriate IS_A relationship
        type_mapping = {
            "global": "Global_Collection",
            "local": "Local_Collection",
            "identity": "Identity_Collection"
        }

        if collection_type not in type_mapping:
            return f"❌ Invalid collection_type '{collection_type}'. Must be 'global', 'local', or 'identity'."

        specific_type = type_mapping[collection_type]

        # Build relationships list with HAS_PART and IS_A relationships
        # Note: Only add IS_A to specific type (Global/Local/Identity_Collection)
        # Those types already have IS_A → Carton_Collection, so transitive relationship is implicit
        relationships = [
            {
                "relationship": "has_part",
                "related": member_concepts
            },
            {
                "relationship": "is_a",
                "related": [specific_type]
            }
        ]

        # Create the collection concept with HAS_PART relationships
        from carton_mcp.add_concept_tool import add_concept_tool_func
        result = add_concept_tool_func(collection_name, description, relationships, shared_connection=_neo4j_conn)

        # Now create the inverse PART_OF relationships from members to collection
        if member_concepts:
            cypher_query = """
            MATCH (collection:Wiki {n: $collection_name})
            UNWIND $concept_names as concept_name
            MATCH (concept:Wiki {n: concept_name})
            MERGE (concept)-[:PART_OF]->(collection)
            RETURN count(concept) as linked_count
            """

            inverse_result = utils.query_wiki_graph(
                cypher_query,
                {
                    "collection_name": collection_name,
                    "concept_names": member_concepts
                }
            )

        return f"✅ Carton_Collection created: {collection_name}\nHas {len(member_concepts)} member concepts\n\n{result}"

    except Exception as e:
        traceback.print_exc()
        return f"❌ Error creating collection: {str(e)}"

# @mcp.tool()  # STRIPPED
def activate_collection(collection_name: str) -> str:
    """Activate a Carton_Collection by retrieving all member concepts

    Recursively traverses HAS_PART relationships to get full collection content.
    Use this to load all concepts from a collection for context engineering.

    Args:
        collection_name: Name of the collection to activate

    Returns:
        JSON string with all member concepts in the collection with their descriptions
    """
    try:
        result = utils.get_collection_concepts(collection_name)

        if result.get("success"):
            return _fmt(result)
        else:
            return f"❌ {result.get('error')}"

    except Exception as e:
        traceback.print_exc()
        return f"❌ Error: {e}"

# @mcp.tool()  # STRIPPED
def add_to_collection(
    collection_name: str,
    concept_names: List[str]
) -> str:
    """Add concepts to an existing Carton_Collection

    Creates HAS_PART relationships from the collection to the specified concepts,
    and PART_OF relationships from concepts back to collection.

    Args:
        collection_name: Name of the collection to add concepts to
        concept_names: List of concept names to add as members

    Returns:
        Result of adding concepts to collection
    """
    try:
        # Query to add HAS_PART and PART_OF relationships (both directions)
        # We use MERGE to avoid duplicate relationships
        cypher_query = """
        MATCH (collection:Wiki {n: $collection_name})
        UNWIND $concept_names as concept_name
        MATCH (concept:Wiki {n: concept_name})
        MERGE (collection)-[:HAS_PART]->(concept)
        MERGE (concept)-[:PART_OF]->(collection)
        RETURN count(concept) as added_count
        """

        result = utils.query_wiki_graph(
            cypher_query,
            {
                "collection_name": collection_name,
                "concept_names": concept_names
            }
        )

        if result.get("success") and result.get("data"):
            added_count = result["data"][0].get("added_count", 0)
            return f"✅ Added {added_count} concepts to collection '{collection_name}'"
        else:
            return f"❌ Failed to add concepts: {result.get('error', 'Unknown error')}"

    except Exception as e:
        traceback.print_exc()
        return f"❌ Error adding to collection: {str(e)}"

# @mcp.tool()  # STRIPPED
def list_collections() -> str:
    """List all Carton_Collection concepts in the knowledge graph

    Shows all concepts with IS_A Carton_Collection relationship.

    Returns:
        JSON string with list of collections and their member counts
    """
    try:
        result = utils.list_all_collections()

        if result.get("success"):
            return _fmt(result)
        else:
            return f"❌ {result.get('error')}"

    except Exception as e:
        traceback.print_exc()
        return f"❌ Error: {e}"


# @mcp.tool()  # STRIPPED
def substrate_projector(
    substrate: dict = None,
    target: str = None,
    description_only: bool = True,
    template: str = None,
    get_instructions: bool = False
) -> str:
    """Project Carton concept to substrate. Use get_instructions=True for usage.

    Args:
        substrate: Dict with 'type' and type-specific fields (file, discord, registry, env)
        target: Carton concept name to project
        description_only: If True, just description; if False, include relationships
        template: Optional metastack template name (e.g., 'reference_document', 'framework_document')
                  If provided, renders concept through template before projecting
        get_instructions: If True, returns usage instructions
    """
    try:
        from carton_mcp.substrate_projector import build_instructions, substrate_project

        if get_instructions:
            return build_instructions()

        if substrate is None or target is None:
            return "❌ Required: substrate (dict) and target (concept name). Use get_instructions=True for help."

        result = substrate_project(substrate, target, description_only, template)
        return f"✅ {result}"

    except Exception as e:
        traceback.print_exc()
        return f"❌ Error: {str(e)}"


def _ensure_daemon_running():
    """Start the observation worker daemon if not already running."""
    import subprocess
    from pathlib import Path

    # Check if daemon already running
    result = subprocess.run(
        ['pgrep', '-f', 'observation_worker_daemon.py'],
        capture_output=True
    )
    if result.returncode == 0:
        return  # Already running

    # Start daemon with env vars
    github_pat = os.getenv('GITHUB_PAT')
    repo_url = os.getenv('REPO_URL')
    neo4j_uri = os.getenv('NEO4J_URI', 'bolt://host.docker.internal:7687')
    neo4j_user = os.getenv('NEO4J_USER', 'neo4j')
    neo4j_password = os.getenv('NEO4J_PASSWORD', 'password')
    heaven_data_dir = os.getenv('HEAVEN_DATA_DIR', '/tmp/heaven_data')
    openai_api_key = os.getenv('OPENAI_API_KEY')

    env = os.environ.copy()
    env_update = {
        'GITHUB_PAT': github_pat,
        'REPO_URL': repo_url,
        'NEO4J_URI': neo4j_uri,
        'NEO4J_USER': neo4j_user,
        'NEO4J_PASSWORD': neo4j_password,
        'HEAVEN_DATA_DIR': heaven_data_dir,
        'OPENAI_API_KEY': openai_api_key
    }
    env.update({k: v for k, v in env_update.items() if v is not None})

    daemon_path = Path(__file__).parent / 'observation_worker_daemon.py'
    log_path = '/tmp/carton_worker.log'

    subprocess.Popen(
        ['python3', str(daemon_path)],
        env=env,
        stdout=open(log_path, 'w'),
        stderr=subprocess.STDOUT,
        start_new_session=True
    )


@mcp.tool()
def create_iteration_summary(
    iteration_id: str,
    conversation_id: str,
    summary: str,
    domain: str,
    tools_used: str,
    key_concepts: str,
    files_touched: str,
    skill_candidate_name: str,
    skill_candidate_domain: str,
    skill_candidate_category: str,
    skill_candidate_description: str,
    skill_candidate_source_iterations: str,
) -> str:
    """Create an iteration summary with all required enrichment fields.

    ALL fields are REQUIRED. Use "" for fields with no value.

    Args:
        iteration_id: Full iteration ID e.g. "A4131Cf7_Aaf1_47D8_9288_02Cbacc5E4Cb_1_10"
        conversation_id: Conversation portion e.g. "A4131Cf7_Aaf1_47D8_9288_02Cbacc5E4Cb_1"
        summary: Distilled summary of what happened in this iteration
        domain: Primary domain e.g. "testing", "infrastructure", "cave"
        tools_used: Comma-separated tool names used in this iteration, or ""
        key_concepts: Comma-separated concept names mentioned or relevant, or ""
        files_touched: Comma-separated file paths read or edited, or ""
        skill_candidate_name: Descriptive name for harvested skill, or ""
        skill_candidate_domain: Domain of the skill candidate, or ""
        skill_candidate_category: understand|single_turn|preflight, or ""
        skill_candidate_description: What the skill would contain, or ""
        skill_candidate_source_iterations: Comma-separated iteration IDs that surfaced this skill, or ""
    """
    try:
        # Build relationships from flat args
        relationships = [
            {"relationship": "has_domain", "related": [domain]},
        ]
        if tools_used.strip():
            relationships.append({"relationship": "uses_tool", "related": [t.strip() for t in tools_used.split(",")]})
        if key_concepts.strip():
            relationships.append({"relationship": "mentions_concept", "related": [c.strip() for c in key_concepts.split(",")]})
        if files_touched.strip():
            relationships.append({"relationship": "touches_file", "related": [f.strip() for f in files_touched.split(",")]})

        # Create the iteration summary concept
        concept_name = f"Iteration_Summary_{iteration_id}"
        raw_result = add_concept_tool_func(
            concept_name,
            summary,
            [
                {"relationship": "is_a", "related": ["Iteration_Summary"]},
                {"relationship": "part_of", "related": [f"Conversation_{conversation_id}"]},
                {"relationship": "instantiates", "related": ["Iteration_Summary_Template"]},
                {"relationship": "summarizes", "related": [f"Iteration_{iteration_id}"]},
            ] + relationships,
            desc_update_mode="replace",
            hide_youknow=True,
            shared_connection=_neo4j_conn,
        )
        result = _format_concept_result(concept_name, raw_result)

        # If skill candidate provided, create that too
        if skill_candidate_name.strip():
            category_map = {
                "understand": "Skill_Candidate_Understand",
                "single_turn": "Skill_Candidate_Single_Turn",
                "preflight": "Skill_Candidate_Preflight",
            }
            parent = category_map.get(skill_candidate_category.strip(), "Skill_Candidate")
            source_iters = [s.strip() for s in skill_candidate_source_iterations.split(",") if s.strip()]

            skill_rels = [
                {"relationship": "is_a", "related": ["Skill_Candidate"]},
                {"relationship": "part_of", "related": [parent]},
                {"relationship": "instantiates", "related": ["Skill_Candidate_Template"]},
                {"relationship": "has_domain", "related": [skill_candidate_domain.strip() or domain]},
            ]
            if source_iters:
                skill_rels.append({"relationship": "surfaced_from", "related": [f"Iteration_Summary_{s}" for s in source_iters]})

            skill_raw = add_concept_tool_func(
                skill_candidate_name.strip(),
                skill_candidate_description,
                skill_rels,
                desc_update_mode="append",
                hide_youknow=True,
                shared_connection=_neo4j_conn,
            )
            result += f"\n+ Skill candidate: {skill_candidate_name.strip()}"

        return result
    except Exception as e:
        traceback.print_exc()
        return f"Error creating iteration summary: {str(e)}"


def main():
    """Entry point for summarizer-mcp. No daemon — uses existing carton daemon."""
    mcp.run()

if __name__ == "__main__":
    main()