#!/usr/bin/env sh set -eu ROOT_PATH="/c/Users/stefan/git" STANDARDS_REPO="/c/Users/stefan/git/project-standards" EXCLUDE_NAME="project-standards" CHECK_ONLY=0 WATCH=0 INTERVAL=60 usage() { cat <<'EOF' Usage: reconcile-project-standards.sh [options] Options: --root Root folder containing repositories --standards-repo Path to project-standards repository --exclude First-level directory name to skip (default: project-standards) --check-only Check drift only, do not update files --watch Continuously scan and reconcile --interval Watch interval in seconds (default: 60) -h, --help Show this help EOF } while [ "$#" -gt 0 ]; do case "$1" in --root) ROOT_PATH=$2 shift 2 ;; --standards-repo) STANDARDS_REPO=$2 shift 2 ;; --exclude) EXCLUDE_NAME=$2 shift 2 ;; --check-only) CHECK_ONLY=1 shift ;; --watch) WATCH=1 shift ;; --interval) INTERVAL=$2 shift 2 ;; -h|--help) usage exit 0 ;; *) echo "Unknown option: $1" >&2 usage >&2 exit 1 ;; esac done if [ "$INTERVAL" -lt 5 ]; then echo "interval must be >= 5" >&2 exit 1 fi AGENTS_TEMPLATE="$STANDARDS_REPO/templates/AGENTS.base.md" DOD_TEMPLATE="$STANDARDS_REPO/templates/DEFINITION_OF_DONE.base.md" if [ ! -f "$AGENTS_TEMPLATE" ]; then echo "AGENTS template not found: $AGENTS_TEMPLATE" >&2 exit 1 fi if [ ! -f "$DOD_TEMPLATE" ]; then echo "DoD template not found: $DOD_TEMPLATE" >&2 exit 1 fi hash_or_missing() { path=$1 if [ ! -f "$path" ]; then printf "%s" "__MISSING__" return 0 fi if command -v sha256sum >/dev/null 2>&1; then sha256sum "$path" | awk '{print $1}' else shasum -a 256 "$path" | awk '{print $1}' fi } ensure_file() { template=$1 target=$2 template_hash=$(hash_or_missing "$template") target_hash=$(hash_or_missing "$target") if [ "$template_hash" = "$target_hash" ]; then printf "%s" "ok" return 0 fi if [ "$CHECK_ONLY" -eq 1 ]; then printf "%s" "drift" return 0 fi mkdir -p "$(dirname "$target")" cp "$template" "$target" printf "%s" "updated" } ensure_gitignore_build_entry() { gitignore_path=$1 if [ ! -f "$gitignore_path" ]; then if [ "$CHECK_ONLY" -eq 1 ]; then printf "%s" "drift" return 0 fi printf '%s\n' '.build/' > "$gitignore_path" printf "%s" "updated" return 0 fi if grep -Fqx '.build/' "$gitignore_path"; then printf "%s" "ok" return 0 fi if [ "$CHECK_ONLY" -eq 1 ]; then printf "%s" "drift" return 0 fi printf '\n%s\n' '.build/' >> "$gitignore_path" printf "%s" "updated" } run_once() { scanned=0 updated=0 drift=0 for repo in "$ROOT_PATH"/*; do [ -d "$repo" ] || continue name=$(basename "$repo") [ "$name" = "$EXCLUDE_NAME" ] && continue scanned=$((scanned + 1)) agents_target="$repo/AGENTS.md" dod_target="$repo/docs/DEFINITION_OF_DONE.md" gitignore_target="$repo/.gitignore" agents_state=$(ensure_file "$AGENTS_TEMPLATE" "$agents_target") dod_state=$(ensure_file "$DOD_TEMPLATE" "$dod_target") gitignore_state=$(ensure_gitignore_build_entry "$gitignore_target") if [ "$agents_state" = "updated" ] || [ "$dod_state" = "updated" ] || [ "$gitignore_state" = "updated" ]; then updated=$((updated + 1)) echo "UPDATED: $repo" continue fi if [ "$agents_state" = "drift" ] || [ "$dod_state" = "drift" ] || [ "$gitignore_state" = "drift" ]; then drift=$((drift + 1)) echo "DRIFT: $repo" continue fi echo "OK: $repo" done echo "Summary -> scanned=$scanned, updated=$updated, drift=$drift" if [ "$CHECK_ONLY" -eq 1 ] && [ "$drift" -gt 0 ]; then return 1 fi return 0 } if [ "$WATCH" -eq 1 ]; then while :; do echo "[$(date '+%Y-%m-%d %H:%M:%S')] Reconciling project standards..." run_once || true sleep "$INTERVAL" done else run_once fi