"""kogni init — set up intelligent development for any project and IDE.

Creates cognigraph.yaml, cognigraph.json (knowledge graph from scan),
MCP server registration (IDE-specific), AI instructions file,
and .kogni/ workspace structure. Works with any IDE or terminal.
"""

from __future__ import annotations

import json
import logging
import os
import re
from pathlib import Path
from typing import Any

import typer
import yaml
from rich.console import Console
from rich.panel import Panel
from rich.prompt import IntPrompt, Prompt
from rich.table import Table

console = Console()
logger = logging.getLogger("cognigraph.cli.init")

# ──────────────────────────────────────────────────────────────────────
# Backend / model registry
# ──────────────────────────────────────────────────────────────────────

BACKENDS: dict[str, dict[str, Any]] = {
    "anthropic": {
        "name": "Anthropic (Claude)",
        "models": [
            ("claude-haiku-4-5-20251001", "Claude Haiku — Fast + cheap ($0.001/query)", True),
            ("claude-sonnet-4-6", "Claude Sonnet — Balanced ($0.01/query)", False),
            ("claude-opus-4-6", "Claude Opus — Most capable ($0.05/query)", False),
        ],
        "api_key_env": "ANTHROPIC_API_KEY",
    },
    "openai": {
        "name": "OpenAI",
        "models": [
            ("gpt-4o-mini", "GPT-4o-mini — Fast + cheap ($0.002/query)", True),
            ("gpt-4o", "GPT-4o — Balanced ($0.01/query)", False),
        ],
        "api_key_env": "OPENAI_API_KEY",
    },
    "bedrock": {
        "name": "AWS Bedrock",
        "models": [
            (
                "anthropic.claude-haiku-4-5-20251001-v1:0",
                "Claude Haiku on Bedrock",
                True,
            ),
            (
                "anthropic.claude-sonnet-4-6-v1:0",
                "Claude Sonnet on Bedrock",
                False,
            ),
        ],
        "api_key_env": "AWS_DEFAULT_REGION",
    },
    "custom": {
        "name": "Custom OpenAI-compatible endpoint",
        "models": [],
        "api_key_env": "CUSTOM_API_KEY",
    },
}

# ──────────────────────────────────────────────────────────────────────
# AI instructions content — universal across IDEs
# ──────────────────────────────────────────────────────────────────────

AI_INSTRUCTIONS_SECTION = r"""
# CogniGraph — Dev Intelligence Layer

> Auto-generated by `kogni init`. Provides your AI coding assistant with
> structured access to your project's knowledge graph, reasoning engine,
> and session memory — regardless of which IDE or AI tool you use.

---

## What CogniGraph Does

CogniGraph turns your codebase into a **knowledge graph** where every module,
service, config, and dependency is a node. It then provides:

1. **Focused context** — 500-token summaries instead of 20-60K brute-force file reads
2. **Graph reasoning** — multi-agent reasoning across your entire architecture
3. **Impact analysis** — "what breaks if I change X?" answered in seconds
4. **Lessons learned** — past mistakes surfaced before you repeat them
5. **Auto-growing graph** — git hooks keep the KG in sync with your code

---

## Available Tools

### CLI (works in any terminal)
| Command | What it does |
|---------|-------------|
| `kogni run "<query>"` | Reason over the knowledge graph |
| `kogni context <name>` | Get 500-token focused context for any entity |
| `kogni inspect --stats` | Graph statistics (nodes, edges, density) |
| `kogni scan repo .` | Re-scan codebase and rebuild the KG |
| `kogni doctor` | Health check your installation |
| `kogni setup-guide` | Step-by-step backend setup |

### MCP Tools (Claude Code, Cursor, VS Code, and MCP-compatible IDEs)
| Tool | Purpose |
|------|---------|
| `kogni_context` | Focused context for a service/module |
| `kogni_reason` | Multi-agent graph reasoning |
| `kogni_inspect` | Graph structure inspection |
| `kogni_preflight` | Pre-change safety check |
| `kogni_impact` | Impact analysis for changes |
| `kogni_lessons` | Past mistake patterns |
| `kogni_learn` | Teach the graph new knowledge |

### Python SDK (works everywhere)
```python
from cognigraph import CogniGraph
from cognigraph.backends.api import AnthropicBackend

graph = CogniGraph.from_json("cognigraph.json")
graph.set_default_backend(AnthropicBackend(model="claude-haiku-4-5-20251001"))
result = graph.reason("What depends on the auth service?")
print(result.answer)
```

---

## Smart Query Routing

Always pick the **cheapest correct approach** before calling expensive tools:

| Query Type | Route | Cost | When |
|-----------|-------|------|------|
| **LOOKUP** | `grep` or read KG files | ~200 tokens | Single fact, named entity |
| **CROSS-CUT** | Read specific KG section | ~300-800 tokens | Spans 2-3 known entities |
| **IMPACT** | `kogni_impact` | ~800-1500 tokens | "What breaks if..." |
| **REASONING** | `kogni_reason` | ~1500-3000 tokens | Multi-hop "why/how" questions |
| **PREFLIGHT** | `kogni_preflight` | ~500-1000 tokens | Pre-change safety check |
| **LESSONS** | `kogni_lessons` | ~400-800 tokens | Past mistake patterns |

**Rules:**
1. **Cheapest first.** Always try grep/read before MCP tools.
2. **Never use kogni_reason for lookups.** A grep is 10x cheaper.
3. **Escalate, don't guess.** If grep finds nothing, escalate to next tier.

---

## Session Continuity (optional — .kogni/ workspace)

CogniGraph can maintain structured memory across sessions:

```
.kogni/
├── main.md           — project roadmap and goals
├── registry.md       — active branches/tasks
├── branches/
│   └── main/
│       ├── commit.md     — milestone summaries
│       ├── log.md        — session trace
│       └── metadata.yaml — branch state
└── checkpoints/      — archived session snapshots
```

### Session Start
1. Read `.kogni/main.md` (~300 tokens)
2. Read `.kogni/branches/{active}/commit.md` (~400 tokens)
3. Resume from last checkpoint

### During Work
- Checkpoint after every completed milestone
- Auto-checkpoint every 30 minutes

### Session End
- Save progress (even if incomplete)
- Create checkpoint for next session

---

## Configuration

- `cognigraph.yaml` — Model backend, cost budgets, activation strategy
- `cognigraph.json` — The knowledge graph (auto-generated by `kogni scan repo .`)
- `.mcp.json` — MCP server registration (IDE-specific)

To re-scan: `kogni scan repo .`
To query: `kogni run "your question"`
To get context: `kogni context <service-name>`
""".strip()

# Keep backward compat for existing code referencing old name
CLAUDE_MD_SECTION = AI_INSTRUCTIONS_SECTION

# ──────────────────────────────────────────────────────────────────────
# Repo scanning (enhanced from scan.py)
# ──────────────────────────────────────────────────────────────────────

_SKIP_DIRS = {
    "__pycache__",
    ".git",
    "node_modules",
    ".venv",
    "venv",
    ".next",
    ".nuxt",
    "dist",
    "build",
    ".tox",
    ".mypy_cache",
    ".pytest_cache",
    ".ruff_cache",
    "egg-info",
    ".eggs",
}

_PYTHON_EXTS = {".py"}
_JS_TS_EXTS = {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}
_CONFIG_NAMES = {
    "package.json",
    "pyproject.toml",
    "setup.py",
    "setup.cfg",
    "Cargo.toml",
    "go.mod",
    "Makefile",
    "Dockerfile",
    "docker-compose.yml",
    "docker-compose.yaml",
    ".env",
    ".env.example",
    "requirements.txt",
    "Pipfile",
}


def _should_skip(path: Path) -> bool:
    """Return True if *path* (or any ancestor) is in the skip set."""
    return any(part in _SKIP_DIRS or part.endswith(".egg-info") for part in path.parts)


def _detect_project_type(root: Path) -> str:
    """Detect whether the project is Python, Node, or monorepo."""
    has_python = (root / "pyproject.toml").exists() or (root / "setup.py").exists()
    has_node = (root / "package.json").exists()
    if has_python and has_node:
        return "monorepo"
    if has_node:
        return "node"
    return "python"


def _extract_python_imports(content: str) -> list[str]:
    """Return local-looking import targets from Python source."""
    imports: list[str] = []
    for line in content.splitlines():
        stripped = line.strip()
        if stripped.startswith("from ") or stripped.startswith("import "):
            match = re.match(r"(?:from|import)\s+([\w.]+)", stripped)
            if match:
                module = match.group(1)
                # Keep relative and dotted (likely local) imports
                if "." in module:
                    imports.append(module)
    return imports


def _extract_js_imports(content: str) -> list[str]:
    """Return local import paths from JS/TS source."""
    imports: list[str] = []
    for match in re.finditer(
        r"""(?:import|require)\s*\(?\s*['"]([./][^'"]+)['"]\s*\)?""", content
    ):
        imports.append(match.group(1))
    for match in re.finditer(r"""from\s+['"]([./][^'"]+)['"]""", content):
        imports.append(match.group(1))
    return imports


def _chunk_source_code(content: str, max_chunk_chars: int = 1500) -> list[dict[str, str]]:
    """Split source code into semantic chunks at function/class boundaries.

    Each chunk is a dict with "text" and "type" keys, matching the format
    expected by CogniNode._build_evidence_text().

    Strategy: split on top-level def/class/function/export boundaries.
    If a block exceeds max_chunk_chars, split it further by blank lines.
    """
    chunks: list[dict[str, str]] = []
    lines = content.splitlines(keepends=True)

    if not lines:
        return []

    # Patterns that start a new semantic block
    boundary_patterns = (
        "def ", "class ", "async def ",           # Python
        "function ", "export ", "const ", "let ",  # JS/TS
        "import ", "from ",                        # import blocks
        "describe(", "it(", "test(",              # tests
    )

    current_block: list[str] = []
    block_type = "source_code"

    def _flush_block() -> None:
        text = "".join(current_block).strip()
        if not text or len(text) < 20:
            return
        # If block is too large, split by blank lines
        if len(text) > max_chunk_chars:
            sub_parts = re.split(r"\n\s*\n", text)
            accum = ""
            for part in sub_parts:
                if len(accum) + len(part) > max_chunk_chars and accum:
                    chunks.append({"text": accum.strip(), "type": block_type})
                    accum = part
                else:
                    accum = accum + "\n\n" + part if accum else part
            if accum.strip():
                chunks.append({"text": accum.strip(), "type": block_type})
        else:
            chunks.append({"text": text, "type": block_type})

    for line in lines:
        stripped = line.lstrip()
        # Check if this line starts a new top-level block
        is_boundary = (
            any(stripped.startswith(p) for p in boundary_patterns)
            and not line[0:1].isspace()  # top-level only (no indentation)
        )

        if is_boundary and current_block:
            _flush_block()
            current_block = [line]
            # Detect block type
            if stripped.startswith(("def ", "async def ")):
                block_type = "function"
            elif stripped.startswith("class "):
                block_type = "class"
            elif stripped.startswith(("import ", "from ")):
                block_type = "imports"
            elif stripped.startswith(("export ",)):
                block_type = "export"
            elif stripped.startswith(("describe(", "it(", "test(")):
                block_type = "test"
            else:
                block_type = "source_code"
        else:
            current_block.append(line)

    # Flush remaining
    if current_block:
        _flush_block()

    return chunks


def _summarize_file(content: str, file_path: str, max_len: int = 300) -> str:
    """Generate a rich description from file content (no LLM needed).

    Extracts exports, function signatures, class names, and docstrings
    to create a meaningful description for the node agent.
    """
    lines = content.splitlines()
    parts: list[str] = []

    # Extract docstring (first triple-quote block or JSDoc)
    for i, line in enumerate(lines[:20]):
        stripped = line.strip()
        if stripped.startswith(('"""', "'''", "/*", "/**")):
            doc_lines = []
            for j in range(i, min(i + 8, len(lines))):
                doc_lines.append(lines[j].strip().strip('"\'*/'))
            doc = " ".join(dl for dl in doc_lines if dl)[:200]
            if doc:
                parts.append(doc)
            break

    # Extract function/class signatures
    signatures: list[str] = []
    for line in lines:
        stripped = line.strip()
        if stripped.startswith(("def ", "async def ", "class ")):
            sig = stripped.split(":", 1)[0].split("{", 1)[0].strip()
            signatures.append(sig)
        elif stripped.startswith(("export function", "export const", "export default")):
            sig = stripped[:100].split("{", 1)[0].strip()
            signatures.append(sig)

    if signatures:
        parts.append("Defines: " + ", ".join(signatures[:10]))

    if not parts:
        # Fallback: first non-empty, non-comment lines
        for line in lines[:10]:
            stripped = line.strip()
            if stripped and not stripped.startswith(("#", "//", "/*", "*")):
                parts.append(stripped[:100])
                break

    summary = f"{file_path}. " + ". ".join(parts)
    return summary[:max_len]


def scan_repository(root: Path) -> dict[str, Any]:
    """Scan a repository and return a NetworkX-compatible node-link dict.

    Returns the dict directly (no NetworkX dependency required at scan time
    for the init command — we build the structure manually).

    Each source file node includes:
    - description: rich summary (signatures, docstrings)
    - chunks: semantic code chunks for evidence-based reasoning
    """
    nodes: list[dict[str, Any]] = []
    edges: list[dict[str, Any]] = []
    node_ids: set[str] = set()
    import_edges: list[tuple[str, str]] = []

    project_type = _detect_project_type(root)

    # ── Collect source files ────────────────────────────────────────
    source_exts = _PYTHON_EXTS | _JS_TS_EXTS

    for fpath in root.rglob("*"):
        if not fpath.is_file():
            continue
        if _should_skip(fpath):
            continue

        rel = fpath.relative_to(root)
        file_id = str(rel).replace("\\", "/")

        # Source files
        if fpath.suffix in source_exts:
            entity_type = "PythonModule" if fpath.suffix in _PYTHON_EXTS else "JSModule"

            # Read file content for chunking + description
            content = ""
            chunks: list[dict[str, str]] = []
            description = f"{entity_type}: {rel}"
            try:
                content = fpath.read_text(encoding="utf-8", errors="ignore")
                # Generate rich description from content
                description = _summarize_file(content, str(rel))
                # Chunk file content for evidence-based reasoning
                chunks = _chunk_source_code(content)
            except Exception:
                pass

            node_data: dict[str, Any] = {
                "id": file_id,
                "label": fpath.stem,
                "type": entity_type,
                "description": description,
                "file_path": str(rel),  # T3: lazy load full file when needed
            }
            if chunks:
                node_data["chunks"] = chunks
                node_data["chunk_count"] = len(chunks)

            nodes.append(node_data)
            node_ids.add(file_id)

            # Directory node
            if len(rel.parts) > 1:
                dir_id = "/".join(rel.parts[:-1])
                if dir_id not in node_ids:
                    nodes.append(
                        {
                            "id": dir_id,
                            "label": rel.parts[-2],
                            "type": "Directory",
                            "description": f"Directory: {dir_id}",
                        }
                    )
                    node_ids.add(dir_id)
                edges.append(
                    {"source": dir_id, "target": file_id, "relationship": "CONTAINS"}
                )

            # Parse imports from already-read content
            try:
                if content and fpath.suffix in _PYTHON_EXTS:
                    for imp in _extract_python_imports(content):
                        target = imp.replace(".", "/") + ".py"
                        import_edges.append((file_id, target))
                elif content and fpath.suffix in _JS_TS_EXTS:
                    for imp in _extract_js_imports(content):
                        import_edges.append((file_id, imp))
            except Exception:
                pass

        # Config files
        elif fpath.name in _CONFIG_NAMES:
            if file_id not in node_ids:
                # Read config content for chunks too
                config_chunks: list[dict[str, str]] = []
                config_desc = f"Configuration: {fpath.name}"
                try:
                    config_content = fpath.read_text(encoding="utf-8", errors="ignore")
                    if config_content.strip():
                        # Store config as a single chunk (configs are usually small)
                        config_chunks = [{"text": config_content[:3000], "type": "config"}]
                        config_desc = f"Configuration: {fpath.name}. " + config_content[:200].replace("\n", " ")
                except Exception:
                    pass

                node_data = {
                    "id": file_id,
                    "label": fpath.stem,
                    "type": "Config",
                    "description": config_desc,
                }
                if config_chunks:
                    node_data["chunks"] = config_chunks
                nodes.append(node_data)
                node_ids.add(file_id)

        # Test files
        elif fpath.name.startswith("test_") or fpath.name.endswith("_test.py"):
            if file_id not in node_ids:
                # Read test content for chunks
                test_chunks: list[dict[str, str]] = []
                test_desc = f"Test: {rel}"
                try:
                    test_content = fpath.read_text(encoding="utf-8", errors="ignore")
                    test_desc = _summarize_file(test_content, str(rel))
                    test_chunks = _chunk_source_code(test_content)
                except Exception:
                    pass

                node_data = {
                    "id": file_id,
                    "label": fpath.stem,
                    "type": "TestModule",
                    "description": test_desc,
                }
                if test_chunks:
                    node_data["chunks"] = test_chunks
                nodes.append(node_data)
                node_ids.add(file_id)

    # ── Resolve import edges ────────────────────────────────────────
    for source, target in import_edges:
        # Try exact match and common variants
        candidates = [target]
        if not target.endswith((".py", ".js", ".ts", ".tsx", ".jsx")):
            candidates.extend([target + ext for ext in [".py", ".ts", ".js", "/index.ts", "/index.js"]])
        for candidate in candidates:
            if candidate in node_ids:
                edges.append(
                    {"source": source, "target": candidate, "relationship": "IMPORTS"}
                )
                break

    return {
        "directed": True,
        "multigraph": False,
        "graph": {"project_type": project_type},
        "nodes": nodes,
        "links": edges,
    }


# ──────────────────────────────────────────────────────────────────────
# File generators
# ──────────────────────────────────────────────────────────────────────


def _build_cognigraph_yaml(
    backend: str, model: str, api_key_ref: str
) -> str:
    """Return the contents of cognigraph.yaml."""
    cfg: dict[str, Any] = {
        "model": {
            "backend": backend if backend != "custom" else "api",
            "model": model,
            "api_key": api_key_ref,
        },
        "graph": {
            "connector": "networkx",
        },
        "activation": {
            "strategy": "pcst",
            "max_nodes": 20,
        },
        "orchestration": {
            "max_rounds": 3,
            "convergence_threshold": 0.92,
        },
        "cost": {
            "budget_per_query": 0.05,
        },
        "observer": {
            "enabled": True,
        },
        "governance": {
            "enabled": True,
        },
    }
    return yaml.dump(cfg, default_flow_style=False, sort_keys=False)


# ──────────────────────────────────────────────────────────────────────
# IDE detection and config
# ──────────────────────────────────────────────────────────────────────

SUPPORTED_IDES = {
    "auto": "Auto-detect from project files",
    "claude": "Claude Code (.mcp.json + CLAUDE.md)",
    "cursor": "Cursor (.cursor/mcp.json + .cursorrules)",
    "vscode": "VS Code (.vscode/mcp.json + .github/copilot-instructions.md)",
    "windsurf": "Windsurf (.mcp.json + .windsurfrules)",
    "generic": "Any IDE / terminal (kogni CLI only, no MCP)",
}


def _detect_ide(root: Path) -> str:
    """Auto-detect which IDE the project uses."""
    if (root / ".cursor").is_dir() or (root / ".cursorrules").exists():
        return "cursor"
    if (root / ".vscode").is_dir():
        return "vscode"
    if (root / ".windsurfrules").exists():
        return "windsurf"
    if (root / "CLAUDE.md").exists() or (root / ".mcp.json").exists():
        return "claude"
    # Default to claude for backward compat, but show as generic-compatible
    return "claude"


def _build_mcp_json(ide: str = "claude") -> dict[str, Any]:
    """Return the MCP server config structure for the target IDE."""
    mcp_entry = {
        "type": "stdio",
        "command": "kogni",
        "args": ["mcp", "serve", "--config", "cognigraph.yaml"],
    }
    return {
        "mcpServers": {
            "cognigraph": mcp_entry,
        }
    }


def _get_mcp_path(root: Path, ide: str) -> Path:
    """Return the MCP config file path for the target IDE."""
    if ide == "cursor":
        return root / ".cursor" / "mcp.json"
    elif ide == "vscode":
        return root / ".vscode" / "mcp.json"
    else:
        # claude, windsurf, generic all use root .mcp.json
        return root / ".mcp.json"


def _get_instructions_path(root: Path, ide: str) -> Path:
    """Return the AI instructions file path for the target IDE."""
    if ide == "cursor":
        return root / ".cursorrules"
    elif ide == "vscode":
        return root / ".github" / "copilot-instructions.md"
    elif ide == "windsurf":
        return root / ".windsurfrules"
    else:
        # claude or generic
        return root / "CLAUDE.md"


def _get_instructions_marker(ide: str) -> str:
    """Marker to detect if instructions already exist."""
    return "# CogniGraph — Dev Intelligence Layer"


def _build_gcc_main_md(root: Path) -> str:
    """Auto-generate .gcc/main.md from project README if it exists."""
    readme_path = root / "README.md"
    header = "# Project Roadmap\n\n"
    if readme_path.exists():
        try:
            content = readme_path.read_text(errors="ignore")
            # Extract first 30 lines as summary
            lines = content.splitlines()[:30]
            summary = "\n".join(lines)
            return header + f"## From README\n\n{summary}\n\n## Goals\n\n- [ ] Define project goals here\n"
        except Exception:
            pass
    return header + "## Goals\n\n- [ ] Define project goals here\n"


def _build_gcc_registry_md() -> str:
    """Return initial .gcc/registry.md content."""
    return (
        "# Branch Registry\n\n"
        "| Branch | Status | Owner | Last Active |\n"
        "|--------|--------|-------|-------------|\n"
        "| main | ACTIVE | — | — |\n"
    )


def _build_gcc_config_yaml() -> str:
    """Return .gcc/config.yaml content."""
    cfg = {
        "project": "auto-detected",
        "token_budget": {
            "session_start": 800,
            "max_gcc_overhead_pct": 5,
        },
        "auto_commit_interval_min": 30,
    }
    return yaml.dump(cfg, default_flow_style=False, sort_keys=False)


def _build_gcc_metadata_yaml() -> str:
    """Return initial branch metadata.yaml."""
    cfg = {
        "branch": "main",
        "parent": None,
        "status": "ACTIVE",
        "file_tree": [],
        "open_questions": [],
    }
    return yaml.dump(cfg, default_flow_style=False, sort_keys=False)


# ──────────────────────────────────────────────────────────────────────
# /kogni skill — smart query router for Claude Code
# ──────────────────────────────────────────────────────────────────────

KOGNI_SKILL_CONTENT = r"""Smart query router for project knowledge. Classifies the user's question and picks the cheapest approach that gives a correct answer. NEVER default to CogniGraph for simple lookups.

## Decision Tree — Execute in order, stop at first match

### Step 1: Classify the query

Read the user's question (passed as $ARGUMENTS) and classify it into ONE of these categories:

| Category | Pattern | Examples |
|----------|---------|----------|
| **LOOKUP** | Single fact, named entity, "which X does Y" | "Which Lambda creates manifest?", "What's the Neo4j URI?", "What env vars does L03 need?" |
| **CROSS-CUT** | Spans 3+ components, services, or files | "What services depend on Neo4j?", "Which Lambdas use shared/tamr_retrieval.py?", "What modules does L06 import?" |
| **IMPACT** | "What happens if", "what breaks", change analysis | "What breaks if I remove trace_scoring.py?", "Impact of changing Neo4j schema?" |
| **REASONING** | Why/how questions, architectural, multi-hop | "Why does TRACE score drop for single-doc queries?", "How does the document pipeline work end-to-end?" |
| **PREFLIGHT** | About to change code, need safety check | "What should I check before modifying L03?", "Any gotchas for CORS changes?" |
| **LESSONS** | Past mistakes, failure patterns | "What went wrong with document naming?", "Common deployment failures?" |

### Step 2: Route to cheapest approach

#### LOOKUP -> Direct grep/read (cost: 100-500 tokens)
```
1. Grep for the key term in .gcc/project-kg.md (or cognigraph.json)
2. If found -> return the relevant rows/section. DONE.
3. If not found -> grep tasks/lessons-distilled.md
4. If still not found -> escalate to CROSS-CUT
```
**Tools:** Grep, Read (targeted lines only)
**DO NOT** use CogniGraph MCP tools for lookups. They are 5-10x more expensive than a grep.

#### CROSS-CUT -> Targeted read of project-kg.md sections (cost: 300-800 tokens)
```
1. Read only the relevant section(s) of .gcc/project-kg.md:
   - Lambda info -> LAMBDA NODES table
   - Dependencies -> SHARED MODULES + DEPENDENCY GRAPH tables
   - Env vars -> ENVVAR REQUIREMENTS table
   - Invocations -> INVOCATION CHAIN table
   - Errors -> MISTAKE NODES table
2. Synthesize answer from the table data. DONE.
3. Only if the answer requires info NOT in project-kg.md -> escalate to REASONING
```
**Tools:** Read (with offset/limit to target specific sections)
**Use CogniGraph only if** project-kg.md doesn't have the relationships needed.

#### IMPACT -> kogni_impact MCP tool (cost: ~800-1500 tokens)
```
1. Call mcp__cognigraph__kogni_impact with the component name
2. Present the impact trace to the user
```
**This is where CogniGraph earns its keep** — traversing dependency chains automatically.

#### REASONING -> kogni_reason MCP tool (cost: ~1500-3000 tokens)
```
1. Call mcp__cognigraph__kogni_reason with the question, max_rounds=2
2. If confidence < 0.5, supplement with targeted file reads
3. Present synthesized answer
```
**This is CogniGraph's sweet spot** — multi-hop reasoning across nodes.

#### PREFLIGHT -> kogni_preflight MCP tool (cost: ~500-1000 tokens)
```
1. Call mcp__cognigraph__kogni_preflight with the action and files
2. Present warnings, lessons, and safety boundaries
```

#### LESSONS -> kogni_lessons MCP tool (cost: ~400-800 tokens)
```
1. Call mcp__cognigraph__kogni_lessons with the operation
2. Present matched lessons with severity
```

### Step 3: Report the routing decision

Always start your response with a one-line routing tag so the user sees the cost/benefit:

```
[KOGNI: {CATEGORY} -> {approach} | ~{estimated_tokens} tokens]
```

Examples:
- `[KOGNI: LOOKUP -> grep project-kg.md | ~200 tokens]`
- `[KOGNI: IMPACT -> kogni_impact | ~1200 tokens]`
- `[KOGNI: REASONING -> kogni_reason (3 rounds) | ~2500 tokens]`

### Step 4: Answer the question

After routing, answer the user's question using the chosen approach. Be concise. Cite the source (file:line or CogniGraph node IDs).

## Token Budget Summary

| Approach | Typical cost | When to use |
|----------|-------------|-------------|
| Grep project-kg.md | 100-300 tokens | Single fact, named entity |
| Read section of project-kg.md | 300-800 tokens | Cross-referencing 2-3 tables |
| kogni_lessons | 400-800 tokens | Past mistake patterns |
| kogni_preflight | 500-1000 tokens | Pre-change safety check |
| kogni_impact | 800-1500 tokens | Change impact analysis |
| kogni_reason | 1500-3000 tokens | Multi-hop reasoning, "why" questions |

## Rules

1. **Cheapest first.** Always try grep before CogniGraph.
2. **Never use kogni_reason for lookups.** It's 10x the cost of a grep.
3. **Always show the routing tag.** Transparency builds trust.
4. **Escalate, don't guess.** If grep returns nothing, escalate to the next tier — don't fabricate.
5. **Cache awareness.** If project-kg.md was already read this session, reuse from memory — don't re-read.
""".strip()


# ──────────────────────────────────────────────────────────────────────
# Backend verification — test that the API key works BEFORE proceeding
# ──────────────────────────────────────────────────────────────────────


def _resolve_api_key(api_key_ref: str) -> str | None:
    """Resolve a key reference like ${ANTHROPIC_API_KEY} to the actual value."""
    if api_key_ref.startswith("${") and api_key_ref.endswith("}"):
        env_var = api_key_ref[2:-1]
        return os.environ.get(env_var)
    return api_key_ref  # raw key


