generated from coulomb/repo-seed
feat(classification-spine): implement STATE-WP-0065 repo-anchored model
Replace the ad-hoc coordination-domain spine with the Repo Classification Standard: 14 market domains, classification columns on managed_repos, and workplans anchored by repo_id (topic_id optional). - Add Alembic migration d8e9f0a1b2c3 with data backfill and workstream→workplan rename - Add api/classification.py validation and register-from-classification tooling - Expose workplan-first REST/MCP surface with legacy workstream aliases - Add C-24 consistency rule and legacy domain frontmatter mapping - Update dashboard repos page with category/capability/stake filters - Update orientation docs; mark STATE-WP-0065 finished
This commit is contained in:
@@ -358,18 +358,29 @@ def create_topic(slug: str, title: str, domain: str, description: str | None = N
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_tasks(workstream_id: str, status: str | None = None) -> str:
|
||||
"""List all tasks in a workstream, optionally filtered by status.
|
||||
def list_tasks(
|
||||
workplan_id: str | None = None,
|
||||
workstream_id: str | None = None,
|
||||
status: str | None = None,
|
||||
) -> str:
|
||||
"""List all tasks in a workplan, optionally filtered by status.
|
||||
|
||||
Args:
|
||||
workstream_id: UUID of the workstream (required).
|
||||
workplan_id: UUID of the workplan (preferred).
|
||||
workstream_id: legacy alias for workplan_id.
|
||||
status: Optional filter — wait | todo | progress | done | cancel.
|
||||
|
||||
Returns [{id, title, status, priority, assignee, due_date, needs_human}] for every
|
||||
matching task. Use this to look up task UUIDs before calling update_task_status,
|
||||
or to check which tasks from a workplan file are already synced to the DB.
|
||||
"""
|
||||
return json.dumps(_get("/tasks", {"workstream_id": workstream_id, "status": status}), indent=2)
|
||||
parent_id = workplan_id or workstream_id
|
||||
if not parent_id:
|
||||
return _json_result(_mcp_error("list_tasks", "workplan_id is required"))
|
||||
return json.dumps(
|
||||
_get("/tasks", {"workplan_id": parent_id, "workstream_id": parent_id, "status": status}),
|
||||
indent=2,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -455,37 +466,30 @@ def advance_workstation(entity_type: str, entity_id: str, target_workstation: st
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Mutate tools
|
||||
# Workplan helpers (preferred) + legacy workstream aliases
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@mcp.tool()
|
||||
def create_workstream(
|
||||
topic_id: str,
|
||||
def _workplan_id_from_response(payload: dict[str, Any]) -> str | None:
|
||||
return payload.get("workplan_id") or payload.get("workstream_id") or payload.get("id")
|
||||
|
||||
|
||||
def _create_workplan_impl(
|
||||
*,
|
||||
repo_id: str,
|
||||
title: str,
|
||||
topic_id: str | None = None,
|
||||
slug: str | None = None,
|
||||
description: str | None = None,
|
||||
owner: str | None = None,
|
||||
due_date: str | None = None,
|
||||
repo_id: str | None = None,
|
||||
planning_priority: str | None = None,
|
||||
planning_order: int | None = None,
|
||||
tool_name: str = "create_workplan",
|
||||
) -> 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)
|
||||
repo_id: UUID of the owning repository (GEMS primary; strongly recommended per ADR-001)
|
||||
planning_priority: optional planning priority (critical/high/medium/low or repo-local value)
|
||||
planning_order: optional numeric ordering hint inside a repo/domain
|
||||
"""
|
||||
if not slug:
|
||||
slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")
|
||||
ws = _post("/workstreams", {
|
||||
wp = _post("/workplans", {
|
||||
"repo_id": repo_id,
|
||||
"topic_id": topic_id,
|
||||
"title": title,
|
||||
"slug": slug,
|
||||
@@ -493,30 +497,147 @@ def create_workstream(
|
||||
"owner": owner,
|
||||
"due_date": due_date,
|
||||
"status": "active",
|
||||
"repo_id": repo_id,
|
||||
"planning_priority": planning_priority,
|
||||
"planning_order": planning_order,
|
||||
})
|
||||
if error := _response_error("create_workstream", ws, ("id",)):
|
||||
if error := _response_error(tool_name, wp, ("id",)):
|
||||
return _json_result(error)
|
||||
|
||||
progress_error = _emit_progress_event("create_workstream", ws, {
|
||||
progress_error = _emit_progress_event(tool_name, wp, {
|
||||
"topic_id": topic_id,
|
||||
"workstream_id": ws["id"],
|
||||
"event_type": "workstream_created",
|
||||
"summary": f"Workstream created: {title}",
|
||||
"workplan_id": wp["id"],
|
||||
"workstream_id": wp["id"],
|
||||
"event_type": "workplan_created",
|
||||
"summary": f"Workplan created: {title}",
|
||||
"author": "custodian",
|
||||
"detail": {"owner": owner, "slug": slug},
|
||||
"detail": {"owner": owner, "slug": slug, "repo_id": repo_id},
|
||||
})
|
||||
if progress_error:
|
||||
return _json_result(progress_error)
|
||||
return _json_result(ws)
|
||||
return _json_result(wp)
|
||||
|
||||
|
||||
def _update_workplan_status_impl(workplan_id: str, status: str, *, tool_name: str) -> str:
|
||||
wp = _patch(f"/workplans/{workplan_id}", {"status": status})
|
||||
if error := _response_error(tool_name, wp, ("id", "title")):
|
||||
return _json_result(error)
|
||||
|
||||
progress_error = _emit_progress_event(tool_name, wp, {
|
||||
"workplan_id": workplan_id,
|
||||
"workstream_id": workplan_id,
|
||||
"topic_id": wp.get("topic_id"),
|
||||
"event_type": "workplan_status_changed",
|
||||
"summary": f"Workplan status → {status}: {wp['title']}",
|
||||
"author": "custodian",
|
||||
})
|
||||
if progress_error:
|
||||
return _json_result(progress_error)
|
||||
return _json_result(wp)
|
||||
|
||||
|
||||
def _update_workplan_impl(
|
||||
workplan_id: str,
|
||||
*,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
owner: str | None = None,
|
||||
due_date: str | None = None,
|
||||
repo_goal_id: str | None = None,
|
||||
status: str | None = None,
|
||||
) -> str:
|
||||
payload: dict[str, Any] = {}
|
||||
if title is not None:
|
||||
payload["title"] = title
|
||||
if description is not None:
|
||||
payload["description"] = description
|
||||
if owner is not None:
|
||||
payload["owner"] = owner
|
||||
if due_date is not None:
|
||||
payload["due_date"] = due_date
|
||||
if status is not None:
|
||||
payload["status"] = status
|
||||
if repo_goal_id is not None:
|
||||
payload["repo_goal_id"] = repo_goal_id if repo_goal_id else None
|
||||
return _json_result(_patch(f"/workplans/{workplan_id}", payload))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Mutate tools
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@mcp.tool()
|
||||
def create_workplan(
|
||||
repo_id: str,
|
||||
title: str,
|
||||
topic_id: str | None = None,
|
||||
slug: str | None = None,
|
||||
description: str | None = None,
|
||||
owner: str | None = None,
|
||||
due_date: str | None = None,
|
||||
planning_priority: str | None = None,
|
||||
planning_order: int | None = None,
|
||||
) -> str:
|
||||
"""Create a new repo-anchored workplan and emit a progress_event.
|
||||
|
||||
Args:
|
||||
repo_id: UUID of the owning repository (required)
|
||||
title: workplan title
|
||||
topic_id: optional topic UUID for cross-repo tagging
|
||||
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)
|
||||
planning_priority: optional planning priority (critical/high/medium/low or repo-local value)
|
||||
planning_order: optional numeric ordering hint inside a repo
|
||||
"""
|
||||
return _create_workplan_impl(
|
||||
repo_id=repo_id,
|
||||
title=title,
|
||||
topic_id=topic_id,
|
||||
slug=slug,
|
||||
description=description,
|
||||
owner=owner,
|
||||
due_date=due_date,
|
||||
planning_priority=planning_priority,
|
||||
planning_order=planning_order,
|
||||
tool_name="create_workplan",
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def create_workstream(
|
||||
title: str,
|
||||
repo_id: str | None = None,
|
||||
topic_id: str | None = None,
|
||||
slug: str | None = None,
|
||||
description: str | None = None,
|
||||
owner: str | None = None,
|
||||
due_date: str | None = None,
|
||||
planning_priority: str | None = None,
|
||||
planning_order: int | None = None,
|
||||
) -> str:
|
||||
"""Legacy alias for create_workplan — prefer create_workplan(repo_id=...)."""
|
||||
if not repo_id:
|
||||
return _json_result(_mcp_error("create_workstream", "repo_id is required"))
|
||||
return _create_workplan_impl(
|
||||
repo_id=repo_id,
|
||||
title=title,
|
||||
topic_id=topic_id,
|
||||
slug=slug,
|
||||
description=description,
|
||||
owner=owner,
|
||||
due_date=due_date,
|
||||
planning_priority=planning_priority,
|
||||
planning_order=planning_order,
|
||||
tool_name="create_workstream",
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def create_task(
|
||||
workstream_id: str,
|
||||
title: str,
|
||||
workplan_id: str | None = None,
|
||||
workstream_id: str | None = None,
|
||||
title: str = "",
|
||||
priority: str = "medium",
|
||||
description: str | None = None,
|
||||
assignee: str | None = None,
|
||||
@@ -525,15 +646,19 @@ def create_task(
|
||||
"""Create a new task and emit a progress_event.
|
||||
|
||||
Args:
|
||||
workstream_id: UUID of the parent workstream
|
||||
workplan_id: UUID of the parent workplan (preferred)
|
||||
workstream_id: legacy alias for workplan_id
|
||||
title: task title
|
||||
priority: low | medium | high | critical
|
||||
description: optional longer description
|
||||
assignee: optional assignee name
|
||||
due_date: optional ISO date string (YYYY-MM-DD)
|
||||
"""
|
||||
parent_id = workplan_id or workstream_id
|
||||
if not parent_id:
|
||||
return _json_result(_mcp_error("create_task", "workplan_id is required"))
|
||||
task = _post("/tasks", {
|
||||
"workstream_id": workstream_id,
|
||||
"workplan_id": parent_id,
|
||||
"title": title,
|
||||
"priority": priority,
|
||||
"description": description,
|
||||
@@ -544,7 +669,8 @@ def create_task(
|
||||
return _json_result(error)
|
||||
|
||||
progress_error = _emit_progress_event("create_task", task, {
|
||||
"workstream_id": workstream_id,
|
||||
"workplan_id": parent_id,
|
||||
"workstream_id": parent_id,
|
||||
"task_id": task["id"],
|
||||
"event_type": "task_created",
|
||||
"summary": f"Task created: {title}",
|
||||
@@ -865,27 +991,81 @@ def add_progress_event(
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def update_workstream_status(workstream_id: str, status: str) -> str:
|
||||
"""Update a workstream's status.
|
||||
def list_workplans(
|
||||
repo_id: str | None = None,
|
||||
topic_id: str | None = None,
|
||||
status: str | None = None,
|
||||
owner: str | None = None,
|
||||
slug: str | None = None,
|
||||
) -> str:
|
||||
"""List workplans with optional filters."""
|
||||
return json.dumps(
|
||||
_get("/workplans", {
|
||||
"repo_id": repo_id,
|
||||
"topic_id": topic_id,
|
||||
"status": status,
|
||||
"owner": owner,
|
||||
"slug": slug,
|
||||
}),
|
||||
indent=2,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_workstreams(
|
||||
topic_id: str | None = None,
|
||||
repo_id: str | None = None,
|
||||
status: str | None = None,
|
||||
owner: str | None = None,
|
||||
slug: str | None = None,
|
||||
) -> str:
|
||||
"""Legacy alias for list_workplans."""
|
||||
return list_workplans(
|
||||
repo_id=repo_id,
|
||||
topic_id=topic_id,
|
||||
status=status,
|
||||
owner=owner,
|
||||
slug=slug,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def update_workplan_status(workplan_id: str, status: str) -> str:
|
||||
"""Update a workplan's status.
|
||||
|
||||
Args:
|
||||
workstream_id: UUID of the workstream
|
||||
workplan_id: UUID of the workplan
|
||||
status: proposed | ready | active | blocked | backlog | finished | archived
|
||||
"""
|
||||
ws = _patch(f"/workstreams/{workstream_id}", {"status": status})
|
||||
if error := _response_error("update_workstream_status", ws, ("id", "title")):
|
||||
return _json_result(error)
|
||||
return _update_workplan_status_impl(workplan_id, status, tool_name="update_workplan_status")
|
||||
|
||||
progress_error = _emit_progress_event("update_workstream_status", ws, {
|
||||
"workstream_id": workstream_id,
|
||||
"topic_id": ws.get("topic_id"),
|
||||
"event_type": "workstream_status_changed",
|
||||
"summary": f"Workstream status → {status}: {ws['title']}",
|
||||
"author": "custodian",
|
||||
})
|
||||
if progress_error:
|
||||
return _json_result(progress_error)
|
||||
return _json_result(ws)
|
||||
|
||||
@mcp.tool()
|
||||
def update_workstream_status(workstream_id: str, status: str) -> str:
|
||||
"""Legacy alias for update_workplan_status."""
|
||||
return _update_workplan_status_impl(workstream_id, status, tool_name="update_workstream_status")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def update_workplan(
|
||||
workplan_id: str,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
owner: str | None = None,
|
||||
due_date: str | None = None,
|
||||
repo_goal_id: str | None = None,
|
||||
status: str | None = None,
|
||||
) -> str:
|
||||
"""Update fields on an existing workplan."""
|
||||
return _update_workplan_impl(
|
||||
workplan_id,
|
||||
title=title,
|
||||
description=description,
|
||||
owner=owner,
|
||||
due_date=due_date,
|
||||
repo_goal_id=repo_goal_id,
|
||||
status=status,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -898,32 +1078,16 @@ def update_workstream(
|
||||
repo_goal_id: str | None = None,
|
||||
status: str | None = None,
|
||||
) -> str:
|
||||
"""Update fields on an existing workstream.
|
||||
|
||||
Args:
|
||||
workstream_id: UUID of the workstream
|
||||
title: new title (optional)
|
||||
description: new description (optional)
|
||||
owner: new owner (optional)
|
||||
due_date: ISO date string YYYY-MM-DD (optional)
|
||||
repo_goal_id: UUID of the repo goal to link (optional; pass empty string to clear)
|
||||
status: proposed | ready | active | blocked | backlog | finished | archived (optional)
|
||||
"""
|
||||
payload: dict = {}
|
||||
if title is not None:
|
||||
payload["title"] = title
|
||||
if description is not None:
|
||||
payload["description"] = description
|
||||
if owner is not None:
|
||||
payload["owner"] = owner
|
||||
if due_date is not None:
|
||||
payload["due_date"] = due_date
|
||||
if status is not None:
|
||||
payload["status"] = status
|
||||
if repo_goal_id is not None:
|
||||
payload["repo_goal_id"] = repo_goal_id if repo_goal_id else None
|
||||
ws = _patch(f"/workstreams/{workstream_id}", payload)
|
||||
return json.dumps(ws, indent=2)
|
||||
"""Legacy alias for update_workplan."""
|
||||
return _update_workplan_impl(
|
||||
workstream_id,
|
||||
title=title,
|
||||
description=description,
|
||||
owner=owner,
|
||||
due_date=due_date,
|
||||
repo_goal_id=repo_goal_id,
|
||||
status=status,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -951,6 +1115,41 @@ def get_next_steps() -> str:
|
||||
# Dependency graph tools (S1.4)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _create_dependency_impl(
|
||||
*,
|
||||
from_workplan_id: str,
|
||||
to_workplan_id: str | None = None,
|
||||
to_task_id: str | None = None,
|
||||
relationship_type: str = "blocks",
|
||||
description: str | None = None,
|
||||
) -> str:
|
||||
dep = _post(f"/workplans/{from_workplan_id}/dependencies", {
|
||||
"to_workplan_id": to_workplan_id,
|
||||
"to_task_id": to_task_id,
|
||||
"relationship_type": relationship_type,
|
||||
"description": description,
|
||||
})
|
||||
return json.dumps(dep, indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def create_workplan_dependency(
|
||||
from_workplan_id: str,
|
||||
to_workplan_id: str | None = None,
|
||||
to_task_id: str | None = None,
|
||||
relationship_type: str = "blocks",
|
||||
description: str | None = None,
|
||||
) -> str:
|
||||
"""Record that one workplan depends on another workplan or task."""
|
||||
return _create_dependency_impl(
|
||||
from_workplan_id=from_workplan_id,
|
||||
to_workplan_id=to_workplan_id,
|
||||
to_task_id=to_task_id,
|
||||
relationship_type=relationship_type,
|
||||
description=description,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def create_dependency(
|
||||
from_workstream_id: str,
|
||||
@@ -959,25 +1158,14 @@ def create_dependency(
|
||||
relationship_type: str = "blocks",
|
||||
description: str | None = None,
|
||||
) -> str:
|
||||
"""Record that one workstream depends on another workstream or task.
|
||||
|
||||
Semantics: from_workstream cannot fully proceed until the target reaches
|
||||
a satisfactory state. Provide exactly one of to_workstream_id or to_task_id.
|
||||
|
||||
Args:
|
||||
from_workstream_id: UUID of the workstream that has the dependency
|
||||
to_workstream_id: UUID of the workstream it depends on
|
||||
to_task_id: UUID of the task it depends on
|
||||
relationship_type: blocks | starts_after | informs | soft_dependency
|
||||
description: optional human-readable explanation of the dependency
|
||||
"""
|
||||
dep = _post(f"/workstreams/{from_workstream_id}/dependencies", {
|
||||
"to_workstream_id": to_workstream_id,
|
||||
"to_task_id": to_task_id,
|
||||
"relationship_type": relationship_type,
|
||||
"description": description,
|
||||
})
|
||||
return json.dumps(dep, indent=2)
|
||||
"""Legacy alias for create_workplan_dependency."""
|
||||
return _create_dependency_impl(
|
||||
from_workplan_id=from_workstream_id,
|
||||
to_workplan_id=to_workstream_id,
|
||||
to_task_id=to_task_id,
|
||||
relationship_type=relationship_type,
|
||||
description=description,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -990,9 +1178,15 @@ def list_dependencies(workstream_id: str) -> str:
|
||||
Args:
|
||||
workstream_id: UUID of the workstream to inspect
|
||||
"""
|
||||
edges = _get(f"/workstreams/{workstream_id}/dependencies")
|
||||
depends_on = [e for e in edges if e["from_workstream_id"] == workstream_id]
|
||||
blocks = [e for e in edges if e.get("to_workstream_id") == workstream_id]
|
||||
edges = _get(f"/workplans/{workstream_id}/dependencies")
|
||||
depends_on = [
|
||||
e for e in edges
|
||||
if e.get("from_workplan_id", e.get("from_workstream_id")) == workstream_id
|
||||
]
|
||||
blocks = [
|
||||
e for e in edges
|
||||
if e.get("to_workplan_id", e.get("to_workstream_id")) == workstream_id
|
||||
]
|
||||
return json.dumps({"depends_on": depends_on, "blocks": blocks}, indent=2)
|
||||
|
||||
|
||||
@@ -1227,13 +1421,48 @@ def archive_domain(slug: str) -> str:
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_domain_repos(domain_slug: str) -> str:
|
||||
"""List all repositories registered under a domain.
|
||||
def list_domain_repos(
|
||||
domain_slug: str,
|
||||
category: str | None = None,
|
||||
capability_tag: str | None = None,
|
||||
business_stake: str | None = None,
|
||||
) -> str:
|
||||
"""List repositories registered under a domain, with optional classification filters.
|
||||
|
||||
Args:
|
||||
domain_slug: Domain slug to filter by
|
||||
category: optional repo classification category
|
||||
capability_tag: optional capability tag filter
|
||||
business_stake: optional business stake filter
|
||||
"""
|
||||
return json.dumps(_get("/repos", {"domain": domain_slug}), indent=2)
|
||||
return json.dumps(
|
||||
_get("/repos", {
|
||||
"domain": domain_slug,
|
||||
"category": category,
|
||||
"capability_tag": capability_tag,
|
||||
"business_stake": business_stake,
|
||||
}),
|
||||
indent=2,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_repos_by_classification(
|
||||
category: str | None = None,
|
||||
domain: str | None = None,
|
||||
capability_tag: str | None = None,
|
||||
business_stake: str | None = None,
|
||||
) -> str:
|
||||
"""List repos filtered by classification spine fields."""
|
||||
return json.dumps(
|
||||
_get("/repos", {
|
||||
"domain": domain,
|
||||
"category": category,
|
||||
"capability_tag": capability_tag,
|
||||
"business_stake": business_stake,
|
||||
}),
|
||||
indent=2,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@@ -1275,6 +1504,60 @@ def register_repo(
|
||||
return json.dumps(repo, indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def register_repo_from_classification(
|
||||
repo_slug: str,
|
||||
dry_run: bool = False,
|
||||
) -> str:
|
||||
"""Register or update a repo from its committed ``.repo-classification.yaml``.
|
||||
|
||||
Reads the classification file from the repo's local checkout (this host's
|
||||
registered path), validates against the canon allowed-values, and upserts the
|
||||
``managed_repo`` row including market-domain assignment.
|
||||
|
||||
Args:
|
||||
repo_slug: Registered repo slug (e.g. 'state-hub', 'the-custodian').
|
||||
dry_run: If True, report what would change without writing.
|
||||
"""
|
||||
import subprocess
|
||||
|
||||
script = Path(__file__).parent.parent / "scripts" / "register_from_classification.py"
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(script),
|
||||
"--slug",
|
||||
repo_slug,
|
||||
"--json",
|
||||
]
|
||||
if dry_run:
|
||||
cmd.append("--dry-run")
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
except json.JSONDecodeError:
|
||||
return (
|
||||
f"register-from-classification failed (exit {result.returncode}):\n"
|
||||
f"{result.stderr or result.stdout or '(no output)'}"
|
||||
)
|
||||
|
||||
summary = data.get("summary", {})
|
||||
lines = [
|
||||
f"register-from-classification: {repo_slug}",
|
||||
(
|
||||
f"registered={summary.get('registered', 0)} "
|
||||
f"updated={summary.get('updated', 0)} "
|
||||
f"skipped={summary.get('skipped', 0)} "
|
||||
f"invalid={summary.get('invalid', 0)}"
|
||||
),
|
||||
]
|
||||
for row in data.get("results", []):
|
||||
lines.append(f" [{row.get('outcome')}] {row.get('detail', '')}")
|
||||
if result.returncode != 0:
|
||||
lines.append("(completed with invalid rows)")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def update_repo_path(repo_slug: str, path: str, host: str | None = None) -> str:
|
||||
"""Register or update the local filesystem path for a repo on a specific host.
|
||||
|
||||
Reference in New Issue
Block a user