diff --git a/state-hub/custodian_cli.py b/state-hub/custodian_cli.py index cabe266..408ad72 100644 --- a/state-hub/custodian_cli.py +++ b/state-hub/custodian_cli.py @@ -161,6 +161,88 @@ def cmd_register(args: argparse.Namespace) -> None: print("Next: restart Claude Code for the MCP server to be active in this project.") +def cmd_create_workstream(args: argparse.Namespace) -> None: + """Create a workstream under a domain's topic.""" + _api_get("/state/health") + + # Resolve topic_id from domain + topics = _api_get("/topics/?status=active") + match = next((t for t in topics if t.get("domain") == args.domain), None) + if not match: + print(f"ERROR: No active topic for domain '{args.domain}'.") + sys.exit(1) + topic_id = match["id"] + + slug = args.slug or re.sub(r"[^a-z0-9]+", "-", args.title.lower()).strip("-") + + ws = _api_post("/workstreams/", { + "topic_id": topic_id, + "title": args.title, + "slug": slug, + "description": args.description, + "owner": args.owner, + "status": "active", + }) + _api_post("/progress/", { + "topic_id": topic_id, + "workstream_id": ws["id"], + "event_type": "workstream_created", + "summary": f"Workstream created: {args.title}", + "author": "custodian", + "detail": {"owner": args.owner, "slug": slug}, + }) + print(f"Created workstream: {ws['title']}") + print(f" id: {ws['id']}") + print(f" slug: {ws['slug']}") + print(f" domain: {args.domain}") + print(f" owner: {ws.get('owner') or 'โ'}") + + +def cmd_create_task(args: argparse.Namespace) -> None: + """Create a task under a workstream (by ID or slug).""" + _api_get("/state/health") + + # Resolve workstream: accept UUID or slug + workstream_id = args.workstream + if not _is_uuid(workstream_id): + wss = _api_get("/workstreams/") + match = next((w for w in wss if w.get("slug") == workstream_id), None) + if not match: + print(f"ERROR: No workstream found with slug '{workstream_id}'.") + print(" Use 'custodian status' or check the dashboard for valid slugs.") + sys.exit(1) + workstream_id = match["id"] + + task = _api_post("/tasks/", { + "workstream_id": workstream_id, + "title": args.title, + "priority": args.priority, + "description": args.description, + "assignee": args.assignee, + }) + _api_post("/progress/", { + "workstream_id": workstream_id, + "task_id": task["id"], + "event_type": "task_created", + "summary": f"Task created: {args.title}", + "author": "custodian", + "detail": {"priority": args.priority}, + }) + print(f"Created task: {task['title']}") + print(f" id: {task['id']}") + print(f" priority: {task['priority']}") + print(f" status: {task['status']}") + + +def _is_uuid(s: str) -> bool: + import uuid as _uuid + try: + _uuid.UUID(s) + return True + except ValueError: + return False + + def cmd_status(_args: argparse.Namespace) -> None: """Quick status: API health + summary totals.""" health = _api_get("/state/health") @@ -202,6 +284,22 @@ def main() -> None: help="Project directory (defaults to current directory)", ) + # create-workstream + cws = sub.add_parser("create-workstream", help="Create a workstream under a domain topic") + cws.add_argument("--domain", choices=VALID_DOMAINS, required=True, help="Domain to create the workstream under") + cws.add_argument("--title", required=True, help="Workstream title") + cws.add_argument("--slug", default=None, help="URL slug (auto-generated from title if omitted)") + cws.add_argument("--owner", default=None, help="Owner name") + cws.add_argument("--description", default=None, help="Optional description") + + # create-task + ctask = sub.add_parser("create-task", help="Create a task under a workstream") + ctask.add_argument("--workstream", required=True, metavar="ID_OR_SLUG", help="Workstream UUID or slug") + ctask.add_argument("--title", required=True, help="Task title") + ctask.add_argument("--priority", choices=["low", "medium", "high", "critical"], default="medium") + ctask.add_argument("--assignee", default=None) + ctask.add_argument("--description", default=None) + # status sub.add_parser("status", help="Show State Hub health and summary totals") @@ -209,6 +307,10 @@ def main() -> None: if args.command == "register-project": cmd_register(args) + elif args.command == "create-workstream": + cmd_create_workstream(args) + elif args.command == "create-task": + cmd_create_task(args) elif args.command == "status": cmd_status(args) diff --git a/state-hub/dashboard/src/index.md b/state-hub/dashboard/src/index.md index f9d3998..1c549df 100644 --- a/state-hub/dashboard/src/index.md +++ b/state-hub/dashboard/src/index.md @@ -134,6 +134,26 @@ display(Plot.plot({ })); ``` +```js +// Registered domains with no workstreams yet โ show a getting-started hint +const regs = regsState ?? []; +const registeredDomains = new Set(regs.map(e => e.detail?.domain).filter(Boolean)); +const emptyRegistered = (summary.topics ?? []).filter(t => + registeredDomains.has(t.domain) && (t.workstreams ?? []).length === 0 +); + +if (emptyRegistered.length > 0) { + display(html`
These registered projects have no workstreams yet:
+custodian create-workstream --domain ${t.domain} --title "My first workstream"
+