CUST-WP-0055 T05: workplan-first progress scope in sinks and resolvers
Prefer workplan_id in State Hub progress writes while dual-writing workstream_id for wire-compat. Extend schedule_health, evidence/report sinks, phase5 checks, k8s triage prose, and SCOPE terminology.
This commit is contained in:
parent
7947961ed4
commit
69fe69041f
12 changed files with 135 additions and 30 deletions
|
|
@ -10,7 +10,7 @@ Supported queries:
|
|||
- workplan_index: GET {STATE_HUB_URL}/workstreams/workplan-index
|
||||
- hub_inbox: GET {STATE_HUB_URL}/messages/?to_agent=hub&unread_only=true
|
||||
- pending_decisions: GET {STATE_HUB_URL}/decisions/?status=open (topic_id/
|
||||
workstream_id/decision_type passed through as given)
|
||||
workplan_id or legacy workstream_id/decision_type passed through)
|
||||
- coding_retro: latest /progress/ item with event_type=coding_retro
|
||||
- daily_triage_digest: curated scalar JSON digest for daily WSJF triage
|
||||
- recently_on_scope_hourly: POST {STATE_HUB_URL}/recently-on-scope/hourly
|
||||
|
|
@ -199,7 +199,9 @@ def _phase5_stabilization_check(params: dict[str, Any]) -> dict[str, Any]:
|
|||
}
|
||||
|
||||
baseline = params.get("baseline") or {}
|
||||
expected_workstreams = int(baseline.get("workstreams", 640))
|
||||
expected_workplan_total = int(
|
||||
baseline.get("workplans", baseline.get("workstreams", 640))
|
||||
)
|
||||
expected_tasks = int(baseline.get("tasks", 4002))
|
||||
expected_topics = int(baseline.get("topics", 14))
|
||||
sweep_limit = _bounded_int(params.get("sweep_limit", 6), default=6, minimum=1, maximum=24)
|
||||
|
|
@ -215,11 +217,12 @@ def _phase5_stabilization_check(params: dict[str, Any]) -> dict[str, Any]:
|
|||
|
||||
summary = _fetch_json("/state/summary")
|
||||
totals = (summary or {}).get("totals") or {}
|
||||
ws_total = int((totals.get("workstreams") or {}).get("total", -1))
|
||||
workplan_totals = totals.get("workplans") or totals.get("workstreams") or {}
|
||||
ws_total = int(workplan_totals.get("total", -1))
|
||||
task_total = int((totals.get("tasks") or {}).get("total", -1))
|
||||
topic_total = int((totals.get("topics") or {}).get("total", -1))
|
||||
totals_pass = (
|
||||
ws_total == expected_workstreams
|
||||
ws_total == expected_workplan_total
|
||||
and task_total == expected_tasks
|
||||
and topic_total == expected_topics
|
||||
)
|
||||
|
|
@ -259,11 +262,13 @@ def _phase5_stabilization_check(params: dict[str, Any]) -> dict[str, Any]:
|
|||
"health": {"pass": health_pass, "status": health.get("status") if isinstance(health, dict) else None},
|
||||
"totals": {
|
||||
"pass": totals_pass,
|
||||
"workplans": ws_total,
|
||||
"workstreams": ws_total,
|
||||
"tasks": task_total,
|
||||
"topics": topic_total,
|
||||
"expected": {
|
||||
"workstreams": expected_workstreams,
|
||||
"workplans": expected_workplan_total,
|
||||
"workstreams": expected_workplan_total,
|
||||
"tasks": expected_tasks,
|
||||
"topics": expected_topics,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from typing import Any
|
|||
import httpx
|
||||
|
||||
from activity_core.context_resolvers.ops_inventory import _sanitize_url
|
||||
from activity_core.state_hub_write import idempotency_headers
|
||||
from activity_core.state_hub_write import apply_progress_scope_fields, idempotency_headers
|
||||
|
||||
_DEFAULT_STATE_HUB_URL = "http://127.0.0.1:8000"
|
||||
_INTER_HUB_SINK_TYPES = {
|
||||
|
|
@ -149,9 +149,7 @@ def _post_state_hub_progress(
|
|||
"probe": compact,
|
||||
},
|
||||
}
|
||||
for key in ("topic_id", "workstream_id", "task_id", "decision_id"):
|
||||
if sink.get(key):
|
||||
body[key] = sink[key]
|
||||
apply_progress_scope_fields(body, sink)
|
||||
|
||||
resp = httpx.post(
|
||||
f"{base_url}/progress/",
|
||||
|
|
@ -529,7 +527,8 @@ def _phase5_summary_text(result: dict[str, Any]) -> str:
|
|||
mode = "closeout" if result.get("closeout") else "daily"
|
||||
return (
|
||||
f"Phase 5 stabilization {mode}: {status}; "
|
||||
f"totals {totals.get('workstreams', '?')}/{totals.get('tasks', '?')}/"
|
||||
f"workplan totals {totals.get('workplans', totals.get('workstreams', '?'))}/"
|
||||
f"{totals.get('tasks', '?')}/"
|
||||
f"{totals.get('topics', '?')}; sweeps sampled={sweeps.get('sampled', 0)}; "
|
||||
f"daily_triage age_h={triage.get('age_hours', '?')}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from activity_core.runtime_paths import (
|
|||
custodian_repo_root,
|
||||
resolve_runtime_path,
|
||||
)
|
||||
from activity_core.state_hub_write import idempotency_headers
|
||||
from activity_core.state_hub_write import apply_progress_scope_fields, idempotency_headers
|
||||
|
||||
_DEFAULT_STATE_HUB_URL = "http://127.0.0.1:8000"
|
||||
|
||||
|
|
@ -146,9 +146,7 @@ def _post_state_hub_progress(
|
|||
body["detail"]["working_memory_status"] = report_entry.get(
|
||||
"working_memory_status"
|
||||
)
|
||||
for key in ("topic_id", "workstream_id", "task_id", "decision_id"):
|
||||
if sink.get(key):
|
||||
body[key] = sink[key]
|
||||
apply_progress_scope_fields(body, sink)
|
||||
|
||||
resp = httpx.post(
|
||||
f"{base_url}/progress/",
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ def post_missed_fire_alert(
|
|||
state_hub_url: str | None = None,
|
||||
author: str = "activity-core",
|
||||
topic_id: str | None = None,
|
||||
workplan_id: str | None = None,
|
||||
workstream_id: str | None = None,
|
||||
timeout_seconds: float = 10.0,
|
||||
) -> dict[str, Any]:
|
||||
|
|
@ -174,8 +175,10 @@ def post_missed_fire_alert(
|
|||
}
|
||||
if topic_id:
|
||||
body["topic_id"] = topic_id
|
||||
if workstream_id:
|
||||
body["workstream_id"] = workstream_id
|
||||
scope_id = workplan_id or workstream_id
|
||||
if scope_id:
|
||||
body["workplan_id"] = scope_id
|
||||
body["workstream_id"] = scope_id
|
||||
|
||||
# Dedup repeated alerts for the same missed window (same schedule + last fire).
|
||||
last_fired = health.last_fired_at.isoformat() if health.last_fired_at else "none"
|
||||
|
|
|
|||
|
|
@ -15,9 +15,26 @@ ignored by State Hub versions that do not yet honour it.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
IDEMPOTENCY_HEADER = "Idempotency-Key"
|
||||
|
||||
|
||||
def apply_progress_scope_fields(body: dict[str, Any], source: dict[str, Any]) -> None:
|
||||
"""Copy topic/workplan/task/decision scope from a sink config into a progress body.
|
||||
|
||||
Prefers ``workplan_id``; dual-writes ``workstream_id`` for wire-compat until
|
||||
legacy-meter retires the alias.
|
||||
"""
|
||||
workplan_id = source.get("workplan_id") or source.get("workstream_id")
|
||||
if workplan_id:
|
||||
body["workplan_id"] = workplan_id
|
||||
body["workstream_id"] = workplan_id
|
||||
for key in ("topic_id", "task_id", "decision_id"):
|
||||
if source.get(key):
|
||||
body[key] = source[key]
|
||||
|
||||
|
||||
def idempotency_key(*parts: str | None) -> str:
|
||||
"""Build a stable, header-safe idempotency key from identity parts.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue