Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,17 @@ alias cu := cargo-update
cargo-update-all *args:
bash scripts/cargo_update_all.sh {{args}}
alias cua := cargo-update-all

# ---------------------------------------------------------------------------- #
# TEMPLATE REVIEW #
# ---------------------------------------------------------------------------- #

# Review a downstream project for improvements to backport into the template (dry-run by default; --execute to run, optional target dir)
template-review *args:
bash scripts/template_review.sh {{args}}
alias tr := template-review

# Review all downstream projects in parallel for backport candidates (dry-run by default; --execute to run)
template-review-all *args:
bash scripts/template_review_all.sh {{args}}
alias tra := template-review-all
96 changes: 96 additions & 0 deletions scripts/template_review.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROMPT_DIR="${SCRIPT_DIR}/template_review"
# The template repo is the parent of scripts/
DEFAULT_TEMPLATE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"

# --- Locate a JS package runner (pnpm dlx preferred, npx fallback) ----------
find_runner() {
if command -v pnpm &>/dev/null; then
echo "pnpm dlx"
elif command -v npx &>/dev/null; then
echo "npx"
else
echo "Error: neither pnpm nor npx found. Install one of them first." >&2
exit 1
fi
}

RUNNER="$(find_runner)"

# --- Parse arguments --------------------------------------------------------
EXECUTE=false
TARGET_DIR=""

for arg in "$@"; do
case "$arg" in
--execute) EXECUTE=true ;;
*) TARGET_DIR="$arg" ;;
esac
done

# Resolve target directory (default: current directory)
if [[ -n "$TARGET_DIR" ]]; then
TARGET_DIR="$(cd "$TARGET_DIR" && pwd)"
else
TARGET_DIR="$(pwd)"
fi

# Allow caller to override the template location
TEMPLATE_DIR="${TEMPLATE_DIR:-$DEFAULT_TEMPLATE_DIR}"

# --- Compose the prompt from markdown files ---------------------------------
compose_prompt() {
local prompt=""

if [[ -f "${PROMPT_DIR}/role.md" ]]; then
prompt+="$(cat "${PROMPT_DIR}/role.md")"
prompt+=$'\n\n'
fi

for f in "${PROMPT_DIR}"/*.md; do
[[ "$(basename "$f")" == "role.md" ]] && continue
prompt+="$(cat "$f")"
prompt+=$'\n\n'
done

echo "$prompt"
}

PROMPT="$(compose_prompt)"

# --- Allowed tools ----------------------------------------------------------
# Read-only review: no Edit/Write, no git mutations.
ALLOWED_TOOLS="Read Bash"

# --- Execute or dry-run -----------------------------------------------------
if [[ "$EXECUTE" == true ]]; then
echo "Using runner: ${RUNNER}"
echo "Target: ${TARGET_DIR}"
echo "Template: ${TEMPLATE_DIR}"
echo "Prompt length: ${#PROMPT} chars"
echo "Allowed tools: ${ALLOWED_TOOLS}"
echo "---"
cd "$TARGET_DIR"
export TEMPLATE_DIR
echo "${PROMPT}" | ${RUNNER} @anthropic-ai/claude-code --print \
--allowed-tools ${ALLOWED_TOOLS}
else
echo "=== DRY RUN ==="
echo ""
echo "${PROMPT}"
echo "---"
echo "Runner: ${RUNNER}"
echo "Target: ${TARGET_DIR}"
echo "Template: ${TEMPLATE_DIR}"
echo "Prompt length: ${#PROMPT} chars"
echo "Allowed tools: ${ALLOWED_TOOLS}"
echo ""
echo "Would run:"
echo " cd ${TARGET_DIR}"
echo " TEMPLATE_DIR=${TEMPLATE_DIR} echo \"\${PROMPT}\" | ${RUNNER} @anthropic-ai/claude-code --print --allowed-tools ${ALLOWED_TOOLS}"
echo ""
echo "Pass --execute to run this against Claude Code."
fi
3 changes: 3 additions & 0 deletions scripts/template_review/role.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
You are an experienced, pragmatic software engineer reviewing a downstream project that was forked from a shared Rust template.
Your job is to identify changes in the downstream project that represent **generalizable improvements** worth pulling back into the template — not project-specific code.
You produce a concise, actionable report. You do NOT modify any files, create branches, or open PRs.
75 changes: 75 additions & 0 deletions scripts/template_review/task.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
## Context

You are in a clone of a downstream project that was originally based on a shared Rust template.
The path to a local checkout of the template is provided in the environment variable `TEMPLATE_DIR`.

The template contains shared infrastructure files that are intended to be identical (or nearly so) across all downstream projects. Examples include:

- `.editorconfig`, `.gitignore`, `.prettierrc.yml`
- `.github/workflows/**`
- `.vscode/settings.json`
- `CLAUDE.md`, `cliff.toml`, `justfile`, `release.toml`, `rust-toolchain.toml`, `rustfmt.toml`
- `scripts/**` (except `scripts/downstream.txt`, which is template-only)

Files that are intentionally project-specific and should be IGNORED:
- `Cargo.toml`, `Cargo.lock`
- `README.md`
- `src/**`
- `LICENSE.md`
- `scripts/downstream.txt`

## Your task

Find changes in this downstream project that represent **generalizable improvements** to the shared infrastructure, which the template itself does not yet have.

### 1. Diff the shared files

For each shared-infrastructure file listed above (and any other non-project file you notice), compare the downstream copy against the template copy at `$TEMPLATE_DIR`. Use:

```
diff -u "$TEMPLATE_DIR/<file>" "<file>"
```

or walk directories with `diff -ruN "$TEMPLATE_DIR/<dir>" "<dir>"`.

### 2. Classify each difference

For every non-trivial difference, decide whether it is:

- **BACKPORT** — a genuine improvement (bug fix, new capability, better default, cleaner config, new script, etc.) that would benefit every downstream project if added to the template.
- **PROJECT-SPECIFIC** — a customization that only makes sense for this project (e.g. a workflow step tailored to this repo's deploy target). Do not recommend these.
- **DRIFT** — stale local edits, accidental changes, or noise. Do not recommend these.
- **TEMPLATE-NEWER** — the template has changes the downstream doesn't. Ignore; `cargo_update_all` handles that direction.

Be conservative: when in doubt, mark it PROJECT-SPECIFIC or DRIFT, not BACKPORT.

### 3. Check git log for intent

For each candidate BACKPORT, run `git log --oneline -- <file>` on the most recent commits touching it, to understand *why* the change was made. A commit message like "fix ci flake on macos" is a strong BACKPORT signal; "tweak for acme deployment" is PROJECT-SPECIFIC.

### 4. Produce the report

Print a single markdown report to stdout with this exact structure:

```
# Template Backport Review: <repo name>

## Summary
<one-line summary: e.g. "3 candidates found" or "No backport candidates — downstream is in sync with template on shared files.">

## Backport Candidates

### <file path>
**What changed:** <1-2 sentence description>
**Why it matters:** <why this benefits all downstream projects>
**Origin commit(s):** <short sha — subject line, if found>
**Suggested action:** <"copy file verbatim" | "merge hunk" | "adapt idea">

<repeat per candidate>

## Skipped (project-specific or drift)
- <file>: <one-line reason>
<repeat, or omit section if empty>
```

If there are no backport candidates, still print the report with an empty "Backport Candidates" section and a clear summary line. Do not create any files, branches, or PRs. Do not modify the working tree.
83 changes: 83 additions & 0 deletions scripts/template_review_all.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOWNSTREAM_FILE="${SCRIPT_DIR}/downstream.txt"
TEMPLATE_REVIEW="${SCRIPT_DIR}/template_review.sh"
TEMPLATE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"

if [[ ! -f "$DOWNSTREAM_FILE" ]]; then
echo "Error: ${DOWNSTREAM_FILE} not found" >&2
exit 1
fi

# Forward all args (e.g. --execute) to each invocation
ARGS=("$@")

WORK_DIR="$(mktemp -d)"
echo "Clone directory: ${WORK_DIR}"
echo "Template: ${TEMPLATE_DIR}"

PIDS=()
REPOS=()
REPORT_FILES=()

while IFS= read -r repo_url; do
[[ -z "$repo_url" || "$repo_url" == \#* ]] && continue

REPO_NAME="$(basename "${repo_url%/}" .git)"
CLONE_PATH="${WORK_DIR}/${REPO_NAME}"
REPORT_FILE="${WORK_DIR}/${REPO_NAME}.report.md"

echo "Cloning: ${repo_url} -> ${CLONE_PATH}"
(
git clone --quiet "$repo_url" "$CLONE_PATH"
TEMPLATE_DIR="$TEMPLATE_DIR" \
bash "$TEMPLATE_REVIEW" ${ARGS[@]+"${ARGS[@]}"} "$CLONE_PATH" \
>"$REPORT_FILE" 2>&1
) &
PIDS+=($!)
REPOS+=("$repo_url")
REPORT_FILES+=("$REPORT_FILE")
done < "$DOWNSTREAM_FILE"

# Wait for all jobs and print each report sequentially
FAILED=0
for i in "${!PIDS[@]}"; do
if wait "${PIDS[$i]}"; then
echo
echo "════════════════════════════════════════════════════════════════"
echo " ${REPOS[$i]}"
echo "════════════════════════════════════════════════════════════════"
cat "${REPORT_FILES[$i]}"
else
echo
echo "════════════════════════════════════════════════════════════════"
echo " FAILED: ${REPOS[$i]}"
echo "════════════════════════════════════════════════════════════════" >&2
cat "${REPORT_FILES[$i]}" >&2 || true
FAILED=$((FAILED + 1))
fi
done

# Aggregate reports into a single file for convenience
AGGREGATE="${WORK_DIR}/REVIEW.md"
{
echo "# Template Backport Review — $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo
for i in "${!REPORT_FILES[@]}"; do
echo "---"
echo "## ${REPOS[$i]}"
echo
cat "${REPORT_FILES[$i]}"
echo
done
} >"$AGGREGATE"

echo
echo "---"
echo "Finished: $((${#PIDS[@]} - FAILED))/${#PIDS[@]} succeeded"
echo "Aggregate report: ${AGGREGATE}"
echo "(clones kept at ${WORK_DIR} so you can inspect — delete when done)"

exit $FAILED
Loading