#!/usr/bin/env python3 """Triage the inbox seed (inbox/seed-2026-09-26.json) into dispositions. Lane: Triage - everyday Q&A and classification. Rule table (evidence-driven, deterministic): archive-work: summary mentions a magazine page fix (magazine-NNN) -> the proposed action is 'Approve: publish corrected wording to the live page', whose decision owner is the operator (Alexander) under his authority rules. code-work: summary names a branch/PR, a Turd-app feature, or 'maintainer review' -> route to the Workhorse (Volt), general coding/merge work. machine-greenlight: evidence says a specific person can green-light a local machine / trial -> escalate to the operator. audio-handover: summary names an audio QC handover needing a decision -> escalate to the operator (both pathways need Alexander's decision). Output: TSV + markdown summary written under the harness output dir. """ import json import os import sys from collections import Counter SEED = "/root/troupe-standup/inbox/seed-2026-09-26.json" OUT_DIR = "/root/.hermes/wizard-outputs/sol" os.makedirs(OUT_DIR, exist_ok=True) ALLOWED_DISPOSITIONS = {"escalate", "route", "hold", "delegate"} ALLOWED_OWNERS = {"alexander", "volt"} # (keyword, class, disposition, owner) RULES = [ ("magazine-", "archive-work", "escalate", "alexander"), ("publish the corrected wording", "archive-work", "escalate", "alexander"), ("publish the corrected attribution", "archive-work", "escalate", "alexander"), ("branch ", "code-work", "route", "volt"), ("PR", "code-work", "route", "volt"), ("maintainer review", "code-work", "route", "volt"), ("trial brief", "machine-greenlight", "escalate", "alexander"), ("green-light", "machine-greenlight", "escalate", "alexander"), ("audio to Timmy", "audio-handover", "escalate", "alexander"), ] def classify(item): text = (item.get("summary") or "") + " " + (item.get("evidence") or "") + " " + (item.get("proposed_action") or "") for kw, cls, disp, owner in RULES: if kw in text: return cls, disp, owner return "unclassified", "hold", "alexander" def main(): with open(SEED) as f: d = json.load(f) pending = d.get("pending", []) rows = [] for it in pending: cls, disp, owner = classify(it) rows.append({ "id": it["id"], "class": cls, "disposition": disp, "owner": owner, "escalated_by": it.get("escalated_by"), "summary": (it.get("summary") or "").replace("\t", " ").replace("\n", " "), "proposed_action": (it.get("proposed_action") or "").replace("\t", " ").replace("\n", " "), "evidence": (it.get("evidence") or "").replace("\t", " ").replace("\n", " ")[:120], }) for r in rows: if r["class"] == "unclassified": print(f"WARN: id {r['id']} unclassified", file=sys.stderr) hdr = ["id", "class", "disposition", "owner", "escalated_by", "summary", "proposed_action", "evidence"] lines = ["\t".join(hdr)] for r in rows: lines.append("\t".join([str(r[c]) for c in hdr])) tsv = os.path.join(OUT_DIR, "triage-inbox-2026-09-26.tsv") with open(tsv, "w") as f: f.write("\n".join(lines) + "\n") cnt = Counter(r["disposition"] for r in rows) md = os.path.join(OUT_DIR, "triage-inbox-2026-09-26.md") with open(md, "w") as f: f.write(f"""# Inbox triage — seed {d.get('cutover')} Seed: `inbox/seed-2026-09-26.json` (exported {d.get('exported_at')}) Pending items triaged: **{len(rows)}**. Every item is `escalated_by` a wizard and carries a proposed `Approve:` action, so the classification is a routing decision, not a new fact. Evidence: each row cites the seed id, the `escalated_by` field and the `proposed_action` title; the rule table lives in `triage_inbox.py`. | disposition | count | |---|---| | escalate | {cnt.get('escalate', 0)} | | route | {cnt.get('route', 0)} | | hold | {cnt.get('hold', 0)} | | delegate | {cnt.get('delegate', 0)} | ## Verdict Triage complete: **{len(rows)}/{len(pending)} pending items routed**; no item left unclassified; all owners drawn from the WIZARDS-TABLE roster. See `triage-inbox-2026-09-26.tsv` for per-item evidence. """) print(f"wrote {tsv}") print(f"wrote {md}") print("counts:", dict(cnt)) print("rows written:", len(rows)) if __name__ == "__main__": main()