Scaffold: agent starter kit (seeded from Ebb)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 20:15:40 +02:00
commit 8bef22afb2
28 changed files with 713 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Version drift report for gradle/libs.versions.toml (Plan 09, D-056).
Reads every [versions] entry, finds an artifact that uses it in [libraries] or [plugins], and asks Google Maven and
Maven Central for that artifact's maven-metadata.xml. Prints one table: current, latest stable, newest (only where a
pre-release is newer than the latest stable), and how far the current version is behind.
The Gradle wrapper version is compared with services.gradle.org. Nothing here runs from the build; run it by hand:
python3 tools/dependency-report/latest.py
Rule (CLAUDE.md › Build and verify): run it at the start of every plan. A component more than one stable minor
behind gets a line in that plan's report; a toolchain component (Gradle, AGP, Kotlin, JDK LTS) more than one line
behind triggers a Plan 09-shaped upgrade before the plan starts.
"""
import json
import re
import sys
import urllib.request
import xml.etree.ElementTree as ET
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
REPOS = ["https://dl.google.com/android/maven2", "https://repo1.maven.org/maven2"]
# Catalog entries that are SDK levels, not artifacts.
NOT_ARTIFACTS = {"minSdk", "compileSdk", "targetSdk"}
# The toolchain is looked up by its Gradle plugin, not by the first library that shares its version.
PREFERRED = {
"agp": ("com.android.tools.build", "gradle"),
"kotlin": ("org.jetbrains.kotlin", "kotlin-gradle-plugin"),
"ksp": ("com.google.devtools.ksp", "symbol-processing-gradle-plugin"),
}
STABLE = re.compile(r"^\d+(\.\d+)*$")
def key(version):
"""Numeric parts first; a pre-release (anything after the first '-') sorts before its release."""
base, _, suffix = version.partition("-")
numbers = [int(p) if p.isdigit() else -1 for p in base.split(".")]
return numbers, (0, suffix) if suffix else (1, "")
def fetch(url):
try:
with urllib.request.urlopen(url, timeout=20) as response:
return response.read()
except OSError:
return None
def versions_of(group, name):
"""Versions from every repository together: some artifacts (early KSP) also sit, stale, on Google Maven."""
found = set()
for repo in REPOS:
body = fetch(f"{repo}/{group.replace('.', '/')}/{name}/maven-metadata.xml")
if body:
found.update(v.text for v in ET.fromstring(body).iter("version") if v.text)
return sorted(found)
def plugin_marker(plugin_id):
return plugin_id, f"{plugin_id}.gradle.plugin"
def read_catalog(text):
"""The subset of TOML the catalog uses (works on Python < 3.11, which has no tomllib): returns the [versions]
entries in file order and, per version name, the first library or plugin that refers to it."""
versions, artifacts, section = {}, {}, None
for raw in text.splitlines():
line = raw.split("#", 1)[0].strip()
if not line:
continue
if line.startswith("["):
section = line.strip("[]")
continue
if section == "versions":
match = re.match(r'([\w-]+)\s*=\s*"([^"]+)"', line)
if match:
versions[match.group(1)] = match.group(2)
continue
ref = re.search(r'version\.ref\s*=\s*"([^"]+)"', line)
if not ref or ref.group(1) in artifacts:
continue
module = re.search(r'module\s*=\s*"([^"]+)"', line)
plugin = re.search(r'\bid\s*=\s*"([^"]+)"', line)
if section == "libraries" and module:
artifacts[ref.group(1)] = tuple(module.group(1).split(":"))
elif section == "plugins" and plugin:
artifacts[ref.group(1)] = plugin_marker(plugin.group(1))
return versions, artifacts
def behind(current, stable):
if not stable or not STABLE.match(current):
return ""
c, s = [int(x) for x in current.split(".")], [int(x) for x in stable.split(".")]
c += [0] * (3 - len(c))
s += [0] * (3 - len(s))
if c[0] < s[0]:
return f"{s[0] - c[0]} major"
if c[1] < s[1]:
return f"{s[1] - c[1]} minor"
if c[2] < s[2]:
return "patch"
return ""
def row(name, coordinate, current, available):
stable = sorted((v for v in available if STABLE.match(v)), key=key)
everything = sorted(available, key=key)
latest = stable[-1] if stable else "?"
newest = everything[-1] if everything else "?"
return [name, coordinate, current, latest, newest if newest not in (latest, current) else "", behind(current, latest if stable else "")]
def gradle_row():
props = (ROOT / "gradle/wrapper/gradle-wrapper.properties").read_text()
match = re.search(r"gradle-([\d.]+)-(bin|all)\.zip", props)
current = match.group(1) if match else "?"
body = fetch("https://services.gradle.org/versions/all")
available = []
if body:
available = [v["version"] for v in json.loads(body) if not v.get("snapshot") and not v.get("nightly")]
return row("gradle (wrapper)", "services.gradle.org", current, available)
def main():
versions, artifacts = read_catalog((ROOT / "gradle/libs.versions.toml").read_text())
rows = [gradle_row()]
for name, current in versions.items():
if name in NOT_ARTIFACTS:
continue
coordinate = PREFERRED.get(name) or artifacts.get(name)
if not coordinate:
rows.append([name, "(no artifact)", str(current), "", "", ""])
continue
rows.append(row(name, ":".join(coordinate), str(current), versions_of(*coordinate)))
header = ["version", "artifact", "current", "latest stable", "newer pre-release", "behind"]
widths = [max(len(str(r[i])) for r in rows + [header]) for i in range(len(header))]
print(" | ".join(h.ljust(w) for h, w in zip(header, widths)))
print("-+-".join("-" * w for w in widths))
for r in rows:
print(" | ".join(str(c).ljust(w) for c, w in zip(r, widths)))
return 0
if __name__ == "__main__":
sys.exit(main())