116 lines
4.3 KiB
Python
116 lines
4.3 KiB
Python
#!/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()
|