atlas-iac/services/hermes/scripts/supervisor_policy.py

498 lines
19 KiB
Python

#!/usr/bin/env python3
"""Pure, fail-closed policy for bounded implementation-review-repair chains."""
from __future__ import annotations
import json
from dataclasses import dataclass
from typing import Any
import cli_lane_goal
import supervisor_lineage as lineage
SUPERVISOR_AUTHOR = "hermes-supervisor"
STAMP_KEY = "supervisor"
REVIEW_KIND = "review"
REPAIR_KIND = "repair"
DONE_STATUSES = frozenset({"done"})
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", "clear_ready", "escalate"})
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-auto"
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 is_lineage_only_anchor(task: Any) -> bool:
"""Keep only trusted, unparented legacy roots out of automated chains."""
meta = metadata(task)
root = meta.get("supervisor_lineage")
trusted = lineage.initial({"id": task_id(task), "metadata": {"supervisor_lineage": root}})
return (isinstance(root, dict) and STAMP_KEY not in meta and trusted is not None
and trusted.root_task_id == task_id(task)
and field(task, "result") == "Lineage-only migration anchor."
and "Lineage-only anchor" in str(field(task, "body", "") or ""))
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 supervisor or external card already covers this revision."""
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, chain: lineage.Lineage
) -> dict:
stamped = {
"kind": kind,
"root": root,
"parent": source,
"head_commit": head_commit,
"cycle": cycle,
**chain.stamp_fields(),
}
return {STAMP_KEY: stamped}
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,
chain: lineage.Lineage,
findings: list[str],
assignee: str,
) -> dict[str, Any]:
meta = _stamp_meta(REVIEW_KIND, root_id, source_id, head_commit, cycle, chain)
meta["task_role"] = cli_lane_goal.REVIEW_ROLE
body = (
"Hermes-Task-Role: review\n\n"
f"Read-only review of commit {head_commit} on {chain.pull_request}, branch "
f"{chain.branch}. 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),
"initial_status": "running",
}
def _build_repair(
root_id: str,
review_id: str,
head_commit: str,
cycle: int,
findings: list[str],
assignee: str,
chain: lineage.Lineage,
) -> dict[str, Any]:
meta = _stamp_meta(REPAIR_KIND, root_id, review_id, head_commit, cycle, chain)
meta["task_role"] = cli_lane_goal.IMPLEMENTATION_ROLE
body = (
"Hermes-Task-Role: repair\n\n"
f"Repair commit {head_commit} on existing PR {chain.pull_request}, branch "
f"{chain.branch}; do not create a branch or PR. 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),
"initial_status": "running",
}
def _latest_head(tasks: list[Any], chain: lineage.Lineage) -> str:
"""Return the newest verified repair head, never treating base drift as stale."""
newest = ""
newest_cycle = 0
for candidate in tasks:
if task_id(candidate) == chain.root_task_id:
root_meta = metadata(candidate)
value = _first_key(root_meta, ("live_pr_head", "latest_head_commit"))
if value:
return value
candidate_stamp = stamp(candidate)
if supervised_kind(candidate) != REPAIR_KIND or lineage.from_stamp(candidate_stamp) != chain:
continue
result, error = parse_result(candidate)
commit = None if error else extract_commit(candidate, result)
cycle = _int(candidate_stamp.get("cycle"), 0)
if commit and cycle >= newest_cycle:
newest, newest_cycle = commit, cycle
return newest
def _validated_chain(task: Any, tasks: list[Any]) -> lineage.Lineage | None:
"""Accept child lineage only when its root and stamped parent corroborate it."""
child_stamp = stamp(task)
chain = lineage.from_stamp(child_stamp)
if chain is None:
return None
root = next((item for item in tasks if task_id(item) == chain.root_task_id), None)
if root is None or lineage.initial(root) != chain:
return None
parent_id = str(child_stamp.get("parent") or "")
if parent_id == chain.root_task_id:
return chain
parent = next((item for item in tasks if task_id(item) == parent_id), None)
return chain if parent is not None and lineage.from_stamp(stamp(parent)) == chain else None
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)
chain = lineage.initial(task)
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 chain is None:
return Decision(
"escalate",
"implementation lacks trusted branch/PR lineage from its assignment payload",
tid,
)
if chain.root_task_id != tid:
return Decision("escalate", "initial assignment root_task_id does not match task", 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, chain, [], 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)
chain = _validated_chain(task, tasks)
if not root_id or not head_commit or cycle <= 0 or chain is None or chain.root_task_id != root_id:
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":
current = _latest_head(tasks, chain)
if current and current != head_commit:
return Decision(
"clear_ready",
f"stale SHIP for {head_commit}; current verified chain head is {current}",
root_id,
{"commit": current, "stale_commit": head_commit},
)
return Decision(
"ship",
f"review shipped {head_commit}",
root_id,
{"commit": head_commit, "pr": chain.pull_request, "branch": chain.branch,
"project": chain.project, "base_branch": chain.base_branch,
"root_task_id": chain.root_task_id},
)
# 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, chain
)
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)
chain = _validated_chain(task, tasks)
if not root_id or cycle <= 0 or chain is None or chain.root_task_id != root_id:
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:
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")
payload = _build_review(
root_id, repair_id, new_commit, next_cycle, chain, [], 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)
if is_lineage_only_anchor(task):
return Decision("none", "lineage-only migration anchors require an explicit continuation")
return plan_implementation(task, tasks, limits)