refactor(doctor): extract section banner + fail-and-issue helpers (#27830)

`hermes_cli/doctor.py` had two recurring patterns:

1. **15 section headers** of the form `print() ; print(color("◆ Name", Colors.CYAN, Colors.BOLD))`
   bracketed by 3-line `# =====` / `# Check: X` / `# =====` comment banners.

2. **Paired `check_fail(...) ; issues.append(...)`** for every diagnostic that emits both a
   user-visible failure and an auto-fix instruction.

Add two helpers and collapse the patterns:

  def _section(title):
      print()
      print(color(f"◆ {title}", Colors.CYAN, Colors.BOLD))

  def _fail_and_issue(text, detail, fix, issues):
      check_fail(text, detail)
      issues.append(fix)

Replacements:
- 15 `# =====/# X/# =====` banner triples + section header pairs compressed to `_section(...)`
- All 18 `check_fail + issues.append` pairs collapsed to `_fail_and_issue(...)` (single-line
  where the call fits under 120 chars, multi-line where it doesn't)
- Net -5 LOC (`+128 / -133`)

The LOC delta is modest after wrapping long calls onto multi-line form for readability — the
real win is uniform call shape and removal of two parallel-pattern footguns. There is now
exactly one way to emit a diagnostic that pairs a user-visible failure with a fix instruction.

Behavior is byte-identical. `_section` produces the same blank line + bold-cyan output the
inline two prints did, and `_fail_and_issue` does the same `check_fail + issues.append`
sequence in the same order. Verified empirically by diffing live `run_doctor()` stdout from
this branch against `origin/main` — `diff -q` reports zero differences.

Test plan:
- All 69 tests across test_doctor.py, test_doctor_command_install.py, and
  test_doctor_dedicated_provider_skip.py pass
- `ruff check hermes_cli/doctor.py` clean
- Live `run_doctor()` output byte-identical to origin/main

Refs #23972 (Phase 2 tracker — dedup-only refactor in line with the "net-LOC-negative"
discipline).
This commit is contained in:
kshitij
2026-05-18 00:45:25 -07:00
committed by GitHub
parent 94c523f0c5
commit 41f1eddee3
+128 -133
View File
@@ -195,6 +195,18 @@ def check_info(text: str):
print(f" {color('', Colors.CYAN)} {text}")
def _section(title: str) -> None:
"""Print a doctor section banner: blank line + bold cyan ◆ title."""
print()
print(color(f"{title}", Colors.CYAN, Colors.BOLD))
def _fail_and_issue(text: str, detail: str, fix: str, issues: list[str]) -> None:
"""Emit a check_fail and append the corresponding fix instruction."""
check_fail(text, detail)
issues.append(fix)
def _check_gateway_service_linger(issues: list[str]) -> None:
"""Warn when a systemd user gateway service will stop after logout."""
try:
@@ -214,9 +226,7 @@ def _check_gateway_service_linger(issues: list[str]) -> None:
if not unit_path.exists():
return
print()
print(color("◆ Gateway Service", Colors.CYAN, Colors.BOLD))
_section("Gateway Service")
linger_enabled, linger_detail = get_systemd_linger_status()
if linger_enabled is True:
check_ok("Systemd linger enabled", "(gateway service survives logout)")
@@ -373,11 +383,7 @@ def run_doctor(args):
print(color("│ 🩺 Hermes Doctor │", Colors.CYAN))
print(color("└─────────────────────────────────────────────────────────┘", Colors.CYAN))
# =========================================================================
# Check: Security advisories (RUNS FIRST — these are the most urgent)
# =========================================================================
print()
print(color("◆ Security Advisories", Colors.CYAN, Colors.BOLD))
_section("Security Advisories")
try:
from hermes_cli.security_advisories import (
detect_compromised,
@@ -423,12 +429,7 @@ def run_doctor(args):
# Never let a bug in the advisory check block the rest of doctor.
check_warn(f"Security advisory check failed: {e}")
# =========================================================================
# Check: Python version
# =========================================================================
print()
print(color("◆ Python Environment", Colors.CYAN, Colors.BOLD))
_section("Python Environment")
py_version = sys.version_info
if py_version >= (3, 11):
check_ok(f"Python {py_version.major}.{py_version.minor}.{py_version.micro}")
@@ -438,8 +439,12 @@ def run_doctor(args):
elif py_version >= (3, 8):
check_warn(f"Python {py_version.major}.{py_version.minor}.{py_version.micro}", "(3.10+ recommended)")
else:
check_fail(f"Python {py_version.major}.{py_version.minor}.{py_version.micro}", "(3.10+ required)")
issues.append("Upgrade Python to 3.10+")
_fail_and_issue(
f"Python {py_version.major}.{py_version.minor}.{py_version.micro}",
"(3.10+ required)",
"Upgrade Python to 3.10+",
issues,
)
# Check if in virtual environment
in_venv = sys.prefix != sys.base_prefix
@@ -448,12 +453,7 @@ def run_doctor(args):
else:
check_warn("Not in virtual environment", "(recommended)")
# =========================================================================
# Check: Required packages
# =========================================================================
print()
print(color("◆ Required Packages", Colors.CYAN, Colors.BOLD))
_section("Required Packages")
required_packages = [
("openai", "OpenAI SDK"),
("rich", "Rich (terminal UI)"),
@@ -473,8 +473,7 @@ def run_doctor(args):
__import__(module)
check_ok(name)
except ImportError:
check_fail(name, "(missing)")
issues.append(f"Install {name}: {_python_install_cmd()} {module}")
_fail_and_issue(name, "(missing)", f"Install {name}: {_python_install_cmd()} {module}", issues)
for module, name in optional_packages:
try:
@@ -483,12 +482,7 @@ def run_doctor(args):
except ImportError:
check_warn(name, "(optional, not installed)")
# =========================================================================
# Check: Configuration files
# =========================================================================
print()
print(color("◆ Configuration Files", Colors.CYAN, Colors.BOLD))
_section("Configuration Files")
# Check ~/.hermes/.env (primary location for user config)
env_path = HERMES_HOME / '.env'
if env_path.exists():
@@ -611,14 +605,15 @@ def run_doctor(args):
and not (provider_ids_to_accept & valid_provider_ids)
):
known_list = ", ".join(sorted(known_providers)) if known_providers else "(unavailable)"
check_fail(
_fail_and_issue(
f"model.provider '{provider_raw}' is not a recognised provider",
f"(known: {known_list})",
)
issues.append(
f"model.provider '{provider_raw}' is unknown. "
f"Valid providers: {known_list}. "
f"Fix: run 'hermes config set model.provider <valid_provider>'"
(
f"model.provider '{provider_raw}' is unknown. "
f"Valid providers: {known_list}. "
f"Fix: run 'hermes config set model.provider <valid_provider>'"
),
issues,
)
# Warn if model is set to a provider-prefixed name on a provider that doesn't use them
@@ -677,14 +672,15 @@ def run_doctor(args):
or status.get("api_key")
)
if not configured:
check_fail(
_fail_and_issue(
f"model.provider '{runtime_provider}' is set but no API key is configured",
"(check ~/.hermes/.env or run 'hermes setup')",
)
issues.append(
f"No credentials found for provider '{runtime_provider}'. "
f"Run 'hermes setup' or set the provider's API key in {_DHH}/.env, "
f"or switch providers with 'hermes config set model.provider <name>'"
(
f"No credentials found for provider '{runtime_provider}'. "
f"Run 'hermes setup' or set the provider's API key in {_DHH}/.env, "
f"or switch providers with 'hermes config set model.provider <name>'"
),
issues,
)
except Exception:
pass
@@ -768,8 +764,7 @@ def run_doctor(args):
from hermes_cli.config import validate_config_structure
config_issues = validate_config_structure()
if config_issues:
print()
print(color("◆ Config Structure", Colors.CYAN, Colors.BOLD))
_section("Config Structure")
for ci in config_issues:
if ci.severity == "error":
check_fail(ci.message)
@@ -782,12 +777,7 @@ def run_doctor(args):
except Exception:
pass
# =========================================================================
# Check: Auth providers
# =========================================================================
print()
print(color("◆ Auth Providers", Colors.CYAN, Colors.BOLD))
_section("Auth Providers")
try:
from hermes_cli.auth import (
get_nous_auth_status,
@@ -859,12 +849,7 @@ def run_doctor(args):
"(optional — only required to import tokens from an existing Codex CLI login)"
)
# =========================================================================
# Check: Directory structure
# =========================================================================
print()
print(color("◆ Directory Structure", Colors.CYAN, Colors.BOLD))
_section("Directory Structure")
hermes_home = HERMES_HOME
if hermes_home.exists():
check_ok(f"{_DHH} directory exists")
@@ -976,13 +961,8 @@ def run_doctor(args):
_check_gateway_service_linger(issues)
# =========================================================================
# Check: Command installation (hermes bin symlink)
# =========================================================================
if sys.platform != "win32":
print()
print(color("◆ Command Installation", Colors.CYAN, Colors.BOLD))
_section("Command Installation")
# Determine the venv entry point location
_venv_bin = None
for _venv_name in ("venv", ".venv"):
@@ -1056,12 +1036,7 @@ def run_doctor(args):
else:
issues.append(f"Missing {_cmd_link_display}/hermes symlink — run 'hermes doctor --fix'")
# =========================================================================
# Check: External tools
# =========================================================================
print()
print(color("◆ External Tools", Colors.CYAN, Colors.BOLD))
_section("External Tools")
# Git
if _safe_which("git"):
check_ok("git")
@@ -1087,11 +1062,14 @@ def run_doctor(args):
if result is not None and result.returncode == 0:
check_ok("docker", "(daemon running)")
else:
check_fail("docker daemon not running")
issues.append("Start Docker daemon")
_fail_and_issue("docker daemon not running", "", "Start Docker daemon", issues)
else:
check_fail("docker not found", "(required for TERMINAL_ENV=docker)")
issues.append("Install Docker or change TERMINAL_ENV")
_fail_and_issue(
"docker not found",
"(required for TERMINAL_ENV=docker)",
"Install Docker or change TERMINAL_ENV",
issues,
)
elif _safe_which("docker"):
check_ok("docker", "(optional)")
elif _is_termux():
@@ -1126,11 +1104,14 @@ def run_doctor(args):
if result is not None and result.returncode == 0:
check_ok(f"SSH connection to {ssh_host}")
else:
check_fail(f"SSH connection to {ssh_host}")
issues.append(f"Check SSH configuration for {ssh_host}")
_fail_and_issue(f"SSH connection to {ssh_host}", "", f"Check SSH configuration for {ssh_host}", issues)
else:
check_fail("TERMINAL_SSH_HOST not set", "(required for TERMINAL_ENV=ssh)")
issues.append("Set TERMINAL_SSH_HOST in .env")
_fail_and_issue(
"TERMINAL_SSH_HOST not set",
"(required for TERMINAL_ENV=ssh)",
"Set TERMINAL_SSH_HOST in .env",
issues,
)
# Daytona (if using daytona backend)
if terminal_env == "daytona":
@@ -1138,14 +1119,22 @@ def run_doctor(args):
if daytona_key:
check_ok("Daytona API key", "(configured)")
else:
check_fail("DAYTONA_API_KEY not set", "(required for TERMINAL_ENV=daytona)")
issues.append("Set DAYTONA_API_KEY environment variable")
_fail_and_issue(
"DAYTONA_API_KEY not set",
"(required for TERMINAL_ENV=daytona)",
"Set DAYTONA_API_KEY environment variable",
issues,
)
try:
from daytona import Daytona # noqa: F401 — SDK presence check
check_ok("daytona SDK", "(installed)")
except ImportError:
check_fail("daytona SDK not installed", "(pip install daytona)")
issues.append("Install daytona SDK: pip install daytona")
_fail_and_issue(
"daytona SDK not installed",
"(pip install daytona)",
"Install daytona SDK: pip install daytona",
issues,
)
# Vercel Sandbox (if using vercel_sandbox backend)
if terminal_env == "vercel_sandbox":
@@ -1155,32 +1144,50 @@ def run_doctor(args):
check_ok("Vercel runtime", f"({runtime})")
else:
supported = ", ".join(_SUPPORTED_VERCEL_RUNTIMES)
check_fail("Vercel runtime unsupported", f"({runtime}; use {supported})")
issues.append(f"Set TERMINAL_VERCEL_RUNTIME to one of: {supported}")
_fail_and_issue(
"Vercel runtime unsupported",
f"({runtime}; use {supported})",
f"Set TERMINAL_VERCEL_RUNTIME to one of: {supported}",
issues,
)
disk = os.getenv("TERMINAL_CONTAINER_DISK", "51200").strip()
if disk in {"", "0", "51200"}:
check_ok("Vercel disk setting", "(uses platform default)")
else:
check_fail("Vercel custom disk unsupported", "(reset terminal.container_disk to 51200)")
issues.append("Vercel Sandbox does not support custom container_disk; use the shared default 51200")
_fail_and_issue(
"Vercel custom disk unsupported",
"(reset terminal.container_disk to 51200)",
"Vercel Sandbox does not support custom container_disk; use the shared default 51200",
issues,
)
if importlib.util.find_spec("vercel") is not None:
check_ok("vercel SDK", "(installed)")
else:
check_fail("vercel SDK not installed", "(pip install 'hermes-agent[vercel]')")
issues.append("Install the Vercel optional dependency: pip install 'hermes-agent[vercel]'")
_fail_and_issue(
"vercel SDK not installed",
"(pip install 'hermes-agent[vercel]')",
"Install the Vercel optional dependency: pip install 'hermes-agent[vercel]'",
issues,
)
auth_status = describe_vercel_auth()
if auth_status.ok:
check_ok("Vercel auth", f"({auth_status.label})")
elif auth_status.label.startswith("partial"):
check_fail("Vercel auth incomplete", f"({auth_status.label})")
issues.append("Set VERCEL_TOKEN, VERCEL_PROJECT_ID, and VERCEL_TEAM_ID together")
_fail_and_issue(
"Vercel auth incomplete",
f"({auth_status.label})",
"Set VERCEL_TOKEN, VERCEL_PROJECT_ID, and VERCEL_TEAM_ID together",
issues,
)
else:
check_fail("Vercel auth not configured", f"({auth_status.label})")
issues.append(
"Configure Vercel Sandbox auth with VERCEL_TOKEN, VERCEL_PROJECT_ID, and VERCEL_TEAM_ID"
_fail_and_issue(
"Vercel auth not configured",
f"({auth_status.label})",
"Configure Vercel Sandbox auth with VERCEL_TOKEN, VERCEL_PROJECT_ID, and VERCEL_TEAM_ID",
issues,
)
for line in auth_status.detail_lines:
check_info(f"Vercel auth {line}")
@@ -1320,12 +1327,7 @@ def run_doctor(args):
for note in _termux_install_all_fallback_notes():
check_info(note)
# =========================================================================
# Check: API connectivity
# =========================================================================
print()
print(color("◆ API Connectivity", Colors.CYAN, Colors.BOLD))
_section("API Connectivity")
# Refactor: every connectivity probe below is HTTP-bound and fully
# independent. Running them in series spent ~5s wall on a typical
# workstation (2s of that was boto3's IMDS lookup for AWS credentials,
@@ -1673,12 +1675,7 @@ def run_doctor(args):
for _issue in _issues_to_add:
issues.append(_issue)
# =========================================================================
# Check: Tool Availability
# =========================================================================
print()
print(color("◆ Tool Availability", Colors.CYAN, Colors.BOLD))
_section("Tool Availability")
try:
# Add project root to path for imports
sys.path.insert(0, str(PROJECT_ROOT))
@@ -1706,12 +1703,7 @@ def run_doctor(args):
except Exception as e:
check_warn("Could not check tool availability", f"({e})")
# =========================================================================
# Check: Skills Hub
# =========================================================================
print()
print(color("◆ Skills Hub", Colors.CYAN, Colors.BOLD))
_section("Skills Hub")
hub_dir = HERMES_HOME / "skills" / ".hub"
if hub_dir.exists():
check_ok("Skills Hub directory exists")
@@ -1752,12 +1744,7 @@ def run_doctor(args):
else:
check_warn("No GITHUB_TOKEN", f"(60 req/hr rate limit — set in {_DHH}/.env for better rates)")
# =========================================================================
# Memory Provider (only check the active provider, if any)
# =========================================================================
print()
print(color("◆ Memory Provider", Colors.CYAN, Colors.BOLD))
_section("Memory Provider")
_active_memory_provider = ""
try:
import yaml as _yaml
@@ -1782,8 +1769,12 @@ def run_doctor(args):
elif not hcfg.enabled:
check_info(f"Honcho disabled (set enabled: true in {_honcho_cfg_path} to activate)")
elif not (hcfg.api_key or hcfg.base_url):
check_fail("Honcho API key or base URL not set", "run: hermes memory setup")
issues.append("No Honcho API key — run 'hermes memory setup'")
_fail_and_issue(
"Honcho API key or base URL not set",
"run: hermes memory setup",
"No Honcho API key — run 'hermes memory setup'",
issues,
)
else:
from plugins.memory.honcho.client import get_honcho_client, reset_honcho_client
reset_honcho_client()
@@ -1794,11 +1785,14 @@ def run_doctor(args):
f"workspace={hcfg.workspace_id} mode={hcfg.recall_mode} freq={hcfg.write_frequency}",
)
except Exception as _e:
check_fail("Honcho connection failed", str(_e))
issues.append(f"Honcho unreachable: {_e}")
_fail_and_issue("Honcho connection failed", str(_e), f"Honcho unreachable: {_e}", issues)
except ImportError:
check_fail("honcho-ai not installed", "pip install honcho-ai")
issues.append("Honcho is set as memory provider but honcho-ai is not installed")
_fail_and_issue(
"honcho-ai not installed",
"pip install honcho-ai",
"Honcho is set as memory provider but honcho-ai is not installed",
issues,
)
except Exception as _e:
check_warn("Honcho check failed", str(_e))
elif _active_memory_provider == "mem0":
@@ -1810,11 +1804,19 @@ def run_doctor(args):
check_ok("Mem0 API key configured")
check_info(f"user_id={mem0_cfg.get('user_id', '?')} agent_id={mem0_cfg.get('agent_id', '?')}")
else:
check_fail("Mem0 API key not set", "(set MEM0_API_KEY in .env or run hermes memory setup)")
issues.append("Mem0 is set as memory provider but API key is missing")
_fail_and_issue(
"Mem0 API key not set",
"(set MEM0_API_KEY in .env or run hermes memory setup)",
"Mem0 is set as memory provider but API key is missing",
issues,
)
except ImportError:
check_fail("Mem0 plugin not loadable", "pip install mem0ai")
issues.append("Mem0 is set as memory provider but mem0ai is not installed")
_fail_and_issue(
"Mem0 plugin not loadable",
"pip install mem0ai",
"Mem0 is set as memory provider but mem0ai is not installed",
issues,
)
except Exception as _e:
check_warn("Mem0 check failed", str(_e))
else:
@@ -1831,17 +1833,13 @@ def run_doctor(args):
except Exception as _e:
check_warn(f"{_active_memory_provider} check failed", str(_e))
# =========================================================================
# Profiles
# =========================================================================
try:
from hermes_cli.profiles import list_profiles, _get_wrapper_dir, profile_exists
import re as _re
named_profiles = [p for p in list_profiles() if not p.is_default]
if named_profiles:
print()
print(color("◆ Profiles", Colors.CYAN, Colors.BOLD))
_section("Profiles")
check_ok(f"{len(named_profiles)} profile(s) found")
wrapper_dir = _get_wrapper_dir()
for p in named_profiles:
@@ -1878,9 +1876,6 @@ def run_doctor(args):
except Exception:
pass
# =========================================================================
# Summary
# =========================================================================
print()
remaining_issues = issues + manual_issues
if should_fix and fixed_count > 0: