diff --git a/src/agent_learner/adapters/hermes.py b/src/agent_learner/adapters/hermes.py index 12ef87d..7eb4683 100644 --- a/src/agent_learner/adapters/hermes.py +++ b/src/agent_learner/adapters/hermes.py @@ -296,15 +296,20 @@ def _render_hooks_block(*, prompt_command: str, auto_command: str) -> str: def _merge_hooks_section(existing_text: str, *, prompt_command: str, auto_command: str) -> str: lines = existing_text.splitlines() - desired = { - "pre_llm_call": [f" - command: {prompt_command!r}", " timeout: 15"], - "on_session_end": [f" - command: {auto_command!r}", " timeout: 15"], + desired_commands = { + "pre_llm_call": prompt_command, + "on_session_end": auto_command, } if not lines: return _render_hooks_block(prompt_command=prompt_command, auto_command=auto_command).rstrip("\n") if len(lines) == 1 and lines[0].strip() == "hooks: {}": return _render_hooks_block(prompt_command=prompt_command, auto_command=auto_command).rstrip("\n") + def _render_entry(command: str, *, compact: bool) -> list[str]: + if compact: + return [f" - command: {command!r}", " timeout: 15"] + return [f" - command: {command!r}", " timeout: 15"] + merged: list[str] = ["hooks:"] index = 1 seen_events: set[str] = set() @@ -323,9 +328,13 @@ def _merge_hooks_section(existing_text: str, *, prompt_command: str, auto_comman index += 1 block = lines[start:index] block_text = "\n".join(block) - addition = desired.get(event) - if addition and addition[0] not in block_text: - block = block + addition + command = desired_commands.get(event) + if command: + compact = any(existing_line.startswith(" - ") for existing_line in block[1:]) + command_match = f"command: {command!r}" + command_match_alt = f"command: {command}" + if command_match not in block_text and command_match_alt not in block_text: + block = block + _render_entry(command, compact=compact) merged.extend(block) seen_events.add(event) continue @@ -335,7 +344,7 @@ def _merge_hooks_section(existing_text: str, *, prompt_command: str, auto_comman if event in seen_events: continue merged.append(f" {event}:") - merged.extend(desired[event]) + merged.extend(_render_entry(desired_commands[event], compact=False)) return "\n".join(merged) diff --git a/tests/test_installers.py b/tests/test_installers.py index 8c89a85..df6c194 100644 --- a/tests/test_installers.py +++ b/tests/test_installers.py @@ -139,6 +139,32 @@ def test_install_hermes_user_scope_merges_hooks_into_existing_config_without_dup assert second_written +def test_install_hermes_user_scope_preserves_compact_yaml_hook_indentation(tmp_path: Path) -> None: + hermes_root = tmp_path / ".hermes" + hermes_root.mkdir(parents=True, exist_ok=True) + config_path = hermes_root / "config.yaml" + config_path.write_text( + "model:\n" + " provider: openai-codex\n" + "hooks:\n" + " pre_llm_call:\n" + " - command: /tmp/existing_prompt.py\n" + " timeout: 5\n" + "hooks_auto_accept: false\n", + encoding="utf-8", + ) + + install_hermes_adapter_with_scope(tmp_path, scope="user") + install_hermes_adapter_with_scope(tmp_path, scope="user") + + config_text = config_path.read_text(encoding="utf-8") + assert config_text.count("hermes_prompt_context.py") == 1 + assert config_text.count("auto_session_learning.py") == 1 + assert " - command: /tmp/existing_prompt.py" in config_text + assert config_text.count(" - command: /tmp/existing_prompt.py") == 1 + assert " - command: '/tmp/existing_prompt.py'" not in config_text + + def test_installers_are_independent(tmp_path: Path) -> None: install_codex_adapter(tmp_path) assert not (tmp_path / ".claude").exists()