498 lines
19 KiB
Python
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)
|