#!/usr/bin/env python3 """UserPromptSubmit hook — block prompts until AirContext config is valid. When state.config_missing is true, refuse the prompt with a guidance message. Otherwise, pass through silently. """ from __future__ import annotations import json import os import sys from pathlib import Path _HERE = Path(__file__).resolve().parent sys.path.insert(0, str(_HERE)) from core.config_loader import validate_config # noqa: E402 from core.state import StateFile # noqa: E402 def _project_root(payload: dict) -> Path: cwd = payload.get("cwd") or os.environ.get("CLAUDE_PROJECT_DIR") or os.getcwd() return Path(cwd) def main() -> int: raw = sys.stdin.read() or "{}" try: payload = json.loads(raw) except json.JSONDecodeError: payload = {} project = _project_root(payload) air = project / "AirContext" # Project hasn't opted in — silently allow. Users opt in by running # `aircontext` (which installs templates) or `/aircontext-init`. if not air.exists(): return 0 config = air / "config.yaml" if not config.exists(): # Opted in but template install never completed. msg = ( "AirContext: AirContext/ directory exists but config.yaml is missing. " "Run `/aircontext-init` to reinstall templates, or remove the AirContext/ " "directory if you no longer want compaction in this project." ) print(json.dumps({"decision": "block", "reason": msg})) return 0 err = validate_config(config) if err: msg = ( f"AirContext config is invalid: {err}\n" "Edit AirContext/config.yaml and resubmit." ) print(json.dumps({"decision": "block", "reason": msg})) return 0 # Healthy — clear stale flag and pass through. state = StateFile(air / "state.json") if state.read().get("config_missing"): state.update(config_missing=False) return 0 if __name__ == "__main__": sys.exit(main())