feat: Phase 5 stabilization schedules and custodian path URIs
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 9s
Build and Publish Container Image / build-and-push (push) Successful in 2m58s

Add phase5_stabilization_check State Hub resolver with progress evidence
sinks, schedule projections for daily and closeout checks, custodian:// and
activity-core:// runtime path resolution, and Railiance mounts under /var/custodian.
This commit is contained in:
tegwick 2026-07-07 00:48:36 +02:00
parent 5c3a89c495
commit 70e3154b05
10 changed files with 672 additions and 18 deletions

View file

@ -13,6 +13,7 @@ Supported queries:
- daily_triage_digest: curated scalar JSON digest for daily WSJF triage
- recently_on_scope_hourly: POST {STATE_HUB_URL}/recently-on-scope/hourly
- consistency_sweep_remote_all: POST {STATE_HUB_URL}/consistency/sweep/remote-all
- phase5_stabilization_check: hub-visible Phase 5 stabilization gates
No caching state hub data is live operational state and must not be stale
within a single workflow run.
@ -134,9 +135,159 @@ class StateHubContextResolver(ContextResolver):
timeout=_SWEEP_TIMEOUT_SECONDS,
)
return _validate_consistency_sweep_remote_all(result)
if query == "phase5_stabilization_check":
return _phase5_stabilization_check(params)
return {}
def _parse_iso_datetime(raw: Any) -> datetime | None:
if not raw:
return None
text = str(raw).replace("Z", "+00:00")
try:
dt = datetime.fromisoformat(text)
except ValueError:
return None
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
def _utc_now() -> datetime:
return datetime.now(timezone.utc)
def _phase5_stabilization_check(params: dict[str, Any]) -> dict[str, Any]:
closeout = bool(params.get("closeout"))
now = _utc_now()
window_start = _parse_iso_datetime(
params.get("window_start", "2026-07-06T17:35:00+00:00")
)
window_end = _parse_iso_datetime(
params.get("window_end", "2026-07-09T17:35:00+00:00")
)
within_window = bool(
window_start
and window_end
and window_start <= now <= window_end
)
if not within_window and not closeout:
return {
"skipped": True,
"reason": "outside_stabilization_window",
"closeout": closeout,
"overall_pass": True,
"window": {
"start": window_start.isoformat() if window_start else None,
"end": window_end.isoformat() if window_end else None,
"within": within_window,
},
"checks": {},
}
baseline = params.get("baseline") or {}
expected_workstreams = int(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)
triage_max_age_hours = _bounded_int(
params.get("triage_max_age_hours", 36),
default=36,
minimum=1,
maximum=168,
)
health = _fetch_json("/state/health")
health_pass = isinstance(health, dict) and health.get("status") == "ok"
summary = _fetch_json("/state/summary")
totals = (summary or {}).get("totals") or {}
ws_total = int((totals.get("workstreams") or {}).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
and task_total == expected_tasks
and topic_total == expected_topics
)
sweep_items = _fetch_json(
"/progress/",
{"event_type": "consistency_sweep_remote_all", "limit": sweep_limit},
)
sweep_rows = sweep_items if isinstance(sweep_items, list) else []
sweep_failures = 0
sweep_missing = 0
for item in sweep_rows:
detail = item.get("detail") or {}
if detail.get("exit_code") != 0 or detail.get("automation_error"):
sweep_failures += 1
missing = detail.get("skipped_missing") or []
if isinstance(missing, list) and missing:
sweep_missing += len(missing)
sweeps_pass = bool(sweep_rows) and sweep_failures == 0 and sweep_missing == 0
triage_items = _fetch_json(
"/progress/",
{"event_type": "daily_triage", "limit": 1},
)
triage_row = triage_items[0] if isinstance(triage_items, list) and triage_items else None
triage_at = _parse_iso_datetime(triage_row.get("created_at") if triage_row else None)
triage_age_hours = (
(now - triage_at).total_seconds() / 3600 if triage_at else None
)
triage_pass = bool(
triage_row
and triage_age_hours is not None
and triage_age_hours <= triage_max_age_hours
)
checks = {
"health": {"pass": health_pass, "status": health.get("status") if isinstance(health, dict) else None},
"totals": {
"pass": totals_pass,
"workstreams": ws_total,
"tasks": task_total,
"topics": topic_total,
"expected": {
"workstreams": expected_workstreams,
"tasks": expected_tasks,
"topics": expected_topics,
},
},
"sweeps": {
"pass": sweeps_pass,
"sampled": len(sweep_rows),
"failures": sweep_failures,
"missing": sweep_missing,
},
"daily_triage": {
"pass": triage_pass,
"last_at": triage_at.isoformat() if triage_at else None,
"age_hours": round(triage_age_hours, 2) if triage_age_hours is not None else None,
"max_age_hours": triage_max_age_hours,
},
}
overall_pass = all(section.get("pass") for section in checks.values())
result = {
"skipped": False,
"closeout": closeout,
"overall_pass": overall_pass,
"operator_signoff_needed": closeout and overall_pass,
"window": {
"start": window_start.isoformat() if window_start else None,
"end": window_end.isoformat() if window_end else None,
"within": within_window,
},
"checks": checks,
"source": str(params.get("source") or "activity-core"),
}
if closeout and not overall_pass:
raise RuntimeError(f"phase5 stabilization closeout failed: {result}")
return result
CONTEXT_RESOLVER_REGISTRY["state-hub"] = StateHubContextResolver