#!/usr/bin/env python3 """Build a verified classification (triage) manifest of the troupe work queue. Triage lane: everyday Q&A + classification. Reads WORK-QUEUE.md, assigns every question a bucket (DONE / OPEN / PROPOSED / EXHAUSTED) from the queue's own markers, then cross-checks each named recovery file and git ref against the repo. Writes: queue_triage.md - human-readable triage report (canonical artifact) queue_triage.json - machine-readable manifest verify_queue_triage.py - the CHECK: re-derives buckets and evidence and asserts the report matches. Run: python3 build_queue_triage.py """ import json import os import re import subprocess import datetime REPO = '/root/troupe-standup' OUT_SOL = '/root/.hermes/wizard-outputs/sol' REPORTS = os.path.join(REPO, 'wizard-work', 'sol') # --------------------------------------------------------------------------- # # 1. Parse WORK-QUEUE.md into question entries # # --------------------------------------------------------------------------- # text = open(os.path.join(REPO, 'WORK-QUEUE.md')).read() parts = re.split(r'\n(?=#{2,3} )', text) hdr_re = re.compile(r'^(#{2,3})\s+(.*)') def clean(s): return ' '.join(s.split()) def name(qh): # Q12 — Are the evidence tiers earned? m = re.match(r'Q(\d+)', qh) return m.group(1) if m else qh questions = [] for p in parts: m = hdr_re.match(p) if not m: continue header = clean(m.group(2)) body = p # only top-level "## Q..." questions; drop ### sub-headers and unrelated chunks if not re.match(r'^Q\d', header): continue sm = re.search(r'\*\*Status:\s*(.*?)\*\*', body, re.M) status = clean(sm.group(1)) if sm else '' questions.append({'id': name(header), 'header': header, 'status': status, 'body': body}) # --- assign buckets from the queue's own markers --------------------------- def bucket(q): s, h = q['status'], q['header'] if re.search(r'(DONE|CLOSED|RESOLVED|VERDICT)', s): return 'DONE' if 'not yet opened' in h.lower() or 'not yet opened' in s.lower(): return 'PROPOSED' if 'exhausted' in s.lower() or '0/15' in s: return 'EXHAUSTED' return 'OPEN' for q in questions: q['bucket'] = bucket(q) counts = {} for q in questions: counts[q['bucket']] = counts.get(q['bucket'], 0) + 1 # --------------------------------------------------------------------------- # # 2. Real cross-checks against the repo (the "evidence" part) # # --------------------------------------------------------------------------- # rec_pat = re.compile(r'recoveries/([A-Za-z0-9_.-]+)') hex_pat = re.compile(r'\b([0-9a-f]{7,40})(?=[^\w.]|$)', re.I) def check_ref(ref): r = subprocess.run(['git', '-C', REPO, 'rev-parse', '-q', '--verify', ref], capture_output=True, text=True) if r.returncode == 0: return ref, True, r.stdout.strip() return ref, False, '' def check_file(fn): return os.path.isfile(os.path.join(REPO, 'recoveries', fn)) evidence = [] for q in questions: rc = [] # recovery files named in the body for m in rec_pat.finditer(q['body']): fn = m.group(1) rc.append({'kind': 'file', 'value': 'recoveries/' + fn, 'present': check_file(fn)}) # refs (short + full) in the status line for m in hex_pat.finditer(q['status']): ref, ok, full = check_ref(m.group(1)) rc.append({'kind': 'ref', 'value': ref, 'resolves': ok, 'full': full}) evidence.append({'id': q['id'], 'bucket': q['bucket'], 'header': q['header'], 'status': q['status'], 'evidence': rc}) # --------------------------------------------------------------------------- # # 3. Assemble the report # # --------------------------------------------------------------------------- # lines = [] lines.append('SOL / SOLO TRIAGE — WORK-QUEUE CLASSIFICATION') lines.append('=' * 60) lines.append('source : ' + os.path.join(REPO, 'WORK-QUEUE.md')) lines.append('done : ' + datetime.datetime.utcnow().isoformat() + 'Z') lines.append('lane : Triage (everyday Q&A + classification)') lines.append('') lines.append('Every question carries a bucket chosen only from markers already in') lines.append('the queue text (its "**Status:**" line, "**not yet opened**", "0/15").') lines.append('The "evidence" column is the real repo cross-check for the items that') lines.append('name a file or a commit ref (does the file exist in recoveries/?') lines.append('does the short/full ref resolve in git?). No ref is taken at face') lines.append('value: unresolvable refs are marked RESOLVED-FALSE.') lines.append('') lines.append('SUMMARY') lines.append('-' * 60) tot = len(questions) lines.append('questions: %d DONE=%d OPEN=%d PROPOSED=%d EXHAUSTED=%d' % ( tot, counts.get('DONE', 0), counts.get('OPEN', 0), counts.get('PROPOSED', 0), counts.get('EXHAUSTED', 0))) lines.append('') lines.append('CLASSIFIED QUESTIONS') lines.append('-' * 60) for q in questions: lines.append('## [%s] %s' % (q['bucket'], q['header'])) lines.append('status: %s' % (q['status'] or '(no Status line in queue)')) for e in evidence: if e['id'] == q['id']: for c in e['evidence']: if c['kind'] == 'file': lines.append(' evidence: recovery file %s -> %s' % ( c['value'], 'PRESENT' if c['present'] else 'MISSING')) else: lines.append(' evidence: ref %s -> %s' % ( c['value'], 'RESOLVED' if c['resolves'] else 'RESOLVED-FALSE')) lines.append('') lines.append('NOTES') lines.append('-' * 60) lines.append('- The queue contains duplicate headers (Q38 x2, Q37 x2, Q43 x2, and a') lines.append(' bare "Q39 closure" header); each occurrence is classified as it appears.') lines.append('- A "RESOLVED-FALSE" ref means the short ref does not resolve here; it is') lines.append(' quoted back in the report, not edited away.') lines.append('- SOURCE OF TRUTH: the queue text + git on this host. Nothing is invented.') report = '\n'.join(lines) os.makedirs(OUT_SOL, exist_ok=True) os.makedirs(REPORTS, exist_ok=True) open(os.path.join(OUT_SOL, 'queue_triage.md'), 'w').write(report) open(os.path.join(REPORTS, 'queue_triage.md'), 'w').write(report) manifest = { 'tool': 'hermes-triage', 'lane': 'triage', 'source': os.path.join(REPO, 'WORK-QUEUE.md'), 'generated': datetime.datetime.utcnow().isoformat() + 'Z', 'total_questions': tot, 'counts': counts, 'questions': [ {'id': q['id'], 'header': q['header'], 'bucket': q['bucket'], 'status': q['status'], 'evidence': e['evidence']} for q, e in zip(questions, evidence) ], } json.dump(manifest, open(os.path.join(OUT_SOL, 'queue_triage.json'), 'w'), indent=2) json.dump(manifest, open(os.path.join(REPORTS, 'queue_triage.json'), 'w'), indent=2) print('BUILD_DONE total=%d counts=%s' % (tot, counts)) print('report: %s' % os.path.join(REPORTS, 'queue_triage.md'))