mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-03 07:21:54 +00:00
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:
parent
b5c1fe78aa
commit
e3823657d6
8 changed files with 149 additions and 14 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2969,7 +2969,7 @@ def block_task(
|
|||
|
||||
|
||||
def unblock_task(conn: sqlite3.Connection, task_id: str) -> bool:
|
||||
"""Transition ``blocked -> ready``.
|
||||
"""Transition ``blocked``/``scheduled`` -> ready or todo.
|
||||
|
||||
Defensively closes any stale ``current_run_id`` pointer before flipping
|
||||
status. In the common path (``block_task`` closed the run already) this
|
||||
|
|
@ -2981,7 +2981,7 @@ def unblock_task(conn: sqlite3.Connection, task_id: str) -> bool:
|
|||
now = int(time.time())
|
||||
with write_txn(conn):
|
||||
stale = conn.execute(
|
||||
"SELECT current_run_id FROM tasks WHERE id = ? AND status = 'blocked'",
|
||||
"SELECT current_run_id FROM tasks WHERE id = ? AND status IN ('blocked', 'scheduled')",
|
||||
(task_id,),
|
||||
).fetchone()
|
||||
if stale and stale["current_run_id"]:
|
||||
|
|
@ -3012,7 +3012,7 @@ def unblock_task(conn: sqlite3.Connection, task_id: str) -> bool:
|
|||
cur = conn.execute(
|
||||
"UPDATE tasks SET status = ?, current_run_id = NULL, "
|
||||
"consecutive_failures = 0, last_failure_error = NULL "
|
||||
"WHERE id = ? AND status = 'blocked'",
|
||||
"WHERE id = ? AND status IN ('blocked', 'scheduled')",
|
||||
(new_status, task_id),
|
||||
)
|
||||
if cur.rowcount != 1:
|
||||
|
|
@ -3447,6 +3447,51 @@ def set_workspace_path(
|
|||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
def schedule_task(
|
||||
conn: sqlite3.Connection,
|
||||
task_id: str,
|
||||
*,
|
||||
reason: Optional[str] = None,
|
||||
expected_run_id: Optional[int] = None,
|
||||
) -> bool:
|
||||
"""Park a task in ``scheduled`` so it is waiting on time, not human input.
|
||||
|
||||
``scheduled`` tasks are intentionally not dispatchable; an external cron,
|
||||
human action, or automation can later call ``unblock_task`` to re-gate them
|
||||
to ``ready`` (or ``todo`` if parents are still incomplete).
|
||||
"""
|
||||
with write_txn(conn):
|
||||
params: list[Any] = [task_id]
|
||||
sql = """
|
||||
UPDATE tasks
|
||||
SET status = 'scheduled',
|
||||
claim_lock = NULL,
|
||||
claim_expires= NULL,
|
||||
worker_pid = NULL
|
||||
WHERE id = ?
|
||||
AND status IN ('todo', 'ready', 'running', 'blocked')
|
||||
"""
|
||||
if expected_run_id is not None:
|
||||
sql += " AND current_run_id = ?"
|
||||
params.append(int(expected_run_id))
|
||||
cur = conn.execute(sql, params)
|
||||
if cur.rowcount != 1:
|
||||
return False
|
||||
run_id = _end_run(
|
||||
conn, task_id,
|
||||
outcome="scheduled", status="scheduled",
|
||||
summary=reason,
|
||||
)
|
||||
if run_id is None and reason:
|
||||
run_id = _synthesize_ended_run(
|
||||
conn, task_id,
|
||||
outcome="scheduled",
|
||||
summary=reason,
|
||||
)
|
||||
_append_event(conn, task_id, "scheduled", {"reason": reason}, run_id=run_id)
|
||||
return True
|
||||
|
||||
|
||||
# Dispatcher (one-shot pass)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue