LoveLogicAI's picture
Upload folder using huggingface_hub
587a933 verified
Raw History Blame Contribute Delete
5.15 kB
#!/usr/bin/env python3
"""
kb β€” the terminal face of KanBot.
One thin client over the same board operations agents and Telegram use. Drive the
whole autonomous board from your shell:
kb board # the whole board, column by column
kb col ready # list one column
kb card CARD-42 # full detail incl. inter-agent thread
kb new "title" --spec "..." [--planner] [--criteria a b] [--heavy]
kb move CARD-42 review [--reason "..."]
kb comment CARD-42 "note"
kb approve CARD-42 # approve a dry-run tool-call proposal
kb reject CARD-42 "reason"
kb block CARD-42 "reason"
kb reap # run one reaper sweep now
Reads DATABASE_URL from env (see .env). Autonomous cards move themselves once the
Conductor is running; this is for you to seed, inspect, and gate.
"""
from __future__ import annotations
import argparse
import asyncio
import json
import sys
# allow running as `python cli/kb.py` from repo root
sys.path.insert(0, ".")
from core import db # noqa: E402
from core.contract import Column, Role # noqa: E402
from core.mcp_server import ( # noqa: E402
block_card,
comment,
create_card,
get_card,
list_column,
move_card,
)
from core.reaper import sweep_once # noqa: E402
from core.workers import approve_and_run # noqa: E402
COLS = [c.value for c in Column]
_ICON = {"backlog": "Β·", "ready": "β—¦", "in-flight": "β–Έ", "review": "?",
"blocked": "βœ•", "done": "βœ“"}
async def cmd_board(_a) -> None:
for col in COLS:
cards = await db.list_column(Column(col))
print(f"\n{_ICON[col]} {col.upper()} ({len(cards)})")
for c in cards:
tag = f" [{','.join(c.tags)}]" if c.tags else ""
hum = " βš‘" if c.requires_human else ""
print(f" {c.id:<9} {c.title[:56]:<56}{tag}{hum}")
async def cmd_col(a) -> None:
print(await list_column(a.column))
async def cmd_card(a) -> None:
data = json.loads(await get_card(a.id))
if "error" in data:
print(data["error"]); return
print(f"{data['id']} [{data['column']}] role={data['role']} "
f"attempt={data['attempt']}")
print(f" {data['title']}")
if data["spec"]:
print(f" spec: {data['spec']}")
if data["acceptance_criteria"]:
print(" acceptance:")
for c in data["acceptance_criteria"]:
print(f" - {c}")
print(" ── thread ──")
for e in data["thread"]:
print(f" [{e['kind']}] {e['author']}: {e['content'][:200]}")
async def cmd_new(a) -> None:
res = json.loads(await create_card(
title=a.title, spec=a.spec or "",
role="planner" if a.planner else "executor",
acceptance_criteria=a.criteria or [],
tags=["heavy"] if a.heavy else []))
print(f"created {res['id']} in {res['column']}"
+ (" (planner will decompose once Conductor runs)" if a.planner else ""))
async def cmd_move(a) -> None:
print(await move_card(a.id, a.to, reason=a.reason or ""))
async def cmd_comment(a) -> None:
print(await comment(a.id, a.text))
async def cmd_approve(a) -> None:
await approve_and_run(a.id)
print(f"approved {a.id} β€” requeued to ready")
async def cmd_reject(a) -> None:
print(await block_card(a.id, a.reason, actor="human:remy"))
async def cmd_block(a) -> None:
print(await block_card(a.id, a.reason, actor="human:remy"))
async def cmd_reap(_a) -> None:
n = await sweep_once()
print(f"reaper reclaimed {n} card(s)")
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(prog="kb", description="KanBot board CLI")
sub = p.add_subparsers(dest="cmd", required=True)
sub.add_parser("board").set_defaults(fn=cmd_board)
c = sub.add_parser("col"); c.add_argument("column", choices=COLS); c.set_defaults(fn=cmd_col)
c = sub.add_parser("card"); c.add_argument("id"); c.set_defaults(fn=cmd_card)
c = sub.add_parser("new")
c.add_argument("title")
c.add_argument("--spec", default="")
c.add_argument("--planner", action="store_true", help="route to planner for decomposition")
c.add_argument("--criteria", nargs="*", default=[])
c.add_argument("--heavy", action="store_true", help="tag for cloud executor escalation")
c.set_defaults(fn=cmd_new)
c = sub.add_parser("move"); c.add_argument("id"); c.add_argument("to", choices=COLS)
c.add_argument("--reason", default=""); c.set_defaults(fn=cmd_move)
c = sub.add_parser("comment"); c.add_argument("id"); c.add_argument("text"); c.set_defaults(fn=cmd_comment)
c = sub.add_parser("approve"); c.add_argument("id"); c.set_defaults(fn=cmd_approve)
c = sub.add_parser("reject"); c.add_argument("id"); c.add_argument("reason"); c.set_defaults(fn=cmd_reject)
c = sub.add_parser("block"); c.add_argument("id"); c.add_argument("reason"); c.set_defaults(fn=cmd_block)
sub.add_parser("reap").set_defaults(fn=cmd_reap)
return p
def main() -> None:
args = build_parser().parse_args()
asyncio.run(args.fn(args))
if __name__ == "__main__":
main()