def _verify_backend(
    backend: str,
    model: str,
    api_key_ref: str,
    no_interactive: bool,
) -> None:
    """Verify the backend actually works by making a minimal API call.

    This is the FIRST thing a new user sees after choosing their backend.
    If it fails, they get clear, actionable instructions to fix it.
    """
    resolved_key = _resolve_api_key(api_key_ref)

    # ── Step 1: Check if the key is even available ──────────────────
    if resolved_key is None:
        env_var = api_key_ref[2:-1] if api_key_ref.startswith("${") else "API_KEY"
        console.print(
            f"  [yellow]WARNING:[/yellow] Environment variable "
            f"[bold]{env_var}[/bold] is not set.\n"
            f"  The graph and MCP tools will be created, but reasoning\n"
            f"  queries (kogni run, kogni_reason) will fail until you set it.\n"
        )
        console.print(
            f"  [dim]Fix: export {env_var}=your-key-here[/dim]\n"
            f"  [dim]Then restart Claude Code to pick up the change.[/dim]\n"
        )
        return

    # ── Step 2: Try a minimal API call ──────────────────────────────
    try:
        if backend == "anthropic":
            import anthropic
            client = anthropic.Anthropic(api_key=resolved_key)
            resp = client.messages.create(
                model=model,
                max_tokens=10,
                messages=[{"role": "user", "content": "hi"}],
            )
            console.print(
                f"  [green]Connected to Anthropic ({model})[/green]\n"
            )

        elif backend == "openai":
            import openai
            client = openai.OpenAI(api_key=resolved_key)
            client.chat.completions.create(
                model=model,
                max_tokens=10,
                messages=[{"role": "user", "content": "hi"}],
            )
            console.print(
                f"  [green]Connected to OpenAI ({model})[/green]\n"
            )

        elif backend == "bedrock":
            import boto3
            region = resolved_key or os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
            client = boto3.client("bedrock-runtime", region_name=region)
            import json as _json
            client.invoke_model(
                modelId=model,
                body=_json.dumps({
                    "anthropic_version": "bedrock-2023-05-31",
                    "max_tokens": 10,
                    "messages": [{"role": "user", "content": "hi"}],
                }),
                contentType="application/json",
            )
            console.print(
                f"  [green]Connected to AWS Bedrock ({model} in {region})[/green]\n"
            )

        elif backend == "custom":
            console.print(
                f"  [dim]Custom backend — skipping verification.[/dim]\n"
                f"  [dim]Test manually: kogni run \"hello\"[/dim]\n"
            )

    except ImportError as e:
        pkg = str(e).split("'")[-2] if "'" in str(e) else "required package"
        console.print(
            f"  [yellow]WARNING:[/yellow] Missing package: {pkg}\n"
            f"  [dim]Fix: pip install {pkg}[/dim]\n"
            f"  The graph will be created, but reasoning needs this package.\n"
        )

    except Exception as e:
        err_msg = str(e)[:200]
        console.print(
            f"  [yellow]WARNING:[/yellow] Backend connection failed.\n"
            f"  [dim]{err_msg}[/dim]\n"
        )

        # Provide backend-specific fix instructions
        if backend == "anthropic":
            console.print(
                "  [bold]To fix:[/bold]\n"
                "  1. Check your API key at https://console.anthropic.com/\n"
                "  2. export ANTHROPIC_API_KEY=sk-ant-...\n"
                "  3. Re-run: kogni init\n"
            )
        elif backend == "openai":
            console.print(
                "  [bold]To fix:[/bold]\n"
                "  1. Check your API key at https://platform.openai.com/api-keys\n"
                "  2. export OPENAI_API_KEY=sk-...\n"
                "  3. Re-run: kogni init\n"
            )
        elif backend == "bedrock":
            console.print(
                "  [bold]To fix:[/bold]\n"
                "  1. Ensure AWS credentials are configured (aws configure)\n"
                "  2. Ensure the model is enabled in your AWS Bedrock console\n"
                "  3. Set the region: export AWS_DEFAULT_REGION=us-east-1\n"
                "  4. Re-run: kogni init\n"
            )

        console.print(
            "  [dim]Continuing setup — graph and MCP tools will still be created.\n"
            "  Reasoning queries will fail until the backend is fixed.[/dim]\n"
        )


# ──────────────────────────────────────────────────────────────────────
# Interactive prompts
# ──────────────────────────────────────────────────────────────────────


def _prompt_backend() -> str:
    """Ask the user to pick a backend, with auto-detection of what's available."""
    import importlib

    # Auto-detect which backends are ready
    detection = {
        "anthropic": {"pkg": "anthropic", "env": "ANTHROPIC_API_KEY"},
        "openai": {"pkg": "openai", "env": "OPENAI_API_KEY"},
        "bedrock": {"pkg": "boto3", "env": "AWS_ACCESS_KEY_ID"},
        "ollama": {"pkg": "httpx", "env": None},
        "custom": {"pkg": None, "env": None},
    }

    table = Table(title="Available Backends", show_header=True, header_style="bold cyan")
    table.add_column("#", style="dim", width=3)
    table.add_column("Backend", style="bold")
    table.add_column("Description")
    table.add_column("Status", style="dim")

    backend_keys = list(BACKENDS.keys())
    recommended_idx = 1  # default

    for i, key in enumerate(backend_keys, 1):
        det = detection.get(key, {})
        pkg = det.get("pkg")
        env = det.get("env")

        # Check package
        pkg_ok = True
        if pkg:
            try:
                importlib.import_module(pkg)
            except ImportError:
                pkg_ok = False

        # Check env var
        env_ok = True
        if env:
            env_ok = bool(os.environ.get(env))

        # Build status string
        if pkg_ok and env_ok and env:
            status = "[green]ready[/green]"
            if recommended_idx == 1:
                recommended_idx = i  # first ready backend
        elif pkg_ok and not env:
            status = "[green]installed[/green]"
        elif pkg_ok and not env_ok:
            status = f"[yellow]need {env}[/yellow]"
        elif not pkg_ok:
            status = f"[dim]pip install {pkg}[/dim]"
        else:
            status = ""

        table.add_row(str(i), key, BACKENDS[key]["name"], status)

    console.print(table)
    console.print("[dim]Backends marked 'ready' have package + API key detected.[/dim]\n")
    choice = IntPrompt.ask(
        "Choose a backend",
        default=recommended_idx,
        choices=[str(i) for i in range(1, len(backend_keys) + 1)],
    )
    return backend_keys[choice - 1]


def _prompt_model(backend: str) -> str:
    """Ask the user to pick a model for the chosen backend."""
    models = BACKENDS[backend]["models"]
    if not models:
        return Prompt.ask("Enter model name or endpoint URL", default="gpt-4o-mini")

    table = Table(title=f"Models for {BACKENDS[backend]['name']}", show_header=True, header_style="bold cyan")
    table.add_column("#", style="dim", width=3)
    table.add_column("Model", style="bold")
    table.add_column("Description")
    table.add_column("Default", style="dim")

    for i, (model_id, desc, is_default) in enumerate(models, 1):
        table.add_row(str(i), model_id, desc, "yes" if is_default else "")

    console.print(table)

    default_idx = next(
        (i for i, (_, _, d) in enumerate(models, 1) if d), 1
    )
    choice = IntPrompt.ask(
        "Choose a model",
        default=default_idx,
        choices=[str(i) for i in range(1, len(models) + 1)],
    )
    return models[choice - 1][0]


def _prompt_api_key(backend: str) -> str:
    """Ask the user how to provide their API key. Returns a ${VAR} reference or raw key."""
    env_var = BACKENDS[backend]["api_key_env"]
    existing = os.environ.get(env_var)

    if existing:
        console.print(f"[green]Found ${env_var} in environment.[/green]")
        use_existing = Prompt.ask(
            f"Use ${{{env_var}}} from environment?",
            choices=["y", "n"],
            default="y",
        )
        if use_existing == "y":
            return f"${{{env_var}}}"

    console.print(f"\nHow should CogniGraph access your API key?")
    console.print(f"  [bold]1.[/bold] Environment variable ${{{env_var}}} (recommended)")
    console.print(f"  [bold]2.[/bold] Enter the key now (stored in cognigraph.yaml)")

    choice = IntPrompt.ask("Choice", default=1, choices=["1", "2"])

    if choice == 1:
        console.print(
            f"\n[dim]Set the variable before running kogni:[/dim]"
            f"\n  export {env_var}=your-key-here\n"
        )
        return f"${{{env_var}}}"
    else:
        key = Prompt.ask("Enter your API key", password=True)
        return key


# ──────────────────────────────────────────────────────────────────────
# /kogni skill installer
# ──────────────────────────────────────────────────────────────────────


