← Files Grok Bot ControlARCHIVED FILE
skills/grok-bot-control/scripts/assess_wait.py
6.66 KB · Oct 5, 2026 · 18:33 UTC
#!/usr/bin/env python3
"""Read a coordination snapshot and advise a follow-up action without side effects."""
from __future__ import annotations
import argparse
import json
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
PHASES = {"drafting", "waiting_bot", "reviewing", "complete", "blocked", "cancelled"}
TIME_FIELDS = ("waiting_since", "last_progress_at", "last_nudge_at", "announced_eta")
class InputError(ValueError):
pass
def parse_time(value: Any, field: str) -> datetime | None:
if value is None:
return None
if not isinstance(value, str):
raise InputError(f"{field} must be a timezone-aware ISO-8601 string or null")
text = value[:-1] + "+00:00" if value.endswith("Z") else value
try:
parsed = datetime.fromisoformat(text)
except ValueError as exc:
raise InputError(f"{field} is not valid ISO-8601") from exc
if parsed.tzinfo is None or parsed.utcoffset() is None:
raise InputError(f"{field} must include a timezone offset")
return parsed.astimezone(timezone.utc)
def format_time(value: datetime) -> str:
return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
def require_schema(data: Any) -> dict[str, Any]:
if not isinstance(data, dict):
raise InputError("state must be a JSON object")
required = {
"schema_version", "phase", *TIME_FIELDS, "nudges_since_progress",
"stall_notice_sent", "nudge_policy", "operator", "conversation", "last_sent",
}
missing = sorted(required - data.keys())
if missing:
raise InputError(f"missing required fields: {', '.join(missing)}")
if type(data["schema_version"]) is not int or data["schema_version"] != 1:
raise InputError("schema_version must be 1")
if not isinstance(data["phase"], str) or data["phase"] not in PHASES:
raise InputError("phase is invalid")
if not isinstance(data["operator"], str):
raise InputError("operator must be a string")
if data["conversation"] is not None and not isinstance(data["conversation"], str):
raise InputError("conversation must be a string or null")
last_sent = data["last_sent"]
if not isinstance(last_sent, dict):
raise InputError("last_sent must be an object")
if "uncertain_send" not in last_sent or not isinstance(last_sent["uncertain_send"], bool):
raise InputError("last_sent.uncertain_send must be boolean")
count = data["nudges_since_progress"]
if type(count) is not int or count < 0:
raise InputError("nudges_since_progress must be a non-negative integer")
if not isinstance(data["stall_notice_sent"], bool):
raise InputError("stall_notice_sent must be boolean")
policy = data["nudge_policy"]
if not isinstance(policy, dict):
raise InputError("nudge_policy must be an object")
for field in ("enabled", "first_after_minutes", "repeat_after_minutes", "max_per_stall"):
if field not in policy:
raise InputError(f"missing nudge_policy field: {field}")
if not isinstance(policy["enabled"], bool):
raise InputError("nudge_policy.enabled must be boolean")
for field in ("first_after_minutes", "repeat_after_minutes", "max_per_stall"):
if type(policy[field]) is not int or policy[field] < 0:
raise InputError(f"nudge_policy.{field} must be a non-negative integer")
if count > policy["max_per_stall"]:
raise InputError("nudges_since_progress exceeds nudge cap")
return data
def decide(data: dict[str, Any], now: datetime) -> dict[str, Any]:
times = {field: parse_time(data[field], field) for field in TIME_FIELDS}
if data["phase"] != "waiting_bot":
return {"action": "none", "reason": f"phase_{data['phase']}"}
policy = data["nudge_policy"]
if not policy["enabled"]:
return {"action": "none", "reason": "nudge_policy_disabled"}
if data["last_sent"]["uncertain_send"]:
return {"action": "inspect", "reason": "uncertain_send_unresolved"}
if (
not data["operator"].strip()
or not data["conversation"]
or not data["conversation"].strip()
):
return {"action": "inspect", "reason": "missing_operator_or_conversation"}
unexpected_future = sorted(
field for field, value in times.items()
if field != "announced_eta" and value is not None and value > now
)
if unexpected_future:
return {"action": "inspect", "reason": "future_timestamp:" + ",".join(unexpected_future)}
waiting = times["waiting_since"]
progress = times["last_progress_at"]
last_nudge = times["last_nudge_at"]
eta = times["announced_eta"]
count = data["nudges_since_progress"]
cap = policy["max_per_stall"]
if waiting is None or progress is None:
return {"action": "inspect", "reason": "missing_wait_or_progress_time"}
if (count == 0) != (last_nudge is None):
return {"action": "inspect", "reason": "inconsistent_nudge_fields"}
if last_nudge is not None and (last_nudge < waiting or last_nudge < progress):
return {"action": "inspect", "reason": "nudge_precedes_wait_or_progress"}
if data["stall_notice_sent"] and count < cap:
return {"action": "inspect", "reason": "stall_notice_before_nudge_cap"}
due = max(waiting, progress) + timedelta(minutes=policy["first_after_minutes"])
if last_nudge is not None:
due = max(due, last_nudge + timedelta(minutes=policy["repeat_after_minutes"]))
if eta is not None and eta > now:
due = max(due, eta)
if now < due:
return {"action": "wait", "reason": "waiting_interval_or_eta", "next_check_at": format_time(due)}
if count >= cap:
if data["stall_notice_sent"]:
return {"action": "none", "reason": "stall_already_reported"}
return {"action": "notify_user", "reason": "nudge_cap_reached_without_progress"}
return {"action": "nudge_due", "reason": "bot_stalled_past_due"}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("state", type=Path)
parser.add_argument("--now", help="timezone-aware ISO-8601 time; defaults to current UTC")
args = parser.parse_args()
try:
now = parse_time(args.now, "now") if args.now else datetime.now(timezone.utc)
assert now is not None
result = decide(
require_schema(json.loads(args.state.read_text(encoding="utf-8"))), now
)
except (InputError, json.JSONDecodeError, UnicodeDecodeError, OSError) as exc:
print(json.dumps({"error": {"code": "invalid_input", "message": str(exc)}}))
return 2
print(json.dumps(result, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: 93a7eba04e0d81c5fe4d2d184a8f3ef12a1daa6587389fd82f266063fc810a29