Add create-workstream: MCP tool, CLI commands, dashboard hint

MCP server: add create_workstream(topic_id, title, slug?, owner?,
  description?, due_date?) — auto-generates slug from title if omitted;
  emits workstream_created progress event. Now 12 tools total.

CLI: add two new subcommands —
  custodian create-workstream --domain DOMAIN --title TITLE [--slug] [--owner] [--description]
  custodian create-task --workstream ID_OR_SLUG --title TITLE [--priority] [--assignee]
  create-task accepts workstream UUID or slug (resolves via API).

Dashboard: hint box below "Open Workstreams by Domain" chart listing
  registered domains that have zero workstreams, with the exact
  custodian create-workstream command to run.

TOOLS.md: updated tool count (11 → 12) and added create_workstream row.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 23:35:54 +01:00
parent 34b1114a01
commit ebe7369249
4 changed files with 173 additions and 3 deletions

View File

@@ -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)