#!/usr/bin/env python3 """Pure decision logic for the in-pod autonomous Kanban supervisor. This module owns only *pure* policy: given one candidate task and a snapshot of its board, it returns the single follow-up action the supervisor should take, a fail-closed escalation, or nothing. It performs no I/O and imports no provider client, so the whole cross-card implement->review->repair->re-review state machine is unit testable and can never, by construction, emit a metered/model call or a merge/approve/close/deploy. Every side effect (create_task, block_task, add_comment) lives in :mod:`kanban_supervisor`. Safety contract enforced here (audited hard - it drives an autonomous loop): * FAIL CLOSED - an unparseable result, an ambiguous SHIP/BLOCK verdict, a done implementation whose produced commit/PR cannot be identified, or a repair that produced no new commit yields an ``escalate`` decision (human attention), never an auto-spawn and never an inferred SHIP. * NO DUPLICATE - a review or repair is proposed only when no card already covers the same ``(root, head_commit)``, detected across supervisor-stamped *and* externally created cards (the codex-shepherd), so it is concurrency-safe. * BOUNDED - a per-chain review<->repair cycle limit and a max-concurrent-chains ceiling turn runaway loops into human escalations instead of capacity burn. * SUBSCRIPTION ONLY - the only spawn is a Kanban card on the existing cli-* subscription lanes; no provider/API-key field is ever produced. """ from __future__ import annotations import json from dataclasses import dataclass from typing import Any import cli_lane_goal SUPERVISOR_AUTHOR = "hermes-supervisor" STAMP_KEY = "supervisor" REVIEW_KIND = "review" REPAIR_KIND = "repair" DONE_STATUSES = frozenset({"done"}) # A supervised card whose status is one of these is no longer an in-flight chain # link (done = finished, blocked = already escalated to a human). INACTIVE_STATUSES = frozenset({"done", "blocked"}) COMMIT_KEYS = ("head_commit", "commit_sha", "commit", "sha", "revision") PR_KEYS = ("pull_request", "pr_url", "pr", "merge_request") BRANCH_KEYS = ("branch", "branch_name", "head_branch") SAFE_ACTIONS = frozenset({"none", "spawn", "ship", "escalate"}) # Keys that would signal a privileged/metered action; a payload must never carry # one. Enforced by :func:`assert_safe` before any card is written. FORBIDDEN_PAYLOAD_KEYS = frozenset( {"merge", "approve", "close", "deploy", "provider", "model", "api_key", "metered"} ) @dataclass(frozen=True) class Limits: """Bounds and routing that gate every autonomous decision.""" max_cycles: int = 5 max_chains: int = 20 review_assignee: str = "cli-claude-xhigh" repair_assignee: str = "cli-auto" @dataclass(frozen=True) class Decision: """One resolved supervisor intent. ``action`` is always in SAFE_ACTIONS.""" action: str reason: str = "" target_id: str = "" payload: dict[str, Any] | None = None def assert_safe(decision: Decision) -> Decision: """Reject any decision outside the allow-listed, non-metered action set.""" if decision.action not in SAFE_ACTIONS: raise ValueError(f"unsafe supervisor action: {decision.action!r}") payload = decision.payload or {} for key in FORBIDDEN_PAYLOAD_KEYS: if key in payload: raise ValueError(f"supervisor payload carries forbidden key {key!r}") return decision def field(task: Any, name: str, default: Any = None) -> Any: if isinstance(task, dict): return task.get(name, default) return getattr(task, name, default) def _as_dict(value: Any) -> dict[str, Any] | None: if isinstance(value, dict): return value if isinstance(value, str): try: parsed = json.loads(value) except (ValueError, TypeError): return None return parsed if isinstance(parsed, dict) else None return None def task_id(task: Any) -> str: return str(field(task, "id", "") or "") def metadata(task: Any) -> dict[str, Any]: return _as_dict(field(task, "metadata")) or {} def stamp(task: Any) -> dict[str, Any]: value = metadata(task).get(STAMP_KEY) return value if isinstance(value, dict) else {} def supervised_kind(task: Any) -> str | None: kind = stamp(task).get("kind") return kind if kind in (REVIEW_KIND, REPAIR_KIND) else None def is_done(task: Any) -> bool: return str(field(task, "status", "") or "") in DONE_STATUSES def objective(task: Any) -> str: title = str(field(task, "title", "") or "") body = str(field(task, "body", "") or "") return f"{title}\n\n{body}".strip() def parents(task: Any) -> list[str]: raw = field(task, "parents") if raw is None: raw = field(task, "task_links") if isinstance(raw, dict): raw = raw.get("parents") ids: list[str] = [] if isinstance(raw, (list, tuple, set)): for item in raw: if isinstance(item, dict): ident = item.get("id") or item.get("parent") or item.get("task_id") else: ident = item if ident: ids.append(str(ident)) return ids def parse_result(task: Any) -> tuple[dict[str, Any] | None, str | None]: """Parse the terminal result, failing closed on anything non-object.""" raw = field(task, "result") if raw is None or raw == "" or raw == {}: return None, "task carries no result to interpret" parsed = _as_dict(raw) if parsed is None: return None, "task result is not a JSON object" return parsed, None def _first_key(source: Any, keys: tuple[str, ...]) -> str | None: if not isinstance(source, dict): return None for key in keys: value = source.get(key) if isinstance(value, str) and value.strip(): return value.strip() return None def _result_sources(task: Any, result: dict[str, Any] | None) -> list[dict[str, Any]]: sources = [metadata(task)] if isinstance(result, dict): sources.append(result) nested = _as_dict(result.get("metadata")) if nested is not None: sources.append(nested) return sources def extract_commit(task: Any, result: dict[str, Any] | None) -> str | None: for source in _result_sources(task, result): found = _first_key(source, COMMIT_KEYS) if found: return found return None def extract_pr(task: Any, result: dict[str, Any] | None) -> str | None: for source in _result_sources(task, result): found = _first_key(source, PR_KEYS) or _first_key(source, BRANCH_KEYS) if found: return found return None def _has_changes(result: dict[str, Any] | None) -> bool: changed = (result or {}).get("changed_files") return isinstance(changed, (list, tuple)) and len(changed) > 0 def _strings(value: Any) -> list[str]: if not isinstance(value, (list, tuple)): return [] return [str(item) for item in value if isinstance(item, str) and item.strip()] def _int(value: Any, default: int) -> int: if isinstance(value, bool) or not isinstance(value, (int, float)): return default return int(value) def _haystack(task: Any) -> str: return "\n".join( ( str(field(task, "title", "") or ""), str(field(task, "body", "") or ""), json.dumps(metadata(task), sort_keys=True, default=str), ) ) def _references(task: Any, ident: str) -> bool: if not ident: return False if ident in parents(task): return True return ident in _haystack(task) def _mentions_commit(task: Any, head_commit: str) -> bool: return bool(head_commit) and head_commit in _haystack(task) def _looks_like_review(task: Any) -> bool: if metadata(task).get("task_role") == cli_lane_goal.REVIEW_ROLE: return True role, _ = cli_lane_goal.task_role(objective(task)) if role == cli_lane_goal.REVIEW_ROLE: return True return "review" in str(field(task, "title", "") or "").lower() def _looks_like_repair(task: Any) -> bool: if metadata(task).get("task_role") == cli_lane_goal.IMPLEMENTATION_ROLE: return True title = str(field(task, "title", "") or "").lower() return "repair" in title or "fix" in title def existing_followup( tasks: list[Any], kind: str, root_id: str, head_commit: str ) -> bool: """True if any card already covers ``(root_id, head_commit)`` for ``kind``. Matches both supervisor-stamped cards and externally created ones (the external shepherd), so concurrent operation never double-spawns. """ looks_like = _looks_like_review if kind == REVIEW_KIND else _looks_like_repair for task in tasks: st = stamp(task) if ( st.get("kind") == kind and str(st.get("root") or "") == root_id and str(st.get("head_commit") or "") == head_commit ): return True if ( _references(task, root_id) and _mentions_commit(task, head_commit) and looks_like(task) ): return True return False def active_chain_count(tasks: list[Any]) -> int: """Distinct chains with an in-flight (not done/blocked) supervised card.""" roots: set[str] = set() for task in tasks: if supervised_kind(task) is None: continue if str(field(task, "status", "") or "") in INACTIVE_STATUSES: continue root = str(stamp(task).get("root") or "") if root: roots.add(root) return len(roots) def _stamp_meta(kind: str, root: str, source: str, head_commit: str, cycle: int) -> dict: return { STAMP_KEY: { "kind": kind, "root": root, "parent": source, "head_commit": head_commit, "cycle": cycle, } } def _idempotency(kind: str, root: str, head_commit: str, cycle: int) -> str: return f"supervisor:{kind}:{root}:{head_commit}:{cycle}" def _findings_block(findings: list[str]) -> str: if not findings: return "" lines = "\n".join(f"- {item}" for item in findings) return f"\nReview findings to address:\n{lines}\n" def _build_review( root_id: str, source_id: str, head_commit: str, cycle: int, pr_ref: str, findings: list[str], assignee: str, ) -> dict[str, Any]: meta = _stamp_meta(REVIEW_KIND, root_id, source_id, head_commit, cycle) meta["task_role"] = cli_lane_goal.REVIEW_ROLE body = ( "Hermes-Task-Role: review\n\n" f"Read-only review of commit {head_commit}" f"{f' on {pr_ref}' if pr_ref else ''}. Do not modify the implementation; " "make no code changes. Report a single SHIP or BLOCK verdict with " "evidence." + _findings_block(findings) ) linked = [root_id] if source_id == root_id else [root_id, source_id] return { "title": f"Review commit {head_commit[:12]} (cycle {cycle})", "body": body, "assignee": assignee, "parents": linked, "metadata": meta, "idempotency_key": _idempotency(REVIEW_KIND, root_id, head_commit, cycle), } def _build_repair( root_id: str, review_id: str, head_commit: str, cycle: int, findings: list[str], assignee: str, ) -> dict[str, Any]: meta = _stamp_meta(REPAIR_KIND, root_id, review_id, head_commit, cycle) meta["task_role"] = cli_lane_goal.IMPLEMENTATION_ROLE body = ( "Hermes-Task-Role: repair\n\n" f"Repair the implementation reviewed at commit {head_commit} per the " "review findings below, then commit and push the fix." + _findings_block(findings) ) return { "title": f"Repair review findings for {head_commit[:12]} (cycle {cycle})", "body": body, "assignee": assignee, "parents": [root_id, review_id], "metadata": meta, "idempotency_key": _idempotency(REPAIR_KIND, root_id, head_commit, cycle), } def plan_implementation(task: Any, tasks: list[Any], limits: Limits) -> Decision: tid = task_id(task) result, error = parse_result(task) role, _ = cli_lane_goal.task_role(objective(task), result or {}) if role != cli_lane_goal.IMPLEMENTATION_ROLE: # A done review-shaped card the supervisor did not create belongs to the # external shepherd's chain; never drive it from the implementation side. return Decision("none", "not a supervisor-owned implementation card") if error is not None: return Decision("escalate", f"implementation result unparseable: {error}", tid) commit = extract_commit(task, result) pr = extract_pr(task, result) if commit is None: if _has_changes(result): return Decision( "escalate", "done implementation changed files but exposes no head commit to review", tid, ) return Decision("none", "implementation produced no commit to review") if pr is None: return Decision( "escalate", "implementation head commit present but no branch/PR reference to review", tid, ) if existing_followup(tasks, REVIEW_KIND, tid, commit): return Decision("none", "a review already covers this commit") if active_chain_count(tasks) >= limits.max_chains: return Decision("none", "supervised-chain ceiling reached; deferring new review") payload = _build_review(tid, tid, commit, 1, pr, [], limits.review_assignee) return Decision("spawn", f"open review for {commit}", tid, payload) def plan_review(task: Any, tasks: list[Any], limits: Limits) -> Decision: st = stamp(task) review_id = task_id(task) root_id = str(st.get("root") or "") head_commit = str(st.get("head_commit") or "") cycle = _int(st.get("cycle"), 0) if not root_id or not head_commit or cycle <= 0: return Decision( "escalate", "review card carries an incomplete supervisor stamp", review_id ) result, error = parse_result(task) if error is not None: return Decision("escalate", f"review result unparseable: {error}", review_id) verdict, problem = cli_lane_goal.review_verdict(result) if problem or verdict is None: detail = problem or "no explicit SHIP or BLOCK verdict" return Decision("escalate", f"review verdict ambiguous: {detail}", review_id) if verdict == "SHIP": return Decision( "ship", f"review shipped {head_commit}", root_id, {"commit": head_commit, "pr": extract_pr(task, result) or ""}, ) # BLOCK -> spawn a bounded repair, unless one exists or the budget is spent. if cycle >= limits.max_cycles: # Escalate the review card itself, not root_id: the impl stays # legitimately done, so re-targeting it re-fires every tick. return Decision( "escalate", f"repair cycle limit ({limits.max_cycles}) reached for chain {root_id}; " "escalating this review instead of spawning another repair", review_id, ) if existing_followup(tasks, REPAIR_KIND, root_id, head_commit): return Decision("none", "a repair already covers this commit") findings = _strings(result.get("findings")) payload = _build_repair( root_id, review_id, head_commit, cycle, findings, limits.repair_assignee ) return Decision("spawn", f"open repair for {head_commit}", review_id, payload) def plan_repair(task: Any, tasks: list[Any], limits: Limits) -> Decision: st = stamp(task) repair_id = task_id(task) root_id = str(st.get("root") or "") old_commit = str(st.get("head_commit") or "") cycle = _int(st.get("cycle"), 0) if not root_id or cycle <= 0: return Decision( "escalate", "repair card carries an incomplete supervisor stamp", repair_id ) result, error = parse_result(task) if error is not None: return Decision("escalate", f"repair result unparseable: {error}", repair_id) new_commit = extract_commit(task, result) if new_commit is None: return Decision( "escalate", "repair completed without an identifiable new commit", repair_id ) if new_commit == old_commit: return Decision( "escalate", "repair produced no new commit over the reviewed revision", repair_id, ) next_cycle = cycle + 1 if next_cycle > limits.max_cycles: # Escalate the repair card itself, not root_id (see plan_review). return Decision( "escalate", f"repair cycle limit ({limits.max_cycles}) reached for chain {root_id}; " "escalating this repair instead of spawning a re-review", repair_id, ) if existing_followup(tasks, REVIEW_KIND, root_id, new_commit): return Decision("none", "a re-review already covers this commit") pr = extract_pr(task, result) or "" payload = _build_review( root_id, repair_id, new_commit, next_cycle, pr, [], limits.review_assignee ) return Decision("spawn", f"open re-review for {new_commit}", repair_id, payload) def plan(task: Any, tasks: list[Any], limits: Limits) -> Decision: """Resolve the single follow-up action for one terminal card.""" if not is_done(task): return Decision("none", "task is not in a terminal done state") kind = supervised_kind(task) if kind == REVIEW_KIND: return plan_review(task, tasks, limits) if kind == REPAIR_KIND: return plan_repair(task, tasks, limits) return plan_implementation(task, tasks, limits)