diff --git a/hermes_cli/kanban_db.py b/hermes_cli/kanban_db.py index 2408a4a7762..dc2ec507e78 100644 --- a/hermes_cli/kanban_db.py +++ b/hermes_cli/kanban_db.py @@ -83,6 +83,8 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Any, Iterable, Optional +from toolsets import get_toolset_names + # --------------------------------------------------------------------------- # Constants @@ -90,6 +92,7 @@ from typing import Any, Iterable, Optional VALID_STATUSES = {"triage", "todo", "ready", "running", "blocked", "done", "archived"} VALID_WORKSPACE_KINDS = {"scratch", "worktree", "dir"} +KNOWN_TOOLSET_NAMES = frozenset(name.casefold() for name in get_toolset_names()) # A running task's claim is valid for 15 minutes; after that the next # dispatcher tick reclaims it. Workers that outlive this window should call @@ -1283,6 +1286,11 @@ def create_task( f"skill name cannot contain comma: {name!r} " f"(pass a list of separate names instead of a comma-joined string)" ) + if name.casefold() in KNOWN_TOOLSET_NAMES: + raise ValueError( + f"{name!r} is a toolset name, not a skill name. " + "Put it in the assignee profile's toolsets instead of task skills." + ) if name in seen: continue seen.add(name) diff --git a/tests/hermes_cli/test_kanban_core_functionality.py b/tests/hermes_cli/test_kanban_core_functionality.py index e660764c6d0..ed5172c82de 100644 --- a/tests/hermes_cli/test_kanban_core_functionality.py +++ b/tests/hermes_cli/test_kanban_core_functionality.py @@ -2691,6 +2691,21 @@ def test_create_task_skills_rejects_comma_embedded(kanban_home): conn.close() +def test_create_task_skills_rejects_toolset_names(kanban_home): + """Toolset names belong in profile config, not per-task skills.""" + conn = kb.connect() + try: + with pytest.raises(ValueError, match="toolset name"): + kb.create_task( + conn, + title="bad toolset skill", + assignee="x", + skills=["web", "translation"], + ) + finally: + conn.close() + + def test_default_spawn_appends_per_task_skills(kanban_home, monkeypatch): """Dispatcher argv must carry one `--skills X` pair per task skill, in addition to the built-in kanban-worker.""" diff --git a/tests/plugins/test_kanban_dashboard_plugin.py b/tests/plugins/test_kanban_dashboard_plugin.py index 8d5ea1afa1b..be57fb549e9 100644 --- a/tests/plugins/test_kanban_dashboard_plugin.py +++ b/tests/plugins/test_kanban_dashboard_plugin.py @@ -1036,6 +1036,20 @@ def test_create_task_without_skills_defaults_to_empty_list(client): assert task.get("skills") in (None, []) +def test_create_task_with_toolset_name_in_skills_is_rejected(client): + """POST /tasks fails fast when callers confuse toolsets with skills.""" + r = client.post( + "/api/plugins/kanban/tasks", + json={ + "title": "bad skills payload", + "assignee": "linguist", + "skills": ["web"], + }, + ) + assert r.status_code == 400, r.text + assert "toolset name" in r.json()["detail"] + + # --------------------------------------------------------------------------- # Dispatcher-presence warning in POST /tasks response diff --git a/tools/kanban_tools.py b/tools/kanban_tools.py index 7311d1b2b27..267f782d611 100644 --- a/tools/kanban_tools.py +++ b/tools/kanban_tools.py @@ -611,6 +611,8 @@ def _handle_create(args: dict, **kw) -> str: ) finally: conn.close() + except ValueError as e: + return tool_error(f"kanban_create: {e}") except Exception as e: logger.exception("kanban_create failed") return tool_error(f"kanban_create: {e}")