kin: KIN-133-backend_dev

This commit is contained in:
Gros Frumos 2026-03-19 15:50:52 +02:00
parent 89595aa077
commit 05915fc7cd
2 changed files with 425 additions and 1 deletions

View file

@ -539,6 +539,62 @@ def _parse_agent_blocked(result: dict) -> dict | None:
}
# ---------------------------------------------------------------------------
# Gate cannot-close detection (KIN-133)
# ---------------------------------------------------------------------------
def _find_gate_result(results: list[dict], role: str) -> dict | None:
"""Return the last successful result for the given gate role.
Iterates results in reverse order to handle auto_fix retry loops where
tester may appear multiple times; returns the latest successful attempt.
"""
for r in reversed(results):
if r.get("role") == role and r.get("success"):
return r
return None
def _parse_gate_cannot_close(result: dict, role: str) -> dict | None:
"""Detect gate rejection from a final gate agent (reviewer or tester).
Returns dict with {reason} if the gate agent signals the task must NOT
be closed. Returns None if the gate approves or if output format is
unrecognised (fail-open: never block on unknown formats).
Reviewer: verdict must be 'approved'; anything else ('changes_requested',
'revise', 'blocked') is a rejection. Note: 'blocked' is handled earlier
by _parse_agent_blocked(), but if it somehow reaches here we treat it as
cannot_close too.
Tester: status must be 'passed'; 'failed' or 'blocked' are rejections.
"""
from datetime import datetime
output = result.get("output")
if not isinstance(output, dict):
return None # fail-open: non-dict output → don't block
if role == "reviewer":
verdict = output.get("verdict")
if verdict is None:
return None # fail-open: no verdict field
if verdict == "approved":
return None # approved → close
reason = output.get("reason") or output.get("summary") or f"Reviewer verdict: {verdict}"
return {"reason": reason}
if role == "tester":
status = output.get("status")
if status is None:
return None # fail-open: no status field
if status == "passed":
return None # passed → close
reason = output.get("reason") or output.get("blocked_reason") or f"Tester status: {status}"
return {"reason": reason}
return None # unknown gate role → fail-open
# ---------------------------------------------------------------------------
# Destructive operation detection (KIN-116)
# ---------------------------------------------------------------------------
@ -2327,7 +2383,66 @@ def run_pipeline(
if current_status in ("done", "cancelled"):
pass # User finished manually — don't overwrite
elif mode == "auto_complete" and auto_eligible:
# Auto-complete mode: last step is tester/reviewer — skip review, approve immediately
# KIN-133: gate check — if final gate agent rejects, block instead of closing
_gate_result = _find_gate_result(results, effective_last_role)
_cannot_close = (
_parse_gate_cannot_close(_gate_result, effective_last_role)
if _gate_result else None
)
if _cannot_close is not None:
_block_reason = _cannot_close["reason"]
models.update_task(
conn, task_id,
status="blocked",
blocked_reason=_block_reason,
blocked_agent_role=effective_last_role,
blocked_pipeline_step=str(len(steps)),
)
if pipeline:
models.update_pipeline(
conn, pipeline["id"],
status="failed",
total_cost_usd=total_cost,
total_tokens=total_tokens,
total_duration_seconds=total_duration,
)
try:
models.write_log(
conn, pipeline["id"] if pipeline else None,
f"Gate cannot_close: {effective_last_role} refused to approve — {_block_reason}",
level="WARN",
extra={"role": effective_last_role, "reason": _block_reason},
)
except Exception:
pass
try:
from core.telegram import send_telegram_escalation
_project = models.get_project(conn, project_id)
_project_name = _project["name"] if _project else project_id
_sent = send_telegram_escalation(
task_id=task_id,
project_name=_project_name,
agent_role=effective_last_role,
reason=_block_reason,
pipeline_step=str(len(steps)),
)
if _sent:
models.mark_telegram_sent(conn, task_id)
except Exception:
pass
return {
"success": False,
"error": f"Gate {effective_last_role} refused to close task: {_block_reason}",
"blocked_by": effective_last_role,
"blocked_reason": _block_reason,
"steps_completed": len(steps),
"results": results,
"total_cost_usd": total_cost,
"total_tokens": total_tokens,
"total_duration_seconds": total_duration,
"pipeline_id": pipeline["id"] if pipeline else None,
}
# Auto-complete mode: gate approved — close task immediately
models.update_task(conn, task_id, status="done")
# KIN-084: log task status
try: