From f57105773366aebdceeafdb51dc3584c42b0e4e4 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Fri, 31 Jul 2026 06:07:34 -0500 Subject: refactor(startup): extract the template sync into a tested script The .ai/ sync is how every workflow, protocol and script change reaches every project, and until now it was untested inline bash in startup.org. Yesterday it failed in two ways at once and nothing anywhere reported either. An uncommitted edit of mine under the synced paths skipped all three rsyncs for a full day, so no project got anything. Five workflow files went stale in .emacs.d alone. One of them was a telegram plugin whose staleness crashed telega-server on every triage run, which is the only reason anyone noticed. Work found the second failure. The --delete rsync silently reverts a local patch to a rulesets-owned file, so they hit a bug, fixed it, restarted, and came back running the broken version. Their own log still said it was patched. I want both guards changed. Neither change is safe to make against inline bash that runs in every session, so this commit only moves the logic and pins what it does. sync-templates is a faithful extraction, rough edges included. I verified it differentially rather than by reading: the old block and the new script over the same fixtures produce identical stdout, exit status and resulting tree. Two of the fifteen characterization tests pin the failures above. "ONE dirty file blocks ALL THREE rsyncs" is the blast radius in one assertion, and the narrowing lands by turning it red on purpose. "a locally-edited template is silently overwritten, with no record kept" pins work's regression, asserting the output is indistinguishable from an ordinary sync and that no warning exists anywhere. A third test pins a false-success path I found but deliberately did not fix: the success line prints unconditionally, so a run whose rsyncs all failed still reports a clean sync. That matters next rather than now, because a last-synced manifest written from that branch would stamp success onto a sync that did nothing. The script carries the warning at the line itself. startup.org keeps a fallback for the case where it holds this file but the script is missing. It announces the skip and names the recovery command, because that state cannot heal itself: the fallback runs instead of the sync, so no later sync can deliver what is missing. --- claude-templates/.ai/scripts/sync-templates | 75 +++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100755 claude-templates/.ai/scripts/sync-templates (limited to 'claude-templates/.ai/scripts/sync-templates') diff --git a/claude-templates/.ai/scripts/sync-templates b/claude-templates/.ai/scripts/sync-templates new file mode 100755 index 0000000..c4db212 --- /dev/null +++ b/claude-templates/.ai/scripts/sync-templates @@ -0,0 +1,75 @@ +#!/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 -- cgit v1.2.3