Executor-Tyrant-Framework Claude Opus 4.6 (1M context) commited on
Commit
792eca8
·
1 Parent(s): 2dcd6e8

Add report_evaluator.py — two-layer evaluation with Reviewer persona (Phase 3)

Browse files

Layer 1 (structural): Mechanical check on report status, step failures,
pending gates, acceptance criteria coverage. No LLM. Pure logic.

Layer 2 (qualitative): Reviewer persona evaluates quality, completeness,
constraint compliance through its meticulous-standards-obsessed lens.
Returns DONE/ITERATE/ESCALATE with specific hints.

Tested: Reviewer correctly caught that an audit-only spec's passing steps
don't satisfy acceptance criteria claiming "no stale paths remain."
Structural pass != actual done. The persona adds judgment the mechanical
check can't.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files changed (2) hide show
  1. Dockerfile +1 -0
  2. report_evaluator.py +294 -0
Dockerfile CHANGED
@@ -71,6 +71,7 @@ COPY ng_embed.py .
71
  COPY work_block_schema.py .
72
  COPY spec_executor.py .
73
  COPY persona_client.py .
 
74
 
75
  # Copy tools directory
76
  COPY tools/ ./tools/
 
71
  COPY work_block_schema.py .
72
  COPY spec_executor.py .
73
  COPY persona_client.py .
74
+ COPY report_evaluator.py .
75
 
76
  # Copy tools directory
77
  COPY tools/ ./tools/
report_evaluator.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ---- Changelog ----
2
+ # [2026-04-07] Josh + Claude — Report evaluator with Reviewer persona (Phase 3)
3
+ # What: Structural acceptance check + Reviewer persona qualitative evaluation
4
+ # Why: The iteration loop needs a decision — done, iterate, or escalate
5
+ # How: Two layers: mechanical criteria check (pass/fail) + persona evaluation (judgment)
6
+ # -------------------
7
+
8
+ """Report Evaluator — decides done / iterate / escalate.
9
+
10
+ Two layers of evaluation:
11
+ 1. Structural: Do the acceptance criteria mechanically pass? (no LLM needed)
12
+ 2. Qualitative: Reviewer persona evaluates quality, completeness, concerns (RP model)
13
+
14
+ The structural check is the gate. If criteria don't pass, it's iterate regardless
15
+ of what the persona thinks. If criteria pass, the persona can still flag concerns
16
+ and recommend iteration or escalation.
17
+ """
18
+
19
+ import json
20
+ import logging
21
+ from typing import Optional
22
+
23
+ logger = logging.getLogger("report_evaluator")
24
+
25
+
26
+ def _structural_check(report: dict) -> dict:
27
+ """Mechanical check — does the report indicate success?
28
+
29
+ No LLM. Pure logic on the report structure.
30
+ Returns: {passed: bool, reasons: [str], details: {criterion: pass/fail}}
31
+ """
32
+ status = report.get("status", "unknown")
33
+ summary = report.get("summary", {})
34
+ step_results = report.get("step_results", {})
35
+
36
+ reasons = []
37
+ criteria_results = {}
38
+
39
+ # Check 1: Block status
40
+ if status == "aborted":
41
+ reasons.append(f"Block aborted: {report.get('abort_reason', 'unknown')}")
42
+ elif status == "partial_failure":
43
+ failed_steps = [sid for sid, r in step_results.items() if r.get("status") == "fail"]
44
+ reasons.append(f"Steps failed: {', '.join(failed_steps)}")
45
+
46
+ # Check 2: Any failed steps?
47
+ failed_count = summary.get("failed", 0)
48
+ if failed_count > 0:
49
+ for sid, r in step_results.items():
50
+ if r.get("status") == "fail":
51
+ reason = r.get("reason", "unknown")
52
+ reasons.append(f"Step {sid} failed: {reason}")
53
+
54
+ # Check 3: Any pending gates?
55
+ pending_count = summary.get("pending_review", 0)
56
+ if pending_count > 0:
57
+ for sid, r in step_results.items():
58
+ if "pending" in r.get("status", ""):
59
+ reasons.append(f"Step {sid} pending: {r.get('description', r.get('gate_type', 'review'))}")
60
+
61
+ # Check 4: Were acceptance criteria addressed?
62
+ # We can't mechanically verify most acceptance criteria — that's the persona's job.
63
+ # But we can flag if the block never ran far enough to address them.
64
+ acceptance = report.get("acceptance_criteria", [])
65
+ total_steps = summary.get("total_steps", 0)
66
+ if total_steps == 0:
67
+ reasons.append("No steps executed — acceptance criteria cannot be evaluated")
68
+
69
+ passed = len(reasons) == 0
70
+ return {
71
+ "passed": passed,
72
+ "reasons": reasons,
73
+ "summary": {
74
+ "status": status,
75
+ "steps_passed": summary.get("passed", 0),
76
+ "steps_failed": failed_count,
77
+ "steps_pending": pending_count,
78
+ "tool_calls": summary.get("tool_calls", 0),
79
+ "elapsed_seconds": summary.get("elapsed_seconds", 0),
80
+ },
81
+ }
82
+
83
+
84
+ def _format_report_for_review(report: dict) -> str:
85
+ """Format an execution report as readable text for the Reviewer persona."""
86
+ lines = []
87
+ lines.append(f"# Execution Report: {report.get('block_name', 'unknown')}")
88
+ lines.append(f"**Block ID:** {report.get('block_id', '?')}")
89
+ lines.append(f"**Status:** {report.get('status', '?')}")
90
+
91
+ if report.get("abort_reason"):
92
+ lines.append(f"**Abort Reason:** {report['abort_reason']}")
93
+
94
+ summary = report.get("summary", {})
95
+ lines.append(f"\n**Summary:** {summary.get('passed', 0)} passed, "
96
+ f"{summary.get('failed', 0)} failed, "
97
+ f"{summary.get('pending_review', 0)} pending, "
98
+ f"{summary.get('tool_calls', 0)} tool calls, "
99
+ f"{summary.get('elapsed_seconds', 0)}s elapsed")
100
+
101
+ lines.append("\n## Step Results\n")
102
+ for sid, result in report.get("step_results", {}).items():
103
+ status = result.get("status", "?")
104
+ tool = result.get("tool", "")
105
+ preview = result.get("result_preview", "")[:200]
106
+ reason = result.get("reason", "")
107
+
108
+ if status == "fail":
109
+ lines.append(f"- **{sid}** [{tool}] FAIL: {reason}")
110
+ elif status == "pass":
111
+ lines.append(f"- **{sid}** [{tool}] PASS: {preview[:100]}...")
112
+ elif "pending" in status:
113
+ lines.append(f"- **{sid}** PENDING: {result.get('description', status)}")
114
+ else:
115
+ lines.append(f"- **{sid}** {status}")
116
+
117
+ lines.append("\n## Acceptance Criteria\n")
118
+ for criterion in report.get("acceptance_criteria", []):
119
+ lines.append(f"- [ ] {criterion}")
120
+
121
+ bindings = report.get("bindings", {})
122
+ if bindings:
123
+ lines.append("\n## Key Bindings (truncated)\n")
124
+ for var, val in bindings.items():
125
+ lines.append(f"- `{var}`: {val[:100]}...")
126
+
127
+ return "\n".join(lines)
128
+
129
+
130
+ def evaluate_report(
131
+ report: dict,
132
+ spec: Optional[dict] = None,
133
+ graph_context: Optional[list] = None,
134
+ use_persona: bool = True,
135
+ ) -> dict:
136
+ """Evaluate an execution report. Returns a decision.
137
+
138
+ Args:
139
+ report: The execution report from SpecExecutor
140
+ spec: The original spec (for context — constraints, acceptance criteria)
141
+ graph_context: Graph recall results for Reviewer context
142
+ use_persona: If True, calls Reviewer persona for qualitative evaluation.
143
+ If False, structural check only (faster, no API call).
144
+
145
+ Returns:
146
+ {
147
+ decision: "done" | "iterate" | "escalate",
148
+ structural: {passed, reasons, summary},
149
+ qualitative: {response, concerns, recommendation} | None,
150
+ iteration_hints: [str] — specific things to fix in a follow-up spec,
151
+ escalate_to: str | None — role to escalate to if decision is escalate,
152
+ escalate_reason: str | None,
153
+ }
154
+ """
155
+ # Layer 1: Structural check (no LLM)
156
+ structural = _structural_check(report)
157
+
158
+ # If structural check fails, we know we need to iterate (or escalate if aborted)
159
+ if not structural["passed"]:
160
+ status = report.get("status", "")
161
+
162
+ # Aborted with escalation = escalate up the chain
163
+ if status == "aborted" and "ESCALATED" in (report.get("abort_reason") or ""):
164
+ return {
165
+ "decision": "escalate",
166
+ "structural": structural,
167
+ "qualitative": None,
168
+ "iteration_hints": structural["reasons"],
169
+ "escalate_to": "strategist",
170
+ "escalate_reason": report.get("abort_reason", "Block escalated"),
171
+ }
172
+
173
+ # Aborted for other reasons or partial failure = iterate
174
+ # But build iteration hints from the failure reasons
175
+ return {
176
+ "decision": "iterate",
177
+ "structural": structural,
178
+ "qualitative": None,
179
+ "iteration_hints": structural["reasons"],
180
+ "escalate_to": None,
181
+ "escalate_reason": None,
182
+ }
183
+
184
+ # Structural passed — everything green mechanically
185
+ if not use_persona:
186
+ return {
187
+ "decision": "done",
188
+ "structural": structural,
189
+ "qualitative": None,
190
+ "iteration_hints": [],
191
+ "escalate_to": None,
192
+ "escalate_reason": None,
193
+ }
194
+
195
+ # Layer 2: Reviewer persona qualitative evaluation
196
+ try:
197
+ from persona_client import call_persona
198
+
199
+ report_text = _format_report_for_review(report)
200
+
201
+ # Build the review task
202
+ constraints_text = ""
203
+ if spec:
204
+ never = spec.get("constraints", {}).get("never", [])
205
+ anti_drift = spec.get("constraints", {}).get("anti_drift", [])
206
+ if never:
207
+ constraints_text += "\n**Constitutional constraints (never):**\n"
208
+ constraints_text += "\n".join(f"- {n}" for n in never)
209
+ if anti_drift:
210
+ constraints_text += "\n**Anti-drift rules:**\n"
211
+ constraints_text += "\n".join(f"- {a}" for a in anti_drift)
212
+
213
+ task = (
214
+ "You are reviewing an execution report. All steps passed structurally. "
215
+ "Your job is to decide if this work is truly DONE, needs ITERATION, or "
216
+ "should be ESCALATED to a higher authority.\n\n"
217
+ "Evaluate:\n"
218
+ "1. Did the work actually accomplish the acceptance criteria, or just not error?\n"
219
+ "2. Are there quality concerns even though steps passed?\n"
220
+ "3. Were any constraints violated (check the 'never' and 'anti-drift' rules)?\n"
221
+ "4. Is there anything a Tracker should investigate further?\n"
222
+ "5. Is there anything Razor should review for security?\n\n"
223
+ "Respond with your assessment, then on the LAST LINE write exactly one of:\n"
224
+ "DECISION: DONE\n"
225
+ "DECISION: ITERATE — [specific reason]\n"
226
+ "DECISION: ESCALATE — [to whom] — [reason]\n\n"
227
+ f"{report_text}"
228
+ f"{constraints_text}"
229
+ )
230
+
231
+ result = call_persona(
232
+ role="reviewer",
233
+ task=task,
234
+ graph_context=graph_context,
235
+ temperature=0.4, # Lower temp for evaluation — we want focused, not creative
236
+ )
237
+
238
+ response_text = result.get("response", "")
239
+
240
+ # Parse the decision from the last line
241
+ decision = "done"
242
+ escalate_to = None
243
+ escalate_reason = None
244
+ iteration_hints = []
245
+
246
+ lines = response_text.strip().split("\n")
247
+ for line in reversed(lines):
248
+ line = line.strip()
249
+ if line.startswith("DECISION:"):
250
+ decision_text = line[len("DECISION:"):].strip()
251
+ if decision_text.startswith("DONE"):
252
+ decision = "done"
253
+ elif decision_text.startswith("ITERATE"):
254
+ decision = "iterate"
255
+ reason = decision_text.split("—", 1)[-1].strip() if "—" in decision_text else decision_text
256
+ iteration_hints = [reason] if reason else []
257
+ elif decision_text.startswith("ESCALATE"):
258
+ decision = "escalate"
259
+ parts = decision_text.split("—")
260
+ if len(parts) >= 3:
261
+ escalate_to = parts[1].strip().lower()
262
+ escalate_reason = parts[2].strip()
263
+ elif len(parts) >= 2:
264
+ escalate_to = parts[1].strip().lower()
265
+ escalate_reason = "Reviewer escalation"
266
+ break
267
+
268
+ qualitative = {
269
+ "response": response_text,
270
+ "decision_raw": decision_text if 'decision_text' in dir() else "",
271
+ "model": result.get("model", ""),
272
+ "elapsed_seconds": result.get("elapsed_seconds", 0),
273
+ }
274
+
275
+ return {
276
+ "decision": decision,
277
+ "structural": structural,
278
+ "qualitative": qualitative,
279
+ "iteration_hints": iteration_hints,
280
+ "escalate_to": escalate_to,
281
+ "escalate_reason": escalate_reason,
282
+ }
283
+
284
+ except Exception as e:
285
+ logger.error("Reviewer persona evaluation failed: %s", e, exc_info=True)
286
+ # Fall back to structural-only decision
287
+ return {
288
+ "decision": "done", # Structural passed, persona unavailable — cautious pass
289
+ "structural": structural,
290
+ "qualitative": {"error": str(e)},
291
+ "iteration_hints": [],
292
+ "escalate_to": None,
293
+ "escalate_reason": None,
294
+ }