#!/usr/bin/env python3
"""
GRIP Mirror - Portable Enforcement Hook
========================================

PreToolUse hook that enforces safety gates for Claude Code projects.
Extracted from GRIP's context-gate-hook.py for universal use.

Gates (priority order):
0. Always Allow - Critical tools (AskUserQuestion, etc.)
1. Dependency Guardian - Block reads from node_modules, venv, etc.
2. Destructive Git - Block force push, hard reset
2a. Git Push Gate - Block force push and protected branch push
3. Secrets Detection - Block committing .env, keys
5. Bash Tool Substitution - Block grep/find/cat misuse
6. Bash Linting - Block dangerous bash patterns
7. Temp Script Prevention - Block temporary script creation
A. Env Var Protection - Block reading sensitive env vars
D. Network Binding - Block 0.0.0.0 without auth
9. Context Gate (85%) - Block when approaching context limit

Cross-platform: Windows, macOS, Linux (Python 3.9+ stdlib only).

Version: 0.2.0 (GRIP Mirror)
"""

import json
import os
import re
import sys
from pathlib import Path

# ---------------------------------------------------------------------------
# fcntl shim (Unix-only file locking) - no-op on Windows
# ---------------------------------------------------------------------------
if sys.platform == "win32":
    class _FcntlShim:
        LOCK_SH = LOCK_EX = LOCK_UN = LOCK_NB = 0
        def flock(self, *a, **kw): pass
        def lockf(self, *a, **kw): pass
    fcntl = _FcntlShim()
else:
    import fcntl  # type: ignore[assignment]


# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

# Resolve project .claude dir relative to this hook's location
# Hook lives at: {project}/.claude/hooks/mirror-hook.py
HOOK_DIR = Path(__file__).resolve().parent
CLAUDE_DIR = HOOK_DIR.parent
STATE_FILE = CLAUDE_DIR / ".mirror-gate-state.json"

# Context limits (env var override for testing)
CONTEXT_MAX = int(os.environ.get("MIRROR_CONTEXT_MAX", 200000))
CONTEXT_WARNING = int(CONTEXT_MAX * 0.70)
CONTEXT_CRITICAL = int(CONTEXT_MAX * 0.85)

# Token estimation cache
_token_cache = {"mtime": 0.0, "size": 0, "estimate": 0}

# ---------------------------------------------------------------------------
# Blocked dependency paths
# ---------------------------------------------------------------------------
BLOCKED_PATHS = [
    "node_modules",
    ".next",
    "dist",
    "build",
    "__pycache__",
    "venv",
    ".venv",
    "target",
    "vendor",
    "Pods",
    ".git/objects",
    ".nuxt",
    ".output",
    "coverage",
    ".cache",
]

# ---------------------------------------------------------------------------
# Destructive git patterns
# ---------------------------------------------------------------------------
DESTRUCTIVE_GIT_PATTERNS = [
    r"git\s+push\s+.*--force",
    r"git\s+push\s+-f\b",
    r"git\s+reset\s+--hard",
    r"git\s+clean\s+-fd",
    r"git\s+checkout\s+--\s+\S",
    r"git\s+checkout\s+[a-f0-9]+\s+--",
    r"git\s+restore\s+(?!--staged)\S",
    r"git\s+stash\s+drop",
]

# ---------------------------------------------------------------------------
# Sensitive file patterns (for secrets detection)
# ---------------------------------------------------------------------------
SENSITIVE_FILE_PATTERNS = [
    ".env",
    ".npmrc",
    "credentials.json",
    "secrets.json",
    "secret.json",
    ".pem",
    ".key",
    "id_rsa",
    "id_ed25519",
    "private_key",
    "api_key",
    ".p12",
    ".pfx",
    "token.json",
    "serviceaccount",
    "service-account",
    "auth.json",
    ".netrc",
    "htpasswd",
]

ENV_SAFE_SUFFIXES = (
    ".env.example",
    ".env.sample",
    ".env.template",
    ".env.dist",
    ".env.defaults",
    ".env.schema",
)

# ---------------------------------------------------------------------------
# Tool substitution rules
# ---------------------------------------------------------------------------
TOOL_SUBSTITUTIONS = {
    r"\bgrep\s+": ("rg", "Use rg (ripgrep) instead of grep for better performance"),
    r"\bfind\s+": ("fd", "Use fd instead of find for better performance"),
    r"\bcat\s+[^|]*\|": ("Read tool", "Use Read tool instead of cat piping"),
    r"(?:^|&&|\|\||;|\|)\s*rm\s": (
        "trash",
        "Use 'trash' (macOS), 'gio trash' (Linux), or 'recycle' (Windows) for recoverable deletion",
    ),
}

