"""Stage 1: Scout — Discover candidate repos from GitHub.""" import json import requests from datetime import datetime, timedelta def scout(config, state=None): """ Search GitHub for repos matching AI workflow queries. Returns list of discovered repos with metadata. """ queries = config.get("scout", {}).get("queries", []) filters = config.get("scout", {}).get("filters", {}) max_results = config.get("scout", {}).get("max_results", 30) cooldown_hours = config.get("scout", {}).get("cooldown_hours", 24) # Check cooldown (skip on first run) if state is None: state = {} if "last_run" in state: last = datetime.fromisoformat(state["last_run"]) if datetime.now() - last < timedelta(hours=cooldown_hours): cooldown_remaining = int((timedelta(hours=cooldown_hours) - (datetime.now() - last)).total_seconds() / 3600) print(f" ⏸ Cooldown active — {cooldown_remaining}h remaining") # Continue anyway on first discovery run — we want results pass discovered = [] seen_urls = set() for query in queries: stars_min = filters.get("stars_min", 50) pushed_after = filters.get("pushed_after", "2026-01-01") language = filters.get("language", "Python") # Build query string safely — requests handles URL encoding of params url = "https://api.github.com/search/repositories" search_q = f"{query} stars:>{stars_min} pushed:>{pushed_after} language:{language}" params = { "q": search_q, "sort": "updated", "order": "desc", "per_page": min(max_results, 30), } headers = {} github_token = config.get("github", {}).get("token", "") if github_token: headers["Authorization"] = f"token {github_token}" headers["Accept"] = "application/vnd.github.v3+json" try: resp = requests.get(url, params=params, headers=headers, timeout=15) if resp.status_code == 403: import time time.sleep(60) # Wait for rate limit window resp = requests.get(url, params=params, headers=headers, timeout=15) if resp.status_code == 403: return {"status": "RATE_LIMITED", "message": "GitHub API rate limit hit after retry."} if resp.status_code != 200: continue data = resp.json() for item in data.get("items", []): repo_url = item.get("html_url", "") if repo_url in seen_urls: continue seen_urls.add(repo_url) discovered.append({ "name": item.get("name", ""), "full_name": item.get("full_name", ""), "url": repo_url, "clone_url": item.get("clone_url", ""), "stars": item.get("stargazers_count", 0), "language": item.get("language", ""), "description": item.get("description", ""), "updated_at": item.get("updated_at", ""), "created_at": item.get("created_at", ""), "archived": item.get("archived", False), "size_kb": item.get("size", 0), "status": "DISCOVERED", "discovered_at": datetime.now().isoformat(), }) if len(discovered) >= max_results: break except Exception as e: discovered.append({"error": str(e), "query": query, "status": "ERROR"}) state["last_run"] = datetime.now().isoformat() return { "status": "OK", "count": len(discovered), "repos": discovered, "state": state, }