def _install_kogni_skill(root: Path) -> bool:
    """Install the /kogni smart-routing skill for Claude Code.

    Installs to TWO locations for maximum coverage:
    1. Project-local: {root}/.claude/commands/kogni.md  (for this project)
    2. User-global:   ~/.claude/commands/kogni.md       (for all projects)

    Returns True if at least one location was written.
    """
    installed = False

    # Project-local installation
    local_dir = root / ".claude" / "commands"
    local_path = local_dir / "kogni.md"
    if local_path.exists():
        existing = local_path.read_text(encoding="utf-8", errors="ignore")
        if "Smart query router" in existing:
            console.print("  [dim].claude/commands/kogni.md already installed — skipping[/dim]")
        else:
            local_dir.mkdir(parents=True, exist_ok=True)
            local_path.write_text(KOGNI_SKILL_CONTENT, encoding="utf-8")
            console.print("  [green]+[/green] .claude/commands/kogni.md (project skill)")
            installed = True
    else:
        local_dir.mkdir(parents=True, exist_ok=True)
        local_path.write_text(KOGNI_SKILL_CONTENT, encoding="utf-8")
        console.print("  [green]+[/green] .claude/commands/kogni.md (project skill)")
        installed = True

    # User-global installation (only if not already there)
    home = Path.home()
    global_dir = home / ".claude" / "commands"
    global_path = global_dir / "kogni.md"
    if global_path.exists():
        existing = global_path.read_text(encoding="utf-8", errors="ignore")
        if "Smart query router" in existing:
            console.print("  [dim]~/.claude/commands/kogni.md already installed — skipping[/dim]")
        else:
            global_dir.mkdir(parents=True, exist_ok=True)
            global_path.write_text(KOGNI_SKILL_CONTENT, encoding="utf-8")
            console.print("  [green]+[/green] ~/.claude/commands/kogni.md (global skill)")
            installed = True
    else:
        global_dir.mkdir(parents=True, exist_ok=True)
        global_path.write_text(KOGNI_SKILL_CONTENT, encoding="utf-8")
        console.print("  [green]+[/green] ~/.claude/commands/kogni.md (global skill)")
        installed = True

    return installed


# ──────────────────────────────────────────────────────────────────────
# File writers (with merge/append logic)
# ──────────────────────────────────────────────────────────────────────


def _write_cognigraph_yaml(root: Path, content: str) -> bool:
    """Write cognigraph.yaml. Returns True if created, False if existed."""
    target = root / "cognigraph.yaml"
    if target.exists():
        console.print(f"  [yellow]cognigraph.yaml already exists — overwriting[/yellow]")
    target.write_text(content, encoding="utf-8")
    return True


def _write_cognigraph_json(root: Path, graph_data: dict[str, Any]) -> bool:
    """Write cognigraph.json (knowledge graph)."""
    target = root / "cognigraph.json"
    if target.exists():
        console.print(f"  [yellow]cognigraph.json already exists — overwriting with fresh scan[/yellow]")
    target.write_text(json.dumps(graph_data, indent=2), encoding="utf-8")
    return True


def _write_mcp_json(root: Path, ide: str = "claude") -> bool:
    """Write or merge MCP config for the target IDE."""
    if ide == "generic":
        return False  # No MCP for generic/terminal mode

    target = _get_mcp_path(root, ide)
    mcp_entry = _build_mcp_json(ide)

    # Ensure parent directory exists
    target.parent.mkdir(parents=True, exist_ok=True)

    if target.exists():
        try:
            existing = json.loads(target.read_text(encoding="utf-8"))
            if "mcpServers" not in existing:
                existing["mcpServers"] = {}
            if "cognigraph" in existing.get("mcpServers", {}):
                console.print(f"  [dim]{target.name} already has cognigraph entry — skipping[/dim]")
                return False
            existing["mcpServers"]["cognigraph"] = mcp_entry["mcpServers"]["cognigraph"]
            target.write_text(json.dumps(existing, indent=2), encoding="utf-8")
            console.print(f"  [green]Merged cognigraph into existing {target.name}[/green]")
            return True
        except (json.JSONDecodeError, KeyError):
            console.print(f"  [yellow]{target.name} exists but is invalid — overwriting[/yellow]")

    target.write_text(json.dumps(mcp_entry, indent=2), encoding="utf-8")
    return True


def _write_claude_md(root: Path, ide: str = "claude") -> bool:
    """Write or append AI instructions for the target IDE."""
    target = _get_instructions_path(root, ide)
    marker = _get_instructions_marker(ide)
    # Also check old marker for backward compat
    old_marker = "# CogniGraph — Governed Development Protocol"

    # Ensure parent directory exists
    target.parent.mkdir(parents=True, exist_ok=True)

    if target.exists():
        existing = target.read_text(encoding="utf-8")
        if marker in existing or old_marker in existing:
            console.print(f"  [dim]{target.name} already has CogniGraph section — skipping[/dim]")
            return False
        # Append
        with open(target, "a", encoding="utf-8") as f:
            f.write("\n\n" + AI_INSTRUCTIONS_SECTION + "\n")
        console.print(f"  [green]Appended CogniGraph section to existing {target.name}[/green]")
        return True

    target.write_text(AI_INSTRUCTIONS_SECTION + "\n", encoding="utf-8")
    return True


def _write_gcc_structure(root: Path) -> bool:
    """Create .kogni/ workspace structure. Skips if it already exists.

    Also supports legacy .gcc/ — if that exists, we skip to avoid confusion.
    """
    # Support legacy .gcc/ and new .kogni/
    legacy = root / ".gcc"
    kogni_dir = root / ".kogni"

    if legacy.exists() or kogni_dir.exists():
        name = ".gcc" if legacy.exists() else ".kogni"
        console.print(f"  [dim]{name}/ already exists — skipping[/dim]")
        return False

    # Create directories using new .kogni/ name
    (kogni_dir / "branches" / "main").mkdir(parents=True, exist_ok=True)
    (kogni_dir / "checkpoints").mkdir(parents=True, exist_ok=True)

    # Write files
    (kogni_dir / "main.md").write_text(_build_gcc_main_md(root), encoding="utf-8")
    (kogni_dir / "registry.md").write_text(_build_gcc_registry_md(), encoding="utf-8")
    (kogni_dir / "config.yaml").write_text(_build_gcc_config_yaml(), encoding="utf-8")
    (kogni_dir / "branches" / "main" / "commit.md").write_text(
        "# Commit Log — main\n\n", encoding="utf-8"
    )
    (kogni_dir / "branches" / "main" / "log.md").write_text(
        "# Session Log — main\n\n", encoding="utf-8"
    )
    (kogni_dir / "branches" / "main" / "metadata.yaml").write_text(
        _build_gcc_metadata_yaml(), encoding="utf-8"
    )
    # .gitkeep for checkpoints
    (kogni_dir / "checkpoints" / ".gitkeep").write_text("", encoding="utf-8")

    return True


# ──────────────────────────────────────────────────────────────────────
# Main init command
# ──────────────────────────────────────────────────────────────────────


def _install_auto_grow_hook(root: Path) -> None:
    """Install a git post-commit hook that auto-updates cognigraph.json.

    This is the core promise: the graph adapts and grows with every commit.
    The hook runs `kogni grow` which does an incremental scan + ingest.
    """
    git_dir = root / ".git"
    if not git_dir.is_dir():
        console.print("  [dim]Not a git repo — skipping auto-grow hook[/dim]")
        return

    hooks_dir = git_dir / "hooks"
    hooks_dir.mkdir(exist_ok=True)
    hook_path = hooks_dir / "post-commit"

    # The hook script
    hook_content = """#!/bin/sh
# CogniGraph auto-grow hook — updates the knowledge graph on every commit.
# Installed by `kogni init`. Remove this file to disable.

# Run in background so commits aren't slowed down
(kogni grow --quiet 2>/dev/null &)
"""

    if hook_path.exists():
        existing = hook_path.read_text(encoding="utf-8", errors="ignore")
        if "kogni grow" in existing:
            console.print("  [dim]post-commit hook already has kogni grow[/dim]")
            return
        # Append to existing hook
        with open(hook_path, "a", encoding="utf-8") as f:
            f.write("\n" + hook_content)
        console.print("  [green]+[/green] Appended kogni grow to existing post-commit hook")
    else:
        hook_path.write_text(hook_content, encoding="utf-8")
        # Make executable on Unix
        try:
            hook_path.chmod(0o755)
        except Exception:
            pass
        console.print("  [green]+[/green] Installed git post-commit hook (auto-grow)")


