diff --git a/timmy-config/bin/load_cap_enforcer.py b/timmy-config/bin/load_cap_enforcer.py index b6059ec..f3d79c5 100755 --- a/timmy-config/bin/load_cap_enforcer.py +++ b/timmy-config/bin/load_cap_enforcer.py @@ -125,9 +125,12 @@ def main(): for iss in issues_sorted[:overflow]: print(f" - #{iss['number']}: {iss.get('title', '')[:50]}") - # Dry-run: show summary and exit + # Dry-run: just show summary and exit if args.dry_run: print("\n=== DRY RUN — no changes made ===") + # For dry-run, after = before (no changes) + for agent in by_agent: + by_agent[agent]["after"] = by_agent[agent]["before"] summary = build_summary(by_agent, unassignment_map) print("\n" + summary) if args.output: @@ -135,26 +138,44 @@ def main(): print(f"\nSummary written to {args.output}") return 0 - # LIVE: perform unassignments and comments + # LIVE: perform unassignments and comments (concurrent) print("\n=== LIVE RUN — executing ===") + from concurrent.futures import ThreadPoolExecutor, as_completed + import threading + lock = threading.Lock() + tasks = [] for agent, issues_to_unassign in unassignment_map.items(): for iss in issues_to_unassign: issue_num = iss["number"] repo_name = next( (r for r in REPOS if f"/{r}/issues/" in iss.get("html_url", "")), REPOS[0] ) - # Unassign: PATCH with empty assignees - _, status = api("PATCH", f"/repos/{ORG}/{repo_name}/issues/{issue_num}", token, {"assignees": []}) - if status not in (200, 201, 204): - print(f" WARNING: unassign #{issue_num} failed (HTTP {status})") - continue - # Comment - comment_body = COMMENT_TEMPLATE.format(assignee=agent) - _, status = api("POST", f"/repos/{ORG}/{repo_name}/issues/{issue_num}/comments", token, {"body": comment_body}) - if status not in (200, 201): - print(f" WARNING: comment on #{issue_num} failed (HTTP {status})") - else: - print(f" ✓ Unassigned & commented #{issue_num} ({repo_name})") + tasks.append((agent, issue_num, repo_name, iss)) + print(f"Total unassignment tasks: {len(tasks)}") + def do_task(agent, issue_num, repo_name, iss): + # Unassign + _, status1 = api("PATCH", f"/repos/{ORG}/{repo_name}/issues/{issue_num}", token, {"assignees": []}) + if status1 not in (200, 201, 204): + return (agent, issue_num, repo_name, False, f"unassign HTTP {status1}") + # Comment + comment_body = COMMENT_TEMPLATE.format(assignee=agent) + _, status2 = api("POST", f"/repos/{ORG}/{repo_name}/issues/{issue_num}/comments", token, {"body": comment_body}) + if status2 not in (200, 201): + return (agent, issue_num, repo_name, True, f"unassigned but comment HTTP {status2}") + return (agent, issue_num, repo_name, True, "OK") + completed = 0 + with ThreadPoolExecutor(max_workers=12) as executor: + futures = [executor.submit(do_task, a, n, r, i) for (a, n, r, i) in tasks] + for fut in as_completed(futures): + agent, num, repo, ok, msg = fut.result() + with lock: + completed += 1 + if completed % 50 == 0: + print(f" Progress: {completed}/{len(tasks)}") + if ok: + print(f" ✓ #{num} ({repo})") + else: + print(f" ✗ #{num} ({repo}): {msg}") # Recompute after counts for summary print("\nRecomputing after counts ...")