From 82c2035992c5e2d104386965fa93aa6b5ee4cdde Mon Sep 17 00:00:00 2001 From: matcha Date: Mon, 5 Oct 2026 20:15:17 +0000 Subject: [PATCH] Make radar deterministic --- scout.py | 257 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 135 insertions(+), 122 deletions(-) diff --git a/scout.py b/scout.py index 02ce031..9b2ce66 100644 --- a/scout.py +++ b/scout.py @@ -1,150 +1,163 @@ #!/usr/bin/env python3 -import argparse -import datetime as dt -import html -import json -import math -import pathlib -import urllib.request -import urllib.error +import argparse, datetime as dt, html, json, math, pathlib, urllib.request, urllib.error ROOT = pathlib.Path(__file__).resolve().parent -WATCHED = [ - "openai/codex", - "anthropics/claude-code", - "vercel-labs/agent-browser", - "browser-use/browser-use", - "block/goose", - "modelcontextprotocol/servers", -] -KEYWORDS = { - "browser": 9, "upload": 12, "file": 3, "preview": 10, "localhost": 8, - "session": 7, "context": 9, "compact": 10, "goal": 10, "plan": 7, - "mcp": 9, "sandbox": 10, "permission": 9, "approval": 8, "guardrail": 10, - "model": 5, "effort": 8, "preset": 10, "workflow": 6, "task": 5, - "project": 5, "sidebar": 5, "hook": 7, "export": 9, "integration": 5, - "extension": 7, "plugin": 8, "api": 5, "cli": 4, "agent": 5, -} def api(url): req = urllib.request.Request(url, headers={ - "User-Agent": "notplanned-scout/0.1", + "User-Agent": "notplanned-radar/0.2", "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", }) - with urllib.request.urlopen(req, timeout=15) as r: + with urllib.request.urlopen(req, timeout=20) as r: return json.load(r) -def score_issue(issue, now): - text = (issue.get("title","") + " " + (issue.get("body") or "")).lower() +def esc(v): return html.escape(str(v), quote=True) + +def repo_heat(repo, now): + pushed = repo.get("pushed_at") or repo.get("updated_at") + try: + age = max(0, (now - dt.datetime.fromisoformat(pushed.replace("Z","+00:00"))).days) + except Exception: + age = 999 + activity = 15 if age <= 7 else 10 if age <= 30 else 5 if age <= 90 else 0 + stars = int(repo.get("stargazers_count") or 0) + star_score = min(15, round(math.log10(max(stars, 1)) * 3.2)) + return activity + star_score, {"repo_activity": activity, "repo_scale": star_score} + +def issue_score(issue, repo_meta, cfg, now): + labels = {str(x.get("name","")).strip().lower() for x in issue.get("labels", [])} + if labels & {x.lower() for x in cfg["exclude_labels"]}: + return None + rejection = 0 + reason = issue.get("state_reason") + if reason == "not_planned": + rejection = max(rejection, 32) + for label, pts in cfg["rejection_labels"].items(): + if label.lower() in labels: + rejection = max(rejection, pts) + if rejection == 0: + return None + closed = issue.get("closed_at") or issue.get("updated_at") try: - when = dt.datetime.fromisoformat(closed.replace("Z","+00:00")) - age = max(0, (now - when).days) + age = max(0, (now - dt.datetime.fromisoformat(closed.replace("Z","+00:00"))).days) except Exception: - age = 365 - recency = max(0, 90 - age) - comments = min(24, int(issue.get("comments") or 0) * 2) + age = 999 + recency = 40 if age <= 7 else 30 if age <= 30 else 16 if age <= 90 else 6 if age <= 180 else 0 + + comments = min(20, int(issue.get("comments") or 0) * 2) reactions = issue.get("reactions") or {} - reaction_score = min(24, int(reactions.get("total_count") or 0) * 3) - keyword_score = sum(weight for word, weight in KEYWORDS.items() if word in text) - return recency + comments + reaction_score + min(48, keyword_score) + reaction_score = min(20, int(reactions.get("total_count") or 0) * 2) -def suggested_move(title): + title = (issue.get("title") or "").lower() + body = (issue.get("body") or "").lower() + text = title + "\n" + body[:4000] + ext = 0 + matched = [] + for word, pts in cfg["externalizable_keywords"].items(): + if word.lower() in text: + ext += pts + matched.append(word) + ext = min(28, ext) + + heat, heat_breakdown = repo_heat(repo_meta, now) + breakdown = { + "rejection": rejection, + "recency": recency, + "engagement": comments + reaction_score, + "externalizable": ext, + **heat_breakdown, + } + score = sum(breakdown.values()) + return { + "score": score, + "breakdown": breakdown, + "age_days": age, + "matched": matched[:6], + } + +def category(title): t = title.lower() - if "upload" in t or "file input" in t: - return "Ship a deterministic upload bridge outside upstream." - if "preview" in t or "localhost" in t: - return "Ship a preview/port sidecar and test it on real agent-built apps." - if "context" in t or "compact" in t: - return "Build a transcript/context sidecar; measure saved tokens and resumability." - if "goal" in t or "session" in t: - return "Wrap session state externally; prove persistence across runs." - if "model" in t or "effort" in t or "preset" in t: - return "Compose the existing knobs into one named preset action." - if "permission" in t or "sandbox" in t or "approval" in t or "guardrail" in t: - return "Put a narrow authority policy in front of the existing agent." - if "browser" in t or "domain" in t or "form" in t: - return "Wrap browser actions with scoped policy + receipts." - if "project" in t or "task" in t or "workflow" in t: - return "Ship an external supervisor instead of waiting for upstream PM." - return "Find the smallest external wrapper that satisfies the request; test one user." + buckets = [ + ("browser", ["browser","upload","preview","localhost","tab","form"]), + ("context", ["context","compact","session","memory","history"]), + ("authority", ["permission","approval","sandbox","security","guardrail"]), + ("integration", ["mcp","api","webhook","integration","plugin","extension"]), + ("interface", ["ui","sidebar","dashboard","preset","menu","export"]), + ("workflow", ["task","project","workflow","plan","goal"]), + ] + for name, words in buckets: + if any(w in t for w in words): return name + return "wrapper" -def esc(s): - return html.escape(str(s), quote=True) +def move_for(cat): + return { + "browser":"external browser sidecar / extension", + "context":"session/context sidecar", + "authority":"policy proxy / scoped authority layer", + "integration":"adapter / MCP / integration bridge", + "interface":"thin UI / control surface", + "workflow":"supervisor / workflow wrapper", + "wrapper":"thin wrapper around existing capability", + }[cat] -def curated_card(o): - return f""" -
{esc(o['repo'])} · #{esc(o['issue'])}{esc(o['heat'])}
-