def init_command(
    path: str = typer.Argument(".", help="Project root directory"),
    backend: str | None = typer.Option(
        None, "--backend", "-b", help="Backend: anthropic, openai, bedrock, custom"
    ),
    model: str | None = typer.Option(
        None, "--model", "-m", help="Model identifier"
    ),
    api_key_env: str | None = typer.Option(
        None, "--api-key-env", help="Environment variable name for API key"
    ),
    api_key: str | None = typer.Option(
        None, "--api-key", help="API key value (prefer --api-key-env instead)"
    ),
    no_interactive: bool = typer.Option(
        False, "--no-interactive", help="Skip interactive prompts"
    ),
    no_scan: bool = typer.Option(
        False, "--no-scan", help="Skip repository scan"
    ),
    no_gcc: bool = typer.Option(
        False, "--no-gcc", help="Skip .gcc/ directory creation"
    ),
    no_claude_md: bool = typer.Option(
        False, "--no-claude-md", help="Skip CLAUDE.md creation"
    ),
    no_mcp: bool = typer.Option(
        False, "--no-mcp", help="Skip .mcp.json creation"
    ),
    no_skill: bool = typer.Option(
        False, "--no-skill", help="Skip /kogni skill installation"
    ),
    ide: str = typer.Option(
        "auto", "--ide", "-i",
        help="Target IDE: auto, claude, cursor, vscode, windsurf, generic",
    ),
) -> None:
    """Initialize CogniGraph for your project and IDE.

    Sets up a knowledge graph, model configuration, MCP server registration
    (for your IDE), and AI instructions — everything needed for intelligent
    development. Works with any IDE, AI tool, or plain terminal.

    \b
    Interactive (default):
        kogni init

    \b
    Specific IDE:
        kogni init --ide cursor
        kogni init --ide vscode
        kogni init --ide generic    (CLI only, no MCP)

    \b
    Non-interactive:
        kogni init --backend anthropic --model claude-haiku-4-5-20251001 \\
                   --api-key-env ANTHROPIC_API_KEY --no-interactive
    """
    root = Path(path).resolve()
    if not root.exists():
        console.print(f"[red]Path not found: {root}[/red]")
        raise typer.Exit(1)

    # ── IDE detection ──────────────────────────────────────────────
    if ide == "auto":
        ide = _detect_ide(root)

    if ide not in SUPPORTED_IDES:
        console.print(f"[red]Unknown IDE '{ide}'. Choose: {', '.join(SUPPORTED_IDES.keys())}[/red]")
        raise typer.Exit(1)

    # ── Banner ──────────────────────────────────────────────────────
    console.print(
        Panel(
            "[bold cyan]CogniGraph[/bold cyan] — Dev Intelligence Layer\n"
            "[dim]Setting up intelligent development for your project[/dim]",
            border_style="cyan",
        )
    )
    ide_label = SUPPORTED_IDES.get(ide, ide)
    console.print(f"Project root: [bold]{root}[/bold]")
    console.print(f"Target IDE:   [bold cyan]{ide_label}[/bold cyan]\n")

    # ── Step 1: Gather configuration ────────────────────────────────
    if no_interactive:
        # Non-interactive: use provided flags or defaults
        chosen_backend = backend or "anthropic"
        if chosen_backend not in BACKENDS:
            console.print(
                f"[red]Unknown backend '{chosen_backend}'. "
                f"Choose from: {', '.join(BACKENDS.keys())}[/red]"
            )
            raise typer.Exit(1)

        chosen_model = model
        if not chosen_model:
            models = BACKENDS[chosen_backend]["models"]
            chosen_model = next(
                (m for m, _, d in models if d),
                models[0][0] if models else "gpt-4o-mini",
            )

        if api_key:
            api_key_ref = api_key
        elif api_key_env:
            api_key_ref = f"${{{api_key_env}}}"
        else:
            api_key_ref = f"${{{BACKENDS[chosen_backend]['api_key_env']}}}"
    else:
        # Interactive
        console.print("[bold]Step 1/3:[/bold] Choose your AI backend\n")
        chosen_backend = _prompt_backend()

        console.print(f"\n[bold]Step 2/3:[/bold] Choose a model\n")
        chosen_model = _prompt_model(chosen_backend)

        console.print(f"\n[bold]Step 3/3:[/bold] API key configuration\n")
        api_key_ref = _prompt_api_key(chosen_backend)

    console.print(
        f"\n[bold green]Configuration:[/bold green] "
        f"{BACKENDS[chosen_backend]['name']} / {chosen_model}\n"
    )

    # ── Step 1b: Verify backend connection ──────────────────────────
    console.print("[bold]Verifying backend connection...[/bold]")
    _verify_backend(chosen_backend, chosen_model, api_key_ref, no_interactive)

    # ── Step 2: Scan + domain detection + ingest ────────────────────
    graph_data: dict[str, Any] | None = None
    if not no_scan:
        # 2a: Code scan
        console.print("[bold]Scanning repository...[/bold]")
        graph_data = scan_repository(root)
        node_count = len(graph_data["nodes"])
        edge_count = len(graph_data["links"])
        project_type = graph_data["graph"].get("project_type", "unknown")
        console.print(
            f"  Code scan: [cyan]{node_count}[/cyan] nodes, "
            f"[cyan]{edge_count}[/cyan] edges "
            f"([dim]{project_type} project[/dim])"
        )

        # 2b: Domain detection + dynamic ontology generation
        console.print("\n[bold]Detecting project domain...[/bold]")
        try:
            from cognigraph.ontology.domain_detector import auto_ontology, detect_domain
            profile = detect_domain(root)
            console.print(
                f"  Domain: [cyan]{profile.primary_domain}[/cyan] "
                f"({', '.join(profile.secondary_domains[:3])})"
            )
            console.print(
                f"  Language: [cyan]{profile.language}[/cyan] | "
                f"Frameworks: [cyan]{', '.join(profile.frameworks[:3]) or 'none'}[/cyan]"
            )

            # Use best LLM for ontology (Sonnet, not Haiku) — one-time cost
            ont_api_key = None
            if api_key and not api_key.startswith("${"):
                ont_api_key = api_key
            elif api_key_ref and not api_key_ref.startswith("${"):
                ont_api_key = api_key_ref
            else:
                ont_api_key = os.environ.get("ANTHROPIC_API_KEY")

            node_shapes, edge_shapes = auto_ontology(
                root, api_key=ont_api_key, register=True,
            )
            console.print(
                f"  Ontology: [green]{len(node_shapes)}[/green] node types, "
                f"[green]{len(edge_shapes)}[/green] edge types"
            )
        except Exception as e:
            console.print(f"  [yellow]Domain detection skipped: {e}[/yellow]")

        # 2c: Knowledge ingestion (markdown KGs, .gcc/, lessons, etc.)
        console.print("\n[bold]Ingesting knowledge sources...[/bold]")
        try:
            from cognigraph.ontology.markdown_parser import parse_and_infer
            from cognigraph.cli.commands.ingest import (
                _discover_sources_from_config,
                _discover_sources_auto,
                _merge_graphs,
            )

            config_path_obj = root / "cognigraph.yaml"
            kg_sources = _discover_sources_from_config(config_path_obj)
            if not kg_sources:
                kg_sources = _discover_sources_auto(root)

            if kg_sources:
                entities, edges_list = parse_and_infer(kg_sources)
                new_nodes = [e.to_node_dict() for e in entities]
                new_links = [
                    {
                        "source": edge.source_id,
                        "target": edge.target_id,
                        "relationship": edge.relationship,
                        "confidence": edge.confidence,
                        "source_file": edge.source_file,
                    }
                    for edge in edges_list
                ]
                graph_data = _merge_graphs(graph_data, new_nodes, new_links)
                graph_data.pop("_merge_stats", None)
                total_n = len(graph_data.get("nodes", []))
                total_e = len(graph_data.get("links", []))
                console.print(
                    f"  Ingested [cyan]{len(kg_sources)}[/cyan] source(s): "
                    f"[green]{total_n}[/green] total nodes, "
                    f"[green]{total_e}[/green] total edges"
                )
            else:
                console.print("  [dim]No markdown KG sources found (code scan only)[/dim]")
        except Exception as e:
            console.print(f"  [yellow]Ingestion skipped: {e}[/yellow]")

        # 2d: Initialize metrics
        console.print("\n[bold]Initializing metrics...[/bold]")
        try:
            from cognigraph.metrics.engine import MetricsEngine
            metrics = MetricsEngine(root / ".cognigraph")
            if graph_data:
                from collections import Counter
                type_counts = Counter(n.get("type", "") for n in graph_data.get("nodes", []))
                metrics.graph_stats_initial = {
                    "nodes": len(graph_data.get("nodes", [])),
                    "edges": len(graph_data.get("links", [])),
                    "node_types": dict(type_counts.most_common()),
                }
                metrics.graph_stats_current = dict(metrics.graph_stats_initial)
            metrics.save()
            console.print("  [green]+[/green] .cognigraph/metrics.json")
        except Exception as e:
            console.print(f"  [yellow]Metrics init skipped: {e}[/yellow]")

        console.print()

    # ── Step 3: Write files ─────────────────────────────────────────
    console.print("[bold]Creating files...[/bold]")

    # cognigraph.yaml
    yaml_content = _build_cognigraph_yaml(chosen_backend, chosen_model, api_key_ref)
    _write_cognigraph_yaml(root, yaml_content)
    console.print(f"  [green]+[/green] cognigraph.yaml")

    # cognigraph.json
    if graph_data is not None:
        _write_cognigraph_json(root, graph_data)
        console.print(f"  [green]+[/green] cognigraph.json")

    # MCP config (IDE-specific location)
    if not no_mcp:
        mcp_path = _get_mcp_path(root, ide)
        if _write_mcp_json(root, ide):
            console.print(f"  [green]+[/green] {mcp_path.relative_to(root)}")

    # AI instructions (IDE-specific file)
    if not no_claude_md:
        instr_path = _get_instructions_path(root, ide)
        if _write_claude_md(root, ide):
            console.print(f"  [green]+[/green] {instr_path.relative_to(root)}")

    # /kogni skill (smart query router)
    if not no_skill:
        _install_kogni_skill(root)

    # .kogni/ workspace
    if not no_gcc:
        created = _write_gcc_structure(root)
        if created:
            console.print(f"  [green]+[/green] .kogni/ (session workspace)")

    # ── Step 4: Install auto-grow git hook ───────────────────────────
    _install_auto_grow_hook(root)

    # ── Auto-detect readiness ──────────────────────────────────────
    # Quick health check inline (not full doctor, just key checks)
    _readiness_warnings: list[str] = []

    # Check embedding model for skill quality
    _has_embeddings = False
    try:
        import sentence_transformers  # noqa: F401
        _has_embeddings = True
    except ImportError:
        pass
    if not _has_embeddings:
        try:
            import boto3
            sts = boto3.client("sts")
            sts.get_caller_identity()
            _has_embeddings = True
        except Exception:
            pass
    if not _has_embeddings:
        _readiness_warnings.append(
            "[yellow]![/yellow] Skill matching: regex-only mode "
            "(pip install sentence-transformers for better skill assignment)"
        )

    # Check API key resolution
    _resolved_key = None
    if api_key_ref and api_key_ref.startswith("${") and api_key_ref.endswith("}"):
        _env = api_key_ref[2:-1]
        _resolved_key = os.environ.get(_env)
        if not _resolved_key:
            _readiness_warnings.append(
                f"[red]![/red] API key: {_env} is NOT SET — reasoning will fail. "
                f"Set it: export {_env}=your-key-here"
            )
    elif api_key_ref:
        _resolved_key = api_key_ref

    # ── Step 5: Track project init (lead capture) ──────────────────
    try:
        from cognigraph.leads.collector import track_project_init
        _node_ct = len(graph_data.get("nodes", [])) if graph_data else 0
        _edge_ct = len(graph_data.get("links", [])) if graph_data else 0
        track_project_init(
            project_path=str(root),
            node_count=_node_ct,
            edge_count=_edge_ct,
            backend=chosen_backend,
            ide=ide,
        )
    except Exception:
        pass  # Never fail on telemetry

    # ── Done ────────────────────────────────────────────────────────
    console.print()
    node_total = len(graph_data.get("nodes", [])) if graph_data else 0
    edge_total = len(graph_data.get("links", [])) if graph_data else 0

    readiness_section = ""
    if _readiness_warnings:
        readiness_section = (
            "\n[bold yellow]Readiness warnings:[/bold yellow]\n"
            + "\n".join(f"  {w}" for w in _readiness_warnings)
            + "\n"
        )

    console.print(
        Panel(
            f"[bold green]Project initialized![/bold green]\n\n"
            f"  Graph: [cyan]{node_total}[/cyan] nodes, [cyan]{edge_total}[/cyan] edges\n"
            f"{readiness_section}\n"
            "[bold]Next steps:[/bold]\n"
            "  1. [bold]kogni doctor[/bold]          — verify your setup is complete\n"
            "  2. [bold]/kogni <question>[/bold]     — smart-routed query (cheapest approach)\n"
            "  3. [bold]kogni run \"query\"[/bold]    — full graph reasoning\n\n"
            "[bold]All commands:[/bold]\n"
            "  [bold]kogni doctor[/bold]             — health check (run this first!)\n"
            "  [bold]kogni context <name>[/bold]    — 500-token focused context\n"
            "  [bold]kogni ingest[/bold]             — re-ingest knowledge sources\n"
            "  [bold]kogni metrics[/bold]            — usage metrics and ROI\n"
            "  [bold]kogni inspect --stats[/bold]   — graph statistics\n\n"
            f"[dim]Auto-grow: git post-commit hook keeps the knowledge graph in sync.\n"
            f"IDE: {ide_label}[/dim]",
            border_style="green",
            title="Done",
        )
    )

    # Registration nudge (soft, non-blocking)
    try:
        from cognigraph.leads.collector import get_registration_nudge
        nudge = get_registration_nudge()
        if nudge:
            console.print(f"\n{nudge}")
    except Exception:
        pass
