diff --git a/hermes_cli/plugins.py b/hermes_cli/plugins.py index 87e46de8290..8d1e3ca9e80 100644 --- a/hermes_cli/plugins.py +++ b/hermes_cli/plugins.py @@ -1532,39 +1532,35 @@ class PluginManager: logger.warning("Plugin '%s' has no register() function", manifest.name) else: ctx = PluginContext(manifest, self) + # Snapshot registry state BEFORE register() so each registry's + # attribution counts only what THIS plugin actually added. + # The previous approach diffed names against all already-loaded + # plugins, which mis-credited a plugin that registered a hook / + # middleware / tool name an earlier plugin had already used: + # the shared name was attributed to the first plugin only, so + # later plugins under-reported in `hermes plugins list`. + _tools_before = set(self._plugin_tool_names) + _hook_counts_before = { + h: len(cbs) for h, cbs in self._hooks.items() + } + _mw_counts_before = { + kind: len(cbs) for kind, cbs in self._middleware.items() + } register_fn(ctx) loaded.tools_registered = [ t for t in self._plugin_tool_names - if t not in { - n - for name, p in self._plugins.items() - for n in p.tools_registered - } + if t not in _tools_before + ] + loaded.hooks_registered = [ + h + for h, cbs in self._hooks.items() + if len(cbs) > _hook_counts_before.get(h, 0) + ] + loaded.middleware_registered = [ + kind + for kind, cbs in self._middleware.items() + if len(cbs) > _mw_counts_before.get(kind, 0) ] - loaded.hooks_registered = list( - { - h - for h, cbs in self._hooks.items() - if cbs # non-empty - } - - { - h - for name, p in self._plugins.items() - for h in p.hooks_registered - } - ) - loaded.middleware_registered = list( - { - kind - for kind, cbs in self._middleware.items() - if cbs - } - - { - kind - for name, p in self._plugins.items() - for kind in p.middleware_registered - } - ) loaded.commands_registered = [ c for c in self._plugin_commands if self._plugin_commands[c].get("plugin") == manifest.name diff --git a/tests/hermes_cli/test_plugins.py b/tests/hermes_cli/test_plugins.py index cd6922a6d80..effeaa0120f 100644 --- a/tests/hermes_cli/test_plugins.py +++ b/tests/hermes_cli/test_plugins.py @@ -1203,6 +1203,36 @@ class TestPluginManagerList: assert "tools" in p assert "hooks" in p + def test_shared_hook_name_credited_to_every_plugin(self, tmp_path, monkeypatch): + """Two plugins registering the SAME hook name are each credited. + + Regression: hook/middleware/tool attribution diffed names against all + already-loaded plugins, so when a later plugin registered a hook name + an earlier plugin had already used, the shared name was attributed to + the first plugin only and the later plugin reported 0 hooks in + `hermes plugins list`. Attribution now counts what each plugin's own + register() added (per-registration delta), so both get credit. + """ + plugins_dir = tmp_path / "hermes_test" / "plugins" + _make_plugin_dir( + plugins_dir, "first_hooker", + register_body='ctx.register_hook("post_tool_call", lambda **kw: None)', + ) + _make_plugin_dir( + plugins_dir, "second_hooker", + register_body='ctx.register_hook("post_tool_call", lambda **kw: None)', + ) + monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes_test")) + + mgr = PluginManager() + mgr.discover_and_load() + + by_name = {p["name"]: p for p in mgr.list_plugins()} + assert by_name["first_hooker"]["hooks"] == 1 + assert by_name["second_hooker"]["hooks"] == 1, ( + "second plugin sharing a hook name was not credited with its hook" + ) + class TestPreLlmCallTargetRouting: