aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ai/workflows/startup.org26
-rw-r--r--claude-templates/.ai/workflows/startup.org26
2 files changed, 42 insertions, 10 deletions
diff --git a/.ai/workflows/startup.org b/.ai/workflows/startup.org
index 47a77c8..4e3d417 100644
--- a/.ai/workflows/startup.org
+++ b/.ai/workflows/startup.org
@@ -126,7 +126,7 @@ These calls have no dependencies on each other. Issue them all together in one m
sc=$(.ai/scripts/session-context-path 2>/dev/null || echo .ai/session-context.org)
[ -e "$sc" ] && echo "present: $sc" || echo "absent: $sc"
#+end_src
-3. *Sync =.ai/= from templates — but only when the synced source paths in rulesets are clean.* Guard the three rsyncs behind a check that =claude-templates/.ai/{protocols.org,workflows/,scripts/}= have no uncommitted changes. Otherwise Phase A copies in-flight rulesets WIP (tracked edits or new untracked files) into this project's =.ai/workflows/= and =.ai/scripts/=, where it shows up as drift the user didn't author. Skipping once is cheap — the next session with rulesets clean catches up. The check is scoped to the synced paths, so unrelated rulesets dirt (a stray =session-context.org=, scratch files) doesn't needlessly block the sync.
+3. *Sync =.ai/= from templates — but only when the synced source paths in rulesets are clean.* Guard the three rsyncs behind a check that =claude-templates/.ai/{protocols.org,workflows/,scripts/}= have no uncommitted changes. Otherwise Phase A copies in-flight rulesets WIP (tracked edits or new untracked files) into this project's =.ai/workflows/= and =.ai/scripts/=, where it shows up as drift the user didn't author. Skipping once is cheap — the next session with rulesets clean catches up. The check is scoped to the synced paths, so unrelated rulesets dirt (a stray =session-context.org=, scratch files) doesn't needlessly block the sync. A second guard skips the same rsyncs when the *project* branch is behind its upstream (=git rev-list --left-right --count @{u}...HEAD= with =behind > 0=): syncing templates onto a stale committed =.ai/= baseline measures the diff against old content, so it comes out huge and conflicts when the branch later reconciles to upstream, whose history already carries the newer templates. It composes with the rulesets-clean guard — a stable rulesets source and a current project branch are both required before the sync runs.
#+begin_src bash
rs="$HOME/code/rulesets"
@@ -134,15 +134,30 @@ These calls have no dependencies on each other. Issue them all together in one m
claude-templates/.ai/protocols.org \
claude-templates/.ai/workflows/ \
claude-templates/.ai/scripts/ 2>/dev/null)
- if [ -z "$synced_dirty" ]; then
+ # Skip the sync when the project branch hasn't reached its upstream. Syncing
+ # templates onto a behind baseline measures the diff against stale committed
+ # .ai/, producing confusing drift that conflicts when the branch reconciles —
+ # the newer .ai/ is already in upstream. 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, 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/
rsync -a --delete --exclude='__pycache__' --exclude='.pytest_cache' --exclude='*.pyc' \
"$rs/claude-templates/.ai/scripts/" .ai/scripts/
echo ".ai/ synced from templates"
- else
- 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/^/ /'
fi
#+end_src
@@ -193,6 +208,7 @@ Notes on the rsync commands:
- protocols.org is a single file, no =--delete= needed.
- The =scripts/= sync excludes Python build artifacts (=__pycache__/=, =.pytest_cache/=, =*.pyc=). Running rulesets' own pytest leaves these in =claude-templates/.ai/scripts/tests/=, and =rsync -a= copies by disk presence regardless of =.gitignore=, so without the excludes every consuming project's tree gets polluted with machine-specific cache files. The excludes also protect existing dest copies from =--delete= cleanup, so a project that already received the cache must remove it once by hand.
- The sync is guarded to skip when rulesets has uncommitted changes under the synced source paths. =rsync -a --delete= copies the working tree by disk presence, so without the guard a downstream session started while rulesets had in-flight WIP would pull that WIP into its =.ai/workflows/= and =.ai/scripts/=, surfacing as drift the user never authored (and tempting a fake "chore: sync .ai tooling" commit). The guard is scoped to the synced paths, not the whole repo, so unrelated rulesets dirt doesn't block the sync. From the jr-estate handoff 2026-05-29.
+- The sync is also guarded to skip when the *project* branch is behind its upstream (=proj_behind=). Phase A.0 correctly declines to fast-forward a diverged or behind-and-dirty branch, but the rsync would then land templates on the stale committed =.ai/= baseline — a huge diff measured against old content that conflicts once the branch reconciles to upstream's newer templates. Skipping is safe: the sync runs next session once the branch is current. Not an auto-discard — startup never =git checkout=s drift away, because a legitimate local stopgap in a synced file is indistinguishable from accidental drift by content alone (home reverted an intentional =flashcard-to-anki.py= fix this way on 2026-06-22). Prevention is safe; blind cleanup-after is not. Phase C's template-sync-churn safety net still surfaces any pre-existing dirt for a human decision. From the home handoff 2026-07-04.
- The sync touches only =protocols.org=, =workflows/=, and =scripts/=. The project-owned dirs =project-workflows/= and =project-scripts/= are deliberately *outside* the synced set, so a project's own workflows and scripts survive startup. This is why a project script that a workflow imports must live in =.ai/project-scripts/=, never =.ai/scripts/= — the latter is wiped to match the template by =--delete= on every startup. Naming: a script imported as a Python module needs an importable name (underscores, e.g. =zlibrary_api.py=); a CLI-invoked script can stay kebab-case like the template tooling (=cmail-action.py=).
Rationale: Every call in Phase A is read-only or writes to a distinct path. Running them sequentially wastes round-trips; running them in parallel gives Claude the complete starting picture in one round-trip.
diff --git a/claude-templates/.ai/workflows/startup.org b/claude-templates/.ai/workflows/startup.org
index 47a77c8..4e3d417 100644
--- a/claude-templates/.ai/workflows/startup.org
+++ b/claude-templates/.ai/workflows/startup.org
@@ -126,7 +126,7 @@ These calls have no dependencies on each other. Issue them all together in one m
sc=$(.ai/scripts/session-context-path 2>/dev/null || echo .ai/session-context.org)
[ -e "$sc" ] && echo "present: $sc" || echo "absent: $sc"
#+end_src
-3. *Sync =.ai/= from templates — but only when the synced source paths in rulesets are clean.* Guard the three rsyncs behind a check that =claude-templates/.ai/{protocols.org,workflows/,scripts/}= have no uncommitted changes. Otherwise Phase A copies in-flight rulesets WIP (tracked edits or new untracked files) into this project's =.ai/workflows/= and =.ai/scripts/=, where it shows up as drift the user didn't author. Skipping once is cheap — the next session with rulesets clean catches up. The check is scoped to the synced paths, so unrelated rulesets dirt (a stray =session-context.org=, scratch files) doesn't needlessly block the sync.
+3. *Sync =.ai/= from templates — but only when the synced source paths in rulesets are clean.* Guard the three rsyncs behind a check that =claude-templates/.ai/{protocols.org,workflows/,scripts/}= have no uncommitted changes. Otherwise Phase A copies in-flight rulesets WIP (tracked edits or new untracked files) into this project's =.ai/workflows/= and =.ai/scripts/=, where it shows up as drift the user didn't author. Skipping once is cheap — the next session with rulesets clean catches up. The check is scoped to the synced paths, so unrelated rulesets dirt (a stray =session-context.org=, scratch files) doesn't needlessly block the sync. A second guard skips the same rsyncs when the *project* branch is behind its upstream (=git rev-list --left-right --count @{u}...HEAD= with =behind > 0=): syncing templates onto a stale committed =.ai/= baseline measures the diff against old content, so it comes out huge and conflicts when the branch later reconciles to upstream, whose history already carries the newer templates. It composes with the rulesets-clean guard — a stable rulesets source and a current project branch are both required before the sync runs.
#+begin_src bash
rs="$HOME/code/rulesets"
@@ -134,15 +134,30 @@ These calls have no dependencies on each other. Issue them all together in one m
claude-templates/.ai/protocols.org \
claude-templates/.ai/workflows/ \
claude-templates/.ai/scripts/ 2>/dev/null)
- if [ -z "$synced_dirty" ]; then
+ # Skip the sync when the project branch hasn't reached its upstream. Syncing
+ # templates onto a behind baseline measures the diff against stale committed
+ # .ai/, producing confusing drift that conflicts when the branch reconciles —
+ # the newer .ai/ is already in upstream. 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, 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/
rsync -a --delete --exclude='__pycache__' --exclude='.pytest_cache' --exclude='*.pyc' \
"$rs/claude-templates/.ai/scripts/" .ai/scripts/
echo ".ai/ synced from templates"
- else
- 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/^/ /'
fi
#+end_src
@@ -193,6 +208,7 @@ Notes on the rsync commands:
- protocols.org is a single file, no =--delete= needed.
- The =scripts/= sync excludes Python build artifacts (=__pycache__/=, =.pytest_cache/=, =*.pyc=). Running rulesets' own pytest leaves these in =claude-templates/.ai/scripts/tests/=, and =rsync -a= copies by disk presence regardless of =.gitignore=, so without the excludes every consuming project's tree gets polluted with machine-specific cache files. The excludes also protect existing dest copies from =--delete= cleanup, so a project that already received the cache must remove it once by hand.
- The sync is guarded to skip when rulesets has uncommitted changes under the synced source paths. =rsync -a --delete= copies the working tree by disk presence, so without the guard a downstream session started while rulesets had in-flight WIP would pull that WIP into its =.ai/workflows/= and =.ai/scripts/=, surfacing as drift the user never authored (and tempting a fake "chore: sync .ai tooling" commit). The guard is scoped to the synced paths, not the whole repo, so unrelated rulesets dirt doesn't block the sync. From the jr-estate handoff 2026-05-29.
+- The sync is also guarded to skip when the *project* branch is behind its upstream (=proj_behind=). Phase A.0 correctly declines to fast-forward a diverged or behind-and-dirty branch, but the rsync would then land templates on the stale committed =.ai/= baseline — a huge diff measured against old content that conflicts once the branch reconciles to upstream's newer templates. Skipping is safe: the sync runs next session once the branch is current. Not an auto-discard — startup never =git checkout=s drift away, because a legitimate local stopgap in a synced file is indistinguishable from accidental drift by content alone (home reverted an intentional =flashcard-to-anki.py= fix this way on 2026-06-22). Prevention is safe; blind cleanup-after is not. Phase C's template-sync-churn safety net still surfaces any pre-existing dirt for a human decision. From the home handoff 2026-07-04.
- The sync touches only =protocols.org=, =workflows/=, and =scripts/=. The project-owned dirs =project-workflows/= and =project-scripts/= are deliberately *outside* the synced set, so a project's own workflows and scripts survive startup. This is why a project script that a workflow imports must live in =.ai/project-scripts/=, never =.ai/scripts/= — the latter is wiped to match the template by =--delete= on every startup. Naming: a script imported as a Python module needs an importable name (underscores, e.g. =zlibrary_api.py=); a CLI-invoked script can stay kebab-case like the template tooling (=cmail-action.py=).
Rationale: Every call in Phase A is read-only or writes to a distinct path. Running them sequentially wastes round-trips; running them in parallel gives Claude the complete starting picture in one round-trip.