feat(consistency): coordination hygiene checks and MCP workplan_id aliases
Add C-25..C-30 fix-consistency checks for blocked-workplan inbox sweeps, stale unread triage, workplan ID prefix/collision lint, and SCOPE freshness. Extend brief generation and get_domain_summary with inbox hygiene warnings. Complete workplan_id aliases on remaining MCP tools and retry transient _api_get failures to reduce false stale-reference errors under load.
This commit is contained in:
parent
cc656a2b16
commit
9693946755
5 changed files with 819 additions and 58 deletions
|
|
@ -344,6 +344,31 @@ def get_domain_summary(domain_slug: str) -> str:
|
|||
if goal_guidance:
|
||||
result["goal_guidance"] = goal_guidance
|
||||
|
||||
inbox_hygiene: dict[str, Any] = {}
|
||||
try:
|
||||
from scripts.consistency_check import collect_inbox_hygiene, STALE_UNREAD_DAYS
|
||||
except ImportError:
|
||||
collect_inbox_hygiene = None # type: ignore[assignment]
|
||||
STALE_UNREAD_DAYS = 3
|
||||
if collect_inbox_hygiene is not None:
|
||||
for repo in repos:
|
||||
repo_slug = repo["slug"]
|
||||
hygiene = collect_inbox_hygiene(API_BASE, repo_slug)
|
||||
if (
|
||||
hygiene["stale_unread_count"]
|
||||
or hygiene["missing_thread"]
|
||||
or hygiene["work_requests_unpromoted"]
|
||||
):
|
||||
inbox_hygiene[repo_slug] = {
|
||||
"stale_unread_count": hygiene["stale_unread_count"],
|
||||
"stale_unread_days": STALE_UNREAD_DAYS,
|
||||
"stale_unread": hygiene["stale_unread"][:5],
|
||||
"missing_thread_count": len(hygiene["missing_thread"]),
|
||||
"work_requests_unpromoted": hygiene["work_requests_unpromoted"][:3],
|
||||
}
|
||||
if inbox_hygiene:
|
||||
result["inbox_hygiene"] = inbox_hygiene
|
||||
|
||||
# Compact capabilities list (type + title + repo_slug only, capped at 20)
|
||||
caps_raw = _get("/capability-catalog/", {"domain": domain_slug, "status": "active"})
|
||||
if isinstance(caps_raw, list):
|
||||
|
|
@ -416,9 +441,21 @@ def list_tasks(
|
|||
|
||||
|
||||
@mcp.tool()
|
||||
def list_blocked_tasks(workstream_id: str | None = None) -> str:
|
||||
"""List all waiting tasks, optionally filtered by workstream_id."""
|
||||
return json.dumps(_get("/tasks", {"status": "wait", "workstream_id": workstream_id}), indent=2)
|
||||
def list_blocked_tasks(
|
||||
workplan_id: str | None = None,
|
||||
workstream_id: str | None = None,
|
||||
) -> str:
|
||||
"""List all waiting tasks, optionally filtered by workplan.
|
||||
|
||||
Args:
|
||||
workplan_id: UUID of the workplan (preferred).
|
||||
workstream_id: legacy alias for workplan_id.
|
||||
"""
|
||||
parent_id = workplan_id or workstream_id
|
||||
return json.dumps(
|
||||
_get("/tasks", {"status": "wait", "workplan_id": parent_id, "workstream_id": parent_id}),
|
||||
indent=2,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
|
|
@ -879,17 +916,25 @@ def clear_human_flag(task_id: str) -> str:
|
|||
|
||||
|
||||
@mcp.tool()
|
||||
def list_human_interventions(workstream_id: str | None = None) -> str:
|
||||
def list_human_interventions(
|
||||
workplan_id: str | None = None,
|
||||
workstream_id: str | None = None,
|
||||
) -> str:
|
||||
"""List all tasks flagged for human intervention.
|
||||
|
||||
Returns tasks where needs_human=True, optionally filtered to one workstream.
|
||||
Returns tasks where needs_human=True, optionally filtered to one workplan.
|
||||
Use this at session start to surface Bernd's action items.
|
||||
|
||||
Args:
|
||||
workstream_id: optional UUID to scope results to one workstream
|
||||
workplan_id: optional UUID to scope results to one workplan (preferred).
|
||||
workstream_id: legacy alias for workplan_id.
|
||||
"""
|
||||
parent_id = workplan_id or workstream_id
|
||||
return json.dumps(
|
||||
_get("/tasks", {"needs_human": "true", "workstream_id": workstream_id}),
|
||||
_get(
|
||||
"/tasks",
|
||||
{"needs_human": "true", "workplan_id": parent_id, "workstream_id": parent_id},
|
||||
),
|
||||
indent=2,
|
||||
)
|
||||
|
||||
|
|
@ -899,6 +944,7 @@ def record_decision(
|
|||
title: str,
|
||||
decision_type: str = "pending",
|
||||
topic_id: str | None = None,
|
||||
workplan_id: str | None = None,
|
||||
workstream_id: str | None = None,
|
||||
description: str | None = None,
|
||||
rationale: str | None = None,
|
||||
|
|
@ -914,17 +960,19 @@ def record_decision(
|
|||
title: decision title
|
||||
decision_type: made | pending
|
||||
topic_id: optional topic UUID
|
||||
workstream_id: optional workstream UUID (at least one required)
|
||||
workplan_id: optional workplan UUID (preferred; at least one required)
|
||||
workstream_id: legacy alias for workplan_id
|
||||
description: optional context
|
||||
rationale: reasoning behind the decision
|
||||
decided_by: person/agent who decided
|
||||
deadline: ISO datetime string for when decision is needed
|
||||
"""
|
||||
parent_id = workplan_id or workstream_id
|
||||
decision = _post("/decisions", {
|
||||
"title": title,
|
||||
"decision_type": decision_type,
|
||||
"topic_id": topic_id,
|
||||
"workstream_id": workstream_id,
|
||||
"workplan_id": parent_id,
|
||||
"description": description,
|
||||
"rationale": rationale,
|
||||
"decided_by": decided_by,
|
||||
|
|
@ -935,7 +983,8 @@ def record_decision(
|
|||
|
||||
progress_error = _emit_progress_event("record_decision", decision, {
|
||||
"topic_id": topic_id,
|
||||
"workstream_id": workstream_id,
|
||||
"workplan_id": parent_id,
|
||||
"workstream_id": parent_id,
|
||||
"decision_id": decision["id"],
|
||||
"event_type": "decision_recorded",
|
||||
"summary": f"Decision recorded ({decision_type}): {title}",
|
||||
|
|
@ -1075,9 +1124,16 @@ def update_workplan_status(workplan_id: str, status: str) -> str:
|
|||
|
||||
|
||||
@mcp.tool()
|
||||
def update_workstream_status(workstream_id: str, status: str) -> str:
|
||||
def update_workstream_status(
|
||||
status: str,
|
||||
workplan_id: str | None = None,
|
||||
workstream_id: str | None = None,
|
||||
) -> str:
|
||||
"""Legacy alias for update_workplan_status."""
|
||||
return _update_workplan_status_impl(workstream_id, status, tool_name="update_workstream_status")
|
||||
parent_id = workplan_id or workstream_id
|
||||
if not parent_id:
|
||||
return _json_result(_mcp_error("update_workstream_status", "workplan_id is required"))
|
||||
return _update_workplan_status_impl(parent_id, status, tool_name="update_workstream_status")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
|
|
@ -1104,7 +1160,8 @@ def update_workplan(
|
|||
|
||||
@mcp.tool()
|
||||
def update_workstream(
|
||||
workstream_id: str,
|
||||
workplan_id: str | None = None,
|
||||
workstream_id: str | None = None,
|
||||
title: str | None = None,
|
||||
description: str | None = None,
|
||||
owner: str | None = None,
|
||||
|
|
@ -1113,8 +1170,11 @@ def update_workstream(
|
|||
status: str | None = None,
|
||||
) -> str:
|
||||
"""Legacy alias for update_workplan."""
|
||||
parent_id = workplan_id or workstream_id
|
||||
if not parent_id:
|
||||
return _json_result(_mcp_error("update_workstream", "workplan_id is required"))
|
||||
return _update_workplan_impl(
|
||||
workstream_id,
|
||||
parent_id,
|
||||
title=title,
|
||||
description=description,
|
||||
owner=owner,
|
||||
|
|
@ -1277,16 +1337,22 @@ def create_workplan_dependency(
|
|||
|
||||
@mcp.tool()
|
||||
def create_dependency(
|
||||
from_workstream_id: str,
|
||||
from_workplan_id: str | None = None,
|
||||
from_workstream_id: str | None = None,
|
||||
to_workplan_id: str | None = None,
|
||||
to_workstream_id: str | None = None,
|
||||
to_task_id: str | None = None,
|
||||
relationship_type: str = "blocks",
|
||||
description: str | None = None,
|
||||
) -> str:
|
||||
"""Legacy alias for create_workplan_dependency."""
|
||||
source_id = from_workplan_id or from_workstream_id
|
||||
target_id = to_workplan_id or to_workstream_id
|
||||
if not source_id:
|
||||
return _json_result(_mcp_error("create_dependency", "from_workplan_id is required"))
|
||||
return _create_dependency_impl(
|
||||
from_workplan_id=from_workstream_id,
|
||||
to_workplan_id=to_workstream_id,
|
||||
from_workplan_id=source_id,
|
||||
to_workplan_id=target_id,
|
||||
to_task_id=to_task_id,
|
||||
relationship_type=relationship_type,
|
||||
description=description,
|
||||
|
|
@ -1294,23 +1360,30 @@ def create_dependency(
|
|||
|
||||
|
||||
@mcp.tool()
|
||||
def list_dependencies(workstream_id: str) -> str:
|
||||
"""Return all dependency edges touching a workstream (both directions).
|
||||
def list_dependencies(
|
||||
workplan_id: str | None = None,
|
||||
workstream_id: str | None = None,
|
||||
) -> str:
|
||||
"""Return all dependency edges touching a workplan (both directions).
|
||||
|
||||
The response distinguishes edges where this workstream is the dependent
|
||||
The response distinguishes edges where this workplan is the dependent
|
||||
(depends_on) from edges where it is the blocker (blocks).
|
||||
|
||||
Args:
|
||||
workstream_id: UUID of the workstream to inspect
|
||||
workplan_id: UUID of the workplan to inspect (preferred).
|
||||
workstream_id: legacy alias for workplan_id.
|
||||
"""
|
||||
edges = _get(f"/workplans/{workstream_id}/dependencies")
|
||||
parent_id = workplan_id or workstream_id
|
||||
if not parent_id:
|
||||
return _json_result(_mcp_error("list_dependencies", "workplan_id is required"))
|
||||
edges = _get(f"/workplans/{parent_id}/dependencies")
|
||||
depends_on = [
|
||||
e for e in edges
|
||||
if e.get("from_workplan_id", e.get("from_workstream_id")) == workstream_id
|
||||
if e.get("from_workplan_id", e.get("from_workstream_id")) == parent_id
|
||||
]
|
||||
blocks = [
|
||||
e for e in edges
|
||||
if e.get("to_workplan_id", e.get("to_workstream_id")) == workstream_id
|
||||
if e.get("to_workplan_id", e.get("to_workstream_id")) == parent_id
|
||||
]
|
||||
return json.dumps({"depends_on": depends_on, "blocks": blocks}, indent=2)
|
||||
|
||||
|
|
@ -1329,6 +1402,7 @@ def register_extension_point(
|
|||
priority: str = "medium",
|
||||
ep_id: str | None = None,
|
||||
topic_id: str | None = None,
|
||||
workplan_id: str | None = None,
|
||||
workstream_id: str | None = None,
|
||||
) -> str:
|
||||
"""Register a discovered extension point — optional future functionality not yet committed.
|
||||
|
|
@ -1345,13 +1419,15 @@ def register_extension_point(
|
|||
priority: low | medium | high | critical
|
||||
ep_id: optional human-readable ID, e.g. EP-CUST-001 (auto-assigned if omitted)
|
||||
topic_id: UUID of related topic
|
||||
workstream_id: UUID of related workstream
|
||||
workplan_id: UUID of related workplan (preferred)
|
||||
workstream_id: legacy alias for workplan_id
|
||||
"""
|
||||
parent_id = workplan_id or workstream_id
|
||||
ep = _post("/extension-points", {
|
||||
"domain": domain, "title": title, "ep_type": ep_type,
|
||||
"description": description, "location": location,
|
||||
"priority": priority, "ep_id": ep_id,
|
||||
"topic_id": topic_id, "workstream_id": workstream_id,
|
||||
"topic_id": topic_id, "workplan_id": parent_id,
|
||||
})
|
||||
_post("/progress", {
|
||||
"summary": f"Extension point registered: [{ep.get('ep_id') or ep['id'][:8]}] {title} ({ep_type}, {domain})",
|
||||
|
|
@ -1406,6 +1482,7 @@ def register_technical_debt(
|
|||
severity: str = "medium",
|
||||
td_id: str | None = None,
|
||||
topic_id: str | None = None,
|
||||
workplan_id: str | None = None,
|
||||
workstream_id: str | None = None,
|
||||
) -> str:
|
||||
"""Register a technical debt item — a known quality compromise to address later.
|
||||
|
|
@ -1422,13 +1499,15 @@ def register_technical_debt(
|
|||
severity: low | medium | high | critical
|
||||
td_id: optional human-readable ID, e.g. TD-CUST-001
|
||||
topic_id: UUID of related topic
|
||||
workstream_id: UUID of related workstream
|
||||
workplan_id: UUID of related workplan (preferred)
|
||||
workstream_id: legacy alias for workplan_id
|
||||
"""
|
||||
parent_id = workplan_id or workstream_id
|
||||
td = _post("/technical-debt", {
|
||||
"domain": domain, "title": title, "debt_type": debt_type,
|
||||
"description": description, "location": location,
|
||||
"severity": severity, "td_id": td_id,
|
||||
"topic_id": topic_id, "workstream_id": workstream_id,
|
||||
"topic_id": topic_id, "workplan_id": parent_id,
|
||||
})
|
||||
_post("/progress", {
|
||||
"summary": f"Technical debt registered: [{td.get('td_id') or td['id'][:8]}] {title} ({debt_type}, {severity}, {domain})",
|
||||
|
|
@ -2032,6 +2111,7 @@ def register_contribution(
|
|||
target_org: str | None = None,
|
||||
target_repo: str | None = None,
|
||||
body_path: str | None = None,
|
||||
related_workplan_id: str | None = None,
|
||||
related_workstream_id: str | None = None,
|
||||
notes: str | None = None,
|
||||
) -> str:
|
||||
|
|
@ -2043,20 +2123,22 @@ def register_contribution(
|
|||
target_org: GitHub org or owner of the upstream project
|
||||
target_repo: Repository name of the upstream project
|
||||
body_path: Relative path to the Markdown artifact file in the repo
|
||||
related_workstream_id: UUID of the related workstream (optional)
|
||||
related_workplan_id: UUID of the related workplan (preferred, optional)
|
||||
related_workstream_id: legacy alias for related_workplan_id
|
||||
notes: Any additional notes (optional)
|
||||
"""
|
||||
parent_id = related_workplan_id or related_workstream_id
|
||||
contrib = _post("/contributions", {
|
||||
"type": type,
|
||||
"title": title,
|
||||
"target_org": target_org,
|
||||
"target_repo": target_repo,
|
||||
"body_path": body_path,
|
||||
"related_workstream_id": related_workstream_id,
|
||||
"related_workplan_id": parent_id,
|
||||
"notes": notes,
|
||||
})
|
||||
_post("/progress", {
|
||||
"workstream_id": related_workstream_id,
|
||||
"workplan_id": parent_id,
|
||||
"event_type": "contribution_registered",
|
||||
"summary": f"Contribution registered [{type.upper()}]: {title}",
|
||||
"author": "custodian",
|
||||
|
|
@ -2441,6 +2523,7 @@ def request_capability(
|
|||
capability_type: str,
|
||||
requesting_agent: str,
|
||||
requesting_domain: str,
|
||||
requesting_workplan_id: str | None = None,
|
||||
requesting_workstream_id: str | None = None,
|
||||
priority: str = "medium",
|
||||
blocking_task_id: str | None = None,
|
||||
|
|
@ -2454,17 +2537,19 @@ def request_capability(
|
|||
capability_type: Category (e.g. 'infrastructure', 'api', 'data', 'security')
|
||||
requesting_agent: Your agent identifier (e.g. 'net-kingdom-worker')
|
||||
requesting_domain: Your domain slug (e.g. 'custodian')
|
||||
requesting_workstream_id: UUID of your workstream (optional)
|
||||
requesting_workplan_id: UUID of your workplan (preferred, optional)
|
||||
requesting_workstream_id: legacy alias for requesting_workplan_id
|
||||
priority: low | medium | high | critical (default: medium)
|
||||
blocking_task_id: UUID of the task blocked until this is fulfilled (optional)
|
||||
"""
|
||||
parent_id = requesting_workplan_id or requesting_workstream_id
|
||||
req = _post("/capability-requests", {
|
||||
"title": title,
|
||||
"description": description,
|
||||
"capability_type": capability_type,
|
||||
"requesting_agent": requesting_agent,
|
||||
"requesting_domain": requesting_domain,
|
||||
"requesting_workstream_id": requesting_workstream_id,
|
||||
"requesting_workplan_id": parent_id,
|
||||
"priority": priority,
|
||||
"blocking_task_id": blocking_task_id,
|
||||
})
|
||||
|
|
@ -2487,6 +2572,7 @@ def patch_capability_request(
|
|||
catalog_entry_id: Optional[str] = None,
|
||||
priority: Optional[str] = None,
|
||||
blocking_task_id: Optional[str] = None,
|
||||
fulfilling_workplan_id: Optional[str] = None,
|
||||
fulfilling_workstream_id: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Correct mutable metadata on a capability request.
|
||||
|
|
@ -2500,11 +2586,13 @@ def patch_capability_request(
|
|||
catalog_entry_id: Correct catalog entry UUID. Re-derives fulfilling domain.
|
||||
priority: New priority (low/medium/high/critical).
|
||||
blocking_task_id: UUID of the task this request unblocks on completion.
|
||||
fulfilling_workstream_id: UUID of the workstream delivering this capability.
|
||||
fulfilling_workplan_id: UUID of the workplan delivering this capability (preferred).
|
||||
fulfilling_workstream_id: legacy alias for fulfilling_workplan_id.
|
||||
|
||||
Returns:
|
||||
Updated capability request dict, or {"error": "..."}.
|
||||
"""
|
||||
parent_id = fulfilling_workplan_id or fulfilling_workstream_id
|
||||
body: dict = {}
|
||||
if catalog_entry_id is not None:
|
||||
body["catalog_entry_id"] = catalog_entry_id
|
||||
|
|
@ -2512,8 +2600,8 @@ def patch_capability_request(
|
|||
body["priority"] = priority
|
||||
if blocking_task_id is not None:
|
||||
body["blocking_task_id"] = blocking_task_id
|
||||
if fulfilling_workstream_id is not None:
|
||||
body["fulfilling_workstream_id"] = fulfilling_workstream_id
|
||||
if parent_id is not None:
|
||||
body["fulfilling_workplan_id"] = parent_id
|
||||
|
||||
if not body:
|
||||
return {"error": "no fields provided to patch"}
|
||||
|
|
@ -2923,6 +3011,7 @@ def record_token_event(
|
|||
tokens_in: int,
|
||||
tokens_out: int,
|
||||
task_id: Optional[str] = None,
|
||||
workplan_id: Optional[str] = None,
|
||||
workstream_id: Optional[str] = None,
|
||||
repo_id: Optional[str] = None,
|
||||
model: Optional[str] = None,
|
||||
|
|
@ -2942,7 +3031,8 @@ def record_token_event(
|
|||
tokens_in: Input token count
|
||||
tokens_out: Output token count
|
||||
task_id: UUID of the task (nullable)
|
||||
workstream_id: UUID of the workstream (nullable; auto-filled from task)
|
||||
workplan_id: UUID of the workplan (preferred; nullable; auto-filled from task)
|
||||
workstream_id: legacy alias for workplan_id
|
||||
repo_id: UUID of the managed repo (nullable)
|
||||
model: Model identifier, e.g. 'claude-sonnet-4-6'
|
||||
agent: Agent name, e.g. 'custodian', 'ralph'
|
||||
|
|
@ -2951,11 +3041,12 @@ def record_token_event(
|
|||
note: Free-text note
|
||||
session_id: Agent session identifier
|
||||
"""
|
||||
parent_id = workplan_id or workstream_id
|
||||
body = {
|
||||
"tokens_in": tokens_in,
|
||||
"tokens_out": tokens_out,
|
||||
"task_id": task_id,
|
||||
"workstream_id": workstream_id,
|
||||
"workplan_id": parent_id,
|
||||
"repo_id": repo_id,
|
||||
"model": model,
|
||||
"agent": agent,
|
||||
|
|
@ -2976,8 +3067,8 @@ def record_token_event(
|
|||
}
|
||||
|
||||
# Append running total for the task if available
|
||||
scope_id = task_id or workstream_id
|
||||
scope = "task" if task_id else ("workstream" if workstream_id else None)
|
||||
scope_id = task_id or parent_id
|
||||
scope = "task" if task_id else ("workstream" if parent_id else None)
|
||||
if scope and scope_id:
|
||||
summary = _get("/token-events/summary", {"scope": scope, "id": scope_id})
|
||||
if "error" not in summary:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue