refactor(terminology): workplan-first state hub resolver internals (CUST-WP-0055 T05)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 8s
Build and Publish Container Image / build-and-push (push) Successful in 1m1s

Rename open-workplan digest helpers, expose open_workplans alongside legacy
open_workstreams in daily triage digests, and update custodian runtime prompt
prose while preserving wire-compat API paths and query params.
This commit is contained in:
tegwick 2026-07-08 16:41:28 +02:00
parent 63cbe7145e
commit 7947961ed4
3 changed files with 37 additions and 25 deletions

View file

@ -66,7 +66,7 @@ data:
Optional enrichment: Optional enrichment:
- `GET /tasks/?workstream_id=<id>` for a top-ranked workstream - `GET /tasks/?workplan_id=<id>` for a top-ranked workplan (legacy alias: workstream_id)
- `GET /progress/?workstream_id=<id>&limit=5` for staleness confidence - `GET /progress/?workstream_id=<id>&limit=5` for staleness confidence
- State Hub domain summaries through MCP when available - State Hub domain summaries through MCP when available
@ -74,11 +74,11 @@ data:
Build the candidate list from: Build the candidate list from:
- all open workstreams in `state_summary.open_workstreams` - all open workplans in `state_summary.open_workstreams` (legacy summary key)
- all derived `state_summary.next_steps` - all derived `state_summary.next_steps`
- blocked tasks and blocking decisions from the summary - blocked tasks and blocking decisions from the summary
- high-priority file-backed workplans surfaced by `workplan-index` - high-priority file-backed workplans surfaced by `workplan-index`
- workstreams with suspicious structure, such as zero parsed tasks or stale - workplans with suspicious structure, such as zero parsed tasks or stale
active plans active plans
Keep the scored table compact. Score at most 15 candidates internally and Keep the scored table compact. Score at most 15 candidates internally and

View file

@ -36,7 +36,7 @@ from activity_core.context_resolvers.base import CONTEXT_RESOLVER_REGISTRY, Cont
_DEFAULT_STATE_HUB_URL = "http://127.0.0.1:8000" _DEFAULT_STATE_HUB_URL = "http://127.0.0.1:8000"
_TIMEOUT_SECONDS = 10.0 _TIMEOUT_SECONDS = 10.0
_SWEEP_TIMEOUT_SECONDS = 330.0 _SWEEP_TIMEOUT_SECONDS = 330.0
_OPEN_WORKSTREAM_STATUSES = {"active", "ready", "blocked"} _OPEN_WORKPLAN_STATUSES = {"active", "ready", "blocked"}
_OPEN_TASK_STATUSES = {"wait", "todo", "progress"} _OPEN_TASK_STATUSES = {"wait", "todo", "progress"}
# Sentinel age for repos that have never had an SBOM ingested. Large enough # Sentinel age for repos that have never had an SBOM ingested. Large enough
# that any threshold-based staleness rule treats them as "very stale" without # that any threshold-based staleness rule treats them as "very stale" without
@ -693,9 +693,9 @@ def _daily_triage_digest(params: dict[str, Any]) -> str:
if not isinstance(inbox, list): if not isinstance(inbox, list):
inbox = [] inbox = []
max_workstreams = int(params.get("max_workstreams", 12)) max_workplans = int(params.get("max_workplans", params.get("max_workstreams", 12)))
max_next_steps = int(params.get("max_next_steps", 8)) max_next_steps = int(params.get("max_next_steps", 8))
open_workstreams = _open_workstream_digest(summary, workplan_index, max_workstreams) open_workplans = _open_workplan_digest(summary, workplan_index, max_workplans)
ranked_suggestions = summary.get("ranked_suggestions") or [] ranked_suggestions = summary.get("ranked_suggestions") or []
if not isinstance(ranked_suggestions, list): if not isinstance(ranked_suggestions, list):
ranked_suggestions = [] ranked_suggestions = []
@ -703,7 +703,8 @@ def _daily_triage_digest(params: dict[str, Any]) -> str:
digest = { digest = {
"generated_at": summary.get("generated_at"), "generated_at": summary.get("generated_at"),
"totals": summary.get("totals", {}), "totals": summary.get("totals", {}),
"open_workstreams": open_workstreams, "open_workplans": open_workplans,
"open_workstreams": open_workplans,
"next_steps": [_safe_next_step(item) for item in next_steps[:max_next_steps]], "next_steps": [_safe_next_step(item) for item in next_steps[:max_next_steps]],
"ranked_suggestions": [ "ranked_suggestions": [
_safe_ranked_suggestion(item) _safe_ranked_suggestion(item)
@ -729,35 +730,36 @@ def _daily_triage_digest(params: dict[str, Any]) -> str:
return json.dumps(digest, sort_keys=True, separators=(",", ":")) return json.dumps(digest, sort_keys=True, separators=(",", ":"))
def _open_workstream_digest( def _open_workplan_digest(
summary: dict[str, Any], summary: dict[str, Any],
workplan_index: dict[str, Any], workplan_index: dict[str, Any],
max_workstreams: int, max_workplans: int,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
index = workplan_index.get("workstreams") or {} index = workplan_index.get("workplans") or workplan_index.get("workstreams") or {}
candidates: list[dict[str, Any]] = [] candidates: list[dict[str, Any]] = []
for topic in summary.get("topics", []): for topic in summary.get("topics", []):
domain = topic.get("domain_slug") or topic.get("slug") domain = topic.get("domain_slug") or topic.get("slug")
for workstream in topic.get("workstreams", []): topic_workplans = topic.get("workplans") or topic.get("workstreams") or []
if workstream.get("status") not in _OPEN_WORKSTREAM_STATUSES: for workplan_row in topic_workplans:
if workplan_row.get("status") not in _OPEN_WORKPLAN_STATUSES:
continue continue
workstream_id = workstream.get("id") workplan_id = workplan_row.get("id")
detail = _fetch_json(f"/workstreams/{workstream_id}") if workstream_id else {} detail = _fetch_json(f"/workstreams/{workplan_id}") if workplan_id else {}
tasks = _fetch_json("/tasks/", {"workstream_id": workstream_id, "limit": 200}) tasks = _fetch_json("/tasks/", {"workstream_id": workplan_id, "limit": 200})
if not isinstance(detail, dict): if not isinstance(detail, dict):
detail = {} detail = {}
if not isinstance(tasks, list): if not isinstance(tasks, list):
tasks = [] tasks = []
counts = _task_counts(tasks) counts = _task_counts(tasks)
indexed = index.get(workstream_id, {}) if isinstance(index, dict) else {} indexed = index.get(workplan_id, {}) if isinstance(index, dict) else {}
candidates.append({ candidates.append({
"id": workstream_id, "id": workplan_id,
"slug": workstream.get("slug"), "slug": workplan_row.get("slug"),
"title": _short_text(workstream.get("title", ""), 120), "title": _short_text(workplan_row.get("title", ""), 120),
"domain": domain, "domain": domain,
"repo_slug": indexed.get("repo_slug"), "repo_slug": indexed.get("repo_slug"),
"status": workstream.get("status"), "status": workplan_row.get("status"),
"owner": workstream.get("owner"), "owner": workplan_row.get("owner"),
"planning_priority": detail.get("planning_priority"), "planning_priority": detail.get("planning_priority"),
"planning_order": detail.get("planning_order"), "planning_order": detail.get("planning_order"),
"file": indexed.get("relative_path"), "file": indexed.get("relative_path"),
@ -768,7 +770,10 @@ def _open_workstream_digest(
}) })
candidates.sort(key=_candidate_sort_key) candidates.sort(key=_candidate_sort_key)
return candidates[:max_workstreams] return candidates[:max_workplans]
_open_workstream_digest = _open_workplan_digest
def _task_counts(tasks: list[dict[str, Any]]) -> dict[str, int]: def _task_counts(tasks: list[dict[str, Any]]) -> dict[str, int]:
@ -799,12 +804,18 @@ def _representative_tasks(tasks: list[dict[str, Any]], limit: int) -> list[dict[
def _safe_next_step(item: dict[str, Any]) -> dict[str, Any]: def _safe_next_step(item: dict[str, Any]) -> dict[str, Any]:
workplan_id = item.get("workplan_id") or item.get("workstream_id")
workplan_slug = item.get("workplan_slug") or item.get("workstream_slug")
workplan_title = item.get("workplan_title") or item.get("workstream_title")
return { return {
"type": item.get("type"), "type": item.get("type"),
"domain": item.get("domain"), "domain": item.get("domain"),
"workstream_id": item.get("workstream_id"), "workplan_id": workplan_id,
"workstream_slug": item.get("workstream_slug"), "workplan_slug": workplan_slug,
"workstream_title": _short_text(item.get("workstream_title", ""), 120), "workplan_title": _short_text(workplan_title, 120),
"workstream_id": workplan_id,
"workstream_slug": workplan_slug,
"workstream_title": _short_text(workplan_title, 120),
"task_id": item.get("task_id"), "task_id": item.get("task_id"),
"task_title": _short_text(item.get("task_title", ""), 120), "task_title": _short_text(item.get("task_title", ""), 120),
} }

View file

@ -690,6 +690,7 @@ def test_daily_triage_digest_is_curated_scalar_json(monkeypatch) -> None:
import json import json
digest = json.loads(raw_digest) digest = json.loads(raw_digest)
assert digest["totals"] == {"tasks": {"todo": 4, "wait": 1}} assert digest["totals"] == {"tasks": {"todo": 4, "wait": 1}}
assert digest["open_workplans"] == digest["open_workstreams"]
assert digest["open_workstreams"][0]["slug"] == "cust-wp-0045" assert digest["open_workstreams"][0]["slug"] == "cust-wp-0045"
assert digest["open_workstreams"][0]["planning_priority"] == "high" assert digest["open_workstreams"][0]["planning_priority"] == "high"
assert digest["open_workstreams"][0]["open_task_counts"] == { assert digest["open_workstreams"][0]["open_task_counts"] == {