"""Canonical Thread Handoff (CTH) protocol for issue and PR comments (#505). A CTH comment is the authoritative workflow handoff in a Gitea issue or PR thread. It records current state, decisions, blockers, proof, and the exact next prompt/action for the next LLM or person. """ from __future__ import annotations import re from datetime import datetime, timezone from typing import Any MARKER = "" CTH_TERM = "Canonical Thread Handoff" CTH_ABBREV = "CTH" CTH_TYPES = frozenset({ "State Handoff", "Controller Decision", "Author Handoff", "Reviewer Handoff", "Merger Handoff", "Supersession Notice", "Blocker", }) _REQUIRED_BASE_FIELDS = ( "status", "next owner", "current blocker", "decision", "proof", "next action", "ready-to-paste prompt", ) _HEADING_RE = re.compile( r"^##\s+CTH:\s*(.+?)\s*$", re.IGNORECASE | re.MULTILINE, ) _FIELD_RE = re.compile( r"^([A-Za-z][A-Za-z0-9 /-]*):\s*(.+?)\s*$", re.MULTILINE, ) def format_cth_body( *, cth_type: str, status: str, next_owner: str, current_blocker: str = "none", decision: str = "none", proof: str = "none", next_action: str = "none", ready_to_paste_prompt: str = "none", extra_fields: dict[str, str] | None = None, ) -> str: """Render a canonical CTH comment body.""" normalized_type = (cth_type or "").strip() if normalized_type not in CTH_TYPES: raise ValueError( f"unknown CTH type '{cth_type}'; expected one of {sorted(CTH_TYPES)}" ) lines = [ MARKER, f"## CTH: {normalized_type}", "", f"Status: {(status or 'unknown').strip() or 'unknown'}", f"Next owner: {(next_owner or 'unknown').strip() or 'unknown'}", f"Current blocker: {(current_blocker or 'none').strip() or 'none'}", f"Decision: {(decision or 'none').strip() or 'none'}", f"Proof: {(proof or 'none').strip() or 'none'}", f"Next action: {(next_action or 'none').strip() or 'none'}", f"Ready-to-paste prompt: {(ready_to_paste_prompt or 'none').strip() or 'none'}", ] for key, value in (extra_fields or {}).items(): label = (key or "").strip() if not label: continue lines.append(f"{label}: {(value or 'none').strip() or 'none'}") return "\n".join(lines) def parse_cth_comment(body: str) -> dict[str, Any] | None: """Parse one CTH comment body, or None when not a CTH comment.""" text = body or "" if MARKER not in text and not _HEADING_RE.search(text): return None heading = _HEADING_RE.search(text) if not heading: return None cth_type = heading.group(1).strip() fields: dict[str, str] = {} for match in _FIELD_RE.finditer(text): key = match.group(1).strip().lower() if key.startswith("cth"): continue fields[key] = match.group(2).strip() return { "cth_type": cth_type, "fields": fields, "raw_body": text, } def assess_cth_comment(body: str) -> dict[str, Any]: """Validate a CTH comment has required base fields and a known type.""" parsed = parse_cth_comment(body) reasons: list[str] = [] if not parsed: return { "valid": False, "block": True, "reasons": ["comment is not a Canonical Thread Handoff (CTH)"], "parsed": None, } cth_type = parsed.get("cth_type") or "" if cth_type not in CTH_TYPES: reasons.append( f"unknown CTH type '{cth_type}'; expected one of {sorted(CTH_TYPES)}" ) fields = parsed.get("fields") or {} missing = [name for name in _REQUIRED_BASE_FIELDS if not (fields.get(name) or "").strip()] if missing: reasons.append( "CTH missing required fields: " + ", ".join(missing) ) vague_prompt = (fields.get("ready-to-paste prompt") or "").strip().lower() if vague_prompt in {"", "none", "tbd", "n/a", "todo"}: reasons.append("ready-to-paste prompt must be concrete and paste-ready") return { "valid": not reasons, "block": bool(reasons), "reasons": reasons, "parsed": parsed, "cth_type": cth_type, } def find_latest_cth(comments: list[dict[str, Any]]) -> dict[str, Any] | None: """Return the newest valid CTH comment from a Gitea comment list.""" latest: dict[str, Any] | None = None latest_ts: datetime | None = None for comment in comments or []: body = comment.get("body") or "" parsed = parse_cth_comment(body) if not parsed: continue created = _parse_timestamp(comment.get("created_at")) if latest is None or (created and (latest_ts is None or created >= latest_ts)): latest = { "comment_id": comment.get("id"), "created_at": comment.get("created_at"), "author": (comment.get("user") or {}).get("login"), **parsed, } latest_ts = created return latest def assess_cth_supersedes_non_cth( *, latest_cth: dict[str, Any] | None, narrative_claim: str, ) -> dict[str, Any]: """Fail closed when narrative relies on stale non-CTH state despite newer CTH.""" if not latest_cth: return {"block": False, "reasons": []} text = (narrative_claim or "").strip() if not text: return {"block": False, "reasons": []} if parse_cth_comment(text): return {"block": False, "reasons": []} return { "block": True, "reasons": [ "workflow narrative must treat the latest CTH comment as authoritative; " f"found newer CTH type '{latest_cth.get('cth_type')}' " f"(comment #{latest_cth.get('comment_id')})" ], } def _parse_timestamp(value: str | None) -> datetime | None: if not value: return None text = value.strip() if text.endswith("Z"): text = text[:-1] + "+00:00" try: parsed = datetime.fromisoformat(text) except ValueError: return None if parsed.tzinfo is None: return parsed.replace(tzinfo=timezone.utc) return parsed.astimezone(timezone.utc) EXAMPLE_SCENARIOS = ( "pr_approved_ready_for_merger", "pr_request_changes_to_author", "duplicate_superseded_pr_closure", "blocked_dirty_root_worktree", "stale_head_requires_fresh_review", "issue_implementation_handoff", )