Skip to content
Merged
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
7 changes: 7 additions & 0 deletions hermes_cli/kanban.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,12 @@ def build_parser(parent_subparsers: argparse._SubParsersAction) -> argparse.Argu
p_list.add_argument("--archived", action="store_true",
help="Include archived tasks")
p_list.add_argument("--json", action="store_true")
p_list.add_argument(
"--sort",
default=None,
choices=sorted(kb.VALID_SORT_ORDERS.keys()),
help="Sort order for listed tasks (default: priority)",
)

# --- show ---
p_show = sub.add_parser("show", help="Show a task with comments + events")
Expand Down Expand Up @@ -1220,6 +1226,7 @@ def _cmd_list(args: argparse.Namespace) -> int:
status=args.status,
tenant=args.tenant,
include_archived=args.archived,
order_by=getattr(args, "sort", None),
)
if getattr(args, "json", False):
print(json.dumps([_task_to_dict(t) for t in tasks], indent=2, ensure_ascii=False))
Expand Down
25 changes: 24 additions & 1 deletion hermes_cli/kanban_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -1531,6 +1531,20 @@ def get_task(conn: sqlite3.Connection, task_id: str) -> Optional[Task]:
return Task.from_row(row) if row else None


# Canonical sort-order mappings for ``hermes kanban list --sort``.
# Each value is a raw SQL fragment appended after ``ORDER BY``.
VALID_SORT_ORDERS: dict[str, str] = {
"created": "created_at ASC, id ASC",
"created-desc": "created_at DESC, id DESC",
"priority": "priority DESC, created_at ASC",
"priority-desc": "priority ASC, created_at ASC",
"status": "status ASC, created_at ASC",
"assignee": "assignee ASC, created_at ASC",
"title": "title ASC, id ASC",
"updated": "started_at DESC NULLS LAST, created_at DESC",
}


def list_tasks(
conn: sqlite3.Connection,
*,
Expand All @@ -1539,6 +1553,7 @@ def list_tasks(
tenant: Optional[str] = None,
include_archived: bool = False,
limit: Optional[int] = None,
order_by: Optional[str] = None,
) -> list[Task]:
query = "SELECT * FROM tasks WHERE 1=1"
params: list[Any] = []
Expand All @@ -1555,7 +1570,15 @@ def list_tasks(
params.append(tenant)
if not include_archived and status != "archived":
query += " AND status != 'archived'"
query += " ORDER BY priority DESC, created_at ASC"
if order_by is not None:
order_by = order_by.strip().lower()
if order_by not in VALID_SORT_ORDERS:
raise ValueError(
f"order_by must be one of {sorted(VALID_SORT_ORDERS.keys())}"
)
query += f" ORDER BY {VALID_SORT_ORDERS[order_by]}"
else:
query += " ORDER BY priority DESC, created_at ASC"
if limit:
query += f" LIMIT {int(limit)}"
rows = conn.execute(query, params).fetchall()
Expand Down
33 changes: 33 additions & 0 deletions tests/hermes_cli/test_kanban_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,39 @@ def test_delete_archived_task_rejects_non_archived_rows(kanban_home):
assert kb.get_task(conn, tid) is not None


def test_list_tasks_order_by(kanban_home):
with kb.connect() as conn:
# Create tasks with different titles and priorities
t_a = kb.create_task(conn, title="alpha", priority=1)
t_b = kb.create_task(conn, title="beta", priority=2)
t_c = kb.create_task(conn, title="gamma", priority=1)

# Default sort: priority DESC, created ASC
default = kb.list_tasks(conn)
assert [t.id for t in default] == [t_b, t_a, t_c]

# Sort by title ASC
by_title = kb.list_tasks(conn, order_by="title")
assert [t.id for t in by_title] == [t_a, t_b, t_c]

# Sort by assignee
kb.assign_task(conn, t_a, "alice")
kb.assign_task(conn, t_b, "bob")
kb.assign_task(conn, t_c, "alice")
by_assignee = kb.list_tasks(conn, order_by="assignee")
# alice's tasks first (alphabetically), then bob's
assignees = [t.assignee for t in by_assignee]
assert assignees[:2] == ["alice", "alice"]
assert assignees[2] == "bob"

# Invalid sort order raises ValueError
try:
kb.list_tasks(conn, order_by="bogus")
assert False, "Should have raised ValueError"
except ValueError as e:
assert "order_by must be one of" in str(e)


# ---------------------------------------------------------------------------
# Comments / events / worker context
# ---------------------------------------------------------------------------
Expand Down
Loading