Extend the review workflow load gate with pre-review command classification, boundary violation tracking, and structured helper-result requirements in final reports. Adds gitea_record_pre_review_command MCP tool. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
259 lines
9.0 KiB
Python
259 lines
9.0 KiB
Python
"""Reviewer session boundary tracking for workflow-load gate (#403).
|
|
|
|
Pre-review commands executed before ``gitea_load_review_workflow`` must be
|
|
classified. Boundary violations block downstream reviewer mutations even when
|
|
workflow hash proof is present.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
from typing import Any
|
|
|
|
CLASSIFICATION_READ_ONLY_INVENTORY = "read_only_inventory"
|
|
CLASSIFICATION_DIAGNOSTIC = "diagnostic"
|
|
CLASSIFICATION_BOUNDARY_VIOLATION = "boundary_violation"
|
|
CLASSIFICATION_UNCLASSIFIED = "unclassified"
|
|
|
|
ALLOWED_CLASSIFICATIONS = frozenset({
|
|
CLASSIFICATION_READ_ONLY_INVENTORY,
|
|
CLASSIFICATION_DIAGNOSTIC,
|
|
CLASSIFICATION_BOUNDARY_VIOLATION,
|
|
CLASSIFICATION_UNCLASSIFIED,
|
|
})
|
|
|
|
_PRE_REVIEW_COMMANDS: list[dict[str, Any]] = []
|
|
|
|
_READ_ONLY_INVENTORY_PATTERNS = (
|
|
re.compile(
|
|
r"\bgitea[_-](?:list|view|whoami|get[-_]|resolve[-_]task|check[-_]pr|route[-_]task)",
|
|
re.I,
|
|
),
|
|
re.compile(r"\bgit\s+(?:fetch|remote\s+update|branch\s+-a|log|show|rev-parse)\b", re.I),
|
|
re.compile(r"\bgit\s+status\b", re.I),
|
|
re.compile(r"\bgit\s+worktree\s+list\b", re.I),
|
|
)
|
|
|
|
_DIAGNOSTIC_PATTERNS = (
|
|
re.compile(r"\bgit\s+diff(?:\s+--stat)?\b", re.I),
|
|
re.compile(r"\bwhich\s+pytest\b", re.I),
|
|
re.compile(r"\bpytest\s+--version\b", re.I),
|
|
)
|
|
|
|
_BOUNDARY_VIOLATION_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
|
|
(re.compile(r"\b(?:pytest|python\s+-m\s+pytest|python\s+-m\s+unittest)\b", re.I),
|
|
"validation command before workflow load"),
|
|
(re.compile(r"\bprofiles\.json\b", re.I), "local profile config inspection"),
|
|
(re.compile(r"\bgitea-mcp(?:\.v2-contexts)?\.json\b", re.I),
|
|
"local Gitea MCP config inspection"),
|
|
(re.compile(r"\b\.env(?:\.|$|\b)", re.I), "credential file inspection"),
|
|
(re.compile(r"\bkeychain\b", re.I), "credential store inspection"),
|
|
(re.compile(r"\bpkill\b", re.I), "MCP repair activity"),
|
|
(re.compile(r"\b(?:edit|write|modify).{0,40}\bmcp\b", re.I),
|
|
"MCP config exploration"),
|
|
(re.compile(r"\bgit\s+(?:add|commit|reset|clean|checkout|merge|rebase|push)\b", re.I),
|
|
"git mutation before workflow load"),
|
|
)
|
|
|
|
|
|
def clear_pre_review_commands() -> None:
|
|
"""Test helper and session reset."""
|
|
global _PRE_REVIEW_COMMANDS
|
|
_PRE_REVIEW_COMMANDS = []
|
|
|
|
|
|
def pre_review_commands() -> list[dict[str, Any]]:
|
|
"""Return a shallow copy of recorded pre-review commands."""
|
|
return [dict(entry) for entry in _PRE_REVIEW_COMMANDS]
|
|
|
|
|
|
def _normalize_path(path: str | None) -> str:
|
|
return os.path.realpath(os.path.abspath((path or "").strip() or os.getcwd()))
|
|
|
|
|
|
def is_main_checkout_path(cwd: str | None, project_root: str | None) -> bool:
|
|
"""True when *cwd* is the stable control checkout (not under branches/)."""
|
|
if not project_root:
|
|
return False
|
|
root = _normalize_path(project_root)
|
|
path = _normalize_path(cwd)
|
|
if path != root:
|
|
return False
|
|
marker = f"{os.sep}branches{os.sep}"
|
|
return marker not in path
|
|
|
|
|
|
def classify_pre_review_command(
|
|
command: str,
|
|
*,
|
|
cwd: str | None = None,
|
|
project_root: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Classify a command executed before workflow load."""
|
|
text = (command or "").strip()
|
|
path = _normalize_path(cwd)
|
|
root = _normalize_path(project_root) if project_root else None
|
|
reasons: list[str] = []
|
|
|
|
for pattern, label in _BOUNDARY_VIOLATION_PATTERNS:
|
|
if pattern.search(text):
|
|
if label.startswith("validation") and root and not is_main_checkout_path(path, root):
|
|
continue
|
|
if label.startswith("git mutation") and root and not is_main_checkout_path(path, root):
|
|
continue
|
|
reasons.append(label)
|
|
return {
|
|
"command": text,
|
|
"cwd": path,
|
|
"classification": CLASSIFICATION_BOUNDARY_VIOLATION,
|
|
"reasons": reasons,
|
|
}
|
|
|
|
for pattern in _READ_ONLY_INVENTORY_PATTERNS:
|
|
if pattern.search(text):
|
|
return {
|
|
"command": text,
|
|
"cwd": path,
|
|
"classification": CLASSIFICATION_READ_ONLY_INVENTORY,
|
|
"reasons": [],
|
|
}
|
|
|
|
for pattern in _DIAGNOSTIC_PATTERNS:
|
|
if pattern.search(text):
|
|
return {
|
|
"command": text,
|
|
"cwd": path,
|
|
"classification": CLASSIFICATION_DIAGNOSTIC,
|
|
"reasons": [],
|
|
}
|
|
|
|
if root and is_main_checkout_path(path, root):
|
|
if re.search(r"\b(?:cat|head|less|read)\b", text, re.I):
|
|
if re.search(r"workflow|skill|runbook", text, re.I):
|
|
return {
|
|
"command": text,
|
|
"cwd": path,
|
|
"classification": CLASSIFICATION_BOUNDARY_VIOLATION,
|
|
"reasons": [
|
|
"canonical workflow viewed as local file without "
|
|
"gitea_load_review_workflow (narrative load is not proof)"
|
|
],
|
|
}
|
|
|
|
return {
|
|
"command": text,
|
|
"cwd": path,
|
|
"classification": CLASSIFICATION_UNCLASSIFIED,
|
|
"reasons": [
|
|
"pre-review command not classified; record via "
|
|
"gitea_record_pre_review_command before workflow load"
|
|
],
|
|
}
|
|
|
|
|
|
def record_pre_review_command(
|
|
command: str,
|
|
*,
|
|
cwd: str | None = None,
|
|
project_root: str | None = None,
|
|
classification: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Record and classify a pre-review command for the current session."""
|
|
assessed = classify_pre_review_command(
|
|
command, cwd=cwd, project_root=project_root)
|
|
if classification:
|
|
if classification not in ALLOWED_CLASSIFICATIONS:
|
|
assessed["classification"] = CLASSIFICATION_UNCLASSIFIED
|
|
assessed["reasons"] = [
|
|
f"unknown classification '{classification}'; fail closed"
|
|
]
|
|
else:
|
|
assessed["classification"] = classification
|
|
assessed["reasons"] = []
|
|
entry = {
|
|
**assessed,
|
|
"session_pid": os.getpid(),
|
|
}
|
|
_PRE_REVIEW_COMMANDS.append(entry)
|
|
return dict(entry)
|
|
|
|
|
|
def assess_boundary_status(project_root: str | None = None) -> dict[str, Any]:
|
|
"""Summarize pre-review boundary state for session proof and reports."""
|
|
violations = [
|
|
entry for entry in _PRE_REVIEW_COMMANDS
|
|
if entry.get("classification") == CLASSIFICATION_BOUNDARY_VIOLATION
|
|
]
|
|
unclassified = [
|
|
entry for entry in _PRE_REVIEW_COMMANDS
|
|
if entry.get("classification") == CLASSIFICATION_UNCLASSIFIED
|
|
]
|
|
reasons: list[str] = []
|
|
for entry in violations:
|
|
reasons.extend(entry.get("reasons") or [
|
|
f"boundary violation: {entry.get('command', '')[:80]}"
|
|
])
|
|
for entry in unclassified:
|
|
reasons.extend(entry.get("reasons") or [
|
|
"unclassified pre-review command blocks reviewer mutations"
|
|
])
|
|
|
|
clean = not reasons
|
|
return {
|
|
"boundary_status": "clean" if clean else "violation",
|
|
"boundary_clean": clean,
|
|
"pre_review_command_count": len(_PRE_REVIEW_COMMANDS),
|
|
"boundary_violation_count": len(violations),
|
|
"unclassified_command_count": len(unclassified),
|
|
"violations": [
|
|
{
|
|
"command": v.get("command"),
|
|
"cwd": v.get("cwd"),
|
|
"reasons": list(v.get("reasons") or []),
|
|
}
|
|
for v in violations
|
|
],
|
|
"reasons": reasons,
|
|
}
|
|
|
|
|
|
def boundary_blockers(project_root: str | None = None) -> list[str]:
|
|
"""Reasons reviewer mutations must fail closed due to boundary state."""
|
|
status = assess_boundary_status(project_root)
|
|
if status.get("boundary_clean"):
|
|
return []
|
|
return list(status.get("reasons") or [
|
|
"reviewer session boundary violation before workflow load"
|
|
])
|
|
|
|
|
|
def workflow_load_helper_result(
|
|
load: dict | None,
|
|
project_root: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Structured helper result for final reports (#403)."""
|
|
boundary = assess_boundary_status(project_root)
|
|
if load is None:
|
|
return {
|
|
"workflow_load_proof_present": False,
|
|
"workflow_source": None,
|
|
"workflow_hash": None,
|
|
"final_report_schema_hash": None,
|
|
"boundary_status": boundary.get("boundary_status"),
|
|
"boundary_clean": False,
|
|
"reasons": [
|
|
"gitea_load_review_workflow helper result missing from report"
|
|
],
|
|
}
|
|
return {
|
|
"workflow_load_proof_present": True,
|
|
"workflow_source": load.get("workflow_source"),
|
|
"workflow_hash": load.get("workflow_hash"),
|
|
"final_report_schema_path": load.get("final_report_schema_path"),
|
|
"final_report_schema_hash": load.get("final_report_schema_hash"),
|
|
"boundary_status": load.get("boundary_status", boundary.get("boundary_status")),
|
|
"boundary_clean": bool(load.get("boundary_clean", boundary.get("boundary_clean"))),
|
|
"pre_review_command_count": boundary.get("pre_review_command_count"),
|
|
"reasons": [],
|
|
} |