From 553aa29d4a19cd4015c5caa88203afc409b67c4d Mon Sep 17 00:00:00 2001 From: Dusk1e Date: Tue, 19 May 2026 15:51:34 +0300 Subject: [PATCH 1/2] fix(tools/kanban): sync kanban_unblock response status with DB state --- tests/tools/test_kanban_tools.py | 34 ++++++++++++++++++++++++++++++++ tools/kanban_tools.py | 5 +++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/tests/tools/test_kanban_tools.py b/tests/tools/test_kanban_tools.py index 7964d2fe5bff..10eaef59695c 100644 --- a/tests/tools/test_kanban_tools.py +++ b/tests/tools/test_kanban_tools.py @@ -1328,6 +1328,40 @@ def test_unblock_happy_path(monkeypatch, worker_env): conn.close() +def test_unblock_with_pending_parents_returns_todo(monkeypatch, tmp_path): + monkeypatch.delenv("HERMES_KANBAN_TASK", raising=False) + home = tmp_path / ".hermes" + home.mkdir() + monkeypatch.setenv("HERMES_HOME", str(home)) + monkeypatch.setenv("HERMES_PROFILE", "orchestrator") + from pathlib import Path as _Path + monkeypatch.setattr(_Path, "home", lambda: tmp_path) + + from hermes_cli import kanban_db as kb + kb._INITIALIZED_PATHS.clear() + kb.init_db() + conn = kb.connect() + try: + parent = kb.create_task(conn, title="parent", assignee="worker") + child = kb.create_task(conn, title="child", assignee="worker", parents=[parent]) + conn.execute("UPDATE tasks SET status='blocked' WHERE id=?", (child,)) + conn.commit() + finally: + conn.close() + + from tools import kanban_tools as kt + out = kt._handle_unblock({"task_id": child}) + d = json.loads(out) + assert d["ok"] is True + assert d["status"] == "todo" + + conn = kb.connect() + try: + assert kb.get_task(conn, child).status == "todo" + finally: + conn.close() + + def test_unblock_rejects_non_blocked_task(monkeypatch, worker_env): monkeypatch.delenv("HERMES_KANBAN_TASK", raising=False) from tools import kanban_tools as kt diff --git a/tools/kanban_tools.py b/tools/kanban_tools.py index 8983a8b1f509..183145e3b2e4 100644 --- a/tools/kanban_tools.py +++ b/tools/kanban_tools.py @@ -1276,7 +1276,7 @@ def _maybe_auto_subscribe(conn: Any, task_id: str) -> bool: def _handle_unblock(args: dict, **kw) -> str: - """Transition a blocked task back to ready.""" + """Transition a blocked task back to its actual post-unblock status.""" guard = _require_orchestrator_tool("kanban_unblock") if guard: return guard @@ -1293,7 +1293,8 @@ def _handle_unblock(args: dict, **kw) -> str: ok = kb.unblock_task(conn, str(tid)) if not ok: return tool_error(f"could not unblock {tid} (not blocked or unknown)") - return _ok(task_id=str(tid), status="ready") + task = kb.get_task(conn, str(tid)) + return _ok(task_id=str(tid), status=task.status if task else None) finally: conn.close() except ValueError as e: From 6ce1c9309654c066508be227e1c18bc827469049 Mon Sep 17 00:00:00 2001 From: Gille <4317663+helix4u@users.noreply.github.com> Date: Fri, 17 Jul 2026 14:15:36 -0600 Subject: [PATCH 2/2] docs(kanban): clarify unblock status routing --- hermes_cli/kanban.py | 5 ++++- tools/kanban_tools.py | 7 ++++--- website/docs/reference/tools-reference.md | 2 +- website/docs/user-guide/features/kanban.md | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/hermes_cli/kanban.py b/hermes_cli/kanban.py index 6f0766a738d1..4de2f8fb8b66 100644 --- a/hermes_cli/kanban.py +++ b/hermes_cli/kanban.py @@ -594,7 +594,10 @@ def build_parser(parent_subparsers: argparse._SubParsersAction) -> argparse.Argu p_schedule.add_argument("--ids", nargs="+", default=None, help="Additional task ids to schedule with the same reason (bulk mode)") - p_unblock = sub.add_parser("unblock", help="Return one or more blocked/scheduled tasks to ready") + p_unblock = sub.add_parser( + "unblock", + help="Return blocked/scheduled tasks to ready, or todo while parents remain open", + ) p_unblock.add_argument( "--reason", default=None, diff --git a/tools/kanban_tools.py b/tools/kanban_tools.py index 183145e3b2e4..8d27522f1829 100644 --- a/tools/kanban_tools.py +++ b/tools/kanban_tools.py @@ -1276,7 +1276,7 @@ def _maybe_auto_subscribe(conn: Any, task_id: str) -> bool: def _handle_unblock(args: dict, **kw) -> str: - """Transition a blocked task back to its actual post-unblock status.""" + """Transition a blocked task to ready, or todo while parents remain open.""" guard = _require_orchestrator_tool("kanban_unblock") if guard: return guard @@ -1875,7 +1875,8 @@ def _board_schema_prop() -> dict[str, str]: KANBAN_UNBLOCK_SCHEMA = { "name": "kanban_unblock", "description": ( - "Move a blocked Kanban task back to ready. Orchestrator-only — only " + "Unblock a Kanban task. It moves to ready when all parents are done, " + "or todo while any parent remains open. Orchestrator-only — only " "profiles with the kanban toolset can unblock routed work; " "dispatcher-spawned task workers never see this tool." ), @@ -1884,7 +1885,7 @@ def _board_schema_prop() -> dict[str, str]: "properties": { "task_id": { "type": "string", - "description": "Blocked task id to return to ready.", + "description": "Blocked task id to move to ready or parent-gated todo.", }, "board": _board_schema_prop(), }, diff --git a/website/docs/reference/tools-reference.md b/website/docs/reference/tools-reference.md index 75866e97a2d4..a8cf61fcf39c 100644 --- a/website/docs/reference/tools-reference.md +++ b/website/docs/reference/tools-reference.md @@ -130,7 +130,7 @@ Registered when the agent is either (a) spawned by the kanban dispatcher (`HERME | `kanban_comment` | Add a comment to the task thread without changing its state — useful for surfacing intermediate findings. | `HERMES_KANBAN_TASK` or `kanban` toolset | | `kanban_create` | Fan out child tasks from the current task. Used by orchestrators and follow-up-spawning workers. | `HERMES_KANBAN_TASK` or `kanban` toolset | | `kanban_link` | Link tasks with a parent → child dependency edge. | `HERMES_KANBAN_TASK` or `kanban` toolset | -| `kanban_unblock` | Return a blocked task to `ready`. Orchestrator-only; hidden from dispatcher-spawned task workers. | profile with `kanban` toolset | +| `kanban_unblock` | Move a blocked task to `ready` when all parents are done, or `todo` while any parent remains open. Orchestrator-only; hidden from dispatcher-spawned task workers. | profile with `kanban` toolset | ## `project` toolset diff --git a/website/docs/user-guide/features/kanban.md b/website/docs/user-guide/features/kanban.md index b47ba730715c..ae5bbde1fb8a 100644 --- a/website/docs/user-guide/features/kanban.md +++ b/website/docs/user-guide/features/kanban.md @@ -279,7 +279,7 @@ hermes kanban block t_abc "need input" --ids t_def t_hij | `kanban_comment` | Append a durable note to the task thread. | `task_id`, `body` | | `kanban_create` | (Orchestrators) fan out into child tasks with an `assignee`, optional `parents`, `skills`, etc. | `title`, `assignee` | | `kanban_link` | (Orchestrators) add a `parent_id → child_id` dependency edge after the fact. | `parent_id`, `child_id` | -| `kanban_unblock` | (Orchestrators) move a blocked task back to `ready`. | `task_id` | +| `kanban_unblock` | (Orchestrators) move a blocked task to `ready` when all parents are done, or `todo` while any parent remains open. | `task_id` | A typical worker turn looks like: