"""kogni init — set up governed development for any project.

Creates cognigraph.yaml, cognigraph.json (knowledge graph from scan),
.mcp.json (MCP server registration), CLAUDE.md (governance protocols),
and .gcc/ directory structure. This is the first thing users run.
"""

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",
    },
}

# ──────────────────────────────────────────────────────────────────────
# CLAUDE.md content — THE product differentiator
# ──────────────────────────────────────────────────────────────────────

CLAUDE_MD_SECTION = r"""
# CogniGraph — Governed Development Protocol

> Auto-generated by `kogni init`. This section turns Claude Code into a
> governed, self-improving development agent using CogniGraph MCP tools,
> GCC context management, GSD structured work, and the Ralph Loop.

---

## MCP Tools Available

CogniGraph registers as an MCP server. The following tools are available
to Claude Code when the server is running (`kogni mcp serve`):

| Tool | Purpose | Example |
|------|---------|---------|
| `kogni context <service>` | Get 500-token focused context for a service | `kogni context auth-lambda` |
| `kogni run "<query>"` | Run a reasoning query over the knowledge graph | `kogni run "what calls the payments API?"` |
| `kogni inspect --stats` | Show graph statistics (nodes, edges, density) | `kogni inspect --stats` |
| `kogni scan repo .` | Re-scan the codebase and rebuild the knowledge graph | `kogni scan repo .` |

**Usage rule:** Before loading large files for context, check if `kogni context <name>` can provide what you need in 500 tokens instead of 20-60K.

---

## GCC Protocol — Global Context Controller

GCC keeps structured memory across sessions so no context is ever lost.

### Session Start (execute silently)
1. Read this `CLAUDE.md`
2. Read `.gcc/main.md` (global roadmap, ~300 tokens)
3. Read `.gcc/registry.md` (active branches, ~100 tokens)
4. Identify active branch from registry
5. Read `.gcc/branches/{active}/commit.md` (latest milestones, ~400 tokens)
6. Announce: "Resuming from COMMIT {N} on {branch}. Last: {summary}. Next: {next_steps}"

### During Work
- **COMMIT** after every completed milestone (feature, fix, test, deploy step)
- **Auto-COMMIT** every 30 minutes of active work — never lose progress
- **BRANCH** when starting a new feature or experiment
- **MERGE** when a branch achieves its purpose

### COMMIT Format
Append to `.gcc/branches/{branch}/commit.md`:
```markdown
### COMMIT {N} — {timestamp}
**Milestone:** {one-line summary}
**State:** WORKING | DONE | BLOCKED | HANDOFF
**Files Changed:**
- CREATED: {path} — {purpose}
- MODIFIED: {path} — {what changed}
**Key Decisions:**
- {decision and rationale}
**Next:**
- [ ] {immediate next step}
**Blockers:** {any blockers, or "None"}
```

### Session End
1. COMMIT current progress (even if incomplete — state: WORKING)
2. Update `.gcc/branches/{branch}/log.md` with session trace
3. Create checkpoint in `.gcc/checkpoints/`
4. Announce: "Session saved. COMMIT {N} on {branch}. Next: {next_steps}"

---

## GSD Protocol — Get Shit Done

GSD provides structured pre-work before execution. Spec before code.

### When to Use Full GSD
- New features spanning 3+ files
- Multi-session work
- Cross-system changes (frontend + backend + infra)

### Phases

**DISCUSS** — Clarify the problem before writing code:
1. What problem? Who benefits? What constraints?
2. Which systems are touched?
3. Produce `context.md` with problem statement, scope, success criteria

**PLAN** — Break into atomic tasks:
1. Break success criteria into ordered phases
2. Each task = one commit, independently revertable
3. Produce `plan.md` with ordered tasks + verification methods

**EXECUTE** — Work through the plan:
1. One task = one COMMIT, referencing plan task ID
2. After each task: run tests, lint, verify
3. Scope creep goes to "deferred" — never mid-sprint

**VERIFY** — Cross-reference THREE sources:
1. Evidence (actual outputs, test results)
2. Plan (all tasks checked off?)
3. Context (success criteria satisfied?)

---

## Ralph Loop — Autonomous Iteration

Ralph enables persistent fix-test-fix cycles with memory.

### Protocol
```
1. ENTRY: Receive task + binary completion criteria
2. WORK: Execute toward criteria
3. CHECK: Binary — PASS or FAIL
   PASS → COMMIT state: DONE → EXIT
   FAIL → COMMIT state: WORKING → increment counter
4. SAFETY: iteration_count >= 20 → COMMIT state: BLOCKED → EXIT
5. RE-FEED: Read last COMMIT → resume from step 2
```

### Safety Guards
| Guard | Default |
|-------|---------|
| max_iterations | 20 |
| commit_per_iteration | YES (never skip) |
| destructive_ops | BLOCKED (require user approval) |
| deploy_to_main | BLOCKED (require user approval) |
| scope_creep | BLOCKED (exit loop, re-DISCUSS) |

### Writing Good Criteria
- BAD: "Fix the auth and make it work"
- GOOD: "All auth tests pass. Login returns 200. Token refresh works. Output: DONE"

---

## Self-Improvement

After ANY correction from the user:
1. Add a one-liner lesson to `tasks/lessons-distilled.md`
2. Categorize: CRITICAL / HIGH / MEDIUM
3. Tag with domain: build | test | deploy | logic | api | db
4. Before starting any task, scan lessons for related failure patterns

---

## Smart Query Routing (Auto-installed /kogni skill)

CogniGraph installs a `/kogni` slash command that routes questions to the
cheapest correct approach. **You MUST use this decision tree before calling
any CogniGraph MCP tool:**

| Query Type | Route | Cost | When |
|-----------|-------|------|------|
| **LOOKUP** | `grep project-kg.md` or `.gcc/` files | ~200 tokens | Single fact, named entity |
| **CROSS-CUT** | Read specific KG section | ~300-800 tokens | Spans 2-3 known tables |
| **IMPACT** | `kogni_impact` MCP tool | ~800-1500 tokens | "What breaks if..." |
| **REASONING** | `kogni_reason` MCP tool | ~1500-3000 tokens | Multi-hop "why/how" questions |
| **PREFLIGHT** | `kogni_preflight` MCP tool | ~500-1000 tokens | Pre-change safety check |
| **LESSONS** | `kogni_lessons` MCP tool | ~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.
4. **Cache awareness.** If a file was already read this session, reuse from memory.

---

## Configuration Reference

- `cognigraph.yaml` — Model backend, graph connector, activation strategy, cost budgets
- `cognigraph.json` — The knowledge graph (auto-generated by `kogni scan repo .`)
- `.mcp.json` — MCP server registration for Claude Code / Cursor / Windsurf
- `.gcc/` — Context controller state (commits, branches, checkpoints)

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

# ──────────────────────────────────────────────────────────────────────
# 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 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).
    """
    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"
            nodes.append(
                {
                    "id": file_id,
                    "label": fpath.stem,
                    "type": entity_type,
                    "description": f"{entity_type}: {rel}",
                }
            )
            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
            try:
                content = fpath.read_text(errors="ignore")
                if fpath.suffix in _PYTHON_EXTS:
                    for imp in _extract_python_imports(content):
                        target = imp.replace(".", "/") + ".py"
                        import_edges.append((file_id, target))
                elif 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:
                nodes.append(
                    {
                        "id": file_id,
                        "label": fpath.stem,
                        "type": "Config",
                        "description": f"Configuration: {fpath.name}",
                    }
                )
                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:
                nodes.append(
                    {
                        "id": file_id,
                        "label": fpath.stem,
                        "type": "TestModule",
                        "description": f"Test: {rel}",
                    }
                )
                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)


def _build_mcp_json() -> dict[str, Any]:
    """Return the .mcp.json structure for CogniGraph."""
    return {
        "mcpServers": {
            "cognigraph": {
                "type": "stdio",
                "command": "kogni",
                "args": ["mcp", "serve", "--config", "cognigraph.yaml"],
            }
        }
    }


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."""
    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")

    backend_keys = list(BACKENDS.keys())
    for i, key in enumerate(backend_keys, 1):
        table.add_row(str(i), key, BACKENDS[key]["name"])

    console.print(table)
    choice = IntPrompt.ask(
        "Choose a backend",
        default=1,
        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) -> bool:
    """Write or merge .mcp.json."""
    target = root / ".mcp.json"
    mcp_entry = _build_mcp_json()

    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].mcp.json 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 .mcp.json[/green]")
            return True
        except (json.JSONDecodeError, KeyError):
            console.print(f"  [yellow].mcp.json 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) -> bool:
    """Write or append to CLAUDE.md."""
    target = root / "CLAUDE.md"
    marker = "# CogniGraph — Governed Development Protocol"

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

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


def _write_gcc_structure(root: Path) -> bool:
    """Create .gcc/ directory structure. Skips if it already exists."""
    gcc = root / ".gcc"
    if gcc.exists():
        console.print(f"  [dim].gcc/ already exists — skipping[/dim]")
        return False

    # Create directories
    (gcc / "branches" / "main").mkdir(parents=True, exist_ok=True)
    (gcc / "checkpoints").mkdir(parents=True, exist_ok=True)

    # Write files
    (gcc / "main.md").write_text(_build_gcc_main_md(root), encoding="utf-8")
    (gcc / "registry.md").write_text(_build_gcc_registry_md(), encoding="utf-8")
    (gcc / "config.yaml").write_text(_build_gcc_config_yaml(), encoding="utf-8")
    (gcc / "branches" / "main" / "commit.md").write_text(
        "# Commit Log — main\n\n", encoding="utf-8"
    )
    (gcc / "branches" / "main" / "log.md").write_text(
        "# Session Log — main\n\n", encoding="utf-8"
    )
    (gcc / "branches" / "main" / "metadata.yaml").write_text(
        _build_gcc_metadata_yaml(), encoding="utf-8"
    )
    # .gitkeep for checkpoints
    (gcc / "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"
    ),
) -> None:
    """Initialize governed development for this project.

    Sets up CogniGraph with a knowledge graph, model configuration,
    MCP server registration, CLAUDE.md governance protocols, and
    GCC context management — everything needed for AI-governed development.

    \b
    Interactive (default):
        kogni init

    \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)

    # ── Banner ──────────────────────────────────────────────────────
    console.print(
        Panel(
            "[bold cyan]CogniGraph[/bold cyan] — Graphs that think\n"
            "[dim]Setting up governed development for your project[/dim]",
            border_style="cyan",
        )
    )
    console.print(f"Project root: [bold]{root}[/bold]\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.json
    if not no_mcp:
        _write_mcp_json(root)
        console.print(f"  [green]+[/green] .mcp.json")

    # CLAUDE.md
    if not no_claude_md:
        _write_claude_md(root)
        console.print(f"  [green]+[/green] CLAUDE.md")

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

    # .gcc/
    if not no_gcc:
        created = _write_gcc_structure(root)
        if created:
            console.print(f"  [green]+[/green] .gcc/ (context controller)")

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

    # ── 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
    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\n"
            "Auto-grow: The knowledge graph updates on every git commit.\n\n"
            "Commands:\n"
            "  [bold]/kogni <question>[/bold]     — smart-routed query (cheapest approach)\n"
            "  [bold]kogni context <name>[/bold] — 500-token focused context\n"
            "  [bold]kogni ingest[/bold]          — re-ingest all knowledge sources\n"
            "  [bold]kogni metrics[/bold]         — view usage metrics and ROI\n"
            "  [bold]kogni run \"query\"[/bold]    — reason over the knowledge graph\n"
            "  [bold]kogni inspect --stats[/bold] — graph statistics\n\n"
            "[dim]The /kogni skill auto-routes queries: grep for lookups,\n"
            "MCP tools for impact/reasoning. Saves 5-10x tokens on simple queries.\n\n"
            "CLAUDE.md has GCC + GSD + Ralph Loop protocols.\n"
            "Git post-commit hook auto-updates cognigraph.json.[/dim]",
            border_style="green",
            title="Done",
        )
    )