{esc(o['name'])}

-
{esc(o['title'])}
-

{esc(o['thesis'])}

-
-
signal{esc(o['signal'])}
-
move{esc(o['move'])}
-
+def card(o): + b=o["breakdown"] + why=f"rej {b['rejection']} · rec {b['recency']} · eng {b['engagement']} · ext {b['externalizable']} · heat {b['repo_activity']+b['repo_scale']}" + return f"""
+
{esc(o['repo'])} · #{o['number']}{o['score']}
+

{esc(o['title'])}

+
{esc(o['category'])} · closed {o['age_days']}d ago
+

{esc(o['move'])}

+
{esc(why)}
""" -def fresh_card(o): - heat = "hot" if o["score"] >= 105 else ("warm" if o["score"] >= 75 else "watch") - return f""" -
{esc(o['repo'])} · #{esc(o['number'])}{heat}
-

{esc(o['title'])}

-
auto score {o['score']} · {esc(o.get('closed_at','')[:10])}
-
{esc(suggested_move(o['title']))}
-
""" - -def find_fresh(curated_urls, limit=6): - now = dt.datetime.now(dt.timezone.utc) - found = [] - for repo in WATCHED: - url = f"https://api.github.com/repos/{repo}/issues?state=closed&sort=updated&direction=desc&per_page=100" +def scan(): + cfg=json.loads((ROOT/"radar.json").read_text()) + now=dt.datetime.now(dt.timezone.utc) + rows=[] + for repo_name in cfg["repos"]: try: - items = api(url) + repo=api(f"https://api.github.com/repos/{repo_name}") + issues=api(f"https://api.github.com/repos/{repo_name}/issues?state=closed&sort=updated&direction=desc&per_page=100") except Exception as e: - print(f"scout warning: {repo}: {type(e).__name__}", flush=True) + print(f"warning {repo_name}: {type(e).__name__}") continue - for issue in items: - if "pull_request" in issue: - continue - if issue.get("state_reason") != "not_planned": - continue - if issue.get("html_url") in curated_urls: - continue - score = score_issue(issue, now) - if score < 45: - continue - found.append({ - "repo": repo, - "number": issue.get("number"), - "title": issue.get("title",""), - "html_url": issue.get("html_url",""), - "closed_at": issue.get("closed_at") or issue.get("updated_at") or "", - "score": score, + for issue in issues: + if "pull_request" in issue: continue + scored=issue_score(issue, repo, cfg, now) + if not scored or scored["score"] < cfg["threshold"]: continue + cat=category(issue.get("title","")) + rows.append({ + "repo":repo_name, + "number":issue.get("number"), + "title":issue.get("title",""), + "html_url":issue.get("html_url",""), + "closed_at":issue.get("closed_at") or "", + "category":cat, + "move":move_for(cat), + **scored, }) - found.sort(key=lambda x: (x["score"], x["closed_at"]), reverse=True) - return found[:limit] + rows.sort(key=lambda x:(x["score"], x["closed_at"]), reverse=True) + return cfg, rows[:cfg["limit"]] -def render(out_path, no_network=False): - data = json.loads((ROOT / "opportunities.json").read_text()) - curated = data["opportunities"] - curated_urls = {x["source"] for x in curated} - fresh = [] if no_network else find_fresh(curated_urls) - template = (ROOT / "template.html").read_text() - now = dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%d %H:%M") - page = (template - .replace("__CURATED_COUNT__", str(len(curated))) - .replace("__FRESH_COUNT__", str(len(fresh))) - .replace("__UPDATED__", now) - .replace("__CURATED_CARDS__", "\n".join(curated_card(x) for x in curated)) - .replace("__FRESH_CARDS__", "\n".join(fresh_card(x) for x in fresh) if fresh else '

scout pass quiet

No machine-surfaced candidate cleared the threshold on this pass. That is a valid result.
') +def render(out_path): + cfg, rows=scan() + tpl=(ROOT/"template.html").read_text() + now=dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%d %H:%M") + html_out=(tpl + .replace("__COUNT__",str(len(rows))) + .replace("__UPDATED__",now) + .replace("__THRESHOLD__",str(cfg["threshold"])) + .replace("__REPOS__",str(len(cfg["repos"]))) + .replace("__CARDS__","\n".join(card(x) for x in rows) if rows else '
No candidate cleared the deterministic threshold on this pass.
') ) - pathlib.Path(out_path).write_text(page) - print(json.dumps({"curated": len(curated), "fresh": len(fresh), "out": str(out_path)})) + pathlib.Path(out_path).write_text(html_out) + print(json.dumps({"candidates":len(rows),"threshold":cfg["threshold"],"repos":len(cfg["repos"]),"out":str(out_path)})) -if __name__ == "__main__": - p = argparse.ArgumentParser() - p.add_argument("--out", default=str(ROOT / "index.html")) - p.add_argument("--no-network", action="store_true") - args = p.parse_args() - render(args.out, args.no_network) +if __name__=="__main__": + p=argparse.ArgumentParser() + p.add_argument("--out",default=str(ROOT/"index.html")) + a=p.parse_args() + render(a.out)