Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion model_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
81 changes: 81 additions & 0 deletions tests/tools/test_kanban_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down