#!/usr/bin/env python3 """ Preflight — verify every Path B dependency before flipping KanBot to live. Probes the real services (no mocks). Prints a checklist and exits non-zero if anything the live cycle needs is missing, so you never flip DRY_RUN=0 into a half-ready stack. ✓ Postgres reachable on DATABASE_URL, schema present ✓ Ollama reachable on OLLAMA_HOST ✓ both configured models pulled ✓ dry-run status reported (informational) Run: python cli/preflight.py """ from __future__ import annotations import asyncio import os import sys sys.path.insert(0, ".") GREEN, RED, YEL, DIM, RST = "\033[32m", "\033[31m", "\033[33m", "\033[2m", "\033[0m" OK, BAD, WARN = f"{GREEN}✓{RST}", f"{RED}✗{RST}", f"{YEL}!{RST}" DSN = os.environ.get("DATABASE_URL", "postgresql://kanbot:kanbot@localhost:5432/kanbot") OLLAMA = os.environ.get("OLLAMA_HOST", "http://localhost:11434") MODEL_FAST = os.environ.get("KANBOT_MODEL_FAST", "qwen2.5:8b-instruct-q4_K_M") MODEL_DAILY = os.environ.get("KANBOT_MODEL_DAILY", "qwen2.5:14b-instruct-q4_K_M") async def check_postgres() -> tuple[bool, str]: try: import asyncpg except ImportError: return False, "asyncpg not installed (pip install -r requirements.txt)" try: conn = await asyncpg.connect(DSN, timeout=4) except Exception as e: return False, f"cannot connect to {DSN}: {e}" try: has = await conn.fetchval( "SELECT to_regclass('public.cards') IS NOT NULL") return (bool(has), "connected, schema present" if has else "connected but 'cards' table missing (apply sql/001_schema.sql)") finally: await conn.close() async def check_ollama() -> tuple[bool, str, set[str]]: try: import httpx except ImportError: return False, "httpx not installed", set() try: async with httpx.AsyncClient(timeout=4) as c: r = await c.get(f"{OLLAMA}/api/tags") r.raise_for_status() names = {m["name"] for m in r.json().get("models", [])} return True, f"reachable at {OLLAMA}", names except Exception as e: return False, f"unreachable at {OLLAMA}: {e}", set() async def main() -> int: print(f"\n KanBot preflight — Path B readiness\n") fail = 0 ok, msg = await check_postgres() print(f" {OK if ok else BAD} postgres {DIM}{msg}{RST}") fail += not ok ok, msg, models = await check_ollama() print(f" {OK if ok else BAD} ollama {DIM}{msg}{RST}") fail += not ok if ok: for label, m in (("daily/critic", MODEL_DAILY), ("fast head", MODEL_FAST)): have = any(m.split(":")[0] in n or m == n for n in models) print(f" {OK if have else BAD} model {DIM}{label}: {m} " f"{'pulled' if have else 'MISSING — ollama pull ' + m}{RST}") fail += not have else: print(f" {WARN} model {DIM}skipped (ollama unreachable){RST}") dry = os.environ.get("KANBOT_DRY_RUN", "1") == "1" print(f" {WARN} dry-run {DIM}KANBOT_DRY_RUN={'1 (proposals only)' if dry else '0 (LIVE — commands execute)'}{RST}") print() if fail: print(f" {RED}NOT READY: {fail} dependency check(s) failed. See GOLIVE.md.{RST}\n") return 1 print(f" {GREEN}READY: all Path B dependencies satisfied.{RST}") if dry: print(f" {DIM}Still in dry-run. Flip KANBOT_DRY_RUN=0 when you want live execution.{RST}\n") else: print(f" {YEL}LIVE MODE ON — approved cards will execute through the sandbox.{RST}\n") return 0 if __name__ == "__main__": raise SystemExit(asyncio.run(main()))