# ---------------------------------------------------------------------------
# Safe bash prefixes (skip all Bash-specific gates for performance)
# ---------------------------------------------------------------------------
_SAFE_BASH_PREFIXES = (
    "git status", "git log", "git diff", "git branch", "git remote",
    "git stash list", "git show", "git rev-parse", "git describe",
    "git tag -l", "git config --get", "git config --list",
    "ls ", "pwd", "date", "whoami", "hostname",
    "wc -l", "wc -c", "wc -w",
    "which ", "type ", "file ",
    "python3 --version", "python --version", "node --version",
    "gh issue view", "gh issue list", "gh pr view", "gh pr list",
    "gh pr checks", "gh api",
)

# Long string threshold for heredoc stripping
_LONG_STRING_THRESHOLD = 20

# ---------------------------------------------------------------------------
# Protected branches (Gate 2a)
# ---------------------------------------------------------------------------
PROTECTED_BRANCHES = {"main", "master", "develop", "release", "production"}
GIT_PUSH_PATTERN = re.compile(r"git\s+push\b", re.IGNORECASE)

# ---------------------------------------------------------------------------
# Bash lint patterns (Gate 6)
# ---------------------------------------------------------------------------
BASH_LINT_PATTERNS = {
    r"\$\([^)]+\)\s*;": (
        "Semicolon after command substitution causes parse error in eval contexts. "
        "Use && instead."
    ),
    r"sed\s+'[^']*;[^']*'": (
        "Semicolon inside sed pattern causes zsh parse error. "
        "Use pipes for multiple sed."
    ),
    r"\[\[\s*!": (
        "Negation in [[ ! ... ]] can cause zsh eval parse errors. "
        "Use positive test with inverted logic."
    ),
}

# ---------------------------------------------------------------------------
# Temp script patterns (Gate 7)
# ---------------------------------------------------------------------------
TEMP_SCRIPT_PATTERNS = ["temp", "tmp", "/var/folders/"]
TEMP_SCRIPT_EXTENSIONS = [".sh", ".py", ".js", ".ts"]

# ---------------------------------------------------------------------------
# Sensitive environment variable patterns (Gate A)
# ---------------------------------------------------------------------------
SENSITIVE_ENV_PATTERNS = [
    r"\$[A-Z_]+(?:API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIALS|PRIVATE_KEY)",
    r"\$\{[A-Z_]+(?:API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIALS|PRIVATE_KEY)",
    r"printenv\s*\|?\s*(?:grep|rg)?\s*(?:SECRET|KEY|TOKEN|PASS|CRED)",
    r"env\s*\|?\s*(?:grep|rg)?\s*(?:SECRET|KEY|TOKEN|PASS|CRED)",
    r"echo\s+\$[A-Z_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|AWS_|ANTHROPIC_|OPENAI_)",
    r'os\.environ(?:\.get)?\s*\(\s*["\'][^"\']*(?:API_KEY|SECRET|TOKEN|PASSWORD)',
    r'process\.env\.[A-Z_]*(?:API_KEY|SECRET|TOKEN|PASSWORD)',
    r"\$AWS_(?:ACCESS_KEY|SECRET_KEY|SESSION_TOKEN)",
    r"\$ANTHROPIC_API_KEY",
    r"\$OPENAI_API_KEY",
    r"\$GITHUB_TOKEN",
]
COMPILED_ENV_PATTERNS = [re.compile(p, re.IGNORECASE) for p in SENSITIVE_ENV_PATTERNS]

# ---------------------------------------------------------------------------
# Network binding patterns (Gate D)
# ---------------------------------------------------------------------------
UNSAFE_BINDING_PATTERNS = [
    r'HTTPServer\s*\(\s*\(\s*["\']0\.0\.0\.0["\']\s*,',
    r'host\s*=\s*["\']0\.0\.0\.0["\']',
    r'\.bind\s*\(\s*\(\s*["\']["\']',
    r'\.bind\s*\(\s*\(\s*["\']0\.0\.0\.0["\']',
    r'\.listen\s*\(\s*[^)]*0\.0\.0\.0',
    r'INADDR_ANY',
    r'socket\.INADDR_ANY',
]
COMPILED_BINDING_PATTERNS = [re.compile(p) for p in UNSAFE_BINDING_PATTERNS]

