From 695fb982b1f32b2cc24185540cb466f1eb1fc52c Mon Sep 17 00:00:00 2001 From: Preston Campbell Date: Sat, 11 Jul 2026 14:51:59 -0700 Subject: [PATCH] fix: preserve Kanban tools for worker tasks --- model_tools.py | 6 ++- tests/tools/test_kanban_tools.py | 81 ++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/model_tools.py b/model_tools.py index c59c189e36d9d..10da8abe54181 100644 --- a/model_tools.py +++ b/model_tools.py @@ -395,9 +395,13 @@ def _compute_tool_definitions( # Always apply disabled toolsets as a subtraction step at the end. # This ensures that even if a composite toolset (like hermes-cli) # is enabled, any tools belonging to a disabled toolset are strictly - # stripped out. See issue #17309. + # stripped out. See issue #17309. Task-scoped Kanban workers are the one + # exception: their check_fns still constrain them to the scoped worker + # lifecycle surface (including child-task creation/dependency linking). if disabled_toolsets: for toolset_name in disabled_toolsets: + if os.environ.get("HERMES_KANBAN_TASK") and toolset_name == "kanban": + continue if validate_toolset(toolset_name): from toolsets import bundle_non_core_tools, get_toolset if toolset_name.startswith("hermes-") or (get_toolset(toolset_name) or {}).get("posture"): diff --git a/tests/tools/test_kanban_tools.py b/tests/tools/test_kanban_tools.py index cdf8c4cdb5dae..488ba2d8f9b14 100644 --- a/tests/tools/test_kanban_tools.py +++ b/tests/tools/test_kanban_tools.py @@ -1434,6 +1434,87 @@ def test_kanban_guidance_in_worker_prompt(monkeypatch, tmp_path): assert "Do not shell out" in prompt or "tools — they work" in prompt +def test_worker_kanban_guidance_survives_disabled_kanban_toolset(monkeypatch, tmp_path): + """Task scope overrides a profile's ordinary-chat Kanban restriction.""" + monkeypatch.setenv("HERMES_KANBAN_TASK", "t_fake") + home = tmp_path / ".hermes" + home.mkdir() + monkeypatch.setenv("HERMES_HOME", str(home)) + from pathlib import Path as _P + monkeypatch.setattr(_P, "home", lambda: tmp_path) + + from tools.registry import invalidate_check_fn_cache + from model_tools import _clear_tool_defs_cache, get_tool_definitions + + invalidate_check_fn_cache() + _clear_tool_defs_cache() + + defs = get_tool_definitions( + enabled_toolsets=["file"], + disabled_toolsets=["kanban"], + quiet_mode=True, + skip_tool_search_assembly=True, + ) + expected_worker_surface = { + "kanban_show", + "kanban_complete", + "kanban_block", + "kanban_heartbeat", + "kanban_comment", + "kanban_create", + "kanban_link", + } + direct_names = {tool["function"]["name"] for tool in defs} + assert {name for name in direct_names if name.startswith("kanban_")} == ( + expected_worker_surface + ) + + from run_agent import AIAgent + + agent = AIAgent( + api_key="test", + base_url="https://openrouter.ai/api/v1", + enabled_toolsets=["file"], + disabled_toolsets=["kanban"], + quiet_mode=True, + skip_context_files=True, + skip_memory=True, + ) + agent_names = {tool["function"]["name"] for tool in getattr(agent, "tools")} + assert {name for name in agent_names if name.startswith("kanban_")} == ( + expected_worker_surface + ) + + prompt = agent._build_system_prompt() + assert "Kanban task execution protocol" in prompt + assert "kanban_show()" in prompt + assert "kanban_complete" in prompt + + +def test_normal_chat_still_honors_disabled_kanban_toolset(monkeypatch, tmp_path): + monkeypatch.delenv("HERMES_KANBAN_TASK", raising=False) + home = tmp_path / ".hermes" + home.mkdir() + monkeypatch.setenv("HERMES_HOME", str(home)) + from pathlib import Path as _P + monkeypatch.setattr(_P, "home", lambda: tmp_path) + + from tools.registry import invalidate_check_fn_cache + from model_tools import _clear_tool_defs_cache, get_tool_definitions + + invalidate_check_fn_cache() + _clear_tool_defs_cache() + + defs = get_tool_definitions( + enabled_toolsets=["kanban"], + disabled_toolsets=["kanban"], + quiet_mode=True, + skip_tool_search_assembly=True, + ) + names = {tool["function"]["name"] for tool in defs} + assert not any(name.startswith("kanban_") for name in names) + + def test_kanban_guidance_prompt_size_bounded(monkeypatch, tmp_path): """Sanity: the guidance block stays lean so it doesn't blow up the cached prompt.