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
|
|
@ -31,6 +31,8 @@ from consistency_check import (
|
|||
RENORMALIZATION_RULES,
|
||||
STATUS_ORDER,
|
||||
_BACKGROUND_CHECKS,
|
||||
_api_get,
|
||||
_check_scope_freshness,
|
||||
_detect_behind_remote,
|
||||
_git_pull,
|
||||
_patch_frontmatter_field,
|
||||
|
|
@ -39,11 +41,14 @@ from consistency_check import (
|
|||
archive_closed_workplans,
|
||||
canonical_workplan_filename,
|
||||
check_repo,
|
||||
collect_inbox_hygiene,
|
||||
consistency_exit_code,
|
||||
fix_repo,
|
||||
get_tasks_from_workplan,
|
||||
infer_wp_prefix,
|
||||
iter_workplan_files,
|
||||
normalise_workstream_status,
|
||||
parse_blocked_on,
|
||||
parse_frontmatter,
|
||||
parse_task_blocks,
|
||||
render_text,
|
||||
|
|
@ -1402,3 +1407,104 @@ class TestReportNeedsAction:
|
|||
"""Diverged repo (both behind and ahead) needs action."""
|
||||
r = self._make_report([])
|
||||
assert _report_needs_action(r, behind_remote=True, ahead_of_remote=5) is True
|
||||
|
||||
|
||||
class TestCoordinationHygieneHelpers:
|
||||
def test_parse_blocked_on_extracts_agent(self):
|
||||
assert parse_blocked_on("message-from:llm-connect") == "llm-connect"
|
||||
assert parse_blocked_on("other-format") is None
|
||||
|
||||
def test_infer_wp_prefix_prefers_on_disk_ids(self, tmp_path):
|
||||
workplans = tmp_path / "workplans"
|
||||
workplans.mkdir()
|
||||
(workplans / "CUST-WP-0001-alpha.md").write_text(
|
||||
"---\nid: CUST-WP-0001\nstatus: active\n---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(workplans / "CUST-WP-0002-beta.md").write_text(
|
||||
"---\nid: STATE-WP-0002\nstatus: active\n---\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert infer_wp_prefix(tmp_path, "state-hub") == "CUST-WP"
|
||||
|
||||
def test_collect_inbox_hygiene_flags_stale_unread(self, monkeypatch):
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
stale = (datetime.now(timezone.utc) - timedelta(days=5)).isoformat()
|
||||
recent = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
def fake_get(api_base, path, params=None, **kwargs):
|
||||
if path.startswith("/messages"):
|
||||
return [
|
||||
{
|
||||
"id": "11111111-1111-1111-1111-111111111111",
|
||||
"from_agent": "llm-connect",
|
||||
"subject": "Please implement the workplan",
|
||||
"body": "multi-step implementation needed",
|
||||
"created_at": stale,
|
||||
},
|
||||
{
|
||||
"id": "22222222-2222-2222-2222-222222222222",
|
||||
"from_agent": "ops-warden",
|
||||
"subject": "FYI",
|
||||
"body": "status update",
|
||||
"created_at": recent,
|
||||
},
|
||||
]
|
||||
return None
|
||||
|
||||
monkeypatch.setattr("consistency_check._api_get", fake_get)
|
||||
hygiene = collect_inbox_hygiene("http://example", "the-custodian")
|
||||
assert hygiene["stale_unread_count"] == 1
|
||||
assert hygiene["work_requests_unpromoted"]
|
||||
assert hygiene["missing_thread"]
|
||||
|
||||
def test_scope_freshness_warns_on_contradiction(self, tmp_path):
|
||||
(tmp_path / "SCOPE.md").write_text(
|
||||
"# SCOPE\n\n## Current State\n\n- Status: active — stable\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
report = ConsistencyReport(repo_slug="demo", repo_path=str(tmp_path))
|
||||
_check_scope_freshness(tmp_path, [], report)
|
||||
assert any(issue.check_id == "C-30" for issue in report.warnings)
|
||||
|
||||
def test_api_get_retries_transient_errors(self, monkeypatch):
|
||||
attempts = {"count": 0}
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, status_code: int):
|
||||
self.status_code = status_code
|
||||
self.reason_phrase = "Server Error"
|
||||
|
||||
def raise_for_status(self):
|
||||
if self.status_code < 400:
|
||||
return
|
||||
import httpx
|
||||
|
||||
request = httpx.Request("GET", "http://example/workstreams/ws-1/")
|
||||
response = httpx.Response(self.status_code, request=request)
|
||||
raise httpx.HTTPStatusError("boom", request=request, response=response)
|
||||
|
||||
def json(self):
|
||||
return {"id": "ws-1"}
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
def get(self, path, params=None):
|
||||
attempts["count"] += 1
|
||||
if attempts["count"] < 2:
|
||||
return FakeResponse(503)
|
||||
return FakeResponse(200)
|
||||
|
||||
monkeypatch.setattr("consistency_check._httpx.Client", FakeClient)
|
||||
result = _api_get("http://example", "/workstreams/ws-1")
|
||||
assert result == {"id": "ws-1"}
|
||||
assert attempts["count"] == 2
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ class TestMCPWriteTools:
|
|||
"/progress",
|
||||
{
|
||||
"topic_id": None,
|
||||
"workstream_id": "ws-1",
|
||||
"workplan_id": "ws-1",
|
||||
"task_id": None,
|
||||
"event_type": "note",
|
||||
"summary": "MCP progress",
|
||||
|
|
@ -260,7 +260,7 @@ class TestMCPWriteTools:
|
|||
"title": body["title"],
|
||||
"decision_type": body["decision_type"],
|
||||
"topic_id": body["topic_id"],
|
||||
"workstream_id": body["workstream_id"],
|
||||
"workplan_id": body.get("workplan_id"),
|
||||
"status": "open",
|
||||
"escalation_note": None,
|
||||
}
|
||||
|
|
@ -321,3 +321,74 @@ class TestMCPWriteTools:
|
|||
assert body["tool"] == "record_decision"
|
||||
assert body["error"] == "API response missing required field(s): id"
|
||||
assert [path for path, _ in calls] == ["/decisions"]
|
||||
|
||||
async def test_record_decision_accepts_workplan_id_alias(self, monkeypatch):
|
||||
calls: list[tuple[str, dict[str, Any]]] = []
|
||||
|
||||
def fake_post(path: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
calls.append((path, body))
|
||||
if path == "/decisions":
|
||||
return {
|
||||
"id": "decision-1",
|
||||
"title": body["title"],
|
||||
"decision_type": body["decision_type"],
|
||||
"topic_id": body["topic_id"],
|
||||
"workplan_id": body["workplan_id"],
|
||||
"status": "open",
|
||||
"escalation_note": None,
|
||||
}
|
||||
if path == "/progress":
|
||||
return {"id": "event-1", **body}
|
||||
raise AssertionError(f"unexpected POST {path}")
|
||||
|
||||
monkeypatch.setattr(server, "_post", fake_post)
|
||||
|
||||
body = await _call_tool(
|
||||
"record_decision",
|
||||
{
|
||||
"title": "Alias check",
|
||||
"decision_type": "made",
|
||||
"topic_id": "topic-1",
|
||||
"workplan_id": "wp-1",
|
||||
},
|
||||
)
|
||||
|
||||
assert body["id"] == "decision-1"
|
||||
assert calls[0][1]["workplan_id"] == "wp-1"
|
||||
assert calls[1][1]["workplan_id"] == "wp-1"
|
||||
|
||||
async def test_list_blocked_tasks_accepts_workplan_id_alias(self, monkeypatch):
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
def fake_get(path: str, params: dict[str, Any] | None = None) -> Any:
|
||||
captured["path"] = path
|
||||
captured["params"] = params
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(server, "_get", fake_get)
|
||||
|
||||
await _call_tool("list_blocked_tasks", {"workplan_id": "wp-1"})
|
||||
|
||||
assert captured["params"]["workplan_id"] == "wp-1"
|
||||
assert captured["params"]["workstream_id"] == "wp-1"
|
||||
|
||||
async def test_record_token_event_accepts_workplan_id_alias(self, monkeypatch):
|
||||
calls: list[tuple[str, dict[str, Any]]] = []
|
||||
|
||||
def fake_post(path: str, body: dict[str, Any]) -> dict[str, Any]:
|
||||
calls.append((path, body))
|
||||
return {"id": "token-1", "tokens_total": 1500}
|
||||
|
||||
def fake_get(path: str, params: dict[str, Any] | None = None) -> Any:
|
||||
return {"tokens_total": 1500, "event_count": 1}
|
||||
|
||||
monkeypatch.setattr(server, "_post", fake_post)
|
||||
monkeypatch.setattr(server, "_get", fake_get)
|
||||
|
||||
body = await _call_tool(
|
||||
"record_token_event",
|
||||
{"tokens_in": 1000, "tokens_out": 500, "workplan_id": "wp-1"},
|
||||
)
|
||||
|
||||
assert body["event_id"] == "token-1"
|
||||
assert calls[0][1]["workplan_id"] == "wp-1"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue