e7792003a7
Internal refactor per docs/MCP-SERVER-SPEC.md §4; CLI output and exit codes unchanged (wrappers reproduce them from the envelopes): - echo_ops: capture_op/resolve_op/link_op — duplicate gate and offline queueing are data (action: duplicate-gate + candidates; queued: true); the --json stdout-swallow hack is deleted (helper chatter -> stderr inside the cores); capture_op takes body_text= directly (the MCP path, no temp file). - echo_recall.recall_op — one structured result backs prose and --json. - echo_triage.list_op/route_op, echo_reflect.apply_op, echo_session.session_end_op — call capture_op internally, count gate outcomes from envelopes. - echo.scope_show_op/scope_set_op/load_op (+ shared _load_gather), and echo_doctor.run_op (checks as data). New eval/test_ops_api.py asserts the contract for every core: envelope shape AND stdout purity (a stray print would corrupt an MCP response stream). All six suites green. The 2.2 server build now starts directly at the app layer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
105 lines
4.7 KiB
Python
105 lines
4.7 KiB
Python
#!/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}")
|
|
|
|
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 <file>`, 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())
|