Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions scripts/impasse_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,22 @@ def render(run: dict) -> str:
by = {"accepted": 0, "rejected": 0, "resolved": 0, "deadlocked": 0, "withdrawn": 0}
for it in items.values():
by[it.get("state")] = by.get(it.get("state"), 0) + 1
# An operator ruling counts as an escalation regardless of channel (SKILL.md): a resolved item
# carrying an escalation object was decided BY the operator, not settled between the two models.
# Credit it separately so the tally can't under-report the operator's own involvement (issue #5) —
# `pending` is the still-deadlocked work, `resolved_shown` the truly model-settled resolutions.
decided_by_you = sum(1 for it in items.values()
if it.get("state") == "resolved" and isinstance(it.get("escalation"), dict))
pending = by["deadlocked"]
resolved_shown = by["resolved"] - decided_by_you
out.append("")
out.append(
f"📊 Decisions: {n} finding(s) raised → ✅ {by['resolved']} resolved · 🤝 {by['accepted']} accepted · "
f"❌ {by['rejected']} rejected · ⚖️ {by['deadlocked']} escalated to you"
tally = (
f"📊 Decisions: {n} finding(s) raised → ✅ {resolved_shown} resolved · 🤝 {by['accepted']} accepted · "
f"❌ {by['rejected']} rejected · ⚖️ {pending} escalated to you"
)
if decided_by_you:
tally += f" · 🧑‍⚖️ {decided_by_you} decided by you"
out.append(tally)
if rec.get("failure"):
out.append(f"⚠️ Failure: {rec['failure'].get('code')} — {rec['failure'].get('message')}")
out.append("─" * 78)
Expand All @@ -192,9 +203,15 @@ def render(run: dict) -> str:
out += _render_finding(f, items.get(f.get("id")))
out.append("─" * 78)

esc = by["deadlocked"]
if esc:
out.append(f"⚖️ {esc} decision(s) need you; the rest the models settled between themselves.")
if pending:
if decided_by_you:
out.append(f"⚖️ {pending} decision(s) need you; you decided {decided_by_you}; "
"the rest the models settled between themselves.")
else:
out.append(f"⚖️ {pending} decision(s) need you; the rest the models settled between themselves.")
elif decided_by_you:
out.append(f"✅ Models settled {len(items) - decided_by_you}; you decided {decided_by_you}. "
"Nothing is waiting on you.")
elif items:
out.append(f"✅ Nothing needed you — the models settled all {len(items)} between themselves.")
else:
Expand Down
33 changes: 33 additions & 0 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1409,6 +1409,39 @@ def _spy_cwd(argv, **kw):
check("Question for you" in out and "decision(s) need you" in out, "report: shows the escalated question")
check(any(r["run_id"] == drid for r in lib.list_runs()), "run record: listed by list_runs")

# --- report credits operator-decided items (issue #5): a resolved item carrying an escalation
# object was decided BY the operator, not settled between the models — the tally + footer must say so ---
_b_run1 = {"reviewer_response": {}, "reconciliation_result": {"review_id": "b-1", "items": [
{"finding_id": "F1", "state": "resolved", "resolution": "Operator chose repair A over B.",
"escalation": {"dispute_kind": "value_or_priority_tradeoff",
"stop_reason": "operator_authority_required", "operator_question": "A or B?"}},
{"finding_id": "F2", "state": "accepted"}]}}
_b_out1 = report.render(_b_run1)
check("decided by you" in _b_out1 and "you decided" in _b_out1
and "Nothing is waiting on you" in _b_out1 and "Nothing needed you" not in _b_out1,
"report: operator-decided (resolved + escalation) item is credited, not counted as autonomous")
_b_run2 = {"reviewer_response": {}, "reconciliation_result": {"review_id": "b-2", "items": [
{"finding_id": "F1", "state": "resolved", "resolution": "host fix"},
{"finding_id": "F2", "state": "accepted"}]}}
_b_out2 = report.render(_b_run2)
check("Nothing needed you — the models settled all 2 between themselves." in _b_out2
and "decided by you" not in _b_out2,
"report: genuinely-autonomous run still says 'Nothing needed you'")
_b_run3 = {"reviewer_response": {}, "reconciliation_result": {"review_id": "b-3", "items": [
{"finding_id": "F1", "state": "deadlocked",
"escalation": {"dispute_kind": "x", "stop_reason": "y", "operator_question": "q1?"}},
{"finding_id": "F2", "state": "resolved", "resolution": "op ruling",
"escalation": {"dispute_kind": "z", "stop_reason": "w", "operator_question": "q2?"}}]}}
_b_out3 = report.render(_b_run3)
# Mixed case: pending deadlock AND an operator-decided item. The footer stays on the pending
# branch, but must CREDIT the operator ("you decided 1") — not attribute their ruling to the
# models. The footer is the last rendered line; "you decided" appears only there (not the tally).
_b_foot3 = _b_out3.splitlines()[-1]
check("decision(s) need you" in _b_out3 and "escalated to you" in _b_out3
and "decided by you" in _b_out3
and "you decided 1" in _b_foot3 and "decision(s) need you" in _b_foot3,
"report: a deadlock still takes footer precedence over an operator-decided item")

# --- lifetime recap: aggregate value across reconciled runs (isolated config dir) ---
recap_dir = tempfile.mkdtemp(prefix="impasse-recap-")
_prev_cfg = os.environ["IMPASSE_CONFIG_DIR"]
Expand Down
Loading