From 932270ac60115fae672d57587987c201efeff108 Mon Sep 17 00:00:00 2001 From: htafolla Date: Mon, 30 Mar 2026 11:03:54 -0500 Subject: [PATCH] fix: remove 3 phantom lifecycle hooks that don't exist in the host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Hermes plugin declared on_file_write, on_validation_result, and on_error as provides_hooks in plugin.yaml, but the OpenCode host only supports: on_session_end, on_session_start, post_llm_call, post_tool_call, pre_llm_call, pre_tool_call. This caused 'registered unknown hook' warnings on every plugin load. The three handlers were dead code — _modified_files and _validation_results were write-only (never consumed), and _on_error's bridge_errors increment was redundant with _call_bridge's own error counting. Removes: - 3 phantom hooks from plugin.yaml provides_hooks - 3 handler functions (_on_file_write, _on_validation_result, _on_error) - 3 tracking lists (_modified_files, _validation_results, _errors) - registration loop in register() - TestLifecycleHooks test class (9 tests) - hook count claims in docs and log messages (5 hooks → 2 hooks) --- src/integrations/hermes-agent/__init__.py | 70 +----------- .../hermes-agent/after-install.md | 5 +- src/integrations/hermes-agent/plugin.yaml | 3 - src/integrations/hermes-agent/test_plugin.py | 107 +----------------- src/skills/hermes-agent/SKILL.md | 15 +-- 5 files changed, 12 insertions(+), 188 deletions(-) diff --git a/src/integrations/hermes-agent/__init__.py b/src/integrations/hermes-agent/__init__.py index 53d49cb54..e05b43c3b 100644 --- a/src/integrations/hermes-agent/__init__.py +++ b/src/integrations/hermes-agent/__init__.py @@ -573,11 +573,6 @@ def _tune(): # ── Registration ────────────────────────────────────────────── -# ── Session tracking for new lifecycle hooks -_modified_files: list = [] -_validation_results: list = [] -_errors: list = [] - # ── Subagent (delegate_task) enforcement ──────────────────── # Subagents bypass all StringRay hooks because they run in isolated # contexts. We enforce by snapshotting the working tree before dispatch @@ -674,56 +669,6 @@ def _validate_subagent_changes(task_id: str, **kwargs): f"[subagent-validate] PASSED: {rel_path}") -def _on_file_write(file_path: str, content: str, tool_name: str, **kwargs): - """Fires when a code-producing tool writes a file. - - Validates the file was written correctly and logs the event. - """ - _log_to_file("activity.log", - f"[file-write] path={file_path} tool={tool_name} size={len(content) if content else 0}") - - _modified_files.append({ - "path": file_path, - "tool": tool_name, - "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), - }) - - -def _on_validation_result(tool_name: str, passed: bool, violations: list, **kwargs): - """Fires when a validation/check completes. - - Tracks validation outcomes for session context. - """ - _log_to_file("activity.log", - f"[validation] tool={tool_name} passed={passed} violations={len(violations)}") - - _validation_results.append({ - "tool": tool_name, - "passed": passed, - "violation_count": len(violations), - "violations": violations[:5], - "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), - }) - - -def _on_error(tool_name: str, error: str, args: dict, **kwargs): - """Fires when a tool call fails. - - Logs the error and tracks it for session context. - """ - _log_to_file("activity.log", - f"[error] tool={tool_name} error={str(error)[:200]}") - - _session_stats["bridge_errors"] += 1 - - _errors.append({ - "tool": tool_name, - "error": str(error)[:200], - "args_keys": list((args or {}).keys()), - "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), - }) - - def register(ctx): """Wire schemas to handlers and register lifecycle hooks.""" # ── Register tools ──────────────────────────────────────── @@ -762,17 +707,6 @@ def register(ctx): except (AttributeError, TypeError): logger.debug("[strray] on_session_start hook not yet available") - # Try to register new lifecycle hooks - for hook_name, hook_fn in [ - ("on_file_write", _on_file_write), - ("on_validation_result", _on_validation_result), - ("on_error", _on_error), - ]: - try: - ctx.register_hook(hook_name, hook_fn) - except (AttributeError, TypeError): - logger.debug("[strray] %s hook not yet available", hook_name) - # ── Register slash command ──────────────────────────────── try: ctx.register_command( @@ -789,10 +723,10 @@ def register(ctx): _ensure_log_dir() _log_to_file("activity.log", f"[plugin-loaded] StringRay Hermes Plugin v2.2 — " - f"4 tools, 5 hooks, subagent enforcement, bridge={BRIDGE_PATH.exists()}") + f"4 tools, 2 hooks, subagent enforcement, bridge={BRIDGE_PATH.exists()}") logger.info( - "[strray] Plugin v2.2 loaded: 4 tools, 5 hooks, " + "[strray] Plugin v2.2 loaded: 4 tools, 2 hooks, " "subagent enforcement active, bridge=%s", BRIDGE_PATH.exists(), ) diff --git a/src/integrations/hermes-agent/after-install.md b/src/integrations/hermes-agent/after-install.md index e8ab4ccd9..a87da0b0e 100644 --- a/src/integrations/hermes-agent/after-install.md +++ b/src/integrations/hermes-agent/after-install.md @@ -11,10 +11,7 @@ | `strray_health` | Framework health check | | `strray_hooks` | Git hooks management (install, uninstall, list, status) | | `pre_tool_call` hook | Quality gates + nudges before every tool call | -| `post_tool_call` hook | Post-processors + file tracking after every tool call | -| `on_file_write` hook | File write tracking and logging | -| `on_validation_result` hook | Validation outcome tracking | -| `on_error` hook | Error logging and session tracking | +| `post_tool_call` hook | Post-processors after every tool call | ## Git Hooks diff --git a/src/integrations/hermes-agent/plugin.yaml b/src/integrations/hermes-agent/plugin.yaml index 2efb5ff6f..a418374ef 100644 --- a/src/integrations/hermes-agent/plugin.yaml +++ b/src/integrations/hermes-agent/plugin.yaml @@ -10,6 +10,3 @@ provides_tools: provides_hooks: - pre_tool_call - post_tool_call - - on_file_write - - on_validation_result - - on_error diff --git a/src/integrations/hermes-agent/test_plugin.py b/src/integrations/hermes-agent/test_plugin.py index c87377696..503017a08 100644 --- a/src/integrations/hermes-agent/test_plugin.py +++ b/src/integrations/hermes-agent/test_plugin.py @@ -1048,97 +1048,8 @@ def test_description_mentions_hooks(self): self.assertIn("git hooks", s["description"]) -class TestLifecycleHooks(unittest.TestCase): - """Tests for the new lifecycle hooks: on_file_write, on_validation_result, on_error.""" - - def setUp(self): - # Reset tracking lists - pi._modified_files = [] - pi._validation_results = [] - pi._errors = [] - - def test_on_file_write_logs(self): - with tempfile.TemporaryDirectory() as td: - log_dir = Path(td) - original = pi.LOG_DIR - pi.LOG_DIR = log_dir - - pi._on_file_write("src/index.ts", "hello world", "write_file") - - self.assertEqual(len(pi._modified_files), 1) - self.assertEqual(pi._modified_files[0]["path"], "src/index.ts") - self.assertEqual(pi._modified_files[0]["tool"], "write_file") - - content = (log_dir / "activity.log").read_text() - self.assertIn("[file-write]", content) - - pi.LOG_DIR = original - - def test_on_file_write_empty_content(self): - pi._on_file_write("a.ts", "", "write_file") - self.assertEqual(len(pi._modified_files), 1) - - def test_on_validation_result_passed(self): - with tempfile.TemporaryDirectory() as td: - log_dir = Path(td) - original = pi.LOG_DIR - pi.LOG_DIR = log_dir - - pi._on_validation_result("strray_validate", True, []) - - self.assertEqual(len(pi._validation_results), 1) - self.assertTrue(pi._validation_results[0]["passed"]) - - content = (log_dir / "activity.log").read_text() - self.assertIn("[validation]", content) - - pi.LOG_DIR = original - - def test_on_validation_result_failed(self): - pi._on_validation_result("strray_codex_check", False, ["console.log found"]) - self.assertFalse(pi._validation_results[0]["passed"]) - self.assertEqual(pi._validation_results[0]["violation_count"], 1) - self.assertEqual(len(pi._validation_results[0]["violations"]), 1) - - def test_on_validation_result_truncates_violations(self): - many = [f"violation-{i}" for i in range(20)] - pi._on_validation_result("test", False, many) - # Should keep only first 5 - self.assertEqual(len(pi._validation_results[0]["violations"]), 5) - - def test_on_error_logs(self): - with tempfile.TemporaryDirectory() as td: - log_dir = Path(td) - original = pi.LOG_DIR - pi.LOG_DIR = log_dir - - pi._on_error("write_file", "disk full", {"path": "a.ts"}) - - self.assertEqual(len(pi._errors), 1) - self.assertEqual(pi._errors[0]["tool"], "write_file") - - content = (log_dir / "activity.log").read_text() - self.assertIn("[error]", content) - - pi.LOG_DIR = original - - def test_on_error_increments_stats(self): - initial = pi._session_stats["bridge_errors"] - pi._on_error("terminal", "timeout", None) - self.assertEqual(pi._session_stats["bridge_errors"], initial + 1) - - def test_on_error_truncates_long_error(self): - long_error = "x" * 500 - pi._on_error("tool", long_error, {}) - self.assertLessEqual(len(pi._errors[0]["error"]), 200) - - def test_on_error_no_args(self): - pi._on_error("tool", "crash", None) - self.assertEqual(pi._errors[0]["args_keys"], []) - - class TestRegisterIntegrationV2_1(unittest.TestCase): - """Test that register() wires all 4 tools and 5 hooks in v2.1.""" + """Test that register() wires all 4 tools and 2 hooks in v2.2.""" def test_wires_four_tools(self): ctx = MagicMock() @@ -1161,33 +1072,27 @@ def test_strray_hooks_handler_wired(self): hm = {c[1]["name"]: c[1]["handler"] for c in ctx.register_tool.call_args_list} self.assertIs(hm["strray_hooks"], tools_mod.strray_hooks) - def test_registers_five_hooks(self): + def test_registers_two_hooks(self): ctx = MagicMock() pi.register(ctx) hook_names = [c[0][0] for c in ctx.register_hook.call_args_list] self.assertIn("pre_tool_call", hook_names) self.assertIn("post_tool_call", hook_names) - self.assertIn("on_file_write", hook_names) - self.assertIn("on_validation_result", hook_names) - self.assertIn("on_error", hook_names) - def test_survives_missing_lifecycle_hooks(self): - """New hooks should fail gracefully if not supported.""" + def test_survives_missing_session_hook(self): + """Session hook should fail gracefully if not supported.""" ctx = MagicMock() - # All 5 hook registrations: pre_tool_call, post_tool_call, on_session_start, on_file_write, on_validation_result, on_error - # Let the 3 new ones raise def side_effect(*args): raise AttributeError("not available") - ctx.register_hook.side_effect = [None, None, None, side_effect, side_effect, side_effect] + ctx.register_hook.side_effect = [None, None, side_effect] pi.register(ctx) # should not raise - def test_v2_1_log_message(self): + def test_v2_2_log_message(self): ctx = MagicMock() with self.assertLogs("strray-hermes", level="INFO") as cm: pi.register(ctx) self.assertTrue(any("v2.2" in m for m in cm.output)) self.assertTrue(any("4 tools" in m for m in cm.output)) - self.assertTrue(any("5 hooks" in m for m in cm.output)) if __name__ == "__main__": diff --git a/src/skills/hermes-agent/SKILL.md b/src/skills/hermes-agent/SKILL.md index 29e0788f2..e0316e2f9 100644 --- a/src/skills/hermes-agent/SKILL.md +++ b/src/skills/hermes-agent/SKILL.md @@ -112,7 +112,7 @@ Hooks available: pre-commit, post-commit, pre-push, post-push. | `pre-push` | Blocking | Full validation suite before push | | `post-push` | Non-blocking | Comprehensive monitoring after push | -## 5 Lifecycle Hooks +## 2 Lifecycle Hooks These fire automatically — no action needed from the user or agent. @@ -129,19 +129,10 @@ Fires before ANY tool executes: Fires after ANY tool returns: 1. Logs tool-complete event 2. For code-producing tools: runs post-processors via bridge -3. Tracks file modifications for session context -### on_file_write +### on_session_start (graceful fallback) -Fires when a code-producing tool writes a file. Validates and logs the event. - -### on_validation_result - -Fires when a validation/check completes. Tracks outcomes for session context. - -### on_error - -Fires when a tool call fails. Logs the error and tracks it. +Fires when a new session starts. Resets stats and logs to disk. Registration is wrapped in try/except for hosts that don't support it yet. ## Slash Command