Compare commits

...

1 Commits

Author SHA1 Message Date
Alexander Whitestone
10196b3804 chore: add fenrir issue reassignment script and execute triage (#823)
Some checks are pending
CI / validate (pull_request) Waiting to run
Classified and reassigned 106 of 107 fenrir-assigned open issues to
active wizards based on issue type:
- Ezra (architecture): 21 EPIC/consolidation/design issues
- Allegro (code): 52 UI/NEXUS/implementation issues
- Bezalel (execution): 33 ops/monitoring/infra issues
- Skipped: #823 (this issue)

Refs #823
2026-04-06 14:18:36 -04:00

184
scripts/reassign_fenrir.py Normal file
View File

@@ -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()