aboutsummaryrefslogtreecommitdiff
path: root/claude-templates/.ai/workflows
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-31 06:07:34 -0500
committerCraig Jennings <c@cjennings.net>2026-07-31 06:07:34 -0500
commitf57105773366aebdceeafdb51dc3584c42b0e4e4 (patch)
treee2291c94586bb962ab732277494958d914eb1b8b /claude-templates/.ai/workflows
parent84bd121683add4440a3652b12b70e6e2d45f5384 (diff)
downloadrulesets-f57105773366aebdceeafdb51dc3584c42b0e4e4.tar.gz
rulesets-f57105773366aebdceeafdb51dc3584c42b0e4e4.zip
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.
Diffstat (limited to 'claude-templates/.ai/workflows')
-rw-r--r--claude-templates/.ai/workflows/startup.org43
1 files changed, 14 insertions, 29 deletions
diff --git a/claude-templates/.ai/workflows/startup.org b/claude-templates/.ai/workflows/startup.org
index 2262eea..bc89256 100644
--- a/claude-templates/.ai/workflows/startup.org
+++ b/claude-templates/.ai/workflows/startup.org
@@ -137,39 +137,24 @@ These calls have no dependencies on each other. Issue them all together in one m
#+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. 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"
- synced_dirty=$(cd "$rs" && git status --porcelain -- \
- claude-templates/.ai/protocols.org \
- claude-templates/.ai/workflows/ \
- claude-templates/.ai/scripts/ 2>/dev/null)
- # 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
+ The logic lives in =.ai/scripts/sync-templates=, not inline here. It was extracted 2026-07-31 after an uncommitted edit in rulesets silently blocked all three rsyncs for a full day — five workflow files went stale in one downstream project alone, with nothing anywhere reporting it. A mechanism that distributes correctness fixes to every project needs tests, and inline bash in an org file cannot have them. The behavior is unchanged by the extraction (verified differentially, output and resulting tree both byte-identical); the guards it applies are described below.
- 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)"
+ #+begin_src bash
+ if [ -x .ai/scripts/sync-templates ]; then
+ .ai/scripts/sync-templates
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"
+ echo "sync-templates not present — .ai/ sync SKIPPED and cannot self-heal; recover with: bash ~/code/rulesets/scripts/audit.sh --apply --force"
fi
#+end_src
+ The fallback should never fire in the ordinary rollout. A project still on the pre-extraction startup.org runs the old inline block this session, which delivers both the script and this file together, and the next session finds the script in place. It covers only the split case — the =workflows/= rsync landing while the =scripts/= one didn't — where a project would otherwise hold this file with no script to call.
+
+ *That state does not self-heal, which is why the message names a command.* The fallback runs /instead of/ the sync, so there is no later sync to deliver the missing script: the project would announce one line per session forever while its templates froze. Recovery is out-of-band, via =scripts/audit.sh --apply --force= in rulesets, which rsyncs =scripts/= directly. =--force= is there because audit skips a tracked project holding uncommitted =.ai/= changes, which a project wedged across several sessions is likely to be.
+
+ One caveat on that recovery, worth knowing before running it: audit's =scripts/= rsync carries none of the =__pycache__= / =.pytest_cache= / =*.pyc= excludes this sync does, so it can deposit python cache artifacts that then need removing by hand. Tracked separately; it is a pre-existing gap in audit rather than something this path introduced.
+
+ Announcing the skip loudly with its remedy is the point; a silent skip is the exact failure this extraction exists to stop.
+
4. =\ls -t .ai/sessions/ 2>/dev/null | head -5= — list 5 most recent session files. The backslash bypasses any =ls= alias in the user's profile. Without it, bare =ls -t= silently returns no output under =exa= (a common =ls= replacement) — which makes a sessions directory full of files look empty, and the agent then skips Phase B step 2.
5. =\ls -la inbox/ 2>/dev/null= — inventory the inbox. Same reason for the backslash escape, applied uniformly across the Phase A =ls= calls.
6. Read =.ai/notes.org= — Project-Specific Context, Active Reminders, Pending Decisions sections (skip About This File).
@@ -213,7 +198,7 @@ These calls have no dependencies on each other. Issue them all together in one m
Fleet descriptions ("the fleet is ratio and velox") and runtime derivations ("run =uname -n= to find the hostname") don't match — only current-identity assertions do. Fixture-verified under bash and zsh.
-Notes on the rsync commands:
+Notes on what =sync-templates= does (the rsync behavior it carries):
- Trailing slashes on both source and destination matter — they tell rsync to sync /contents/ rather than nest a directory inside.
- =--delete= on the directory syncs lets retired template files actually disappear from each project on next startup.
- protocols.org is a single file, no =--delete= needed.