feat(kanban): add scheduled status for delayed follow-ups

Salvages #24533 by @roycepersonalassistant. Adds a first-class
'scheduled' Kanban status for time-delay follow-ups that aren't
waiting on human input.

- hermes kanban schedule <task_id> [reason] CLI command
- Dashboard/API transitions to/from Scheduled
- unblock_task() now releases both 'blocked' AND 'scheduled' tasks
  (re-checking parent dependencies before moving to ready/todo)
- i18n + docs updates

Resolved conflicts: kept HEAD's failure-counter reset on unblock
alongside the PR's scheduled state, kept HEAD's 'running' direct-set
rejection, combined both bulk-status branches. Dropped the dist/
bundle changes (months-stale; would need rebuild from source).
This commit is contained in:
roycepersonalassistant
2026-05-18 21:38:57 -07:00
committed by Teknium
parent b5c1fe78aa
commit e3823657d6
8 changed files with 149 additions and 14 deletions

View File

@@ -1,6 +1,6 @@
"""CLI for the Hermes Kanban board — ``hermes kanban …`` subcommand.
Exposes the full 15-verb surface documented in the design spec
Exposes the full Kanban command surface documented in the design spec
(``docs/hermes-kanban-v1-spec.pdf``). All DB work is delegated to
``kanban_db``. This module adds:
@@ -36,6 +36,7 @@ _STATUS_ICONS = {
"todo": "",
"ready": "",
"running": "",
"scheduled":"",
"blocked": "",
"done": "",
"archived": "",
@@ -540,7 +541,13 @@ def build_parser(parent_subparsers: argparse._SubParsersAction) -> argparse.Argu
p_block.add_argument("--ids", nargs="+", default=None,
help="Additional task ids to block with the same reason (bulk mode)")
p_unblock = sub.add_parser("unblock", help="Return one or more blocked tasks to ready")
p_schedule = sub.add_parser("schedule", help="Park one or more tasks in Scheduled (waiting on time, not human input)")
p_schedule.add_argument("task_id")
p_schedule.add_argument("reason", nargs="*", help="Reason/timing note (also appended as a comment)")
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.add_argument("task_ids", nargs="+")
p_archive = sub.add_parser("archive", help="Archive one or more tasks")
@@ -890,6 +897,7 @@ def kanban_command(args: argparse.Namespace) -> int:
"complete": _cmd_complete,
"edit": _cmd_edit,
"block": _cmd_block,
"schedule": _cmd_schedule,
"unblock": _cmd_unblock,
"archive": _cmd_archive,
"tail": _cmd_tail,
@@ -1909,6 +1917,28 @@ def _cmd_block(args: argparse.Namespace) -> int:
return 0 if not failed else 1
def _cmd_schedule(args: argparse.Namespace) -> int:
reason = " ".join(args.reason).strip() if args.reason else None
author = _profile_author()
ids = [args.task_id] + list(getattr(args, "ids", None) or [])
failed: list[str] = []
with kb.connect() as conn:
for tid in ids:
if reason:
kb.add_comment(conn, tid, author, f"SCHEDULED: {reason}")
if not kb.schedule_task(
conn,
tid,
reason=reason,
expected_run_id=_worker_run_id_for(tid),
):
failed.append(tid)
print(f"cannot schedule {tid}", file=sys.stderr)
else:
print(f"Scheduled {tid}" + (f": {reason}" if reason else ""))
return 0 if not failed else 1
def _cmd_unblock(args: argparse.Namespace) -> int:
ids = list(args.task_ids or [])
if not ids:
@@ -1919,7 +1949,7 @@ def _cmd_unblock(args: argparse.Namespace) -> int:
for tid in ids:
if not kb.unblock_task(conn, tid):
failed.append(tid)
print(f"cannot unblock {tid} (not blocked?)", file=sys.stderr)
print(f"cannot unblock {tid} (not blocked/scheduled?)", file=sys.stderr)
else:
print(f"Unblocked {tid}")
return 0 if not failed else 1
@@ -2220,7 +2250,7 @@ def _cmd_stats(args: argparse.Namespace) -> int:
print(json.dumps(stats, indent=2, ensure_ascii=False))
return 0
print("By status:")
for k in ("triage", "todo", "ready", "running", "blocked", "done"):
for k in ("triage", "todo", "scheduled", "ready", "running", "blocked", "done"):
print(f" {k:8s} {stats['by_status'].get(k, 0)}")
if stats["by_assignee"]:
print("\nBy assignee:")
@@ -2556,7 +2586,7 @@ Common subcommands:
`create <title>…` Create a task (auto-subscribes you to events)
`comment <id> <msg>` Append a comment
`complete <id>…` Mark task(s) done
`block <id> [reason]` Mark blocked; `unblock <id>` to revive
`block <id> [reason]` Mark blocked; `schedule <id> [reason]` parks time-delay work; `unblock <id>` to revive
`assign <id> <profile>` Reassign
`boards list` Show all boards
`assignees` Known profiles + counts