#!/usr/bin/env python3 """Reassign Fenrir's orphaned issues to active wizards based on issue type.""" import json import urllib.request import urllib.error import time TOKEN = "dc0517a965226b7a0c5ffdd961b1ba26521ac592" BASE_URL = "https://forge.alexanderwhitestone.com/api/v1" REPO = "Timmy_Foundation/the-nexus" HEADERS = { "Authorization": f"token {TOKEN}", "Content-Type": "application/json", } # Wizard assignments EZRA = "ezra" # Architecture, docs, epics, planning ALLEGRO = "allegro" # Code implementation, UI, features BEZALEL = "bezalel" # Execution, ops, testing, infra, monitoring def classify_issue(number, title): """Classify issue based on number and title.""" title_upper = title.upper() # Skip the triage issue itself and the permanent escalation issue if number == 823: return None # Skip - this is the issue we're working on if number == 431: return EZRA # Master escalation -> Ezra (archivist) # Allegro self-improvement milestones (M0-M7) -> Bezalel (execution/ops) import re if re.match(r'^M\d+:', title): return BEZALEL # EPIC/GRAND EPIC/CONSOLIDATION/FRONTIER -> Ezra (architecture) if any(tag in title_upper for tag in [ '[EPIC]', '[GRAND EPIC]', 'EPIC:', '[CONSOLIDATION]', '[FRONTIER]', '[CRITIQUE]', '[PROPOSAL]', '[RETROSPECTIVE', '[REPORT]', 'EPIC #', 'GRAND EPIC' ]): return EZRA # Allegro self-improvement epic -> Bezalel if 'ALLEGRO SELF-IMPROVEMENT' in title_upper or 'ALLEGRO HYBRID PRODUCTION' in title_upper: return BEZALEL # Ops/Monitoring/Infra/Testing -> Bezalel if any(tag in title_upper for tag in [ '[OPS]', '[MONITORING]', '[PRUNE]', '[OFFLOAD]', '[BUG]', '[CRON]', '[INSTALL]', '[INFRA]', '[TRAINING]', '[CI/', '[TRIAGE]', # triage/ops tasks ]): return BEZALEL # Allegro backlog items -> Allegro if '[ALLEGRO-BACKLOG]' in title_upper: return ALLEGRO # Reporting -> Bezalel if any(tag in title_upper for tag in ['[REPORT]', 'BURN-MODE', 'PERFORMANCE REPORT']): return BEZALEL # Fleet management/wizard ops -> Bezalel if any(tag in title_upper for tag in ['FLEET', 'WIZARD', 'GHOST WIZARD', 'TIMMY', 'BRING LIVE']): # But EPICs about fleet -> Ezra (already handled above) return BEZALEL # Code implementation: NEXUS UI/3D, MIGRATION, UI, UX, PORTALS, CHAT, PANELS -> Allegro if any(tag in title_upper for tag in [ '[NEXUS]', '[MIGRATION]', '[UI]', '[UX]', '[PORTALS]', '[PORTAL]', '[CHAT]', '[PANELS]', '[DATA]', '[PERF]', '[RESPONSIVE]', '[A11Y]', '[VISUAL]', '[AUDIO]', '[MEDIA]', '[CONCEPT]', '[BRIDGE]', '[AUTH]', '[VISITOR]', '[IDENTITY]', '[SESSION]', '[RELIABILITY]', '[HARNESS]', '[VALIDATION]', '[SOVEREIGNTY]', '[M6-P', 'PROSE ENGINE', 'AUTO-SKILL', 'GITEA_API', 'CRON JOB', 'HEARTBEAT DAEMON', 'FLEET HEALTH JSON', '[FENRIR] NEXUS', # fenrir's nexus issues -> allegro ]): return ALLEGRO # Default: Bezalel for anything else return BEZALEL def get_all_fenrir_issues(): """Fetch all open issues assigned to fenrir.""" issues = [] page = 1 while True: url = f"{BASE_URL}/repos/{REPO}/issues?assignee=fenrir&state=open&limit=50&page={page}" req = urllib.request.Request(url, headers={"Authorization": f"token {TOKEN}"}) with urllib.request.urlopen(req) as resp: data = json.loads(resp.read()) if not data: break issues.extend(data) if len(data) < 50: break page += 1 return issues def reassign_issue(number, assignee): """Reassign an issue to a new wizard.""" url = f"{BASE_URL}/repos/{REPO}/issues/{number}" body = json.dumps({"assignees": [assignee]}).encode() req = urllib.request.Request(url, data=body, headers=HEADERS, method="PATCH") try: with urllib.request.urlopen(req) as resp: return resp.status, None except urllib.error.HTTPError as e: return e.code, e.read().decode() def main(): print("Fetching all fenrir issues...") issues = get_all_fenrir_issues() print(f"Found {len(issues)} open issues assigned to fenrir\n") # Classify assignments = {EZRA: [], ALLEGRO: [], BEZALEL: [], None: []} for issue in issues: num = issue["number"] title = issue["title"] wizard = classify_issue(num, title) assignments[wizard].append((num, title)) print(f"Classification:") print(f" Ezra (architecture): {len(assignments[EZRA])} issues") print(f" Allegro (code): {len(assignments[ALLEGRO])} issues") print(f" Bezalel (execution): {len(assignments[BEZALEL])} issues") print(f" Skip (unchanged): {len(assignments[None])} issues") print() # Show classification for wizard, label in [(EZRA, 'EZRA'), (ALLEGRO, 'ALLEGRO'), (BEZALEL, 'BEZALEL')]: print(f"\n--- {label} ---") for num, title in assignments[wizard]: print(f" #{num}: {title[:70]}") print(f"\n--- SKIPPED ---") for num, title in assignments[None]: print(f" #{num}: {title[:70]}") # Execute reassignments print("\n\nExecuting reassignments...") results = {"success": [], "failed": []} for wizard in [EZRA, ALLEGRO, BEZALEL]: for num, title in assignments[wizard]: status, error = reassign_issue(num, wizard) if status in (200, 201): print(f" ✓ #{num} -> {wizard}") results["success"].append((num, wizard)) else: print(f" ✗ #{num} -> {wizard} (HTTP {status}: {error[:100] if error else 'unknown'})") results["failed"].append((num, wizard, error)) time.sleep(0.1) # Rate limiting print(f"\n\nSummary:") print(f" Successfully reassigned: {len(results['success'])}") print(f" Failed: {len(results['failed'])}") # Save results for PR with open("/tmp/reassignment_results.json", "w") as f: json.dump({ "total": len(issues), "ezra": [(n, t) for n, t in assignments[EZRA]], "allegro": [(n, t) for n, t in assignments[ALLEGRO]], "bezalel": [(n, t) for n, t in assignments[BEZALEL]], "skipped": [(n, t) for n, t in assignments[None]], "success_count": len(results["success"]), "failed_count": len(results["failed"]), "failed_details": [(n, w, str(e)) for n, w, e in results["failed"]], }, f, indent=2) return results if __name__ == "__main__": main()