aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-15 18:21:12 -0500
committerCraig Jennings <c@cjennings.net>2026-05-15 18:21:12 -0500
commit23a77929b4a67fd80c99de1a925b94f1b8b7ac74 (patch)
tree5261cb499ac5ed51c7b45c6d64e81ba7839ea7f8
parentb7d5ba97f08e668f49c45642035820d70099d785 (diff)
downloadrulesets-23a77929b4a67fd80c99de1a925b94f1b8b7ac74.tar.gz
rulesets-23a77929b4a67fd80c99de1a925b94f1b8b7ac74.zip
chore(ai): wrap fold-claude-templates + audit/install-ai + ratio migration
-rw-r--r--.ai/sessions/2026-05-15-18-19-consolidate-ai-template-infra.org179
-rw-r--r--todo.org348
2 files changed, 352 insertions, 175 deletions
diff --git a/.ai/sessions/2026-05-15-18-19-consolidate-ai-template-infra.org b/.ai/sessions/2026-05-15-18-19-consolidate-ai-template-infra.org
new file mode 100644
index 0000000..0baac74
--- /dev/null
+++ b/.ai/sessions/2026-05-15-18-19-consolidate-ai-template-infra.org
@@ -0,0 +1,179 @@
+#+TITLE: Session Context — consolidate .ai/ template infrastructure epic
+#+AUTHOR: Craig Jennings & Claude
+#+DATE: 2026-05-15
+
+* Summary
+
+** Active Goal
+
+Started as a reconcile + cross-project =.ai/= sweep. Surfaced a canonical-source rule violation (the date-coverage scan landed in rulesets without going through claude-templates first; the startup rsync was about to silently revert it). Fix that, sweep 21 projects, then escalate into the full /Consolidate =.ai/= template infrastructure/ epic: fold claude-templates into rulesets via =git subtree=, build =make audit= + =make install-ai= + =make catchup-machine= targets, encode follow-up filing rules into =/start-work=, migrate ratio over ssh, and delete both machines' standalone claude-templates checkouts.
+
+** Decisions
+
+- *Keep the date-coverage scan in wrap-up.* It was added to rulesets in =372fb76= without landing in claude-templates first. Fix by adding the subsection to claude-templates (canonical), re-rsync into rulesets, commit consolidated delta.
+- *Subtree merge with =--squash=*. Cleaner =git log= than full 84-commit history merge; standalone history persists on =cjennings.net:git/claude-templates.git=. Reversible if you want unsquashed.
+- *Bin/ai launcher install folds into rulesets =Makefile=.* Existing user symlinks at =~/.local/bin/ai= pointing at the old path get auto-relinked to the new canonical (=~/code/rulesets/claude-templates/bin/ai=).
+- *=make audit= uses =diff -rq= for content comparison*, not =rsync --itemize-changes= (which counts attribute-only drift as changes). Skips canonical source, the in-repo prefix, and the legacy standalone path.
+- *Default audit mode is report-only*; =APPLY=1= rsyncs drift; =FORCE=1= overrides the dirty-skip safety; =NO_DOCTOR=1= skips the doctor sub-invoke. Exit 1 if anything non-ok.
+- *=make install-ai= refuses on existing =.ai/=*; points the user at =make audit APPLY=1=. =TRACK=1= adds =.gitkeep= files; =GITIGNORE=1= appends =.ai/= to project =.gitignore=.
+- *Follow-ups discovered during an epic file as level-2 siblings of the parent*, not as descendants of any child. Stays visible after parent closure. Saved as project memory + encoded into =start-work.md= Phase 4 step 5 for cross-project effect.
+- *Force-apply ratio's 14 dirty tracked projects.* The "uncommitted" =.ai/= content was accumulated template-sync drift (one project showed +615 lines on =daily-prep.org= and 5 deleted workflow files), not project-specific work. Clobbering with current canonical IS the desired state.
+- *Delete the standalone =~/projects/claude-templates/= on both machines.* History persists in the subtree under =rulesets/claude-templates/= and at =cjennings.net:git/claude-templates.git=.
+
+** Data Collected / Findings
+
+- claude-templates was 4 commits ahead of rulesets' last sync (=bb2ab6f=, =6ac1f79=, =06cf882=, =039e2e8=).
+- =make doctor= caught a drifted symlink: =todo-format.md= was in the Makefile install list but never linked in =~/.claude/rules/=. Doctor went from 35/0/1 → 36/0/0 after =make install=.
+- All 21 non-rulesets =.ai/= projects on velox were clean before the morning sweep. After sweep: 7 gitignored converged silently; 14 tracked got uncommitted working-tree deltas.
+- =git subtree add --squash= produces two commits — a "Squashed 'claude-templates/' content from commit X" commit and a "Merge commit Y as 'claude-templates'" commit. Content under the prefix is byte-identical to the source.
+- Phase A.0 of startup needed structural changes for the fold: it now refreshes /rulesets/ (the parent repo), not /claude-templates/ (the source dir). Inside rulesets sessions the pull is redundant-but-harmless. Outside rulesets sessions it's the only mechanism that pulls template updates.
+- =make audit= run #1 surfaced false positives at 86 items/project (attribute-only drift via =rsync --itemize-changes=). Switching to =diff -rq= dropped the count to 3 items/project — the actual content drift (today's path-ref updates).
+- Ratio had wider per-project drift than velox: =clipper= showed 5 deleted workflow files + 615-line =daily-prep.org= addition, =jr-estate= 564-line drift. Accumulated template-sync gap from many sessions of partial syncs. Force-applying converged everything in one operation.
+- Memory dir at =~/.claude/projects/-home-cjennings-code-rulesets/memory/= didn't exist on velox (despite session notes referencing it). Created on first memory save this session.
+
+** Files Modified
+
+claude-templates (pushed to =origin/main=):
+- =f116888 docs(wrap-it-up): add date-coverage scan for undated [#A]/[#B] tasks= — restored the subsection that rulesets had added but claude-templates missed.
+
+rulesets (pushed to =origin/main=, in order):
+- =ee721ee chore(ai): sync scripts and workflows from claude-templates= — pulled in the 4 upstream claude-templates commits.
+- =2b471da docs(todo): start consolidate .ai/ template infra epic= — parent task at =todo.org:1774= with 5 children, marked DOING.
+- =69c5e4a + c1d4e3c= — =git subtree add --squash= of claude-templates content under =claude-templates/= prefix.
+- =3a4af17 docs(ai): point template references at in-repo claude-templates/= — path swap across protocols.org, startup.org, cross-agent-comms.org (canonical + in-project copies).
+- =2d645fc chore(make): fold bin/ai launcher install into rulesets= — new install/uninstall/list logic for =~/.local/bin/ai=. Includes "relink" case for users with stale symlinks.
+- =cd35f66 docs(todo): file Makefile redundancy as fold-epic sibling= — first follow-up filed as level-2 sibling of the parent epic.
+- =e0f6002 docs(todo): file start-work follow-up filing rule task= — captured Craig's meta-observation about /start-work needing explicit placement guidance.
+- =94782ee feat(make): add audit target for cross-project .ai/ drift detection= — =scripts/audit.sh= + Makefile target.
+- =d364cf2 feat(make): add install-ai target for bootstrapping .ai/ in fresh projects= — =scripts/install-ai.sh= + Makefile target.
+- =2eab96f docs(todo): close fold-epic test plan, file edge-case follow-up= — child 4 DONE + new sibling for untested edges (=--force= on dirty, missing =.ai/=, fzf-pick).
+- =9bef4ca feat(make): add catchup-machine target for cross-machine .ai/ sync= — =scripts/catchup-machine.sh= for ratio migration and any future machine.
+- =3e895de docs(todo): mark fold-epic children 1-3 done= — status flips.
+- =cea44a9 docs(start-work): encode follow-up filing placement rules= — siblings-for-epics rule now in =.claude/commands/start-work.md= Phase 4 step 5.
+- =b7d5ba9 docs(todo): close consolidate .ai/ template infra epic; ratio migrated= — parent + child 5 DONE.
+
+System-side (no commit):
+- =~/.claude/rules/todo-format.md= → new symlink created by =make install=.
+- =~/.local/bin/ai= → relinked from old standalone path to new in-repo canonical (both machines).
+- =~/projects/claude-templates/= deleted on velox AND ratio.
+- Project memory =feedback_followups_as_siblings.md= + MEMORY.md index created at =~/.claude/projects/-home-cjennings-code-rulesets/memory/=.
+
+Ratio machine state (via ssh):
+- =~/code/rulesets= pulled to current main.
+- 23 projects audited: 22 ok + 1 applied (=danneel= had 20 items drift).
+- =~/projects/claude-templates/= deleted.
+- =make doctor= 36/0/0.
+
+** Next Steps
+
+- =[#C]= sibling open: consolidate =rulesets/claude-templates/Makefile= (still has standalone install/uninstall/list/test-scripts targets that overlap with =rulesets/Makefile=).
+- =[#C]= sibling open: build a test harness for =make audit= + =make install-ai= edge cases (=--force= on dirty, missing =.ai/=, install-ai fzf-pick form).
+- Carryover still open from prior sessions: =DOING [#A]= memory-sync investigation (pending VERIFY on stow approach — adjacent to today's work, may want coordination), =[#A]= =/update-skills= skill, =[#A]= =create-documentation= skill, =[#A]= 2026-05-04 audit review pass.
+- =/lint-org= TODO at =todo.org:1292= is stale — the work shipped last session but the entry isn't marked DONE. Should be moved to Rulesets Resolved at next cleanup.
+
+* Session Log
+
+** Startup (15:13 CDT)
+
+Clean startup — previous session wrapped cleanly (no =session-context.org=), inbox empty, no cross-agent messages, no reminders, no pending decisions. Surfaced one drift: =todo.org:1292= still shows the =/lint-org= TODO as =TODO= even though the work shipped last session. Read the 5 most recent session Summaries. Offered top open A items.
+
+** Reconcile + canonical-source fix
+
+Craig asked to reconcile rulesets and make sure all =.ai/= packages have a current updated version. rulesets =0/0= against =origin/main= but Phase A startup rsync had brought in template-sync deltas — 3 modified files, 6 untracked new files. Mapped each to specific upstream commits. The =wrap-it-up.org= =-33= delta stood out: no claude-templates commit had ever touched the date-coverage scan, meaning rulesets =372fb76= added it without going through claude-templates first. Craig picked "keep the scan" (option 1).
+
+Executed the canonical-source fix: claude-templates =f116888 docs(wrap-it-up): add date-coverage scan= committed and pushed; re-rsync into rulesets dropped =wrap-it-up.org= out of the diff; consolidated rulesets =ee721ee chore(ai): sync scripts and workflows from claude-templates= committed and pushed.
+
+** 21-project sweep
+
+Surveyed 21 non-rulesets =.ai/=-using projects on disk. All clean before sweep. Ran =protocols.org=, =workflows/=, =scripts/= rsync into each. Post-sweep: 7 gitignored converged silently; 14 tracked got uncommitted working-tree deltas. Craig picked "leave as-is" (option 1) — each project commits at its own next wrap-up rather than a 14-repo commit cascade.
+
+** Install drift discovery
+
+Craig asked whether new skills and commands would be updated/symlinked. Inventoried: =~/.claude/commands= is a directory-level symlink (children auto-pick-up). Skills are individual symlinks. Ran =make doctor= → 35 ok, 0 warn, *1 fail*: =rule todo-format.md: not installed=. =make install= created the missing symlink. Doctor re-ran 36/0/0.
+
+** Architecture clarification
+
+Craig mentioned he thought we'd do =make install= against each =.ai/= project. Clarified: =make install= is machine-wide (single symlink set into =~/.claude/=). Per-project layer is the =.ai/= rsync, which is already done. He acknowledged the correction.
+
+** make audit design pass
+
+Conversation pivoted to "build =make audit= for next time." Walked through:
+- Clean-project verification path (5 checks per project, all silent).
+- =--apply= mode for fixing drift (vs report-only default).
+- Install-ai is a separate target — operating on projects without =.ai/= is a distinct concern from audit.
+- The =[#B]= fold and audit TODOs from 2026-05-09 already had partial designs; Craig wanted both built now along with new install-ai + test plan + ratio migration as children of a single epic.
+
+Created parent task =** TODO [#A] Consolidate .ai/ template infrastructure= at =todo.org:1774= with five level-3 children. Demoted the existing [#B] tasks to children, promoted to [#A]. Build order encoded in parent prose: fold first (others depend on the path), then audit + install-ai in parallel, then test, then ratio.
+
+** Epic Phase 0 + 1 + 2
+
+/start-work on the parent task. Pre-flight reconcile clean. Existence check confirmed all three problem-statements still hold: two-repo split exists, canonical-source rule violation exists today, ratio not migrated. Justify gate covered all 9 dimensions; estimated L (4-6 hours). Craig picked option 1 (push through all 5 children today).
+
+** Child 1: Fold claude-templates into rulesets
+
+Approach gate 2: subtree-merge with =--squash= for clean log; 3 path-reference files to update; bin/ai install needs folding into rulesets =Makefile=; commit decomposition planned for 3 commits.
+
+Implemented:
+- =git subtree add --prefix=claude-templates --squash ~/projects/claude-templates main= — landed cleanly, byte-identical content vs source.
+- 3 path-reference files updated (canonical + in-project copies, 6 files total). Phase A.0 of startup.org reworked: it now refreshes rulesets, not claude-templates.
+- bin/ai install folded into rulesets =Makefile= with auto-relink for existing stale symlinks. Verified by running =make install= — relinked Craig's =~/.local/bin/ai= from old to new path.
+
+Phase 5 verification: rsync from new path produces zero diff; Phase A.0 ran cleanly from inside =~/projects/homelab/= (outside-rulesets simulation); doctor 36/0/0; tests 341 green (296 pytest + 22 lint-org ERT + 23 todo-cleanup ERT). Surface follow-up: =claude-templates/Makefile= now partially redundant with rulesets =Makefile=.
+
+Gate 3 → push (5 commits including subtree merge pair).
+
+** Sibling task pattern emerges
+
+After surfacing the Makefile redundancy follow-up, Craig instructed that all follow-ups from this epic file as siblings of the parent (not as descendants of any child). Reason: siblings stay visible after parent closure; descendants get orphaned when their parent subtree archives. Saved as project memory at =~/.claude/projects/-home-cjennings-code-rulesets/memory/feedback_followups_as_siblings.md= for future sessions.
+
+Filed first sibling: =[#C] Consolidate claude-templates/Makefile after fold=.
+
+** Mid-flight: start-work follow-up rules
+
+Craig asked whether filing follow-ups should be part of /start-work itself. Phase 4 step 5 already says "file a ticket or todo.org entry" as a disposition, but doesn't specify /where/ to file. Filed =[#B] Encode follow-up filing rules into /start-work= as a top-level task. (Implemented this later in the session — see below.)
+
+** Child 2: Build =make audit=
+
+Wrote =scripts/audit.sh= matching =doctor.sh= conventions. First implementation used =rsync --dry-run --itemize-changes= for drift detection but counted 86 items/project — attribute-only drift (mtime, permissions). Switched to =diff -rq= for content comparison; drift count dropped to 3 items/project (the actual path-ref changes).
+
+Project discovery via =find -maxdepth 3 -type d -name .ai= under =~/code=, =~/projects=, =~/.emacs.d=. Skip list: rulesets root, in-repo canonical (=rulesets/claude-templates=), and legacy standalone (=~/projects/claude-templates=) during transition.
+
+Per-project flow: dir-exists check, git-tracked-vs-gitignored detection, dirty check (skip on dirty unless =--force=), drift count via three =diff -rq= calls, optional rsync apply, convergence verify, working-tree state report. Flags: =--apply=, =--force=, =--no-doctor=. Output mirrors doctor: per-project ok/drift/applied/skipped/FAIL with summary tally.
+
+Makefile target uses Make variables (=APPLY=1=, =FORCE=1=, =NO_DOCTOR=1=) rather than passing CLI flags through =$(MAKECMDGOALS)= (cleaner; doesn't need a catchall =%:= rule).
+
+Phase 5: tested in 3 modes (report-only, =APPLY=1=, idempotent re-run). Skipped destructive tests (=--force= on real dirty project, missing =.ai/= dir) — filed as sibling follow-up for a future test harness.
+
+Gate 3 → push.
+
+** Child 3: Build =make install-ai=
+
+Wrote =scripts/install-ai.sh=. Refuses if =PROJECT/.ai/= already exists (points to =make audit APPLY=1= for sync). Creates =.ai/= structure, rsyncs canonical content (=protocols.org=, =workflows/=, =scripts/=, =someday-maybe.org=), templates =notes.org= with project-name + date placeholder substitution.
+
+Tracking modes: =--track= adds =.gitkeep= files inside otherwise-empty =sessions/= / =references/= / =retrospectives/=; =--gitignore= appends =.ai/= to project =.gitignore=. Prompts interactively if neither flag set.
+
+Fzf-pick fallback when no =PROJECT= arg: walks =~/code/*= and =~/projects/*=, filters to git checkouts without existing =.ai/=.
+
+Phase 5: tested fresh install (=TRACK=1=, =GITIGNORE=1=), refusal-on-existing, placeholder substitution, =.gitkeep= creation. fzf-pick form not exercised (interactive) — covered by the same sibling test-harness follow-up.
+
+Gate 3 → push.
+
+** Child 4: Test plan
+
+Ran the validation battery: =make doctor= 36/0/0; pytest 296+1 skipped; lint-org ERT 22/22; audit clean (7 ok + 14 dirty-skip as expected); install-ai fresh + refusal both verified. Three destructive edge cases (=--force= on dirty, missing =.ai/=, install-ai fzf) deferred to the sibling test-harness task. Child 4 marked DONE.
+
+** Mid-flight again: implement start-work follow-up rule
+
+Came back to the earlier =[#B]= task. Edited =.claude/commands/start-work.md= Phase 4 step 5's "Disposition for each candidate": new "Where to file in todo.org" subsection spelling out siblings-for-epics and new-top-level-for-standalone. Marked the task DONE.
+
+** Child 5: Migrate ratio (the destructive convergence)
+
+Force-applied audit on velox first to converge the 14 dirty projects. All 21 velox projects now reference new canonical path. Deleted velox =~/projects/claude-templates/= entirely; verified ai launcher (symlink already relinked by earlier =make install=) still works.
+
+Then ratio via ssh. Pulled rulesets on ratio (15 commits behind), ran =bash scripts/catchup-machine.sh=. Ratio's 9 gitignored converged silently. 14 tracked skipped due to dirty =.ai/=. Spot-checked one (=clipper= showed 5 deleted workflow files + 615-line =daily-prep.org= addition — accumulated template-sync drift across many sessions); spot-checked three more (homelab 151 lines, work 87, jr-estate 564) — all template drift, no project-specific work. Force-applied on ratio: 22 ok + 1 applied (=danneel= had 20 items). All 23 ratio projects converged. Deleted ratio =~/projects/claude-templates/= via ssh. Verified ratio doctor 36/0/0.
+
+Marked child 5 DONE and parent epic DONE. Committed and pushed =b7d5ba9=.
+
+** Wrap-up
+
+Craig validated the audit design: "some of those projects haven't been updated in a while. this is why I wanted to create the audit Makefile target and run it across those projects." Picked option 1 (wrap up).
diff --git a/todo.org b/todo.org
index dca4329..e7da3e1 100644
--- a/todo.org
+++ b/todo.org
@@ -1771,181 +1771,6 @@ The four canonical rules (=commits=, =testing=, =verification=, =subagents=) are
The Elisp pair is the most suspicious — three repos using essentially the same rules. Audit: diff these across the projects, check for drift, then decide whether to canonicalize them under =~/code/rulesets/claude-rules/languages/<lang>/= and symlink, or leave them as project-local.
-** DONE [#B] Encode follow-up filing rules into =/start-work=
-CLOSED: [2026-05-15 Fri]
-
-Phase 4 step 5 of =/start-work= ("refactor audit") says any candidate that isn't fix-now must land in one of three buckets: fold-into-related-commit, separate =refactor:= commit, or "file a ticket or todo.org entry." The third disposition doesn't say *where* — which leaves the orchestrator picking a location ad-hoc. Result: follow-ups buried under children of an epic parent get orphaned when the parent closes, or follow-ups for standalone tasks scatter across the file with no convention.
-
-Proposed placement rule (already memorized for this project as =feedback_followups_as_siblings.md=, generalizing):
-
-- *Epic-style parent task* (level-2 with multiple level-3 children) → follow-ups file as level-2 *siblings* of the parent. Stays visible after parent closure.
-- *Standalone task* (level-2 with no children, or a level-3 inside another structure) → follow-up files as a new level-2 top-level entry in the same =* Open Work= section. Don't nest under the originating task.
-
-Both cases: include a "Triggered by: <date> <task or commit>" line so a future reader sees what surfaced it.
-
-Update =.claude/commands/start-work.md= Phase 4 step 5's "Disposition for each candidate" section to spell this out. Update any cross-references in =commits.md= or other files that touch the discipline.
-
-Triggered by: 2026-05-15 fold-epic session — Craig flagged the gap mid-flight after I'd surfaced a follow-up but hadn't filed it.
-
-** DONE [#A] Consolidate =.ai/= template infrastructure (fold + audit + install-ai + ratio) :feature:
-CLOSED: [2026-05-15 Fri]
-
-End-state: one repo (=rulesets=) is the single source of truth for =.ai/= template content. =make audit= verifies and applies drift across every =.ai/=-using project on the machine. =make install-ai= bootstraps new projects. Same setup propagated to ratio so both machines run the same way.
-
-Today (2026-05-15) the canonical-source rule got violated again: rulesets commit =372fb76= added a wrap-up subsection to =rulesets= without going through =claude-templates= first, and the next session's startup rsync was about to silently undo it. Two-repo coordination is the root cause; fold solves it.
-
-Build order: fold first (others depend on the new canonical path), then audit + install-ai in parallel, then test, then propagate to ratio.
-
-*** DONE [#A] Fold =claude-templates= into rulesets
-CLOSED: [2026-05-15 Fri]
-
-Two repos, one source of truth. =~/projects/claude-templates/= is the canonical =.ai/= template that gets rsync'd into every project at session start. Keeping it standalone means a second =git pull= in startup Phase A.0, a second remote to push to at wrap-up, and a split history any time a change touches both. Folding it into =rulesets/claude-templates/= gives one repo to clone on a fresh machine and one place to edit templates.
-
-**** Open design choices
-
-- *History.* =git subtree add --prefix=claude-templates ~/projects/claude-templates main= preserves the 84-commit history under the new prefix. Plain content copy (=cp -a= + =git add=) is simpler but loses history. Either is fine since the standalone repo stays archived on =cjennings.net=.
-- *Layout.* =rulesets/claude-templates/= mirrors the old repo name and sits next to =claude-rules/= cleanly. Alternative: absorb =.ai/= directly under a different name (=rulesets/.ai-template/= or similar). First option is clearer.
-- *bin/ai.* The standalone Makefile symlinks =$HOME/.local/bin/ai → bin/ai=. After the move, fold that into rulesets' Makefile as another install target.
-
-**** Mechanical steps
-
-1. Subtree-merge or copy =~/projects/claude-templates/= into =rulesets/claude-templates/=.
-2. Update 3 references in rulesets:
- - =.ai/protocols.org= line 163 — pointer in the "Let's run/do the X workflow" section.
- - =.ai/workflows/cross-agent-comms.org= line 8 — promotion-target path.
- - =.ai/workflows/startup.org= lines 22, 96-98 — Phase A.0 pull + Phase A rsync sources.
-3. Update Phase A.0 of =startup.org= to pull rulesets instead of claude-templates. Inside rulesets sessions, the existing project-repo pull already covers it. Outside rulesets (every other project's session), Phase A.0 needs an explicit =git pull= on =~/code/rulesets/= before the rsync — otherwise the templates will be stale.
-4. Replace =~/projects/claude-templates/= with a symlink to =~/code/rulesets/claude-templates/= for transition continuity.
-5. After every active project has had one session start (and rsync'd the new =startup.org=), drop the symlink and archive =cjennings.net:git/claude-templates.git=.
-
-**** Bootstrap gap
-
-Every project on the machine has a =.ai/workflows/startup.org= that rsyncs from =~/projects/claude-templates/=. Until each project's startup.org gets refreshed (which happens via the rsync itself), the old path needs to keep resolving. The symlink at step 4 is the bridge: old paths resolve into the new location, the rsync delivers the updated startup.org, next session uses the new path directly.
-
-*** DONE [#A] Add =make audit= — drift detector across all =.ai/=-using projects
-CLOSED: [2026-05-15 Fri]
-
-Companion to =make doctor= (single-machine scope, checks =~/.claude/=). =audit= is cross-project scope: walks every directory on the machine that has a =.ai/=, diffs the synced template files against the canonical source, and reports drift. =--apply= flag rsyncs the drift into the project's working tree (no auto-commit). Catches stale projects without forcing a session start in each one.
-
-**** Open design choices
-
-- *Scope.* Template-sync drift is the useful flavor: for each project, diff =.ai/protocols.org=, =.ai/workflows/=, =.ai/scripts/= against the canonical source.
-- *Source path.* Post-fold: =~/code/rulesets/claude-templates/.ai/=. Build =audit= against the new path from day one.
-- *Project discovery.* Walk =~/code/=, =~/projects/=, =~/.emacs.d/= up to depth 3 for any directory containing =.ai/=. Skip the canonical source itself.
-- *Default mode is report-only.* =--apply= triggers rsync; =--force= overrides the dirty-skip safety.
-
-**** Per-project flow (designed 2026-05-15)
-
-For each discovered project, in order:
-
-1. Verify =.ai/= exists (path probe). If missing → =FAIL=, skip, continue loop.
-2. Detect git tracking via =git check-ignore .ai/= → =tracked= or =gitignored=.
-3. Verify no uncommitted =.ai/= changes (=git status --porcelain .ai/=). Dirty → =WARN=, skip rsync unless =--force=.
-4. Verify content matches canonical via three =rsync -a --dry-run --itemize-changes= calls (=protocols.org=, =workflows/=, =scripts/=). Zero items = clean.
-5. Action (=--apply= only, drift detected): three =rsync -a [--delete]= calls.
-6. Verify rsync converged (re-run the dry-runs; zero now).
-7. Verify working-tree state after rsync (tracked projects). Report deltas. Do not auto-commit.
-8. Verify no unpushed =.ai/= commits (=git log @{u}..HEAD -- .ai/=). Informational only.
-
-**** Output format (mirrors =doctor=)
-
-#+begin_example
-Claude-templates source:
- ok rulesets/claude-templates is current (origin/main)
-
-Per-project .ai/ drift:
- ok ~/projects/work
- applied ~/projects/homelab 3 files changed
- skipped ~/code/winvm uncommitted .ai/ (use --force)
- ok ~/projects/clipper
-
-Summary: 18 ok, 3 applied, 1 skipped, 0 failed
-#+end_example
-
-Exit code: =0= if all clean, no skips, no failures. =1= otherwise.
-
-**** Why not extend =make doctor= instead
-
-=doctor= has a clean meaning today: "is this machine's =~/.claude/= consistent with rulesets?" Mixing in cross-project =.ai/= drift muddies the exit code. Keep them separate. =audit= can optionally invoke =doctor= as its last check since both ask "did the symlinks keep up with the source?". A future =make all-checks= can wrap both.
-
-*** DONE [#A] Add =make install-ai PROJECT=<path>= — bootstrap =.ai/= in a fresh project
-CLOSED: [2026-05-15 Fri]
-
-Separate target from =audit= because operating on projects that lack =.ai/= is a distinct action. The absence might be intentional, so =audit= skips them. Bootstrap is explicit opt-in.
-
-**** Flow
-
-1. Refuse if =.ai/= already exists in =PROJECT=. Message: "already installed; use =make audit --apply= to update."
-2. Verify =PROJECT= is a git checkout (warn if not — works without git, loses some lifecycle benefits).
-3. Create =PROJECT/.ai/= directory.
-4. Rsync canonical content: =protocols.org=, =workflows/=, =scripts/= (same three rsyncs as =audit=).
-5. Seed =PROJECT/.ai/notes.org= from a canonical template with project-name placeholder.
-6. Create empty =PROJECT/.ai/sessions/= (with =.gitkeep= for tracked projects).
-7. Track or gitignore =.ai/=? Default: ask. Flag: =--track= / =--gitignore=.
-8. Print next-steps banner: =make install-lang LANG=<lang> PROJECT=<path>=; open Claude Code in the project.
-
-**** Symmetry with existing install targets
-
-#+begin_example
-make install-lang LANG=python PROJECT=/path # language bundle (existing)
-make install-ai PROJECT=/path # .ai/ template (new)
-make install-lang # no args → fzf-pick
-make install-ai # no args → fzf-pick from
- # ~/projects/* + ~/code/* dirs
- # without an existing .ai/
-#+end_example
-
-*** DONE [#A] Test plan for audit + install-ai before propagating to ratio
-CLOSED: [2026-05-15 Fri]
-
-Test against the current state of this machine before pushing changes to ratio.
-
-**** =make audit= tests
-
-1. Dry-run report only (no =--apply=). Should show: claude-templates current; per-project drift; correct =ok=/=drift= classifications; summary line and exit code match.
-2. After the fold lands, every project should be reported as drift (their =startup.org= still points at the old path). Run =--apply= → rsync converges. Re-run audit → all =ok=.
-3. Manually edit one =.ai/workflows/foo.org= in a tracked project. Re-run audit → should report =skipped: uncommitted .ai/=. Run =--apply --force= → rsync clobbers the edit. Verify the edit is gone.
-4. Manually delete one =.ai/= dir. Re-run audit → =FAIL: .ai/ missing=. Loop continues.
-5. Idempotency: =--apply= twice in a row converges to all =ok= on the second pass.
-
-**** =make install-ai= tests
-
-1. Create =/tmp/test-fresh-project= as a git repo. Run =make install-ai PROJECT=/tmp/test-fresh-project=. Verify =.ai/= structure matches canonical, =notes.org= has placeholder, =sessions/= exists.
-2. Run =make install-ai PROJECT=/tmp/test-fresh-project= again → should refuse (=.ai/= already exists).
-3. Open Claude Code in the new project. Startup workflow runs cleanly (Phase A.0 + Phase A rsync should be a no-op since the install just ran).
-4. fzf form: =make install-ai= with no args. Lists candidate dirs (=~/projects/*=, =~/code/*= without =.ai/=).
-
-**** Pass criteria
-
-- =audit= behavior matches the per-project flow spec for every classification path.
-- =install-ai= produces a project indistinguishable from one that's been running sessions for a while.
-- =make doctor= still passes 36/0/0 after all the work.
-- =make test= (pytest + ERT) passes.
-
-*** DONE [#A] Migrate projects on ratio (second machine)
-CLOSED: [2026-05-15 Fri]
-
-After local fold + audit + install-ai are working, propagate to ratio.
-
-**** Steps
-
-1. On ratio: =git -C ~/code/rulesets pull= — picks up the folded =claude-templates/= subdir and updated =Makefile= targets.
-2. On ratio: archive or =mv= the standalone =~/projects/claude-templates/= aside, replace with symlink to =~/code/rulesets/claude-templates/= (same bridge mechanic as local).
-3. On ratio: =make audit= → see drift across ratio's projects.
-4. On ratio: =make audit --apply= → rsync into each tracked/gitignored project. Surface projects with uncommitted =.ai/= drift for manual handling.
-5. On ratio: =make doctor= → catch any =~/.claude/= install drift (likely some, since ratio hasn't seen recent rulesets updates).
-6. Verify by opening Claude Code in a few ratio projects. Startup should be a no-op or near-zero rsync.
-
-**** Known unknowns
-
-- Ratio may have its own project list overlapping with this machine's but not identical. =audit= discovers projects via the walk, so this is automatic.
-- Ratio might have uncommitted =.ai/= work in some projects that this machine doesn't. =audit= surfaces them; handle case-by-case.
-- If anything goes wrong, ratio's archived =~/projects/claude-templates/= is the safety net — restore the symlink target and re-run audit.
-
-**** Adjacent: cross-machine memory sync
-
-The =[#A] DOING= memory-sync investigation (todo.org:10) is adjacent. Both involve "make my Claude setup portable across machines." Coordinate so the memory-sync stow approach (if approved) doesn't conflict with this fold's symlink mechanics.
-
** TODO [#C] Test harness for =make audit= + =make install-ai= edge cases :test:
Three edge cases from the fold-epic test plan were not exercised because they're destructive on real projects:
@@ -2134,3 +1959,176 @@ Origin: came up while scrubbing a project's todo.org on 2026-05-11 — moving a
Built and shipped 2026-05-11: =--archive-done= added to =.ai/scripts/todo-cleanup.el= test-first; 13-test ERT suite (=tests/test-todo-cleanup.el=) + realistic synthetic fixture (=tests/fixtures/todo-sample.org=), wired into =make test= / =make test-scripts= alongside pytest. The CLI dispatch moved into =tc-main= behind a guard so the suite can =require= the file without firing it. Section matching is case-insensitive and tolerates the =<Project> Open Work= / =<Project> Resolved= naming variants. Opt-in only — not wired into the wrap-up flow. Source of truth is =~/projects/claude-templates/=; rsync'd into this repo.
+** DONE [#B] Encode follow-up filing rules into =/start-work=
+CLOSED: [2026-05-15 Fri]
+
+Phase 4 step 5 of =/start-work= ("refactor audit") says any candidate that isn't fix-now must land in one of three buckets: fold-into-related-commit, separate =refactor:= commit, or "file a ticket or todo.org entry." The third disposition doesn't say *where* — which leaves the orchestrator picking a location ad-hoc. Result: follow-ups buried under children of an epic parent get orphaned when the parent closes, or follow-ups for standalone tasks scatter across the file with no convention.
+
+Proposed placement rule (already memorized for this project as =feedback_followups_as_siblings.md=, generalizing):
+
+- *Epic-style parent task* (level-2 with multiple level-3 children) → follow-ups file as level-2 *siblings* of the parent. Stays visible after parent closure.
+- *Standalone task* (level-2 with no children, or a level-3 inside another structure) → follow-up files as a new level-2 top-level entry in the same =* Open Work= section. Don't nest under the originating task.
+
+Both cases: include a "Triggered by: <date> <task or commit>" line so a future reader sees what surfaced it.
+
+Update =.claude/commands/start-work.md= Phase 4 step 5's "Disposition for each candidate" section to spell this out. Update any cross-references in =commits.md= or other files that touch the discipline.
+
+Triggered by: 2026-05-15 fold-epic session — Craig flagged the gap mid-flight after I'd surfaced a follow-up but hadn't filed it.
+** DONE [#A] Consolidate =.ai/= template infrastructure (fold + audit + install-ai + ratio) :feature:
+CLOSED: [2026-05-15 Fri]
+
+End-state: one repo (=rulesets=) is the single source of truth for =.ai/= template content. =make audit= verifies and applies drift across every =.ai/=-using project on the machine. =make install-ai= bootstraps new projects. Same setup propagated to ratio so both machines run the same way.
+
+Today (2026-05-15) the canonical-source rule got violated again: rulesets commit =372fb76= added a wrap-up subsection to =rulesets= without going through =claude-templates= first, and the next session's startup rsync was about to silently undo it. Two-repo coordination is the root cause; fold solves it.
+
+Build order: fold first (others depend on the new canonical path), then audit + install-ai in parallel, then test, then propagate to ratio.
+
+*** DONE [#A] Fold =claude-templates= into rulesets
+CLOSED: [2026-05-15 Fri]
+
+Two repos, one source of truth. =~/projects/claude-templates/= is the canonical =.ai/= template that gets rsync'd into every project at session start. Keeping it standalone means a second =git pull= in startup Phase A.0, a second remote to push to at wrap-up, and a split history any time a change touches both. Folding it into =rulesets/claude-templates/= gives one repo to clone on a fresh machine and one place to edit templates.
+
+**** Open design choices
+
+- *History.* =git subtree add --prefix=claude-templates ~/projects/claude-templates main= preserves the 84-commit history under the new prefix. Plain content copy (=cp -a= + =git add=) is simpler but loses history. Either is fine since the standalone repo stays archived on =cjennings.net=.
+- *Layout.* =rulesets/claude-templates/= mirrors the old repo name and sits next to =claude-rules/= cleanly. Alternative: absorb =.ai/= directly under a different name (=rulesets/.ai-template/= or similar). First option is clearer.
+- *bin/ai.* The standalone Makefile symlinks =$HOME/.local/bin/ai → bin/ai=. After the move, fold that into rulesets' Makefile as another install target.
+
+**** Mechanical steps
+
+1. Subtree-merge or copy =~/projects/claude-templates/= into =rulesets/claude-templates/=.
+2. Update 3 references in rulesets:
+ - =.ai/protocols.org= line 163 — pointer in the "Let's run/do the X workflow" section.
+ - =.ai/workflows/cross-agent-comms.org= line 8 — promotion-target path.
+ - =.ai/workflows/startup.org= lines 22, 96-98 — Phase A.0 pull + Phase A rsync sources.
+3. Update Phase A.0 of =startup.org= to pull rulesets instead of claude-templates. Inside rulesets sessions, the existing project-repo pull already covers it. Outside rulesets (every other project's session), Phase A.0 needs an explicit =git pull= on =~/code/rulesets/= before the rsync — otherwise the templates will be stale.
+4. Replace =~/projects/claude-templates/= with a symlink to =~/code/rulesets/claude-templates/= for transition continuity.
+5. After every active project has had one session start (and rsync'd the new =startup.org=), drop the symlink and archive =cjennings.net:git/claude-templates.git=.
+
+**** Bootstrap gap
+
+Every project on the machine has a =.ai/workflows/startup.org= that rsyncs from =~/projects/claude-templates/=. Until each project's startup.org gets refreshed (which happens via the rsync itself), the old path needs to keep resolving. The symlink at step 4 is the bridge: old paths resolve into the new location, the rsync delivers the updated startup.org, next session uses the new path directly.
+
+*** DONE [#A] Add =make audit= — drift detector across all =.ai/=-using projects
+CLOSED: [2026-05-15 Fri]
+
+Companion to =make doctor= (single-machine scope, checks =~/.claude/=). =audit= is cross-project scope: walks every directory on the machine that has a =.ai/=, diffs the synced template files against the canonical source, and reports drift. =--apply= flag rsyncs the drift into the project's working tree (no auto-commit). Catches stale projects without forcing a session start in each one.
+
+**** Open design choices
+
+- *Scope.* Template-sync drift is the useful flavor: for each project, diff =.ai/protocols.org=, =.ai/workflows/=, =.ai/scripts/= against the canonical source.
+- *Source path.* Post-fold: =~/code/rulesets/claude-templates/.ai/=. Build =audit= against the new path from day one.
+- *Project discovery.* Walk =~/code/=, =~/projects/=, =~/.emacs.d/= up to depth 3 for any directory containing =.ai/=. Skip the canonical source itself.
+- *Default mode is report-only.* =--apply= triggers rsync; =--force= overrides the dirty-skip safety.
+
+**** Per-project flow (designed 2026-05-15)
+
+For each discovered project, in order:
+
+1. Verify =.ai/= exists (path probe). If missing → =FAIL=, skip, continue loop.
+2. Detect git tracking via =git check-ignore .ai/= → =tracked= or =gitignored=.
+3. Verify no uncommitted =.ai/= changes (=git status --porcelain .ai/=). Dirty → =WARN=, skip rsync unless =--force=.
+4. Verify content matches canonical via three =rsync -a --dry-run --itemize-changes= calls (=protocols.org=, =workflows/=, =scripts/=). Zero items = clean.
+5. Action (=--apply= only, drift detected): three =rsync -a [--delete]= calls.
+6. Verify rsync converged (re-run the dry-runs; zero now).
+7. Verify working-tree state after rsync (tracked projects). Report deltas. Do not auto-commit.
+8. Verify no unpushed =.ai/= commits (=git log @{u}..HEAD -- .ai/=). Informational only.
+
+**** Output format (mirrors =doctor=)
+
+#+begin_example
+Claude-templates source:
+ ok rulesets/claude-templates is current (origin/main)
+
+Per-project .ai/ drift:
+ ok ~/projects/work
+ applied ~/projects/homelab 3 files changed
+ skipped ~/code/winvm uncommitted .ai/ (use --force)
+ ok ~/projects/clipper
+
+Summary: 18 ok, 3 applied, 1 skipped, 0 failed
+#+end_example
+
+Exit code: =0= if all clean, no skips, no failures. =1= otherwise.
+
+**** Why not extend =make doctor= instead
+
+=doctor= has a clean meaning today: "is this machine's =~/.claude/= consistent with rulesets?" Mixing in cross-project =.ai/= drift muddies the exit code. Keep them separate. =audit= can optionally invoke =doctor= as its last check since both ask "did the symlinks keep up with the source?". A future =make all-checks= can wrap both.
+
+*** DONE [#A] Add =make install-ai PROJECT=<path>= — bootstrap =.ai/= in a fresh project
+CLOSED: [2026-05-15 Fri]
+
+Separate target from =audit= because operating on projects that lack =.ai/= is a distinct action. The absence might be intentional, so =audit= skips them. Bootstrap is explicit opt-in.
+
+**** Flow
+
+1. Refuse if =.ai/= already exists in =PROJECT=. Message: "already installed; use =make audit --apply= to update."
+2. Verify =PROJECT= is a git checkout (warn if not — works without git, loses some lifecycle benefits).
+3. Create =PROJECT/.ai/= directory.
+4. Rsync canonical content: =protocols.org=, =workflows/=, =scripts/= (same three rsyncs as =audit=).
+5. Seed =PROJECT/.ai/notes.org= from a canonical template with project-name placeholder.
+6. Create empty =PROJECT/.ai/sessions/= (with =.gitkeep= for tracked projects).
+7. Track or gitignore =.ai/=? Default: ask. Flag: =--track= / =--gitignore=.
+8. Print next-steps banner: =make install-lang LANG=<lang> PROJECT=<path>=; open Claude Code in the project.
+
+**** Symmetry with existing install targets
+
+#+begin_example
+make install-lang LANG=python PROJECT=/path # language bundle (existing)
+make install-ai PROJECT=/path # .ai/ template (new)
+make install-lang # no args → fzf-pick
+make install-ai # no args → fzf-pick from
+ # ~/projects/* + ~/code/* dirs
+ # without an existing .ai/
+#+end_example
+
+*** DONE [#A] Test plan for audit + install-ai before propagating to ratio
+CLOSED: [2026-05-15 Fri]
+
+Test against the current state of this machine before pushing changes to ratio.
+
+**** =make audit= tests
+
+1. Dry-run report only (no =--apply=). Should show: claude-templates current; per-project drift; correct =ok=/=drift= classifications; summary line and exit code match.
+2. After the fold lands, every project should be reported as drift (their =startup.org= still points at the old path). Run =--apply= → rsync converges. Re-run audit → all =ok=.
+3. Manually edit one =.ai/workflows/foo.org= in a tracked project. Re-run audit → should report =skipped: uncommitted .ai/=. Run =--apply --force= → rsync clobbers the edit. Verify the edit is gone.
+4. Manually delete one =.ai/= dir. Re-run audit → =FAIL: .ai/ missing=. Loop continues.
+5. Idempotency: =--apply= twice in a row converges to all =ok= on the second pass.
+
+**** =make install-ai= tests
+
+1. Create =/tmp/test-fresh-project= as a git repo. Run =make install-ai PROJECT=/tmp/test-fresh-project=. Verify =.ai/= structure matches canonical, =notes.org= has placeholder, =sessions/= exists.
+2. Run =make install-ai PROJECT=/tmp/test-fresh-project= again → should refuse (=.ai/= already exists).
+3. Open Claude Code in the new project. Startup workflow runs cleanly (Phase A.0 + Phase A rsync should be a no-op since the install just ran).
+4. fzf form: =make install-ai= with no args. Lists candidate dirs (=~/projects/*=, =~/code/*= without =.ai/=).
+
+**** Pass criteria
+
+- =audit= behavior matches the per-project flow spec for every classification path.
+- =install-ai= produces a project indistinguishable from one that's been running sessions for a while.
+- =make doctor= still passes 36/0/0 after all the work.
+- =make test= (pytest + ERT) passes.
+
+*** DONE [#A] Migrate projects on ratio (second machine)
+CLOSED: [2026-05-15 Fri]
+
+After local fold + audit + install-ai are working, propagate to ratio.
+
+**** Steps
+
+1. On ratio: =git -C ~/code/rulesets pull= — picks up the folded =claude-templates/= subdir and updated =Makefile= targets.
+2. On ratio: archive or =mv= the standalone =~/projects/claude-templates/= aside, replace with symlink to =~/code/rulesets/claude-templates/= (same bridge mechanic as local).
+3. On ratio: =make audit= → see drift across ratio's projects.
+4. On ratio: =make audit --apply= → rsync into each tracked/gitignored project. Surface projects with uncommitted =.ai/= drift for manual handling.
+5. On ratio: =make doctor= → catch any =~/.claude/= install drift (likely some, since ratio hasn't seen recent rulesets updates).
+6. Verify by opening Claude Code in a few ratio projects. Startup should be a no-op or near-zero rsync.
+
+**** Known unknowns
+
+- Ratio may have its own project list overlapping with this machine's but not identical. =audit= discovers projects via the walk, so this is automatic.
+- Ratio might have uncommitted =.ai/= work in some projects that this machine doesn't. =audit= surfaces them; handle case-by-case.
+- If anything goes wrong, ratio's archived =~/projects/claude-templates/= is the safety net — restore the symlink target and re-run audit.
+
+**** Adjacent: cross-machine memory sync
+
+The =[#A] DOING= memory-sync investigation (todo.org:10) is adjacent. Both involve "make my Claude setup portable across machines." Coordinate so the memory-sync stow approach (if approved) doesn't conflict with this fold's symlink mechanics.