diff --git a/model_tools.py b/model_tools.py index ad938b5f18b..f461afff5ba 100644 --- a/model_tools.py +++ b/model_tools.py @@ -20,6 +20,7 @@ Public API (signatures preserved from the original 2,400-line version): check_tool_availability(quiet) -> tuple """ +import os import json import re import asyncio @@ -299,6 +300,7 @@ def get_tool_definitions( frozenset(disabled_toolsets) if disabled_toolsets else None, registry._generation, cfg_fp, + bool(os.environ.get("HERMES_KANBAN_TASK")), ) cached = _tool_defs_cache.get(cache_key) if cached is not None: @@ -334,7 +336,15 @@ def _compute_tool_definitions( tools_to_include: set = set() if enabled_toolsets is not None: - for toolset_name in enabled_toolsets: + effective_enabled_toolsets = list(enabled_toolsets) + if os.environ.get("HERMES_KANBAN_TASK") and "kanban" not in effective_enabled_toolsets: + # Dispatcher-spawned workers are scoped by HERMES_KANBAN_TASK and + # must always receive the lifecycle handoff tools. Assignee + # profiles may intentionally restrict their normal chat toolsets + # (for token/cost reasons), but that should not strip the kanban + # worker's completion/block/heartbeat surface. + effective_enabled_toolsets.append("kanban") + for toolset_name in effective_enabled_toolsets: if validate_toolset(toolset_name): resolved = resolve_toolset(toolset_name) tools_to_include.update(resolved) diff --git a/tests/tools/test_kanban_tools.py b/tests/tools/test_kanban_tools.py index b50f35b0682..077e9af488f 100644 --- a/tests/tools/test_kanban_tools.py +++ b/tests/tools/test_kanban_tools.py @@ -61,6 +61,32 @@ def test_kanban_tools_visible_with_env_var(monkeypatch, tmp_path): assert kanban == expected, f"expected {expected}, got {kanban}" +def test_kanban_worker_env_overrides_profile_toolset_filter(monkeypatch, tmp_path): + """Dispatcher-spawned workers must get lifecycle tools even when the + assignee profile restricts enabled toolsets and does not list kanban. + """ + monkeypatch.setenv("HERMES_KANBAN_TASK", "t_fake") + home = tmp_path / ".hermes" + home.mkdir() + monkeypatch.setenv("HERMES_HOME", str(home)) + + import tools.kanban_tools # ensure registered + from model_tools import _clear_tool_defs_cache, get_tool_definitions + from tools.registry import invalidate_check_fn_cache + + invalidate_check_fn_cache() + _clear_tool_defs_cache() + schema = get_tool_definitions( + enabled_toolsets=["terminal"], + quiet_mode=True, + ) + names = {s["function"].get("name") for s in schema if "function" in s} + assert "kanban_show" in names + assert "kanban_complete" in names + assert "kanban_block" in names + assert "kanban_list" not in names + + def test_worker_with_kanban_toolset_still_hides_board_routing(monkeypatch, tmp_path): """Task scope wins over profile config for board-routing tools.