diff --git a/scripts/reassign_fenrir.py b/scripts/reassign_fenrir.py new file mode 100644 index 0000000..ed33c7f --- /dev/null +++ b/scripts/reassign_fenrir.py @@ -0,0 +1,184 @@ +#!/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()