1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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
|