Scaffold: agent starter kit (seeded from Ebb)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/bin/sh
|
||||
# Deletes Gradle and Kotlin build output in the main checkout and in every agent worktree (.claude/worktrees/*):
|
||||
# each project's build/ (a directory named build next to a build.gradle.kts or settings.gradle.kts), .gradle/configuration-cache
|
||||
# and .kotlin. Never looks into export/, libs/, models/, spike/ or "samsung sdk/", and never deeper than four levels.
|
||||
# Prints what it removes and the space freed. Waits for the Gradle lock of tools/gradle-serial.sh, so it never deletes
|
||||
# under a running build. The orchestrator runs it after removing a worktree or when the disk is low.
|
||||
# Usage from any checkout: tools/clean-builds.sh [--dry-run]
|
||||
dry=0
|
||||
case "$1" in
|
||||
--dry-run) dry=1 ;;
|
||||
"") ;;
|
||||
*) echo "usage: $0 [--dry-run]" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
common=$(git rev-parse --path-format=absolute --git-common-dir) || exit 1
|
||||
main=$(dirname "$common")
|
||||
|
||||
# Candidates under one checkout root; .claude is skipped here because the worktrees are walked as roots of their own.
|
||||
candidates() {
|
||||
find "$1" -maxdepth 4 \
|
||||
\( -path "$1/export" -o -path "$1/libs" -o -path "$1/models" -o -path "$1/spike" -o -path "$1/samsung sdk" \
|
||||
-o -path "$1/.claude" -o -name .git -o -name src -o -name node_modules \) -prune \
|
||||
-o -type d \( -name build -o -name .kotlin -o -path '*/.gradle/configuration-cache' \) -print -prune
|
||||
}
|
||||
|
||||
list=$(mktemp) || exit 1
|
||||
trap 'rm -f "$list"' EXIT
|
||||
for root in "$main" "$main"/.claude/worktrees/*; do
|
||||
[ -d "$root" ] || continue
|
||||
candidates "$root" | while IFS= read -r dir; do
|
||||
if [ "$(basename "$dir")" = build ]; then
|
||||
parent=$(dirname "$dir")
|
||||
[ -f "$parent/build.gradle.kts" ] || [ -f "$parent/settings.gradle.kts" ] || continue
|
||||
fi
|
||||
printf '%s\n' "$dir"
|
||||
done
|
||||
done > "$list"
|
||||
|
||||
if [ ! -s "$list" ]; then echo "Nothing to remove."; exit 0; fi
|
||||
|
||||
if [ "$dry" -eq 0 ]; then
|
||||
lock=/tmp/kvitt-gradle.lock
|
||||
while ! mkdir "$lock" 2>/dev/null; do
|
||||
pid=$(cat "$lock/pid" 2>/dev/null)
|
||||
if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then rm -rf "$lock"; continue; fi
|
||||
sleep 5
|
||||
done
|
||||
echo $$ > "$lock/pid"
|
||||
trap 'rm -rf "$lock"; rm -f "$list"' EXIT INT TERM HUP
|
||||
fi
|
||||
|
||||
total=0
|
||||
while IFS= read -r dir; do
|
||||
kb=$(du -sk "$dir" 2>/dev/null | cut -f1)
|
||||
kb=${kb:-0}
|
||||
total=$((total + kb))
|
||||
printf '%8d MB %s\n' $((kb / 1024)) "${dir#"$main"/}"
|
||||
[ "$dry" -eq 1 ] || rm -rf "$dir"
|
||||
done < "$list"
|
||||
|
||||
if [ "$dry" -eq 1 ]; then verb="Would free"; else verb="Freed"; fi
|
||||
echo "$verb $((total / 1024)) MB in $(wc -l < "$list" | tr -d ' ') directories."
|
||||
Executable
+147
@@ -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())
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
# Kvitt pre-commit hook: the fast subset of `./gradlew check`. Installed by tools/install-git-hooks.sh.
|
||||
set -e
|
||||
top=$(git rev-parse --show-toplevel)
|
||||
|
||||
# Secrets and personal data never reach git. Adjust the list to this project (see CLAUDE.md › Never commit).
|
||||
bad=$(git -C "$top" ls-files --cached -- local.properties '*.jks' '*.keystore' '*.p12' '*.aar' 'models/' 'export/' | head -5)
|
||||
if [ -n "$bad" ]; then
|
||||
echo "pre-commit: a secret, keystore, vendor binary or personal-data file is staged or tracked:" >&2
|
||||
echo "$bad" >&2
|
||||
echo "Unstage with: git rm --cached <file>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[ -x "$top/gradlew" ] || exit 0
|
||||
|
||||
# The build runs on the pinned JDK (gradle.properties); the shell's default java may be newer or a macOS stub.
|
||||
case "$("${JAVA_HOME:+$JAVA_HOME/bin/}java" -version 2>&1 | head -1)" in
|
||||
*\"25*) ;;
|
||||
*) [ -d /opt/homebrew/opt/openjdk@25 ] && export JAVA_HOME=/opt/homebrew/opt/openjdk@25/libexec/openjdk.jdk/Contents/Home ;;
|
||||
esac
|
||||
|
||||
cd "$top"
|
||||
# Replace with this project's fast checks (architecture rules, the pure-module tests). Keep it under a minute.
|
||||
./gradlew --quiet :core:model:test || {
|
||||
echo "pre-commit: fast tests failed (see above). Run ./gradlew check for the full picture." >&2
|
||||
exit 1
|
||||
}
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
# Runs ./gradlew with the given arguments, one Gradle build at a time across every worktree and agent on this machine
|
||||
# (CLAUDE.md › Build and verify: parallel builds exhaust memory). Waits for a running build to finish, then runs.
|
||||
# Usage from any worktree root: tools/gradle-serial.sh check
|
||||
lock=/tmp/kvitt-gradle.lock
|
||||
top=$(git rev-parse --show-toplevel) || exit 1
|
||||
while ! mkdir "$lock" 2>/dev/null; do
|
||||
pid=$(cat "$lock/pid" 2>/dev/null)
|
||||
if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then rm -rf "$lock"; continue; fi
|
||||
sleep 5
|
||||
done
|
||||
echo $$ > "$lock/pid"
|
||||
trap 'rm -rf "$lock"' EXIT INT TERM HUP
|
||||
cd "$top" && ./gradlew "$@"
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installs the Kvitt pre-commit hook into this repository's hooks folder (shared by all worktrees).
|
||||
# The hook guards itself: on branches without the app build it only runs the export/ check.
|
||||
set -euo pipefail
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
hooks="$(git -C "$here" rev-parse --path-format=absolute --git-common-dir)/hooks"
|
||||
target="$hooks/pre-commit"
|
||||
if [[ -e "$target" ]] && ! cmp -s "$here/git-hooks/pre-commit" "$target"; then
|
||||
echo "A different pre-commit hook already exists at $target; not overwriting. Merge them by hand." >&2
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$hooks"
|
||||
cp "$here/git-hooks/pre-commit" "$target"
|
||||
chmod +x "$target"
|
||||
echo "Installed $target"
|
||||
Reference in New Issue
Block a user