Skip to content
Closed
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
5 changes: 4 additions & 1 deletion hermes_cli/kanban.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
34 changes: 34 additions & 0 deletions tests/tools/test_kanban_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions tools/kanban_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 to ready, or todo while parents remain open."""
guard = _require_orchestrator_tool("kanban_unblock")
if guard:
return guard
Expand All @@ -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:
Expand Down Expand Up @@ -1874,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."
),
Expand All @@ -1883,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(),
},
Expand Down
2 changes: 1 addition & 1 deletion website/docs/reference/tools-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion website/docs/user-guide/features/kanban.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Loading