AUTH_PATTERNS = [
    r'@require_auth',
    r'@authenticate',
    r'verify_token',
    r'check_auth',
    r'Authorization',
    r'Bearer\s+',
    r'api_key\s*=',
    r'authenticate\(',
]
COMPILED_AUTH_PATTERNS = [re.compile(p, re.IGNORECASE) for p in AUTH_PATTERNS]


# =========================================================================
# Response helpers
# =========================================================================

def deny(reason: str) -> dict:
    """Return a deny response."""
    return {
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": reason,
        }
    }


def allow(system_message: str = None) -> dict:
    """Return an allow response, optionally with a system message."""
    response = {
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "allow",
        }
    }
    if system_message:
        response["systemMessage"] = system_message
    return response


# =========================================================================
# Gate implementations
# =========================================================================

def is_blocked_path(path: str) -> str:
    """Check if path contains a blocked directory.

    Returns the blocked component or empty string.
    """
    if not path:
        return ""

    path_lower = path.lower()
    path_parts = Path(path).parts

    # Allow task output files in tmp directories
    if "/tmp/" in path_lower and path.endswith(".output"):
        return ""

    for blocked in BLOCKED_PATHS:
        blocked_lower = blocked.lower()
        # Check if any path component matches exactly
        for part in path_parts:
            if part.lower() == blocked_lower:
                return blocked
        # Check substring with path separator boundaries
        if blocked_lower in path_lower:
            boundary_pattern = r"(?:^|/)" + re.escape(blocked_lower) + r"(?:/|$)"
            if re.search(boundary_pattern, path_lower):
                return blocked

    return ""


def is_destructive_git(command: str) -> str:
    """Check if command is a destructive git operation.

    Returns description or empty string.
    """
    if not command:
        return ""

    for pattern in DESTRUCTIVE_GIT_PATTERNS:
        if re.search(pattern, command, re.IGNORECASE):
            if "push" in command and "force" in command.lower():
                return "git push --force (destructive: rewrites remote history)"
            if "reset --hard" in command:
                return "git reset --hard (destructive: discards uncommitted changes)"
            if "clean -fd" in command:
                return "git clean -fd (destructive: removes untracked files)"
            if "checkout" in command and "--" in command:
                return "git checkout -- (destructive: discards uncommitted changes)"
            if "restore" in command and "--staged" not in command:
                return "git restore (destructive: discards uncommitted changes)"
            if "stash" in command and "drop" in command:
                return "git stash drop (destructive: permanently deletes stashed changes)"
            return "destructive git operation"

    # Case-sensitive: git branch -D (uppercase D = force delete)
    if re.search(r"git\s+branch\s+-[A-Za-z]*D\b", command):
        return "git branch -D (destructive: force deletes unmerged branch)"

    return ""


def contains_sensitive_file(command: str) -> str:
    """Check if git command might commit sensitive files.

    Returns the matched pattern or empty string.
    """
    if not command:
        return ""

    if not re.search(r"git\s+(add|commit)", command, re.IGNORECASE):
        return ""

    cmd_lower = command.lower()
    for pattern in SENSITIVE_FILE_PATTERNS:
        if pattern.lower() in cmd_lower:
            if pattern == ".env":
                if any(suffix in cmd_lower for suffix in ENV_SAFE_SUFFIXES):
                    continue
            return pattern

    return ""


def _strip_heredocs_and_strings(command: str) -> str:
    """Strip heredoc content and quoted strings to avoid false positives."""
    # Strip inline heredoc patterns
    result = re.sub(
        r"\$\(cat\s+<<-?'?([\w-]+)'?\s*\n.*?\n\s*\1\s*\)",
        "", command, flags=re.DOTALL,
    )
    # Strip heredoc blocks
    result = re.sub(
        r"<<-?'?([\w-]+)'?\s*\n.*?\n\s*\1",
        "", result, flags=re.DOTALL,
    )
    # Strip long quoted strings
    t = _LONG_STRING_THRESHOLD
    result = re.sub(rf'"[^"]{{{t},}}"', '""', result)
    result = re.sub(rf"'[^']{{{t},}}'", "''", result)
    return result


def check_bash_tools(command: str) -> dict:
    """Block bash commands using wrong tools.

    Returns deny dict or None.
    """
    if not command:
        return None

    stripped = _strip_heredocs_and_strings(command)

    for pattern, (replacement, reason) in TOOL_SUBSTITUTIONS.items():
        if re.search(pattern, stripped):
            # Allow cat piped to data processing commands
            if "cat" in pattern and re.search(
                r"\|\s*(?:python3?|jq|sort|uniq|awk|wc|xargs)\b", stripped
            ):
                continue
            return deny(
                f"[GUARDRAIL: TOOL SUBSTITUTION] {reason}. "
                f"Use {replacement} for better performance and token efficiency."
            )
    return None


def _is_safe_bash(command: str) -> bool:
    """Check if a Bash command is known-safe (read-only, no side effects)."""
    cmd = command.strip()
    if not cmd:
        return False
    for prefix in _SAFE_BASH_PREFIXES:
        if cmd.startswith(prefix) or cmd == prefix.rstrip():
            return True
    return False


# =========================================================================
# Gate 2a: Git Push Gate
# =========================================================================

def check_git_push_gate(command: str) -> dict:
    """Block force push and protected branch push. Allow feature branch push.

    Returns deny dict or None.
    """
    if not command:
        return None

    match = GIT_PUSH_PATTERN.search(command)
    if not match:
        return None

    is_force = bool(re.search(r"--force\b|-f\b|\+refs/", command))

    is_protected = False
    for branch in PROTECTED_BRANCHES:
        if re.search(rf"\b{branch}\b", command):
            is_protected = True
            break

    if is_force or is_protected:
        reason = "force push" if is_force else "protected branch"
        return deny(
            f"[GUARDRAIL: GIT PUSH GATE] Push blocked ({reason}). "
            f"Use AskUserQuestion to confirm before pushing."
        )

    # Non-force push to feature branch: auto-allow
    return None


# =========================================================================
# Gate 6: Bash Linting
# =========================================================================

def check_bash_lint(command: str) -> dict:
    """Block bash commands with dangerous patterns.

    Returns deny dict or None.
    """
    if not command:
        return None

    for pattern, reason in BASH_LINT_PATTERNS.items():
        if re.search(pattern, command):
            return deny(f"[GUARDRAIL: BASH LINT] {reason}")
    return None


# =========================================================================
# Gate 7: Temp Script Prevention
# =========================================================================

def check_temp_script(file_path: str) -> dict:
    """Block creation of temporary scripts.

    Returns deny dict or None.
    """
    if not file_path:
        return None

    path_lower = file_path.lower()

    # Check if path contains temp indicators
    has_temp = any(p in path_lower for p in TEMP_SCRIPT_PATTERNS)
    if not has_temp:
        return None

    # Check if it's a script file
    is_script = any(file_path.endswith(ext) for ext in TEMP_SCRIPT_EXTENSIONS)
    if not is_script:
        return None

    return deny(
        f"[GUARDRAIL: TEMP SCRIPT] Creating temporary script '{file_path}'. "
        f"Use Edit for direct changes or execute inline. Temp scripts waste tokens."
    )


# =========================================================================
# Gate A: Environment Variable Protection
# =========================================================================

def check_env_var_exposure(command: str, content: str = "") -> dict:
    """Block reading sensitive environment variables.

    Returns deny dict or None.
    """
    text_to_check = f"{command} {content}"

    # Pre-strip GitHub Actions template expressions (${{ ... }})
    # These are declarative YAML references, not shell variable reads
    text_to_check = re.sub(r"\$\{\{[^}]*\}\}", "", text_to_check)

    for pattern in COMPILED_ENV_PATTERNS:
        if pattern.search(text_to_check):
            return deny(
                "[GUARDRAIL: ENV VAR PROTECTION] Attempting to read sensitive "
                "environment variable. API keys, secrets, and tokens should not "
                "be exposed in command output. "
                'If you need to verify a key exists, use `test -n "$VAR_NAME"` instead.'
            )
    return None


# =========================================================================
# Gate D: Network Binding Protection
# =========================================================================

def check_network_binding(content: str) -> dict:
    """Block binding to 0.0.0.0 without authentication.

    Returns deny dict or None.
    """
    has_unsafe = False
    for pattern in COMPILED_BINDING_PATTERNS:
        if pattern.search(content):
            has_unsafe = True
            break

    if not has_unsafe:
        return None

    # Check if auth patterns are present (which would make it acceptable)
    for pattern in COMPILED_AUTH_PATTERNS:
        if pattern.search(content):
            return None

    return deny(
        "[GUARDRAIL: NETWORK BINDING] Binding to 0.0.0.0 without authentication "
        "detected. This exposes the service to all network interfaces. Either: "
        "1) Bind to 127.0.0.1 for local-only access, or "
        "2) Add authentication (e.g., @require_auth decorator, verify_token check)."
    )


# =========================================================================
# Context gate (Gate 9)
# =========================================================================

def estimate_tokens_from_jsonl(transcript_path: str) -> int:
    """Estimate token count from conversation JSONL file.

    Caches result keyed on file mtime+size to avoid re-parsing on every call.
    """
    if not transcript_path or not Path(transcript_path).exists():
        return 0

    try:
        st = os.stat(transcript_path)
        cur_mtime = st.st_mtime
        cur_size = st.st_size
    except OSError:
        return 0

    if cur_mtime == _token_cache["mtime"] and cur_size == _token_cache["size"]:
        return _token_cache["estimate"]

    total_chars = 0

    try:
        with open(transcript_path, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                try:
                    entry = json.loads(line)
                    if "message" in entry:
                        msg = entry["message"]
                        if isinstance(msg, dict):
                            content = msg.get("content", "")
                            if isinstance(content, str):
                                total_chars += len(content)
                            elif isinstance(content, list):
                                for block in content:
                                    if isinstance(block, dict):
                                        total_chars += len(block.get("text", ""))
                    if "tool_input" in entry:
                        total_chars += len(json.dumps(entry["tool_input"]))
                    if "tool_output" in entry:
                        total_chars += len(str(entry.get("tool_output", "")))
                except json.JSONDecodeError:
                    continue
    except Exception:
        return 0

    estimate = total_chars // 4
    _token_cache["mtime"] = cur_mtime
    _token_cache["size"] = cur_size
    _token_cache["estimate"] = estimate
    return estimate


def get_state() -> dict:
    """Load gate state from file."""
    if STATE_FILE.exists():
        try:
            return json.loads(STATE_FILE.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError):
            pass
    return {"warning_issued": False, "session_id": None}


def save_state(state: dict):
    """Save gate state to file."""
    try:
        STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
        STATE_FILE.write_text(json.dumps(state), encoding="utf-8")
    except OSError:
        pass


# =========================================================================
# Main hook entry point
# =========================================================================

def main():
    """Parse hook input and evaluate gates."""
    try:
        raw = sys.stdin.read()
        if not raw.strip():
            print(json.dumps(allow()))
            return
        hook_input = json.loads(raw)
    except (json.JSONDecodeError, OSError, ValueError):
        print(json.dumps(allow()))
        return

    tool_name = hook_input.get("tool_name", "")
    tool_input = hook_input.get("tool_input", {})
    session_id = hook_input.get("session_id", "")
    transcript_path = hook_input.get("transcript_path", "")

    # =====================================================================
    # GATE 0: Always Allow - Critical tools
    # =====================================================================
    ALLOWED_TOOLS = {
        "AskUserQuestion",
        "TodoWrite",
        "TaskCreate",
        "TaskUpdate",
        "TaskGet",
        "TaskList",
    }
    if tool_name in ALLOWED_TOOLS:
        print(json.dumps(allow()))
        return

    # =====================================================================
    # GATE 1: Dependency Guardian - Block reads from dependency folders
    # =====================================================================
    if tool_name in ("Read", "Glob", "Grep"):
        path = tool_input.get("file_path") or tool_input.get("path") or ""
        pattern = tool_input.get("pattern", "")

        blocked = is_blocked_path(path) or is_blocked_path(pattern)
        if blocked:
            print(json.dumps(deny(
                f"[GUARDRAIL: DEPENDENCY GUARDIAN] Cannot read from '{blocked}'. "
                f"These folders waste 50k+ tokens. Use rg/fd with --exclude instead."
            )))
            return

    # =====================================================================
    # GATE 2: Destructive Git - Block dangerous git operations
    # =====================================================================
    if tool_name == "Bash":
        command = tool_input.get("command", "")

        # Fast-path: skip Bash-specific gates for known-safe commands
        bash_fast_path = _is_safe_bash(command)

        destructive = is_destructive_git(command) if not bash_fast_path else ""
        if destructive:
            print(json.dumps(deny(
                f"[GUARDRAIL: DESTRUCTIVE GIT] {destructive}. "
                f"This operation cannot be undone. Ask user for explicit approval first."
            )))
            return

        # =================================================================
        # GATE 2a: Git Push Gate - Block force push + protected branch push
        # =================================================================
        if not bash_fast_path:
            push_block = check_git_push_gate(command)
            if push_block:
                print(json.dumps(push_block))
                return

        # =================================================================
        # GATE 3: Secrets Detection - Block git commits with sensitive files
        # =================================================================
        sensitive = contains_sensitive_file(command) if not bash_fast_path else ""
        if sensitive:
            print(json.dumps(deny(
                f"[GUARDRAIL: SECRETS DETECTION] Command may commit '{sensitive}'. "
                f"Never commit secrets. Remove sensitive files from staging first."
            )))
            return

        # =================================================================
        # GATE 5: Bash Tool Substitution - Block grep/find/cat misuse
        # =================================================================
        if not bash_fast_path:
            sub_block = check_bash_tools(command)
            if sub_block:
                print(json.dumps(sub_block))
                return

        # =================================================================
        # GATE 6: Bash Linting - Block dangerous patterns
        # =================================================================
        if not bash_fast_path:
            lint_block = check_bash_lint(command)
            if lint_block:
                print(json.dumps(lint_block))
                return

        # =================================================================
        # GATE A: Env Var Protection - Block sensitive env var reads
        # =================================================================
        if not bash_fast_path:
            env_block = check_env_var_exposure(command)
            if env_block:
                print(json.dumps(env_block))
                return

    # =====================================================================
    # GATE 7: Temp Script Prevention - Block temp script creation (Write)
    # =====================================================================
    if tool_name == "Write":
        file_path = tool_input.get("file_path", "")
        content = tool_input.get("content", "")

        temp_block = check_temp_script(file_path)
        if temp_block:
            print(json.dumps(temp_block))
            return

        # Gate A: Env Var in written content
        if content:
            env_block = check_env_var_exposure("", content)
            if env_block:
                print(json.dumps(env_block))
                return

        # Gate D: Network Binding in written content
        if content:
            binding_block = check_network_binding(content)
            if binding_block:
                print(json.dumps(binding_block))
                return

    # =====================================================================
    # GATE A/D: Content checks for Edit tool
    # =====================================================================
    if tool_name == "Edit":
        content = tool_input.get("new_string", "")

        # Gate A: Env Var in edited content
        if content:
            env_block = check_env_var_exposure("", content)
            if env_block:
                print(json.dumps(env_block))
                return

        # Gate D: Network Binding in edited content
        if content:
            binding_block = check_network_binding(content)
            if binding_block:
                print(json.dumps(binding_block))
                return

    # =====================================================================
    # GATE 9: Context Gate - Block at 85% threshold
    # =====================================================================
    estimated_tokens = estimate_tokens_from_jsonl(transcript_path)
    usage_percent = (estimated_tokens / CONTEXT_MAX) * 100 if CONTEXT_MAX else 0

    state = get_state()
    if state.get("session_id") != session_id:
        state = {"warning_issued": False, "session_id": session_id}

    state["usage_percent"] = round(usage_percent, 1)
    state["estimated_tokens"] = estimated_tokens
    save_state(state)

    # CRITICAL threshold (85%) - BLOCK
    if estimated_tokens >= CONTEXT_CRITICAL:
        # Allow memory consolidation writes
        if tool_name == "Edit":
            edit_path = tool_input.get("file_path", "")
            if "/memory/MEMORY.md" in edit_path or "\\memory\\MEMORY.md" in edit_path:
                print(json.dumps(allow(
                    f"[CONTEXT GATE - {usage_percent:.0f}%] "
                    f"Memory write allowed (consolidation protection)."
                )))
                return

        print(json.dumps(deny(
            f"[GUARDRAIL: CONTEXT GATE - {usage_percent:.0f}%] "
            f"Estimated {estimated_tokens:,} tokens ({CONTEXT_MAX:,} max). "
            f"HALT: Output MEMORY.md pending items and 'What's Up Next' summary. "
            f"Then AskUserQuestion: compact, fresh session, or push through."
        )))
        return

    # WARNING threshold (70%) - Allow but warn once
    if estimated_tokens >= CONTEXT_WARNING and not state.get("warning_issued"):
        state["warning_issued"] = True
        save_state(state)
        print(json.dumps(allow(
            f"[CONTEXT WARNING - {usage_percent:.0f}%] "
            f"Approaching context limit ({estimated_tokens:,}/{CONTEXT_MAX:,} tokens). "
            f"Consider wrapping up current task and consolidating MEMORY.md."
        )))
        return

    # All gates passed
    print(json.dumps(allow()))


if __name__ == "__main__":
    main()
