diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-22 16:59:56 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-22 16:59:56 -0500 |
| commit | 3cb467e6aa4356f9912d661ef12d581d61b65cb6 (patch) | |
| tree | 6cc1e0634dedcdbbf68fcfc6d6696492fa96ad82 /scripts/sync-language-bundle.sh | |
| parent | b37e4f8cb494ca14b9a00c967c7df8bd8d0a9ee1 (diff) | |
| download | rulesets-3cb467e6aa4356f9912d661ef12d581d61b65cb6.tar.gz rulesets-3cb467e6aa4356f9912d661ef12d581d61b65cb6.zip | |
feat: split team publishing rules into an installable overlay
The global commits.md carried DeepSat-specific publishing steps — Linear ticket-state moves, the Slack notification protocol with its channel ID and engineer names, the deepsat.ghe.com host, the team merge norm. Those are symlinked into every project on the machine, so they sat as dead weight in personal repos and risked misfiring where there's no Linear ticket to move or Slack mpdm to ping.
I split them out. commits.md keeps the universal skeleton (identity, attribution, commit format, the review-and-publish gate, verification) and replaces the team steps with seams: "run the project's publishing overlay here if it defines one," the same pattern startup.org uses for startup-extras. A project with no overlay runs the complete flow, just without ticket and chat integration.
The DeepSat specifics move to teams/deepsat/claude/rules/publishing.md. That file is not a global rule — install-team.sh copies it into one project's .claude/rules/ (make install-team TEAM=deepsat PROJECT=...), keyed on the PROJECT argument, so only the named project gets it. Location decides distribution: claude-rules/ is the global-symlink set, teams/ is targeted-copy, so the overlay reaches DeepSat and nowhere else.
The startup freshness check (sync-language-bundle.sh) now covers team overlays alongside language bundles: a process_bundle function handles both, with a team syncing only its own rule (no generic rules, hooks, or settings — those belong to a language bundle). A drifted overlay rule auto-fixes from canonical at the project's next startup, the same mechanism language bundles already ride.
Tested: 3 new bats cases (team overlay clean / drifted-and-fixed / does-not-pull-generic-rules) on top of the 11 existing; install-team + sync verified end-to-end against a temp project. make test green, shellcheck clean.
Diffstat (limited to 'scripts/sync-language-bundle.sh')
| -rwxr-xr-x | scripts/sync-language-bundle.sh | 123 |
1 files changed, 73 insertions, 50 deletions
diff --git a/scripts/sync-language-bundle.sh b/scripts/sync-language-bundle.sh index 8858f74..b2db8cb 100755 --- a/scripts/sync-language-bundle.sh +++ b/scripts/sync-language-bundle.sh @@ -1,19 +1,26 @@ #!/usr/bin/env bash -# Per-project language-bundle freshness check, designed to run at startup. +# Per-project bundle freshness check, designed to run at startup. # -# Detects which language bundle(s) a project has by fingerprint (no marker -# file), then reconciles them against the canonical rulesets source: -# - AUTO-FIX (rulesets-owned, safe to overwrite): .claude/rules/*.md, -# .claude/hooks/*, githooks/* -# - SURFACE (project may customize — reported, never written): -# .claude/settings.json +# Covers two kinds of bundle copied into a project's .claude/: +# - LANGUAGE bundles (languages/<lang>/) — generic rules + language rules + +# hooks + githooks, with settings.json surfaced (not auto-fixed). +# - TEAM overlays (teams/<team>/) — only the overlay's own rule file(s); +# no generic rules, hooks, githooks, or settings. +# +# Detection is by fingerprint (no marker file): a project "has" a bundle iff +# one of that bundle's own rule files is present in the project's +# .claude/rules/. For each detected bundle it reconciles against the canonical +# rulesets source: +# - AUTO-FIX (rulesets-owned, safe to overwrite): .claude/rules/*.md, and +# for language bundles also .claude/hooks/* and githooks/* +# - SURFACE (project may customize — reported, never written): settings.json # CLAUDE.md is intentionally not tracked: it is seed-only in install-lang # and project-owned afterward (diff-lang skips it for the same reason). # # Usage: sync-language-bundle.sh [project-path] (default: $PWD) # # Exit: 0 no bundle, or clean / rules+hooks auto-fixed (resolved) -# 3 manual action recommended (settings.json / CLAUDE.md drift) +# 3 manual action recommended (settings.json drift) # 1 usage / path error # # Quiet when there is nothing to report. Resolves the canonical source @@ -25,6 +32,7 @@ PROJECT="${1:-$PWD}" REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" LANG_ROOT="$REPO_ROOT/languages" +TEAM_ROOT="$REPO_ROOT/teams" GENERIC_RULES="$REPO_ROOT/claude-rules" if [ ! -d "$PROJECT" ]; then @@ -33,8 +41,6 @@ if [ ! -d "$PROJECT" ]; then fi PROJECT="$(cd "$PROJECT" && pwd)" -[ -d "$LANG_ROOT" ] || exit 0 # no bundles available — nothing to do - OUT="" # buffered report, printed once at the end FIXED=0 MANUAL=0 @@ -78,64 +84,81 @@ surface() { MANUAL=$((MANUAL + 1)) } -for src_lang in "$LANG_ROOT"/*/; do - [ -d "$src_lang" ] || continue - src_lang="${src_lang%/}" - lang="$(basename "$src_lang")" - lang_rules="$src_lang/claude/rules" - [ -d "$lang_rules" ] || continue - - # Fingerprint: project has this bundle iff any of the language's own - # rule files is present in .claude/rules/. - detected=0 - for rf in "$lang_rules"/*.md; do +# process_bundle KIND SRC_DIR — reconcile one bundle (KIND is language|team). +# A team overlay owns only its own rule files; a language bundle also owns the +# shared generic rules, its hooks/githooks, and surfaces settings.json. +process_bundle() { + local kind="$1" src="${2%/}" + local name rules rf f rel manual_before + name="$(basename "$src")" + rules="$src/claude/rules" + [ -d "$rules" ] || return 0 + + # Fingerprint: project has this bundle iff one of its own rule files is + # present in .claude/rules/. + local detected=0 + for rf in "$rules"/*.md; do [ -f "$rf" ] || continue if [ -f "$PROJECT/.claude/rules/$(basename "$rf")" ]; then detected=1 break fi done - [ "$detected" -eq 1 ] || continue + [ "$detected" -eq 1 ] || return 0 - CUR_HEADER="language bundle '$lang' — $PROJECT:" + CUR_HEADER="$kind bundle '$name' — $PROJECT:" HEADER_DONE=0 manual_before=$MANUAL - # AUTO-FIX: generic rules (shared) + language rules - for f in "$GENERIC_RULES"/*.md; do - [ -f "$f" ] || continue - fix "$f" "$PROJECT/.claude/rules/$(basename "$f")" - done - for f in "$lang_rules"/*.md; do + # AUTO-FIX: language bundles carry the shared generic rules; team overlays + # carry only their own rule(s). + if [ "$kind" = language ]; then + for f in "$GENERIC_RULES"/*.md; do + [ -f "$f" ] || continue + fix "$f" "$PROJECT/.claude/rules/$(basename "$f")" + done + fi + for f in "$rules"/*.md; do [ -f "$f" ] || continue fix "$f" "$PROJECT/.claude/rules/$(basename "$f")" done - # AUTO-FIX: language hooks (executable) - if [ -d "$src_lang/claude/hooks" ]; then - while IFS= read -r f; do - rel="${f#"$src_lang"/claude/}" - fix "$f" "$PROJECT/.claude/$rel" exec - done < <(find "$src_lang/claude/hooks" -type f) - fi - - # AUTO-FIX: githooks (executable) - if [ -d "$src_lang/githooks" ]; then - while IFS= read -r f; do - rel="${f#"$src_lang"/githooks/}" - fix "$f" "$PROJECT/githooks/$rel" exec - done < <(find "$src_lang/githooks" -type f) + # Language bundles also own hooks, githooks, and settings; teams don't. + if [ "$kind" = language ]; then + if [ -d "$src/claude/hooks" ]; then + while IFS= read -r f; do + rel="${f#"$src"/claude/}" + fix "$f" "$PROJECT/.claude/$rel" exec + done < <(find "$src/claude/hooks" -type f) + fi + if [ -d "$src/githooks" ]; then + while IFS= read -r f; do + rel="${f#"$src"/githooks/}" + fix "$f" "$PROJECT/githooks/$rel" exec + done < <(find "$src/githooks" -type f) + fi + surface "$src/claude/settings.json" "$PROJECT/.claude/settings.json" fi - # SURFACE-ONLY: settings.json may carry project customization, so report - # rather than overwrite. (CLAUDE.md is intentionally untracked here — it's - # seed-only in install-lang and project-owned after, same as diff-lang skips it.) - surface "$src_lang/claude/settings.json" "$PROJECT/.claude/settings.json" - if [ "$MANUAL" -gt "$manual_before" ]; then - OUT+=" → reconcile: make install-$lang PROJECT=."$'\n' + if [ "$kind" = team ]; then + OUT+=" → reconcile: make install-team TEAM=$name PROJECT=."$'\n' + else + OUT+=" → reconcile: make install-$name PROJECT=."$'\n' + fi fi -done +} + +if [ -d "$LANG_ROOT" ]; then + for d in "$LANG_ROOT"/*/; do + [ -d "$d" ] && process_bundle language "$d" + done +fi +if [ -d "$TEAM_ROOT" ]; then + for d in "$TEAM_ROOT"/*/; do + [ -d "$d" ] && process_bundle team "$d" + done +fi [ -n "$OUT" ] && printf '%s' "$OUT" [ "$MANUAL" -gt 0 ] && exit 3 |
