From f639715cd91ea02389c886dc45a9b1adcbdc3525 Mon Sep 17 00:00:00 2001 From: Erosika Date: Thu, 2 Apr 2026 13:41:15 -0400 Subject: [PATCH 1/9] fix(honcho): add default profile fallback to config resolution chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolve_config_path() had a two-step fallback: 1. $HERMES_HOME/honcho.json (profile-local) 2. ~/.honcho/config.json (global) This missed ~/.hermes/honcho.json — the default profile's config where host blocks accumulate via hermes honcho setup and clone_honcho_for_profile. When a non-default profile runs under its own HERMES_HOME, it couldn't find the host blocks written by profile creation, causing 'Session not found' errors on first chat. Add a middle tier to the resolution chain: 1. $HERMES_HOME/honcho.json (profile-local) 2. ~/.hermes/honcho.json (default profile — shared host blocks) 3. ~/.honcho/config.json (global, cross-app interop) Fix existing tests that didn't isolate Path.home() and were brittle when ~/.hermes/honcho.json existed on the test machine. --- honcho_integration/client.py | 15 ++++- tests/honcho_integration/test_client.py | 15 +++-- .../test_config_isolation.py | 65 ++++++++++++++++++- 3 files changed, 85 insertions(+), 10 deletions(-) diff --git a/honcho_integration/client.py b/honcho_integration/client.py index 6a567b073406..09606af2409f 100644 --- a/honcho_integration/client.py +++ b/honcho_integration/client.py @@ -56,13 +56,22 @@ def resolve_active_host() -> str: def resolve_config_path() -> Path: """Return the active Honcho config path. - Checks $HERMES_HOME/honcho.json first (instance-local), then falls back - to ~/.honcho/config.json (global). Returns the global path if neither - exists (for first-time setup writes). + Resolution order: + 1. $HERMES_HOME/honcho.json (profile-local, if it exists) + 2. ~/.hermes/honcho.json (default profile — shared host blocks live here) + 3. ~/.honcho/config.json (global, cross-app interop) + + Returns the global path if none exist (for first-time setup writes). """ local_path = get_hermes_home() / "honcho.json" if local_path.exists(): return local_path + + # Default profile's config — host blocks accumulate here via setup/clone + default_path = Path.home() / ".hermes" / "honcho.json" + if default_path != local_path and default_path.exists(): + return default_path + return GLOBAL_CONFIG_PATH diff --git a/tests/honcho_integration/test_client.py b/tests/honcho_integration/test_client.py index 655e786c4322..a347df9788cd 100644 --- a/tests/honcho_integration/test_client.py +++ b/tests/honcho_integration/test_client.py @@ -346,15 +346,18 @@ def test_prefers_hermes_home_when_exists(self, tmp_path): def test_falls_back_to_global_when_no_local(self, tmp_path): hermes_home = tmp_path / "hermes" hermes_home.mkdir() - # No honcho.json in HERMES_HOME - - with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}): + # No honcho.json in HERMES_HOME — and no ~/.hermes/honcho.json either + with patch.dict(os.environ, {"HERMES_HOME": str(hermes_home)}), \ + patch.object(Path, "home", staticmethod(lambda: tmp_path)): result = resolve_config_path() assert result == GLOBAL_CONFIG_PATH - def test_falls_back_to_global_without_hermes_home_env(self): - with patch.dict(os.environ, {}, clear=False): - os.environ.pop("HERMES_HOME", None) + def test_falls_back_to_global_without_hermes_home_env(self, tmp_path): + # Point HERMES_HOME to a temp dir that has NO honcho.json + empty_home = tmp_path / ".hermes" + empty_home.mkdir() + with patch.dict(os.environ, {"HERMES_HOME": str(empty_home)}, clear=False), \ + patch.object(Path, "home", staticmethod(lambda: tmp_path)): result = resolve_config_path() assert result == GLOBAL_CONFIG_PATH diff --git a/tests/honcho_integration/test_config_isolation.py b/tests/honcho_integration/test_config_isolation.py index 4d9898e681d4..96e8141b342c 100644 --- a/tests/honcho_integration/test_config_isolation.py +++ b/tests/honcho_integration/test_config_isolation.py @@ -92,7 +92,7 @@ def test_explicit_path_override_still_works(self, isolated_home): class TestReadConfigFallback: - """_read_config falls back to global when no local file exists.""" + """_read_config falls back through the config chain.""" def test_reads_local_when_exists(self, isolated_home): isolated_home["local_config"].write_text( @@ -121,6 +121,69 @@ def test_local_takes_priority_over_global(self, isolated_home): assert cfg["source"] == "local" +class TestDefaultProfileFallback: + """Non-default profiles fall back to ~/.hermes/honcho.json (default profile config).""" + + def test_profile_reads_default_profile_config(self, tmp_path, monkeypatch): + """A non-default profile with no local honcho.json reads ~/.hermes/honcho.json.""" + home = tmp_path / "home" + default_hermes = home / ".hermes" + default_hermes.mkdir(parents=True) + profile_hermes = home / ".hermes" / "profiles" / "coder" + profile_hermes.mkdir(parents=True) + global_dir = home / ".honcho" + global_dir.mkdir(parents=True) + + monkeypatch.setattr(Path, "home", staticmethod(lambda: home)) + + # Default profile has honcho.json with host blocks + default_config = default_hermes / "honcho.json" + default_config.write_text(json.dumps({ + "apiKey": "key", + "hosts": { + "hermes": {"peerName": "alice"}, + "hermes.coder": {"peerName": "alice", "aiPeer": "hermes.coder"}, + }, + })) + + # Global config has different data + (global_dir / "config.json").write_text(json.dumps({"source": "global"})) + + import honcho_integration.client as _client_mod + import honcho_integration.cli as _cli_mod + monkeypatch.setattr(_client_mod, "GLOBAL_CONFIG_PATH", global_dir / "config.json") + monkeypatch.setattr(_cli_mod, "GLOBAL_CONFIG_PATH", global_dir / "config.json") + + # Profile's HERMES_HOME points to its own dir (no honcho.json) + monkeypatch.setenv("HERMES_HOME", str(profile_hermes)) + assert not (profile_hermes / "honcho.json").exists() + + # Should find ~/.hermes/honcho.json, not fall through to global + cfg = _read_config() + assert cfg.get("apiKey") == "key" + assert "hermes.coder" in cfg.get("hosts", {}) + + def test_default_profile_skips_self(self, tmp_path, monkeypatch): + """Default profile doesn't double-read its own config.""" + home = tmp_path / "home" + default_hermes = home / ".hermes" + default_hermes.mkdir(parents=True) + + monkeypatch.setattr(Path, "home", staticmethod(lambda: home)) + monkeypatch.setenv("HERMES_HOME", str(default_hermes)) + + # No honcho.json anywhere + import honcho_integration.client as _client_mod + import honcho_integration.cli as _cli_mod + global_cfg = home / ".honcho" / "config.json" + global_cfg.parent.mkdir(parents=True) + monkeypatch.setattr(_client_mod, "GLOBAL_CONFIG_PATH", global_cfg) + monkeypatch.setattr(_cli_mod, "GLOBAL_CONFIG_PATH", global_cfg) + + cfg = _read_config() + assert cfg == {} + + class TestMultiProfileIsolation: """Two profiles writing config don't interfere with each other.""" From 9ef1d13937d68e6f99939dae5dde309e256b20ac Mon Sep 17 00:00:00 2001 From: Erosika Date: Thu, 2 Apr 2026 13:41:23 -0400 Subject: [PATCH 2/9] fix(honcho): pass host key to from_global_config in CLI commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cmd_status, cmd_setup, and cmd_identity called from_global_config() without host=_host_key(), so --target-profile was ignored — they always resolved the OS-level active profile instead of the overridden one. hermes honcho --target-profile dreamer status showed Host: hermes instead of Host: hermes.dreamer. --- honcho_integration/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/honcho_integration/cli.py b/honcho_integration/cli.py index 51f686dea7e7..1aac7868ef79 100644 --- a/honcho_integration/cli.py +++ b/honcho_integration/cli.py @@ -422,7 +422,7 @@ def cmd_setup(args) -> None: try: from honcho_integration.client import HonchoClientConfig, get_honcho_client, reset_honcho_client reset_honcho_client() - hcfg = HonchoClientConfig.from_global_config() + hcfg = HonchoClientConfig.from_global_config(host=_host_key()) get_honcho_client(hcfg) print("OK") except Exception as e: @@ -517,7 +517,7 @@ def cmd_status(args) -> None: try: from honcho_integration.client import HonchoClientConfig, get_honcho_client - hcfg = HonchoClientConfig.from_global_config() + hcfg = HonchoClientConfig.from_global_config(host=_host_key()) except Exception as e: print(f" Config error: {e}\n") return @@ -836,7 +836,7 @@ def cmd_identity(args) -> None: try: from honcho_integration.client import HonchoClientConfig, get_honcho_client from honcho_integration.session import HonchoSessionManager - hcfg = HonchoClientConfig.from_global_config() + hcfg = HonchoClientConfig.from_global_config(host=_host_key()) client = get_honcho_client(hcfg) mgr = HonchoSessionManager(honcho=client, config=hcfg) session_key = hcfg.resolve_session_name() From 5c981b284d34a1c508f2f158513a3c7cdd6480fb Mon Sep 17 00:00:00 2001 From: Erosika Date: Thu, 2 Apr 2026 13:41:36 -0400 Subject: [PATCH 3/9] test(honcho): add 39 tests for profile capabilities Covers cmd_enable, cmd_disable, cmd_sync, cmd_peers, _host_key, _all_profile_host_configs, --target-profile routing, clone edge cases, sync_honcho_profiles_quiet edge cases, honcho_command routing, and a full enable/disable/enable integration cycle. --- .../test_profile_capabilities.py | 700 ++++++++++++++++++ 1 file changed, 700 insertions(+) create mode 100644 tests/honcho_integration/test_profile_capabilities.py diff --git a/tests/honcho_integration/test_profile_capabilities.py b/tests/honcho_integration/test_profile_capabilities.py new file mode 100644 index 000000000000..a5ea06e4f6f1 --- /dev/null +++ b/tests/honcho_integration/test_profile_capabilities.py @@ -0,0 +1,700 @@ +"""Tests for Honcho profile capabilities (PR #4616). + +Covers: clone_honcho_for_profile, cmd_enable, cmd_disable, cmd_sync, +cmd_status --all, cmd_peers, _host_key, _all_profile_host_configs, +--target-profile flag, and honcho_command routing. +""" + +import json +import os +from io import StringIO +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import patch, MagicMock + +import pytest + +from honcho_integration.cli import ( + _host_key, + _all_profile_host_configs, + _read_config, + _write_config, + clone_honcho_for_profile, + cmd_enable, + cmd_disable, + cmd_sync, + cmd_peers, + honcho_command, + sync_honcho_profiles_quiet, +) + + +# ── Fixtures ──────────────────────────────────────────────────────────────── + + +class FakeProfile: + def __init__(self, name): + self.name = name + self.is_default = name == "default" + + +@pytest.fixture +def honcho_env(tmp_path, monkeypatch): + """Isolated Honcho config environment for testing.""" + hermes_home = tmp_path / "hermes_home" + hermes_home.mkdir() + config_file = hermes_home / "honcho.json" + + monkeypatch.setenv("HERMES_HOME", str(hermes_home)) + + # Patch _config_path and _local_config_path to use our temp file + monkeypatch.setattr( + "honcho_integration.cli._config_path", lambda: config_file + ) + monkeypatch.setattr( + "honcho_integration.cli._local_config_path", lambda: config_file + ) + # Prevent real peer creation + monkeypatch.setattr( + "honcho_integration.cli._ensure_peer_exists", lambda host_key=None: True + ) + + return {"config_file": config_file, "hermes_home": hermes_home} + + +def _write_cfg(path, data): + path.write_text(json.dumps(data, indent=2)) + + +def _read_cfg(path): + return json.loads(path.read_text()) + + +# ── _host_key() ───────────────────────────────────────────────────────────── + + +class TestHostKey: + def test_default_profile_returns_hermes(self, monkeypatch): + import honcho_integration.cli as mod + monkeypatch.setattr(mod, "_profile_override", None) + with patch("honcho_integration.cli.resolve_active_host", return_value="hermes"): + assert _host_key() == "hermes" + + def test_profile_override_default_returns_base_host(self, monkeypatch): + import honcho_integration.cli as mod + monkeypatch.setattr(mod, "_profile_override", "default") + assert _host_key() == "hermes" + + def test_profile_override_custom_returns_base_host(self, monkeypatch): + import honcho_integration.cli as mod + monkeypatch.setattr(mod, "_profile_override", "custom") + assert _host_key() == "hermes" + + def test_profile_override_named_returns_scoped(self, monkeypatch): + import honcho_integration.cli as mod + monkeypatch.setattr(mod, "_profile_override", "coder") + assert _host_key() == "hermes.coder" + + def test_no_override_delegates_to_resolve(self, monkeypatch): + import honcho_integration.cli as mod + monkeypatch.setattr(mod, "_profile_override", None) + with patch("honcho_integration.cli.resolve_active_host", return_value="hermes.dreamer"): + assert _host_key() == "hermes.dreamer" + + +# ── _all_profile_host_configs() ───────────────────────────────────────────── + + +class TestAllProfileHostConfigs: + def test_returns_default_and_named_profiles(self, honcho_env): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": { + "hermes": {"peerName": "alice"}, + "hermes.coder": {"peerName": "alice-code"}, + }, + }) + + profiles = [FakeProfile("default"), FakeProfile("coder")] + with patch("hermes_cli.profiles.list_profiles", return_value=profiles), \ + patch("honcho_integration.cli._active_profile_name", return_value="default"): + rows = _all_profile_host_configs() + + assert len(rows) == 2 + names = [r[0] for r in rows] + assert "default" in names + assert "coder" in names + + # Default profile should have its host block + default_row = [r for r in rows if r[0] == "default"][0] + assert default_row[1] == "hermes" + assert default_row[2].get("peerName") == "alice" + + # Coder profile gets hermes.coder + coder_row = [r for r in rows if r[0] == "coder"][0] + assert coder_row[1] == "hermes.coder" + assert coder_row[2].get("peerName") == "alice-code" + + def test_missing_host_block_returns_empty_dict(self, honcho_env): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes": {"peerName": "alice"}}, + }) + + profiles = [FakeProfile("default"), FakeProfile("newprofile")] + with patch("hermes_cli.profiles.list_profiles", return_value=profiles), \ + patch("honcho_integration.cli._active_profile_name", return_value="default"): + rows = _all_profile_host_configs() + + new_row = [r for r in rows if r[0] == "newprofile"][0] + assert new_row[1] == "hermes.newprofile" + assert new_row[2] == {} # no host block yet + + def test_profiles_import_failure_returns_active_only(self, honcho_env): + _write_cfg(honcho_env["config_file"], {"apiKey": "key"}) + + with patch("honcho_integration.cli._active_profile_name", return_value="default"), \ + patch("hermes_cli.profiles.list_profiles", side_effect=ImportError("nope")): + rows = _all_profile_host_configs() + + assert len(rows) == 1 + assert rows[0][0] == "default" + + +# ── cmd_enable ────────────────────────────────────────────────────────────── + + +class TestCmdEnable: + def test_enables_existing_profile(self, honcho_env, capsys, monkeypatch): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes.coder": {"enabled": False, "aiPeer": "hermes.coder"}}, + }) + monkeypatch.setattr("honcho_integration.cli._host_key", lambda: "hermes.coder") + + cmd_enable(SimpleNamespace()) + + cfg = _read_cfg(honcho_env["config_file"]) + assert cfg["hosts"]["hermes.coder"]["enabled"] is True + output = capsys.readouterr().out + assert "enabled" in output.lower() + + def test_already_enabled_prints_message(self, honcho_env, capsys, monkeypatch): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes": {"enabled": True, "aiPeer": "hermes"}}, + }) + monkeypatch.setattr("honcho_integration.cli._host_key", lambda: "hermes") + + cmd_enable(SimpleNamespace()) + + output = capsys.readouterr().out + assert "already enabled" in output.lower() + + def test_enable_new_profile_clones_from_default(self, honcho_env, capsys, monkeypatch): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": { + "hermes": { + "memoryMode": "honcho", + "recallMode": "tools", + "peerName": "alice", + "workspace": "shared", + }, + }, + }) + monkeypatch.setattr("honcho_integration.cli._host_key", lambda: "hermes.writer") + + cmd_enable(SimpleNamespace()) + + cfg = _read_cfg(honcho_env["config_file"]) + block = cfg["hosts"]["hermes.writer"] + assert block["enabled"] is True + assert block["memoryMode"] == "honcho" + assert block["recallMode"] == "tools" + assert block["peerName"] == "alice" + assert block["aiPeer"] == "hermes.writer" + assert block["workspace"] == "shared" + + +# ── cmd_disable ───────────────────────────────────────────────────────────── + + +class TestCmdDisable: + def test_disables_profile(self, honcho_env, capsys, monkeypatch): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes.coder": {"enabled": True}}, + }) + monkeypatch.setattr("honcho_integration.cli._host_key", lambda: "hermes.coder") + + cmd_disable(SimpleNamespace()) + + cfg = _read_cfg(honcho_env["config_file"]) + assert cfg["hosts"]["hermes.coder"]["enabled"] is False + + def test_already_disabled_prints_message(self, honcho_env, capsys, monkeypatch): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes": {"enabled": False}}, + }) + monkeypatch.setattr("honcho_integration.cli._host_key", lambda: "hermes") + + cmd_disable(SimpleNamespace()) + + output = capsys.readouterr().out + assert "already disabled" in output.lower() + + def test_disable_nonexistent_block_prints_message(self, honcho_env, capsys, monkeypatch): + _write_cfg(honcho_env["config_file"], {"apiKey": "key", "hosts": {}}) + monkeypatch.setattr("honcho_integration.cli._host_key", lambda: "hermes.ghost") + + cmd_disable(SimpleNamespace()) + + output = capsys.readouterr().out + assert "already disabled" in output.lower() + + +# ── cmd_sync ──────────────────────────────────────────────────────────────── + + +class TestCmdSync: + def test_syncs_new_profiles(self, honcho_env, capsys): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes": {"peerName": "alice", "memoryMode": "honcho"}}, + }) + + profiles = [FakeProfile("default"), FakeProfile("coder"), FakeProfile("dreamer")] + with patch("hermes_cli.profiles.list_profiles", return_value=profiles): + cmd_sync(SimpleNamespace()) + + cfg = _read_cfg(honcho_env["config_file"]) + assert "hermes.coder" in cfg["hosts"] + assert "hermes.dreamer" in cfg["hosts"] + + output = capsys.readouterr().out + assert "coder" in output + assert "dreamer" in output + assert "2 profile(s) synced" in output + + def test_sync_skips_already_configured(self, honcho_env, capsys): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": { + "hermes": {"peerName": "alice"}, + "hermes.coder": {"peerName": "existing"}, + }, + }) + + profiles = [FakeProfile("default"), FakeProfile("coder")] + with patch("hermes_cli.profiles.list_profiles", return_value=profiles): + cmd_sync(SimpleNamespace()) + + output = capsys.readouterr().out + assert "All profiles already have Honcho config" in output + + def test_sync_with_no_config_prints_error(self, honcho_env, capsys): + _write_cfg(honcho_env["config_file"], {}) + + cmd_sync(SimpleNamespace()) + + output = capsys.readouterr().out + assert "No Honcho config found" in output + + def test_sync_with_no_default_block_but_api_key_works(self, honcho_env, capsys): + _write_cfg(honcho_env["config_file"], {"apiKey": "key"}) + + profiles = [FakeProfile("default"), FakeProfile("writer")] + with patch("hermes_cli.profiles.list_profiles", return_value=profiles): + cmd_sync(SimpleNamespace()) + + cfg = _read_cfg(honcho_env["config_file"]) + assert "hermes.writer" in cfg["hosts"] + + +# ── cmd_peers ─────────────────────────────────────────────────────────────── + + +class TestCmdPeers: + def test_shows_all_profile_peers(self, honcho_env, capsys): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": { + "hermes": {"peerName": "alice", "aiPeer": "hermes"}, + "hermes.coder": {"peerName": "alice-code", "aiPeer": "hermes.coder"}, + }, + }) + + profiles = [FakeProfile("default"), FakeProfile("coder")] + with patch("hermes_cli.profiles.list_profiles", return_value=profiles), \ + patch("honcho_integration.cli._active_profile_name", return_value="default"): + cmd_peers(SimpleNamespace()) + + output = capsys.readouterr().out + assert "alice" in output + assert "hermes.coder" in output + assert "alice-code" in output + + def test_shows_not_set_for_missing_peers(self, honcho_env, capsys): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes": {}}, + }) + + profiles = [FakeProfile("default")] + with patch("hermes_cli.profiles.list_profiles", return_value=profiles), \ + patch("honcho_integration.cli._active_profile_name", return_value="default"): + cmd_peers(SimpleNamespace()) + + output = capsys.readouterr().out + assert "(not set)" in output + + +# ── --target-profile routing ──────────────────────────────────────────────── + + +class TestTargetProfile: + def test_target_profile_sets_override(self, honcho_env, monkeypatch): + """--target-profile should route to the specified profile's host block.""" + import honcho_integration.cli as mod + + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": { + "hermes": {"enabled": True}, + "hermes.coder": {"enabled": True, "aiPeer": "hermes.coder"}, + }, + }) + + # Simulate: hermes honcho --target-profile coder disable + args = SimpleNamespace(target_profile="coder", honcho_command="disable") + + honcho_command(args) + + cfg = _read_cfg(honcho_env["config_file"]) + assert cfg["hosts"]["hermes.coder"]["enabled"] is False + # Default should be untouched + assert cfg["hosts"]["hermes"]["enabled"] is True + + # Clean up global state + mod._profile_override = None + + def test_target_profile_enable_for_new_profile(self, honcho_env, monkeypatch): + """Enable with --target-profile on a profile that has no block yet.""" + import honcho_integration.cli as mod + + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": { + "hermes": {"peerName": "alice", "memoryMode": "hybrid"}, + }, + }) + + args = SimpleNamespace(target_profile="dreamer", honcho_command="enable") + honcho_command(args) + + cfg = _read_cfg(honcho_env["config_file"]) + assert "hermes.dreamer" in cfg["hosts"] + assert cfg["hosts"]["hermes.dreamer"]["enabled"] is True + assert cfg["hosts"]["hermes.dreamer"]["aiPeer"] == "hermes.dreamer" + + mod._profile_override = None + + def test_target_profile_default_maps_to_base_host(self, honcho_env, monkeypatch): + """--target-profile default should use 'hermes' as the host key.""" + import honcho_integration.cli as mod + + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes": {"enabled": True}}, + }) + + args = SimpleNamespace(target_profile="default", honcho_command="disable") + honcho_command(args) + + cfg = _read_cfg(honcho_env["config_file"]) + assert cfg["hosts"]["hermes"]["enabled"] is False + + mod._profile_override = None + + +# ── clone_honcho_for_profile edge cases ───────────────────────────────────── + + +class TestCloneEdgeCases: + def test_clone_calls_ensure_peer_exists(self, honcho_env, monkeypatch): + """Cloning should eagerly create the peer.""" + calls = [] + monkeypatch.setattr( + "honcho_integration.cli._ensure_peer_exists", + lambda host_key=None: (calls.append(host_key), True)[1], + ) + + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes": {"peerName": "alice"}}, + }) + + clone_honcho_for_profile("coder") + assert "hermes.coder" in calls + + def test_clone_shares_workspace_not_profile_derived(self, honcho_env): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes": {"workspace": "team-workspace"}}, + }) + + clone_honcho_for_profile("analyst") + + cfg = _read_cfg(honcho_env["config_file"]) + assert cfg["hosts"]["hermes.analyst"]["workspace"] == "team-workspace" + + def test_clone_uses_hermes_as_default_workspace(self, honcho_env): + """When no workspace is set anywhere, defaults to 'hermes'.""" + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes": {}}, + }) + + clone_honcho_for_profile("tester") + + cfg = _read_cfg(honcho_env["config_file"]) + assert cfg["hosts"]["hermes.tester"]["workspace"] == "hermes" + + def test_clone_inherits_enabled_state(self, honcho_env): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes": {"enabled": False, "peerName": "alice"}}, + }) + + clone_honcho_for_profile("quiet") + + cfg = _read_cfg(honcho_env["config_file"]) + assert cfg["hosts"]["hermes.quiet"]["enabled"] is False + + def test_clone_copies_dialectic_reasoning_level(self, honcho_env): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes": {"dialecticReasoningLevel": "high"}}, + }) + + clone_honcho_for_profile("thinker") + + cfg = _read_cfg(honcho_env["config_file"]) + assert cfg["hosts"]["hermes.thinker"]["dialecticReasoningLevel"] == "high" + + def test_clone_does_not_copy_unknown_keys(self, honcho_env): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes": {"customField": "should-not-copy", "peerName": "alice"}}, + }) + + clone_honcho_for_profile("strict") + + cfg = _read_cfg(honcho_env["config_file"]) + assert "customField" not in cfg["hosts"]["hermes.strict"] + assert cfg["hosts"]["hermes.strict"]["peerName"] == "alice" + + +# ── sync_honcho_profiles_quiet edge cases ─────────────────────────────────── + + +class TestSyncQuietEdgeCases: + def test_sync_quiet_no_profiles_module(self, honcho_env, monkeypatch): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes": {}}, + }) + with patch("hermes_cli.profiles.list_profiles", side_effect=ImportError): + count = sync_honcho_profiles_quiet() + assert count == 0 + + def test_sync_quiet_no_api_key_no_default_block(self, honcho_env, monkeypatch): + _write_cfg(honcho_env["config_file"], {"hosts": {}}) + monkeypatch.delenv("HONCHO_API_KEY", raising=False) + + count = sync_honcho_profiles_quiet() + assert count == 0 + + def test_sync_quiet_with_env_api_key(self, honcho_env, monkeypatch): + """Should sync even when apiKey is in env, not config.""" + _write_cfg(honcho_env["config_file"], { + "hosts": {"hermes": {"peerName": "alice"}}, + }) + monkeypatch.setenv("HONCHO_API_KEY", "env-key") + + profiles = [FakeProfile("default"), FakeProfile("envprofile")] + with patch("hermes_cli.profiles.list_profiles", return_value=profiles): + count = sync_honcho_profiles_quiet() + + assert count == 1 + cfg = _read_cfg(honcho_env["config_file"]) + assert "hermes.envprofile" in cfg["hosts"] + + +# ── honcho_command routing ────────────────────────────────────────────────── + + +class TestHonchoCommandRouting: + def test_routes_enable(self, honcho_env, monkeypatch, capsys): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes": {"enabled": False, "aiPeer": "hermes"}}, + }) + monkeypatch.setattr("honcho_integration.cli._host_key", lambda: "hermes") + + import honcho_integration.cli as mod + args = SimpleNamespace(target_profile=None, honcho_command="enable") + honcho_command(args) + mod._profile_override = None + + cfg = _read_cfg(honcho_env["config_file"]) + assert cfg["hosts"]["hermes"]["enabled"] is True + + def test_routes_disable(self, honcho_env, monkeypatch, capsys): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes": {"enabled": True}}, + }) + monkeypatch.setattr("honcho_integration.cli._host_key", lambda: "hermes") + + import honcho_integration.cli as mod + args = SimpleNamespace(target_profile=None, honcho_command="disable") + honcho_command(args) + mod._profile_override = None + + cfg = _read_cfg(honcho_env["config_file"]) + assert cfg["hosts"]["hermes"]["enabled"] is False + + def test_routes_sync(self, honcho_env, capsys): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes": {"peerName": "alice"}}, + }) + + profiles = [FakeProfile("default"), FakeProfile("coder")] + import honcho_integration.cli as mod + with patch("hermes_cli.profiles.list_profiles", return_value=profiles): + args = SimpleNamespace(target_profile=None, honcho_command="sync") + honcho_command(args) + mod._profile_override = None + + cfg = _read_cfg(honcho_env["config_file"]) + assert "hermes.coder" in cfg["hosts"] + + def test_routes_peers(self, honcho_env, capsys): + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": {"hermes": {"peerName": "alice"}}, + }) + + profiles = [FakeProfile("default")] + import honcho_integration.cli as mod + with patch("hermes_cli.profiles.list_profiles", return_value=profiles), \ + patch("honcho_integration.cli._active_profile_name", return_value="default"): + args = SimpleNamespace(target_profile=None, honcho_command="peers") + honcho_command(args) + mod._profile_override = None + + output = capsys.readouterr().out + assert "alice" in output + + def test_unknown_command_prints_error(self, honcho_env, capsys): + import honcho_integration.cli as mod + args = SimpleNamespace(target_profile=None, honcho_command="foobar") + honcho_command(args) + mod._profile_override = None + + output = capsys.readouterr().out + assert "Unknown honcho command" in output + + +# ── Integration: full enable→disable→enable cycle ────────────────────────── + + +# ── cmd_status with --target-profile ──────────────────────────────────────── + + +class TestCmdStatusTargetProfile: + """Verify cmd_status passes _host_key() to from_global_config so + --target-profile actually shows the targeted profile's config.""" + + def test_status_uses_host_key_for_config(self, honcho_env, monkeypatch, capsys): + """from_global_config must receive the overridden host, not the + default active host.""" + import honcho_integration.cli as mod + + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": { + "hermes": {"peerName": "alice", "aiPeer": "hermes", "enabled": True}, + "hermes.dreamer": {"peerName": "alice", "aiPeer": "dreamer", "enabled": True}, + }, + }) + + # Track what host= value gets passed to from_global_config + captured_hosts = [] + original_from_global = None + + from honcho_integration.client import HonchoClientConfig + original_from_global = HonchoClientConfig.from_global_config + + def spy_from_global_config(**kwargs): + captured_hosts.append(kwargs.get("host")) + # Return a minimal config that won't trigger connection + return HonchoClientConfig( + host=kwargs.get("host", "hermes"), + ai_peer=kwargs.get("host", "hermes"), + enabled=False, # skip connection attempt + ) + + monkeypatch.setattr(HonchoClientConfig, "from_global_config", + staticmethod(spy_from_global_config)) + + # Simulate --target-profile dreamer + mod._profile_override = "dreamer" + try: + cmd_status_args = SimpleNamespace(all=False) + # Import honcho at module scope so the import check passes + monkeypatch.setitem(__import__("sys").modules, "honcho", MagicMock()) + from honcho_integration.cli import cmd_status + cmd_status(cmd_status_args) + finally: + mod._profile_override = None + + # The critical assertion: from_global_config was called with host="hermes.dreamer" + assert "hermes.dreamer" in captured_hosts + + +# ── Integration: full enable→disable→enable cycle ────────────────────────── + + +class TestEnableDisableCycle: + def test_full_cycle(self, honcho_env, monkeypatch, capsys): + """Enable, disable, then re-enable a profile and verify state.""" + _write_cfg(honcho_env["config_file"], { + "apiKey": "key", + "hosts": { + "hermes": {"peerName": "alice", "memoryMode": "hybrid"}, + }, + }) + monkeypatch.setattr("honcho_integration.cli._host_key", lambda: "hermes.tester") + + # Enable (should create block) + cmd_enable(SimpleNamespace()) + cfg = _read_cfg(honcho_env["config_file"]) + assert cfg["hosts"]["hermes.tester"]["enabled"] is True + assert cfg["hosts"]["hermes.tester"]["aiPeer"] == "hermes.tester" + assert cfg["hosts"]["hermes.tester"]["memoryMode"] == "hybrid" + + # Disable + cmd_disable(SimpleNamespace()) + cfg = _read_cfg(honcho_env["config_file"]) + assert cfg["hosts"]["hermes.tester"]["enabled"] is False + + # Re-enable (block already exists with aiPeer, should just flip) + capsys.readouterr() # clear + cmd_enable(SimpleNamespace()) + cfg = _read_cfg(honcho_env["config_file"]) + assert cfg["hosts"]["hermes.tester"]["enabled"] is True From 564c42f39ad8ff77f5cf95dba2730b3bdcca0ad0 Mon Sep 17 00:00:00 2001 From: Erosika Date: Thu, 2 Apr 2026 13:49:07 -0400 Subject: [PATCH 4/9] fix(honcho): make add_peers non-fatal during session init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit session.add_peers() was outside the try/except in _get_or_create_honcho_session, so a server-side 'Session not found' on a new AI peer's first access propagated up to run_agent.py and set self._honcho = None — killing message uploads for the entire session. Wrap add_peers in its own try/except so the session still gets cached and messages still get uploaded even if peer configuration fails on first contact. --- honcho_integration/session.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/honcho_integration/session.py b/honcho_integration/session.py index 23b96d1cb196..25694910c126 100644 --- a/honcho_integration/session.py +++ b/honcho_integration/session.py @@ -162,11 +162,17 @@ def _get_or_create_honcho_session( # Configure peer observation settings. # observe_me=True for AI peer so Honcho watches what the agent says # and builds its representation over time — enabling identity formation. - from honcho.session import SessionPeerConfig - user_config = SessionPeerConfig(observe_me=True, observe_others=True) - ai_config = SessionPeerConfig(observe_me=True, observe_others=True) + try: + from honcho.session import SessionPeerConfig + user_config = SessionPeerConfig(observe_me=True, observe_others=True) + ai_config = SessionPeerConfig(observe_me=True, observe_others=True) - session.add_peers([(user_peer, user_config), (assistant_peer, ai_config)]) + session.add_peers([(user_peer, user_config), (assistant_peer, ai_config)]) + except Exception as e: + logger.warning( + "Honcho session '%s' add_peers failed (non-fatal): %s", + session_id, e, + ) # Load existing messages via context() - single call for messages + metadata existing_messages = [] From 7c33e0d38a25f9c06ea7af5e67e201b21926b424 Mon Sep 17 00:00:00 2001 From: Erosika Date: Thu, 2 Apr 2026 13:55:40 -0400 Subject: [PATCH 5/9] fix(honcho): eagerly create session to prevent 'Session not found' on new peers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit honcho.session(id) is lazy — it creates a local Session object without making an API call. Subsequent add_peers() and context() calls fail with 'Session not found' for new AI peers that have never used the session before, because the session doesn't exist server-side yet. This caused the fatal 'Honcho init failed: Session not found' error that set self._honcho = None, killing all message uploads for the entire chat session. New profiles appeared to work (config loaded, context injected from shared workspace) but no messages were ever sent to Honcho. Fix: pass metadata={} to session() to trigger immediate get-or-create on the server. The SDK explicitly documents this behavior: 'This method does not make an API call unless configuration or metadata is provided.' Also keep add_peers() wrapped in try/except as defense-in-depth. --- honcho_integration/session.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/honcho_integration/session.py b/honcho_integration/session.py index 25694910c126..13893052d311 100644 --- a/honcho_integration/session.py +++ b/honcho_integration/session.py @@ -157,7 +157,11 @@ def _get_or_create_honcho_session( logger.debug("Honcho session '%s' retrieved from cache", session_id) return self._sessions_cache[session_id], [] - session = self.honcho.session(session_id) + # Eagerly create the session on the server by passing metadata={}. + # Without this, session() is lazy and add_peers / context calls fail + # with "Session not found" for new AI peers that have never used this + # session before. + session = self.honcho.session(session_id, metadata={}) # Configure peer observation settings. # observe_me=True for AI peer so Honcho watches what the agent says From d7912a1f56c14ec750437c216ab4eca993c3e931 Mon Sep 17 00:00:00 2001 From: Erosika Date: Thu, 2 Apr 2026 14:02:29 -0400 Subject: [PATCH 6/9] fix(honcho): sanitize AI peer ID before passing to Honcho API assistant_peer_id was passed raw from config (e.g. 'hermes.design-researcher') without sanitization. The Honcho API requires IDs to match ^[a-zA-Z0-9_-]+$ so the dot caused a validation error that killed session init. User peer and session ID were already sanitized via _sanitize_id(). Apply the same sanitization to assistant_peer_id so 'hermes.design-researcher' becomes 'hermes-design-researcher'. --- honcho_integration/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/honcho_integration/session.py b/honcho_integration/session.py index 13893052d311..6184f81b106d 100644 --- a/honcho_integration/session.py +++ b/honcho_integration/session.py @@ -241,7 +241,7 @@ def get_or_create(self, key: str) -> HonchoSession: chat_id = parts[1] if len(parts) > 1 else key user_peer_id = self._sanitize_id(f"user-{channel}-{chat_id}") - assistant_peer_id = ( + assistant_peer_id = self._sanitize_id( self._config.ai_peer if self._config else "hermes-assistant" ) From 7e6b2050585b250bc433cd06097adb581e50dfb6 Mon Sep 17 00:00:00 2001 From: Erosika Date: Thu, 2 Apr 2026 14:04:04 -0400 Subject: [PATCH 7/9] fix(honcho): use bare profile name as AI peer, not host key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clone_honcho_for_profile set aiPeer to the host key ('hermes.design-researcher') instead of the bare profile name ('design-researcher'). The dot in the host key violates Honcho's ID pattern (^[a-zA-Z0-9_-]+$). Also fix cmd_enable which had the same issue when creating new host blocks. The host key (hermes.) is for config resolution only. The AI peer name is the identity in Honcho — should be clean. --- honcho_integration/cli.py | 7 +++++-- tests/honcho_integration/test_cli.py | 4 ++-- tests/honcho_integration/test_profile_capabilities.py | 6 +++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/honcho_integration/cli.py b/honcho_integration/cli.py index 1aac7868ef79..36f06d727a3e 100644 --- a/honcho_integration/cli.py +++ b/honcho_integration/cli.py @@ -55,7 +55,8 @@ def clone_honcho_for_profile(profile_name: str) -> bool: # AI peer is profile-specific; workspace is shared so all profiles # see the same user context, sessions, and project history. - new_block["aiPeer"] = new_host + # Use the bare profile name as the peer identity (not the host key). + new_block["aiPeer"] = profile_name new_block["workspace"] = default_block.get("workspace") or cfg.get("workspace") or HOST new_block["enabled"] = default_block.get("enabled", True) @@ -112,7 +113,9 @@ def cmd_enable(args) -> None: peer_name = default_block.get("peerName") or cfg.get("peerName") if peer_name and "peerName" not in block: block["peerName"] = peer_name - block.setdefault("aiPeer", host) + # Use bare profile name as AI peer, not the host key + ai_peer = host.split(".", 1)[1] if "." in host else host + block.setdefault("aiPeer", ai_peer) block.setdefault("workspace", default_block.get("workspace") or cfg.get("workspace") or HOST) _write_config(cfg) diff --git a/tests/honcho_integration/test_cli.py b/tests/honcho_integration/test_cli.py index ed4337061dcb..0c17f9ab4aea 100644 --- a/tests/honcho_integration/test_cli.py +++ b/tests/honcho_integration/test_cli.py @@ -60,7 +60,7 @@ def test_clones_default_settings_to_new_profile(self, tmp_path): assert new_block["memoryMode"] == "honcho" assert new_block["recallMode"] == "tools" assert new_block["writeFrequency"] == "turn" - assert new_block["aiPeer"] == "hermes.coder" + assert new_block["aiPeer"] == "coder" assert new_block["workspace"] == "hermes" # shared, not profile-derived assert new_block["enabled"] is True @@ -117,7 +117,7 @@ def test_works_with_api_key_only_no_host_block(self, tmp_path): assert result is True cfg = json.loads(config_file.read_text()) - assert cfg["hosts"]["hermes.coder"]["aiPeer"] == "hermes.coder" + assert cfg["hosts"]["hermes.coder"]["aiPeer"] == "coder" assert cfg["hosts"]["hermes.coder"]["workspace"] == "hermes" # shared diff --git a/tests/honcho_integration/test_profile_capabilities.py b/tests/honcho_integration/test_profile_capabilities.py index a5ea06e4f6f1..16b3d16e70cd 100644 --- a/tests/honcho_integration/test_profile_capabilities.py +++ b/tests/honcho_integration/test_profile_capabilities.py @@ -213,7 +213,7 @@ def test_enable_new_profile_clones_from_default(self, honcho_env, capsys, monkey assert block["memoryMode"] == "honcho" assert block["recallMode"] == "tools" assert block["peerName"] == "alice" - assert block["aiPeer"] == "hermes.writer" + assert block["aiPeer"] == "writer" assert block["workspace"] == "shared" @@ -397,7 +397,7 @@ def test_target_profile_enable_for_new_profile(self, honcho_env, monkeypatch): cfg = _read_cfg(honcho_env["config_file"]) assert "hermes.dreamer" in cfg["hosts"] assert cfg["hosts"]["hermes.dreamer"]["enabled"] is True - assert cfg["hosts"]["hermes.dreamer"]["aiPeer"] == "hermes.dreamer" + assert cfg["hosts"]["hermes.dreamer"]["aiPeer"] == "dreamer" mod._profile_override = None @@ -685,7 +685,7 @@ def test_full_cycle(self, honcho_env, monkeypatch, capsys): cmd_enable(SimpleNamespace()) cfg = _read_cfg(honcho_env["config_file"]) assert cfg["hosts"]["hermes.tester"]["enabled"] is True - assert cfg["hosts"]["hermes.tester"]["aiPeer"] == "hermes.tester" + assert cfg["hosts"]["hermes.tester"]["aiPeer"] == "tester" assert cfg["hosts"]["hermes.tester"]["memoryMode"] == "hybrid" # Disable From 70d6ea6825247270146606830a934e7b53ba9551 Mon Sep 17 00:00:00 2001 From: Erosika Date: Thu, 2 Apr 2026 14:13:32 -0400 Subject: [PATCH 8/9] docs: add profiles guide with Honcho integration walkthrough --- honcho_integration/session.py | 8 ++- website/docs/user-guide/features/honcho.md | 59 ++++++++++++++++++++++ website/docs/user-guide/profiles.md | 6 +++ 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/honcho_integration/session.py b/honcho_integration/session.py index 6184f81b106d..b86ceae0ad22 100644 --- a/honcho_integration/session.py +++ b/honcho_integration/session.py @@ -157,11 +157,9 @@ def _get_or_create_honcho_session( logger.debug("Honcho session '%s' retrieved from cache", session_id) return self._sessions_cache[session_id], [] - # Eagerly create the session on the server by passing metadata={}. - # Without this, session() is lazy and add_peers / context calls fail - # with "Session not found" for new AI peers that have never used this - # session before. - session = self.honcho.session(session_id, metadata={}) + # honcho.session() makes a get-or-create API call on the server. + # This ensures the session exists before add_peers / context calls. + session = self.honcho.session(session_id) # Configure peer observation settings. # observe_me=True for AI peer so Honcho watches what the agent says diff --git a/website/docs/user-guide/features/honcho.md b/website/docs/user-guide/features/honcho.md index 4adb015c2c3c..065d496e1aa4 100644 --- a/website/docs/user-guide/features/honcho.md +++ b/website/docs/user-guide/features/honcho.md @@ -352,6 +352,12 @@ hermes honcho tokens --dialectic N # Set dialectic char cap hermes honcho identity # Show AI peer identity hermes honcho identity # Seed AI peer identity from file (SOUL.md, etc.) hermes honcho migrate # Migration guide: OpenClaw → Hermes + Honcho +hermes honcho enable # Enable Honcho for the active profile +hermes honcho disable # Disable Honcho for the active profile +hermes honcho sync # Create host blocks for all profiles missing one +hermes honcho status --all # Overview of Honcho config across all profiles +hermes honcho peers # Peer identities across all profiles +hermes honcho --target-profile NAME status # Inspect another profile's Honcho config ``` ### Doctor Integration @@ -399,6 +405,59 @@ Shows the current AI peer representation from Honcho. - **Cross-platform memory** — same user understanding across CLI, Telegram, Discord, etc. - **Multi-user support** — each user (via messaging platforms) gets their own user model +## Profiles + +Honcho is profile-aware. When you create a [profile](../profiles.md), Honcho automatically creates a host block with inherited settings. Each profile gets its own AI peer while sharing the user peer and workspace — so your user model carries across agents, but each agent develops its own identity. + +### What `profile create` does + +| Created | Value | Purpose | +|---------|-------|---------| +| Host key | `hermes.` | Scopes config to this profile | +| AI peer | `` | Distinct identity in Honcho | +| User peer | *(inherited)* | Same person across all agents | +| Workspace | *(inherited)* | Shared user history | + +No per-profile `hermes honcho setup` required. + +### Cross-profile commands + +```bash +hermes honcho status --all # table of all profiles +hermes honcho peers # peer identities across profiles +hermes honcho --target-profile coder status # inspect without switching +hermes honcho --target-profile coder disable # disable for one profile +hermes honcho sync # backfill profiles created before Honcho +``` + +`--target-profile` reads or modifies another profile's host block without switching context. Use `-p` to fully activate a profile: + +```bash +hermes -p coder honcho status # runs as the coder profile +``` + +### Config resolution + +Honcho config uses a three-tier lookup so profiles find host blocks regardless of where they were written: + +| Priority | Path | Purpose | +|----------|------|---------| +| 1 | `$HERMES_HOME/honcho.json` | Profile-local config | +| 2 | `~/.hermes/honcho.json` | Default profile (where host blocks accumulate) | +| 3 | `~/.honcho/config.json` | Global (shared with Cursor, SillyTavern, etc.) | + +Writes go to the active profile's local config. Reads check all three tiers. Host blocks created by `profile create` are written to tier 2 and are visible to all profiles automatically. + +### Syncing + +If you created profiles before configuring Honcho, or before upgrading to profile-aware Honcho: + +```bash +hermes honcho sync +``` + +Creates missing host blocks for all existing profiles. Also runs automatically during `hermes update`. + :::tip Honcho is fully opt-in — zero behavior change when disabled or unconfigured. All Honcho calls are non-fatal; if the service is unreachable, the agent continues normally. ::: diff --git a/website/docs/user-guide/profiles.md b/website/docs/user-guide/profiles.md index 5da6d8ab2af7..9fc2c9cbc01c 100644 --- a/website/docs/user-guide/profiles.md +++ b/website/docs/user-guide/profiles.md @@ -200,3 +200,9 @@ Add the line to your `~/.bashrc` or `~/.zshrc` for persistent completion. Comple Profiles use the `HERMES_HOME` environment variable. When you run `coder chat`, the wrapper script sets `HERMES_HOME=~/.hermes/profiles/coder` before launching hermes. Since 119+ files in the codebase resolve paths via `get_hermes_home()`, everything automatically scopes to the profile's directory — config, sessions, memory, skills, state database, gateway PID, logs, and cron jobs. The default profile is simply `~/.hermes` itself. No migration needed — existing installs work identically. + +## Honcho memory + +If [Honcho](../features/honcho.md) is configured, profiles are automatically memory-aware. Each profile gets its own AI peer while sharing your user identity and workspace. No per-profile setup required — `profile create` handles the wiring. + +See the [Profiles section in the Honcho docs](../features/honcho.md#profiles) for details on cross-profile commands, config resolution, and syncing. From bdc0b8127b515dc6d772ca14d19abeaf354693a5 Mon Sep 17 00:00:00 2001 From: Erosika Date: Thu, 2 Apr 2026 14:46:22 -0400 Subject: [PATCH 9/9] fix(honcho): only clone Honcho peer on --clone/--clone-all profile create Bare 'profile create' is intentionally a blank slate. Honcho peer auto-creation now requires --clone or --clone-all, matching the existing behavior where config.yaml and .env are only copied with those flags. Users who create bare profiles can enable Honcho later via 'hermes honcho setup' or 'hermes honcho enable'. --- hermes_cli/main.py | 15 ++++++++------- website/docs/user-guide/features/honcho.md | 12 +++++++++--- website/docs/user-guide/profiles.md | 2 +- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 75e55b2cd212..b3af3fb0ac5d 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -3555,13 +3555,14 @@ def cmd_profile(args): else: print(f"Cloned config, .env, SOUL.md from {source_label}.") - # Auto-clone Honcho config for the new profile - try: - from honcho_integration.cli import clone_honcho_for_profile - if clone_honcho_for_profile(name): - print(f"Honcho config cloned (host: hermes.{name})") - except Exception: - pass # Honcho not installed or not configured + # Auto-clone Honcho config for the new profile (only with --clone/--clone-all) + if clone or clone_all: + try: + from honcho_integration.cli import clone_honcho_for_profile + if clone_honcho_for_profile(name): + print(f"Honcho config cloned (peer: {name})") + except Exception: + pass # Honcho not installed or not configured # Seed bundled skills (skip if --clone-all already copied them) if not clone_all: diff --git a/website/docs/user-guide/features/honcho.md b/website/docs/user-guide/features/honcho.md index 065d496e1aa4..7af4afeffeb2 100644 --- a/website/docs/user-guide/features/honcho.md +++ b/website/docs/user-guide/features/honcho.md @@ -407,9 +407,13 @@ Shows the current AI peer representation from Honcho. ## Profiles -Honcho is profile-aware. When you create a [profile](../profiles.md), Honcho automatically creates a host block with inherited settings. Each profile gets its own AI peer while sharing the user peer and workspace — so your user model carries across agents, but each agent develops its own identity. +Honcho is profile-aware. When you create a [profile](../profiles.md) with `--clone` or `--clone-all`, Honcho automatically creates a host block with inherited settings. Each profile gets its own AI peer while sharing the user peer and workspace — so your user model carries across agents, but each agent develops its own identity. -### What `profile create` does +### What `profile create --clone` does + +```bash +hermes profile create research --clone +``` | Created | Value | Purpose | |---------|-------|---------| @@ -418,7 +422,9 @@ Honcho is profile-aware. When you create a [profile](../profiles.md), Honcho aut | User peer | *(inherited)* | Same person across all agents | | Workspace | *(inherited)* | Shared user history | -No per-profile `hermes honcho setup` required. +Bare `profile create` (without `--clone`) does not create a Honcho peer — run `hermes honcho setup` or `hermes honcho enable` from that profile to configure it later. + +No per-profile `hermes honcho setup` required when using `--clone`. ### Cross-profile commands diff --git a/website/docs/user-guide/profiles.md b/website/docs/user-guide/profiles.md index 9fc2c9cbc01c..46724defc7a2 100644 --- a/website/docs/user-guide/profiles.md +++ b/website/docs/user-guide/profiles.md @@ -203,6 +203,6 @@ The default profile is simply `~/.hermes` itself. No migration needed — existi ## Honcho memory -If [Honcho](../features/honcho.md) is configured, profiles are automatically memory-aware. Each profile gets its own AI peer while sharing your user identity and workspace. No per-profile setup required — `profile create` handles the wiring. +If [Honcho](../features/honcho.md) is configured, profiles created with `--clone` or `--clone-all` are automatically memory-aware — each gets its own AI peer while sharing your user identity and workspace. See the [Profiles section in the Honcho docs](../features/honcho.md#profiles) for details on cross-profile commands, config resolution, and syncing.