#!/usr/bin/env bash # sync-templates — copy rulesets' canonical .ai/ templates into this project. # # Extracted verbatim from startup.org Phase A step 3 (2026-07-31). This is the # mechanism that distributes every workflow, protocol and script change to every # project, and until the extraction it was untested inline bash running in every # session. The extraction exists so the guard changes that follow can be tested # before they reach a file whose failure mode is "no project starts". # # Behavior is deliberately identical to the inline block it replaces, including # its rough edges. Anything that looks like a defect here is characterized by a # test rather than fixed in passing — a change of behavior belongs in its own # commit, not smuggled into an extraction. # # Usage: sync-templates [project-root] (default: $PWD) # Output: one line naming the outcome, matching the previous inline wording # Exit: 0 always, as the inline block did — the outcome is on stdout # # Two guards decide whether the sync runs at all: # # Rulesets dirty under the synced paths → skip. rsync -a --delete copies the # working tree by disk presence, so an in-flight edit in rulesets would land # downstream as drift the project never authored. # # Project branch behind its upstream → skip. Syncing onto a stale committed # .ai/ baseline measures the diff against old content, so it comes out huge # and conflicts once the branch reconciles to an upstream that already # carries the newer templates. # # The rulesets location is injectable (SYNC_RULESETS_DIR) so the guards can be # exercised against a fixture instead of the real checkout. rs="${SYNC_RULESETS_DIR:-$HOME/code/rulesets}" proj="${1:-$PWD}" cd "$proj" || { echo "sync-templates: cannot enter '$proj'" >&2 exit 0 } synced_dirty=$(cd "$rs" && git status --porcelain -- \ claude-templates/.ai/protocols.org \ claude-templates/.ai/workflows/ \ claude-templates/.ai/scripts/ 2>/dev/null) # behind==0 (up-to-date or ahead-only) means HEAD contains all of upstream, so # the baseline is current. No upstream (new/unpushed branch) → rev-list fails → # proj_behind stays 0 → the sync runs. proj_behind=0 if [ -d .git ]; then counts=$(git rev-list --left-right --count '@{u}...HEAD' 2>/dev/null) \ && [ "$(printf '%s' "$counts" | cut -f1)" -gt 0 ] 2>/dev/null \ && proj_behind=1 fi if [ -n "$synced_dirty" ]; then echo "rulesets has uncommitted changes under the synced template paths — skipping .ai/ sync this session (catches up when rulesets is clean):" echo "$synced_dirty" | sed 's/^/ /' elif [ "$proj_behind" -eq 1 ]; then echo "project branch is behind upstream — skipping .ai/ sync this session (templates never land on a stale baseline; the sync runs once the branch is current)" else rsync -a "$rs/claude-templates/.ai/protocols.org" .ai/protocols.org rsync -a --delete "$rs/claude-templates/.ai/workflows/" .ai/workflows/ # Running rulesets' own pytest leaves these in the canonical scripts/tests/, # and rsync -a copies by disk presence regardless of .gitignore, so without # the excludes every project's tree collects machine-specific cache files. rsync -a --delete --exclude='__pycache__' --exclude='.pytest_cache' --exclude='*.pyc' \ "$rs/claude-templates/.ai/scripts/" .ai/scripts/ # Known false-success path, inherited and characterized rather than fixed # here: this line prints unconditionally, so a run where all three rsyncs # failed (an absent canonical source, say) still reports a successful sync. # A last-synced manifest must not be written from this branch as it stands — # it would stamp success onto a sync that did nothing. echo ".ai/ synced from templates" fi