Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dev/refactor/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ Status:
- Public setup docs now cover Claude Code, Codex, and Hermes Agent separately so each orchestrator path matches the actual setup and runtime boundary.
- Hermes-facing install surfaces exist in three explicit modes: `hermit install --print-hermes-mcp-config`, `hermit install --fix-hermes-mcp`, and `hermit install --test-hermes-mcp`.
- The Hermes-facing fix/test surfaces now also accept `--hermes-home` so registration and smoke can be exercised against an isolated Hermes config directory.
- `hermit doctor` now also accepts `--hermes-home` so the Hermes MCP diagnostic can inspect the same isolated config target before or after repair.
- Live runtime delivery remains the existing Hermit MCP server path; the Hermes wrapper is a setup/health adapter, not a second runtime.
- Release smoke has already verified npm and PyPI packages can print, register, and test the Hermes-facing MCP configuration without requiring OpenAI API-key onboarding.

Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
- Added `hermit install --print-hermes-mcp-config` as a print-only Hermes Agent registration aid; it does not mutate the user's Hermes config.
- Added `hermit install --fix-hermes-mcp` as an explicit Hermes Agent MCP registration path that calls `hermes mcp add hermit-channel --command hermit --args mcp-server` only when requested.
- Added `hermit install --test-hermes-mcp` as an explicit live MCP wiring smoke that runs `hermes mcp test hermit-channel` without mutating Hermes config.
- Added optional `--hermes-home` targeting for `hermit install --fix-hermes-mcp` and `hermit install --test-hermes-mcp` so registration/smoke can run against an isolated Hermes config directory instead of the operator's default `~/.hermes`.
- Added optional `--hermes-home` targeting for `hermit doctor`, `hermit install --fix-hermes-mcp`, and `hermit install --test-hermes-mcp` so isolated Hermes config directories can be diagnosed, repaired, and smoke-tested without touching the operator's default `~/.hermes`.
- Tightened the Hermes MCP smoke so it treats `hermes mcp test hermit-channel` output such as `Server 'hermit-channel' not found in config` as a failure even when the Hermes CLI exits with status 0.
- Tightened `hermit install --fix-hermes-mcp` so it answers Hermes Agent's tool-enable prompt, rejects `Cancelled` output even with exit code 0, and verifies the registration is visible in `hermes mcp list` before reporting success.
- Added an optional `Hermes MCP` doctor diagnostic that checks for `hermit-channel -> hermit mcp-server` registration and tolerates current `hermes mcp list` text-only output.
Expand Down
3 changes: 2 additions & 1 deletion docs/hermes-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@ Run the bounded live probe:
hermit install --test-hermes-mcp
```

If you want an isolated Hermes config during registration or smoke, point Hermit at a separate home:
If you want an isolated Hermes config during registration, smoke, or doctor checks, point Hermit at a separate home:

```bash
hermit install --fix-hermes-mcp --hermes-home /tmp/hermes-home
hermit install --test-hermes-mcp --hermes-home /tmp/hermes-home
hermit doctor --hermes-home /tmp/hermes-home
```

Under the hood this executes Hermes Agent's live MCP check for `hermit-channel` without mutating config beyond the explicit `--fix-hermes-mcp` path.
Expand Down
5 changes: 3 additions & 2 deletions hermit_agent/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ def _build_doctor_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Diagnose or repair Hermit setup")
parser.add_argument("--cwd", default=os.getcwd(), help="Working directory")
parser.add_argument("--fix", action="store_true", help="Attempt common setup/runtime repairs automatically")
parser.add_argument("--hermes-home", default=None, help="Optional Hermes Agent config directory for isolated MCP diagnostics/repairs")
return parser


Expand Down Expand Up @@ -695,9 +696,9 @@ def main():

doctor_args = _build_doctor_parser().parse_args(sys.argv[2:])
if doctor_args.fix:
print(format_doctor_fix_summary(cwd=doctor_args.cwd))
print(format_doctor_fix_summary(cwd=doctor_args.cwd, hermes_home=doctor_args.hermes_home))
else:
print(run_diagnostics(cwd=doctor_args.cwd).format())
print(run_diagnostics(cwd=doctor_args.cwd, hermes_home=doctor_args.hermes_home).format())
return

# ── codex-channels dispatch ──────────────────────────────────────
Expand Down
15 changes: 10 additions & 5 deletions hermit_agent/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from enum import Enum
from pathlib import Path

from .install_flow import resolve_hermit_mcp_stdio_entry, run_install, run_startup_self_heal
from .install_flow import _hermes_cli_env, resolve_hermit_mcp_stdio_entry, run_install, run_startup_self_heal


class DiagStatus(Enum):
Expand Down Expand Up @@ -256,18 +256,21 @@ def _server_transport(server: object) -> dict[str, object]:
return server


def _check_hermes_mcp(cwd: str) -> DiagCheck:
def _check_hermes_mcp(cwd: str, hermes_home: str | None = None) -> DiagCheck:
if shutil.which("hermes") is None:
return DiagCheck(
"Hermes MCP",
DiagStatus.WARN,
"hermes command not found — run `hermit install --print-hermes-mcp-config` after installing Hermes Agent",
)

hermes_env = _hermes_cli_env(hermes_home=hermes_home)

try:
proc = subprocess.run(
["hermes", "mcp", "list", "--json"],
cwd=cwd,
env=hermes_env,
capture_output=True,
text=True,
timeout=15,
Expand All @@ -283,6 +286,7 @@ def _check_hermes_mcp(cwd: str) -> DiagCheck:
proc = subprocess.run(
["hermes", "mcp", "list"],
cwd=cwd,
env=hermes_env,
capture_output=True,
text=True,
timeout=15,
Expand Down Expand Up @@ -336,7 +340,7 @@ def _check_hermes_mcp(cwd: str) -> DiagCheck:
)


def run_diagnostics(cwd: str | None = None, home: str | None = None) -> DiagReport:
def run_diagnostics(cwd: str | None = None, home: str | None = None, hermes_home: str | None = None) -> DiagReport:
"""Diagnose HermitAgent configuration. Testable by injecting cwd/home."""
cwd = cwd or os.getcwd()
home = home or os.path.expanduser("~")
Expand All @@ -348,18 +352,19 @@ def run_diagnostics(cwd: str | None = None, home: str | None = None) -> DiagRepo
_check_sensitive_deny(),
_check_local_backend(cwd),
_check_agent_learner(home),
_check_hermes_mcp(cwd),
_check_hermes_mcp(cwd, hermes_home=hermes_home),
]
return DiagReport(checks=checks)


def format_doctor_fix_summary(*, cwd: str) -> str:
def format_doctor_fix_summary(*, cwd: str, hermes_home: str | None = None) -> str:
startup = run_startup_self_heal(cwd=cwd)
install = run_install(
cwd=cwd,
assume_yes=True,
skip_mcp_register=False,
skip_codex=False,
hermes_home=hermes_home,
)

lines = ["Hermit doctor --fix complete.", "", "Repairs:"]
Expand Down
21 changes: 17 additions & 4 deletions tests/test_cli_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ def test_install_parser_accepts_explicit_hermes_home_for_isolated_smoke_runs():
assert ns.hermes_home == "/tmp/hermes-home"


def test_doctor_parser_accepts_explicit_hermes_home_for_isolated_checks():
from hermit_agent.__main__ import _build_doctor_parser

ns = _build_doctor_parser().parse_args(["--hermes-home", "/tmp/hermes-home"])

assert ns.hermes_home == "/tmp/hermes-home"


def test_main_dispatches_install(monkeypatch, capsys):
from hermit_agent import __main__ as main_mod
from hermit_agent.install_flow import InstallSummary
Expand Down Expand Up @@ -253,28 +261,33 @@ def test_main_dispatches_status_watch(monkeypatch):
def test_main_dispatches_doctor(monkeypatch, capsys):
from hermit_agent import __main__ as main_mod

monkeypatch.setattr(main_mod.sys, "argv", ["hermit-agent", "doctor", "--cwd", "/tmp/demo"])
monkeypatch.setattr(main_mod.sys, "argv", ["hermit-agent", "doctor", "--cwd", "/tmp/demo", "--hermes-home", "/tmp/hermes-home"])

seen = []

class DummyReport:
def format(self):
return "HermitAgent Doctor — overall: PASS"

monkeypatch.setattr("hermit_agent.doctor.run_diagnostics", lambda **kwargs: DummyReport())
monkeypatch.setattr("hermit_agent.doctor.run_diagnostics", lambda **kwargs: seen.append(kwargs) or DummyReport())

main_mod.main()

assert "HermitAgent Doctor — overall: PASS" in capsys.readouterr().out
assert seen == [{"cwd": "/tmp/demo", "hermes_home": "/tmp/hermes-home"}]


def test_main_dispatches_doctor_fix(monkeypatch, capsys):
from hermit_agent import __main__ as main_mod

monkeypatch.setattr(main_mod.sys, "argv", ["hermit-agent", "doctor", "--cwd", "/tmp/demo", "--fix"])
monkeypatch.setattr("hermit_agent.doctor.format_doctor_fix_summary", lambda **kwargs: "Hermit doctor --fix complete.")
monkeypatch.setattr(main_mod.sys, "argv", ["hermit-agent", "doctor", "--cwd", "/tmp/demo", "--fix", "--hermes-home", "/tmp/hermes-home"])
seen = []
monkeypatch.setattr("hermit_agent.doctor.format_doctor_fix_summary", lambda **kwargs: seen.append(kwargs) or "Hermit doctor --fix complete.")

main_mod.main()

assert "Hermit doctor --fix complete." in capsys.readouterr().out
assert seen == [{"cwd": "/tmp/demo", "hermes_home": "/tmp/hermes-home"}]


def test_main_prints_startup_self_heal_summary_when_repairs_happen(monkeypatch, capsys):
Expand Down
13 changes: 9 additions & 4 deletions tests/test_doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,21 @@ class Result:
stdout = json.dumps({"servers": {"hermit-channel": {"command": "hermit", "args": ["mcp-server"]}}})
stderr = ""

calls: list[list[str]] = []
calls: list[dict[str, object]] = []
monkeypatch.setattr(doctor_mod.shutil, "which", lambda name: "/usr/local/bin/hermes" if name == "hermes" else None)
monkeypatch.setattr(doctor_mod.subprocess, "run", lambda args, **kwargs: calls.append(args) or Result())
monkeypatch.setattr(
doctor_mod.subprocess,
"run",
lambda args, **kwargs: calls.append({"args": args, "env": kwargs.get("env")}) or Result(),
)

with tempfile.TemporaryDirectory() as project, tempfile.TemporaryDirectory() as home:
report = run_diagnostics(cwd=project, home=home)
hermes_home = Path(home) / ".hermes-isolated"
report = run_diagnostics(cwd=project, home=home, hermes_home=str(hermes_home))
chk = next(c for c in report.checks if c.name == "Hermes MCP")
assert chk.status == DiagStatus.PASS
assert "hermit-channel registered" in chk.message
assert calls == [["hermes", "mcp", "list", "--json"]]
assert calls == [{"args": ["hermes", "mcp", "list", "--json"], "env": {**__import__("os").environ, "HERMES_HOME": str(hermes_home)}}]


def test_hermes_mcp_list_falls_back_when_json_flag_is_unsupported(monkeypatch):
Expand Down
15 changes: 9 additions & 6 deletions tests/test_doctor_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@


def test_format_doctor_fix_summary_includes_repair_status(monkeypatch):
seen = {}
monkeypatch.setattr(
"hermit_agent.doctor.run_startup_self_heal",
lambda **kwargs: StartupHealSummary(
Expand All @@ -15,9 +16,9 @@ def test_format_doctor_fix_summary_includes_repair_status(monkeypatch):
codex_runtime_status="missing",
),
)
monkeypatch.setattr(
"hermit_agent.doctor.run_install",
lambda **kwargs: InstallSummary(
def fake_run_install(**kwargs):
seen["run_install"] = kwargs
return InstallSummary(
settings_path="/tmp/settings.json",
gateway_api_key_created=True,
gateway_api_key_present=True,
Expand All @@ -26,14 +27,16 @@ def test_format_doctor_fix_summary_includes_repair_status(monkeypatch):
codex_install_status="installed",
codex_runtime_version="0.1.31",
next_steps=["Run Hermit."],
),
)
)

monkeypatch.setattr("hermit_agent.doctor.run_install", fake_run_install)

text = format_doctor_fix_summary(cwd="/tmp/demo")
text = format_doctor_fix_summary(cwd="/tmp/demo", hermes_home="/tmp/hermes-home")

assert "Hermit doctor --fix complete." in text
assert "gateway=started" in text
assert "mcp=registered" in text
assert "codex=installed" in text
assert "codex-facing surface remains: hermit-channel MCP" in text
assert "codex integration runtime version: 0.1.31" in text
assert seen["run_install"]["hermes_home"] == "/tmp/hermes-home"