Agent web jobs now stop after Brain consolidation and enter needs_review with a persisted review queue (blocking: high-severity, low-confidence, sensitive-category findings; audit sample of clean clusters). Humans decide confirm/reject/unsure/needs_clarification via new review API and frontend queue; a finalizer applies decisions (rejections suppressed with reason codes), performs bounded targeted reruns for clarifications, drafts RFIs only for kept issues, and only then marks the job done and sends the final email. Two-phase email (review-required, then final report), per-decision feedback labels with redacted aggregate metrics, restart recovery from job artifacts, and CLI --no-review bypass. Classic pipeline unchanged. 65 non-LLM tests.
71 lines
2.2 KiB
Python
71 lines
2.2 KiB
Python
"""Review-trigger policy: which findings block on human review."""
|
|
|
|
from typing import Any, Dict, List
|
|
|
|
_SENSITIVE_CATEGORIES = {
|
|
"missing_element",
|
|
"ada",
|
|
"tas_tdlr",
|
|
"egress",
|
|
"fire_separation",
|
|
"occupancy",
|
|
"spatial_clash",
|
|
"clearance_conflict",
|
|
"penetration_conflict",
|
|
}
|
|
|
|
|
|
def requires_review(issue: Dict) -> List[str]:
|
|
"""Return trigger reasons that require human review for one issue."""
|
|
reasons: List[str] = []
|
|
severity = str(issue.get("severity") or "").lower()
|
|
confidence = str(issue.get("confidence") or "").lower()
|
|
category = str(issue.get("category") or "").lower()
|
|
if severity in {"critical", "high"}:
|
|
reasons.append("severity_high")
|
|
if confidence == "low":
|
|
reasons.append("confidence_low")
|
|
if category in _SENSITIVE_CATEGORIES or issue.get("source_stage") == "code":
|
|
reasons.append("sensitive_category")
|
|
return reasons
|
|
|
|
|
|
def build_audit_sample(
|
|
memory_snapshot: Dict,
|
|
prioritized: List[Dict],
|
|
limit: int = 5,
|
|
) -> List[Dict[str, Any]]:
|
|
"""Build non-blocking spot-check items for clean (finding-free) clusters."""
|
|
implicated = {
|
|
str(finding.get("scope_id") or "")
|
|
for finding in (memory_snapshot.get("findings") or []) + list(prioritized)
|
|
}
|
|
items: List[Dict[str, Any]] = []
|
|
for cluster in memory_snapshot.get("clusters") or []:
|
|
if len(items) >= limit:
|
|
break
|
|
assertions = cluster.get("assertions") or []
|
|
if len(assertions) < 2:
|
|
continue
|
|
cluster_key = cluster.get("key") or "unknown"
|
|
if any(cluster_key in scope_id for scope_id in implicated):
|
|
continue
|
|
items.append({
|
|
"review_item_id": f"clean_cluster:{cluster_key}",
|
|
"kind": "clean_cluster",
|
|
"blocking": False,
|
|
"reasons": ["audit_sample"],
|
|
"payload": _without_base64(cluster),
|
|
})
|
|
return items
|
|
|
|
|
|
def _without_base64(cluster: Dict) -> Dict:
|
|
return {
|
|
**cluster,
|
|
"assertions": [
|
|
{key: value for key, value in assertion.items() if key != "base64"}
|
|
for assertion in cluster.get("assertions") or []
|
|
],
|
|
}
|