generated from coulomb/repo-seed
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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`<div class="hint-box">
|
||||
<strong>💡 Getting started</strong>
|
||||
<p>These registered projects have no workstreams yet:</p>
|
||||
<ul>${emptyRegistered.map(t => html`<li>
|
||||
<strong>${t.domain}</strong> — open the repo in Claude Code and ask the Custodian to create one, or run:<br>
|
||||
<code>custodian create-workstream --domain ${t.domain} --title "My first workstream"</code>
|
||||
</li>`)}</ul>
|
||||
</div>`);
|
||||
}
|
||||
```
|
||||
|
||||
## Blocking Decisions
|
||||
|
||||
```js
|
||||
@@ -183,4 +203,6 @@ display(Inputs.table((summary.recent_progress ?? []).map(e => ({
|
||||
.card.warn { border: 2px solid orange; }
|
||||
.big-num { font-size: 2.5rem; font-weight: bold; margin: 0.25rem 0; }
|
||||
.warning { background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; padding: 0.75rem; }
|
||||
.hint-box { background: var(--theme-background-alt); border-left: 3px solid steelblue; border-radius: 4px; padding: 0.75rem 1rem; margin-top: 0.75rem; font-size: 0.9rem; }
|
||||
.hint-box code { background: var(--theme-background); padding: 0.15rem 0.4rem; border-radius: 3px; font-size: 0.85rem; }
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# State Hub MCP — Tool Reference Card
|
||||
|
||||
Quick reference for all 11 tools and 5 resources. Read this instead of `server.py`.
|
||||
Quick reference for all 12 tools and 5 resources. Read this instead of `server.py`.
|
||||
|
||||
## Query Tools (read-only)
|
||||
|
||||
@@ -16,6 +16,7 @@ Quick reference for all 11 tools and 5 resources. Read this instead of `server.p
|
||||
|
||||
| Tool | Key Args | Notes |
|
||||
|------|----------|-------|
|
||||
| `create_workstream(topic_id, title, ...)` | `slug?` (auto-generated); `owner?`; `description?`; `due_date?` | Creates workstream under a topic. Use `get_state_summary()` to find topic IDs. |
|
||||
| `create_task(workstream_id, title, ...)` | `priority`: low/medium/high/critical; `assignee?`; `due_date?` | Creates task under a workstream. |
|
||||
| `update_task_status(task_id, status, ...)` | `status`: todo/in_progress/blocked/done/cancelled; `blocking_reason` required when blocked | |
|
||||
| `record_decision(title, ...)` | `decision_type`: made/pending; `topic_id?`; `workstream_id?`; `deadline?` | Financial/legal + pending → auto-escalated per constitution §4. At least one of topic_id/workstream_id required. |
|
||||
@@ -40,8 +41,11 @@ Quick reference for all 11 tools and 5 resources. Read this instead of `server.p
|
||||
## Common Patterns
|
||||
|
||||
```python
|
||||
# New workstream (via API — no MCP tool yet):
|
||||
# POST /workstreams/ {"topic_id": "...", "slug": "...", "title": "...", "status": "active", "owner": "..."}
|
||||
# New workstream:
|
||||
create_workstream(topic_id="<uuid>", title="My Workstream", owner="me")
|
||||
|
||||
# New task:
|
||||
create_task(workstream_id="<uuid>", title="Do the thing", priority="high")
|
||||
|
||||
# Session start ritual:
|
||||
get_state_summary()
|
||||
|
||||
@@ -7,6 +7,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
@@ -156,6 +157,47 @@ def get_recent_progress(limit: int = 20, since: str | None = None) -> str:
|
||||
# Mutate tools
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@mcp.tool()
|
||||
def create_workstream(
|
||||
topic_id: str,
|
||||
title: str,
|
||||
slug: str | None = None,
|
||||
description: str | None = None,
|
||||
owner: str | None = None,
|
||||
due_date: str | None = None,
|
||||
) -> str:
|
||||
"""Create a new workstream under a topic and emit a progress_event.
|
||||
|
||||
Args:
|
||||
topic_id: UUID of the parent topic
|
||||
title: workstream title
|
||||
slug: URL-friendly identifier (auto-generated from title if omitted)
|
||||
description: optional longer description
|
||||
owner: optional owner name
|
||||
due_date: optional ISO date string (YYYY-MM-DD)
|
||||
"""
|
||||
if not slug:
|
||||
slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")
|
||||
ws = _post("/workstreams", {
|
||||
"topic_id": topic_id,
|
||||
"title": title,
|
||||
"slug": slug,
|
||||
"description": description,
|
||||
"owner": owner,
|
||||
"due_date": due_date,
|
||||
"status": "active",
|
||||
})
|
||||
_post("/progress", {
|
||||
"topic_id": topic_id,
|
||||
"workstream_id": ws["id"],
|
||||
"event_type": "workstream_created",
|
||||
"summary": f"Workstream created: {title}",
|
||||
"author": "custodian",
|
||||
"detail": {"owner": owner, "slug": slug},
|
||||
})
|
||||
return json.dumps(ws, indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def create_task(
|
||||
workstream_id: str,
|
||||
|
||||
Reference in New Issue
Block a user