#!/usr/bin/env python3 """echo_doctor.py — M3: one-call readiness check. [v1.0 SCAFFOLD — run() implemented] There is no single "is everything OK" command. doctor checks, in order, the things that make ECHO usable this session and prints a green/red report. Intended to back an `echo.py doctor` subcommand and the start of /echo-load when something looks wrong. Also tracked here (TODO): complete the `load` fallback — echo.cmd_load reads the heartbeat pointer but never lists _agent/sessions/ when it's missing/stale, though SKILL.md:122 documents that fallback. See patch_load_fallback() below. Exit: 0 all green · 1 a check is red. """ from __future__ import annotations import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import echo # noqa: E402 MIN_PY = (3, 9) def run_op() -> dict: """Core doctor (Phase 0): the readiness checks as data — {checks: [{ok, label, detail}], endpoint, fatal, ok}. `fatal` names an early-exit condition (not configured / unreachable) after which later checks were skipped.""" checks: list[dict] = [] def line(ok: bool, label: str, detail: str = "") -> None: checks.append({"ok": ok, "label": label, "detail": detail}) import echo_config cfg = echo_config.load() fatal = None # 1. interpreter line(sys.version_info >= MIN_PY, f"python >= {MIN_PY[0]}.{MIN_PY[1]}", f"running {sys.version_info.major}.{sys.version_info.minor}") # 2. machine-local config (owner/endpoint/key). Without a usable endpoint+key # nothing else can run, so report it clearly before touching the network. # A still-placeholder `config init` template counts as not configured. ep_real = bool(cfg["endpoint"]) and "your-obsidian-rest-endpoint" not in cfg["endpoint"] key_real = bool(cfg["key"]) and not cfg["key"].startswith("<") line(ep_real, "endpoint configured", f"[{echo_config.source('endpoint')}]" if ep_real else (f"placeholder — edit {echo_config.config_path()}" if cfg["endpoint"] else f"create {echo_config.config_path()} (run `echo.py config init`)")) line(key_real, "API key configured", f"[{echo_config.source('key')}]" if key_real else ("placeholder — not yet filled in" if cfg["key"] else "missing — see `echo.py config`")) line(bool(cfg["owner"]), "vault owner set", cfg["owner"] if cfg["owner"] else "optional, but recommended for third-person writes") if not echo_config.is_configured(cfg): fatal = "not-configured" else: # 3. reachability + auth + marker, in one GET of the bootstrap marker status, body = echo.request("GET", echo.vault_url("_agent/echo-vault.md")) if status == 0: line(False, "vault reachable", body.decode(errors="replace")[:120]) fatal = "unreachable" else: line(status not in (401, 403), "auth accepted", f"HTTP {status}" if status in (401, 403) else "") if status == 404: line(False, "vault bootstrapped", "marker absent — run bootstrap.py") elif status == 200: text = body.decode(errors="replace") ver = next((ln.split(":", 1)[1].strip() for ln in text.splitlines() if ln.startswith("schema_version:")), "?") line(True, "vault bootstrapped", f"schema_version={ver}") else: line(status < 400, "marker fetch", f"HTTP {status}") # Index freshness (2.3) — informational, never red: how stale this machine's # local indexes are (the fast sweep trues them up at load past 7 days). if fatal is None: try: import sweep as sweep_mod age = sweep_mod.fast_sweep_age_days() line(True, "index freshness", f"last local sweep {age:.1f}d ago" if age is not None else "no local sweep yet — runs automatically at the next load") except Exception: # noqa: BLE001 pass reds = sum(1 for c in checks if not c["ok"]) return {"ok": reds == 0 and fatal is None, "action": "doctor", "endpoint": cfg["endpoint"], "fatal": fatal, "reds": reds, "checks": checks} def run() -> int: env = run_op() print(f"echo doctor — endpoint {env['endpoint'] or '(not configured)'}") for c in env["checks"]: print(f" [{'OK ' if c['ok'] else 'RED'}] {c['label']}" + (f" — {c['detail']}" if c["detail"] else "")) if env["fatal"] == "not-configured": print("\ndoctor: not configured — ask the operator for their key file and install it " "(`echo.py config import `, or `config set --owner … --endpoint … --key …`), " "then re-run.") return 1 if env["fatal"] == "unreachable": print("\ndoctor: endpoint unreachable — is Obsidian + the Local REST API running?") return 1 # invariants pointer. print(" [i] invariants — run `/echo-health` (vault_lint.py) for the full check") summary = "all green" if env["reds"] == 0 else f"{env['reds']} issue(s) — see RED above" print(f"\ndoctor: {summary}") return 1 if env["reds"] else 0 if __name__ == "__main__": raise SystemExit(run())