diff options
Diffstat (limited to '.ai/workflows/startup.org')
| -rw-r--r-- | .ai/workflows/startup.org | 64 |
1 files changed, 38 insertions, 26 deletions
diff --git a/.ai/workflows/startup.org b/.ai/workflows/startup.org index 47a77c8..bc89256 100644 --- a/.ai/workflows/startup.org +++ b/.ai/workflows/startup.org @@ -1,5 +1,5 @@ #+TITLE: Startup Workflow -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-04-25 * Summary @@ -29,10 +29,16 @@ Inside a rulesets session, the project-repo refresh below covers this — the ru #+begin_src bash rs="$HOME/code/rulesets" if [ -d "$rs/.git" ]; then - if (cd "$rs" && git diff --quiet --ignore-submodules HEAD -- 2>/dev/null); then + gate="$rs/claude-templates/bin/git-worktree-gate" + if [ -x "$gate" ] && "$gate" sync-safe "$rs"; then + (cd "$rs" && git pull --ff-only origin main 2>&1) | tail -3 + elif [ ! -x "$gate" ] \ + && (cd "$rs" && git diff --quiet --ignore-submodules HEAD -- 2>/dev/null); then + # Bootstrap fallback for a checkout old enough not to have the shared + # gate yet. The pull that follows installs it for subsequent starts. (cd "$rs" && git pull --ff-only origin main 2>&1) | tail -3 else - echo "rulesets: dirty working tree — using as-is, skipping pull" + echo "rulesets: changes beyond untracked inbox deliveries — using as-is, skipping pull" fi else echo "rulesets: not a git checkout — skipping" @@ -40,11 +46,11 @@ fi #+end_src Behavior: -- *Clean working tree* → fast-forward pull. =git pull --ff-only= refuses any merge or rebase, so the operation is either a no-op (already current) or a clean advance. -- *Dirty working tree* → skip the pull. Don't auto-stash and don't auto-merge — those would either lose work or invite conflicts at the worst possible moment (session start). +- *Clean working tree, or untracked deliveries only beneath =inbox/=* → fast-forward pull. =git pull --ff-only= refuses any merge or rebase, so the operation is either a no-op (already current) or a clean advance. Inbox files are queue input, not source-tree work, and do not block other projects from receiving rulesets updates. +- *Any staged or tracked change, dirty submodule, Git operation in progress, or untracked file outside =inbox/=* → skip the pull. Don't auto-stash and don't auto-merge — those would either lose work or invite conflicts at the worst possible moment (session start). - *Non-fast-forward history* → =--ff-only= aborts with an error. Surface that to the user; the rsync still proceeds against the working tree as-is. -*Template-freshness policy (applies to every dirty-check in the synced workflows).* "Dirty" means *tracked modifications only*. Untracked and gitignored files — an inbox drop, a file left in the tree to read, scratch output — never block a template pull, a fast-forward, or a monitoring gate. Projects were falling behind on templates because somebody sent them a task; that's the failure this policy closes. The checks here already comply (=git diff --quiet HEAD= sees only tracked changes; the ff gate uses =--untracked-files=no=), and any dirty-check added to a synced workflow follows the same rule. One deliberate exception: the rsync WIP-guard below counts untracked files *within rulesets' own synced source paths*, because an untracked half-written template is exactly the WIP it exists to hold back — that guard is about rulesets' outbound content, not the consuming project's local state. +*Template-freshness policy (applies to every dirty-check in the synced workflows).* The shared =git-worktree-gate sync-safe= policy is the source of truth: untracked files beneath =inbox/= and gitignored files do not block a pull, fast-forward, or monitoring gate; every other staged, tracked, untracked, submodule, or in-progress-operation state does. Projects must not fall behind merely because somebody sent them a task, but an arbitrary scratch file is not silently treated as safe. One deliberate exception remains: the rsync WIP-guard below is narrower than the repository gate and counts untracked files within rulesets' own synced source paths, because an untracked half-written template is exactly the WIP it exists to hold back. *** Install rulesets symlinks into ~/.claude (idempotent) @@ -74,8 +80,11 @@ if [ -d .git ]; then current=$(git symbolic-ref --short HEAD 2>/dev/null) dirty=0 - if ! git diff --quiet --ignore-submodules HEAD -- 2>/dev/null \ - || [ -n "$(git status --porcelain --untracked-files=no)" ]; then + gate="$HOME/code/rulesets/claude-templates/bin/git-worktree-gate" + if [ -x "$gate" ]; then + "$gate" sync-safe "$PWD" >/dev/null 2>&1 || dirty=1 + elif ! git diff --quiet --ignore-submodules HEAD -- 2>/dev/null \ + || [ -n "$(git status --porcelain --untracked-files=no)" ]; then dirty=1 fi @@ -107,8 +116,8 @@ fi #+end_src Behavior, per branch: -- *Behind only, current branch, clean tree* → =git merge --ff-only= advances HEAD. -- *Behind only, current branch, dirty tree* → fetched but not advanced. Surface so Craig can ff manually after dealing with the dirty state. +- *Behind only, current branch, sync-safe tree* → =git merge --ff-only= advances HEAD. An untracked =inbox/= delivery is sync-safe. +- *Behind only, current branch, sync-blocking tree* → fetched but not advanced. Surface so Craig can ff manually after dealing with the reported state. - *Behind only, non-checkout branch* → =git fetch . upstream:branch= advances the ref without touching the working tree. - *Diverged* (ahead and behind) → leave alone. Surface for Craig to resolve. Don't auto-rebase or auto-merge. - *Ahead only* or *up to date* → silent no-op. @@ -126,26 +135,26 @@ 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. + + 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. #+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) - if [ -z "$synced_dirty" ]; then - 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" + if [ -x .ai/scripts/sync-templates ]; then + .ai/scripts/sync-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/^/ /' + 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). @@ -155,12 +164,14 @@ These calls have no dependencies on each other. Issue them all together in one m 10. =[ -f "$HOME/org/roam/inbox.org" ] && grep -cE '^\*\* ' "$HOME/org/roam/inbox.org" || true= — count items in the roam global inbox (=~/org/roam/inbox.org=), the roam-mode startup nudge. Silent if the roam clone isn't on this machine. Phase C reads the file when the count is non-zero, splits total vs items related to this project, and surfaces the offer (see =inbox.org= roam mode). Read-only; never files at startup. 11. KB surface prep (the read + contribute startup nudges; see =docs/specs/2026-06-16-encourage-kb-contribution-spec.org=). Gated on the agent KB clone. Counts =:agent:= nodes, lists up to 5 whose content matches the current project basename (titles only; a few most-recent nodes as a fallback when nothing matches), and resolves the best-practices node path. Read-only; silent when the clone is absent. Phase C surfaces the relevant titles (consult) and the best-practices link (contribute). + The best-practices lookup matches the node's *filename*, not its content. A roam node's slug lives only in its filename, so the earlier content-grep (=rg -l 'agent-kb-best-practices'=) matched nothing and the contribute nudge silently pointed at an empty path in every project, every session, for as long as it shipped. =find= rather than a glob keeps the probe identical under bash and zsh (zsh aborts on an unmatched glob) — the same reason the spec-sort probe below uses =find=. + #+begin_src bash ra="$HOME/org/roam/agents" if [ -d "$ra" ]; then proj=$(basename "$PWD") echo "kb-total: $(rg -l '#\+filetags:.*:agent:' "$ra" 2>/dev/null | wc -l)" - echo "kb-bestpractices: $(rg -l 'agent-kb-best-practices' "$ra" 2>/dev/null | head -1)" + echo "kb-bestpractices: $(find "$ra" -maxdepth 1 -name '*agent-kb-best-practices*.org' -print -quit 2>/dev/null)" matches=$(rg -il "$proj" "$ra" 2>/dev/null | head -5) [ -z "$matches" ] && matches=$(\ls -t "$ra"/*.org 2>/dev/null | head -3) echo "kb-relevant-titles:" @@ -187,12 +198,13 @@ 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. - 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. |
