feat(cli): add kanban swarm topology helper

Salvages #26791 by @Niraven. Adds 'hermes kanban swarm' to create a
durable Kanban Swarm v1 graph: a completed root/blackboard card,
parallel worker cards, a verifier gated on all workers, and a
synthesizer gated on the verifier. Stores shared swarm blackboard
updates as structured JSON comments on the root card.

Self-contained: new hermes_cli/kanban_swarm.py module + CLI wiring +
unit tests.
This commit is contained in:
Niraven
2026-05-18 21:10:12 -07:00
committed by Teknium
parent 79f6654d16
commit 3ee7a5546d
3 changed files with 451 additions and 0 deletions
+54
View File
@@ -24,6 +24,7 @@ from pathlib import Path
from typing import Any, Optional
from hermes_cli import kanban_db as kb
from hermes_cli import kanban_swarm as ks
from hermes_cli.profiles import get_active_profile_name, get_profile_dir, seed_profile_skills
@@ -313,6 +314,27 @@ def build_parser(parent_subparsers: argparse._SubParsersAction) -> argparse.Argu
"to skip the brief running-to-blocked transition.")
p_create.add_argument("--json", action="store_true", help="Emit JSON output")
# --- swarm ---
p_swarm = sub.add_parser(
"swarm",
help="Create a Kanban Swarm v1 graph (parallel workers → verifier → synthesizer)",
)
p_swarm.add_argument("goal", help="Swarm goal / final outcome")
p_swarm.add_argument(
"--worker",
action="append",
default=[],
metavar="PROFILE:TITLE[:SKILL,SKILL]",
help="Parallel worker card (repeatable)",
)
p_swarm.add_argument("--verifier", required=True, help="Verifier profile")
p_swarm.add_argument("--synthesizer", required=True, help="Synthesizer/writer profile")
p_swarm.add_argument("--tenant", default=None, help="Tenant namespace")
p_swarm.add_argument("--priority", type=int, default=0, help="Priority tiebreaker")
p_swarm.add_argument("--created-by", default=None, help="Creator/anchor profile")
p_swarm.add_argument("--idempotency-key", default=None, help="Dedup key for the root card")
p_swarm.add_argument("--json", action="store_true", help="Emit JSON output")
# --- list ---
p_list = sub.add_parser("list", aliases=["ls"], help="List tasks")
p_list.add_argument("--mine", action="store_true",
@@ -778,6 +800,7 @@ def kanban_command(args: argparse.Namespace) -> int:
handlers = {
"init": _cmd_init,
"create": _cmd_create,
"swarm": _cmd_swarm,
"list": _cmd_list,
"ls": _cmd_list,
"show": _cmd_show,
@@ -1212,6 +1235,37 @@ def _cmd_create(args: argparse.Namespace) -> int:
return 0
def _cmd_swarm(args: argparse.Namespace) -> int:
try:
workers = [ks.parse_worker_arg(raw) for raw in (args.worker or [])]
except ValueError as exc:
print(f"kanban swarm: {exc}", file=sys.stderr)
return 2
if not workers:
print("kanban swarm: at least one --worker is required", file=sys.stderr)
return 2
with kb.connect() as conn:
created = ks.create_swarm(
conn,
goal=args.goal,
workers=workers,
verifier_assignee=args.verifier,
synthesizer_assignee=args.synthesizer,
tenant=args.tenant,
created_by=args.created_by or _profile_author(),
priority=args.priority,
idempotency_key=getattr(args, "idempotency_key", None),
)
if getattr(args, "json", False):
print(json.dumps(created.as_dict(), indent=2, ensure_ascii=False))
else:
print(f"Swarm root: {created.root_id}")
print("Workers: " + ", ".join(created.worker_ids))
print(f"Verifier: {created.verifier_id}")
print(f"Synthesizer: {created.synthesizer_id}")
return 0
def _cmd_list(args: argparse.Namespace) -> int:
assignee = args.assignee
if args.mine and not assignee: