diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-01 21:40:11 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-01 21:40:11 -0400 |
| commit | 909b21be04865da56f76b1ac5416c1bc97ba73d2 (patch) | |
| tree | ef748931d0f38143189fc19f748ac34fb05d467d /scripts | |
| parent | 356b905db4a90dc2a89dc32f0beb2957b8a47518 (diff) | |
| download | rulesets-909b21be04865da56f76b1ac5416c1bc97ba73d2.tar.gz rulesets-909b21be04865da56f76b1ac5416c1bc97ba73d2.zip | |
fix(sweep): recognize anchored /.ai/ style; warn on publicly reachable tooling
sweep-gitignore-tooling.sh decided gitignore-mode with an exact unanchored match on `.ai/`, so a project using the anchored `/.ai/` form was misclassified as track-mode and silently skipped — which left .emacs.d's tracked tooling on a public GitHub mirror until its 2026-06-30 scrub. Both forms now count for mode detection and per-pattern presence, and appended lines follow whichever style the file already uses.
Track-mode projects also get a new check: tracked tooling paths combined with a non-cjennings.net remote draw a loud WARN, since a track-mode repo on a public host is the exposure the convention exists to prevent. The convention itself is now written down in protocols.org: a non-cjennings.net remote means the tooling set is gitignored, a deliberate team-shared config being the only explicit exception, and a private remote is not proof of privacy because a server-side mirror hook republishes invisibly.
From the .emacs.d handoff (2026-06-30 tooling-exposure broadcast).
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/sweep-gitignore-tooling.sh | 57 | ||||
| -rw-r--r-- | scripts/tests/sweep-gitignore-tooling.bats | 67 |
2 files changed, 115 insertions, 9 deletions
diff --git a/scripts/sweep-gitignore-tooling.sh b/scripts/sweep-gitignore-tooling.sh index 63fc066..f04d3cd 100755 --- a/scripts/sweep-gitignore-tooling.sh +++ b/scripts/sweep-gitignore-tooling.sh @@ -10,13 +10,19 @@ # # For each AI project (a directory with .ai/protocols.org) under the search # roots, if it's a git checkout in gitignore mode (.ai/ already appears in its -# .gitignore), ensure .ai/, .claude/, CLAUDE.md, and AGENTS.md are all ignored. -# Append only the missing lines, so a re-run is a no-op. +# .gitignore, in either the unanchored `.ai/` or anchored `/.ai/` form), ensure +# .ai/, .claude/, CLAUDE.md, and AGENTS.md are all ignored. Append only the +# missing lines, in whichever style the file already uses, so a re-run is a +# no-op. # # Track-mode projects (.ai/ NOT in .gitignore) are skipped by design: they # track their tooling on purpose — team repos sharing config with teammates who # don't run rulesets, or private-remote personal repos where the history IS the -# project. +# project. But a track-mode project whose tracked tooling is reachable from a +# non-cjennings.net remote gets a loud WARN: per convention the tooling set is +# gitignored anywhere the repo can reach a public host, and a server-side +# mirror hook can publish even a "private" remote (the 2026-06-30 .emacs.d +# exposure rode exactly that). # # A line added here only stops *future* commits. If a target path is already # tracked, the ignore has no effect until it's untracked; the sweep warns so @@ -30,6 +36,14 @@ set -euo pipefail IGNORE_SET=('.ai/' '.claude/' 'CLAUDE.md' 'AGENTS.md') +# A pattern counts as present in either the unanchored (`.ai/`) or anchored +# (`/.ai/`) form — both ignore the root-level path; treating them as different +# is what silently skipped anchored-style projects. +has_ignore() { + local pat="$1" gi="$2" + grep -qFx "$pat" "$gi" || grep -qFx "/$pat" "$gi" +} + dry_run=0 roots=() for arg in "$@"; do @@ -72,16 +86,39 @@ for project in "${projects[@]}"; do continue fi - # Gitignore mode iff .ai/ is already ignored. Otherwise track-mode: leave it. - if [ ! -f "$gi" ] || ! grep -qFx '.ai/' "$gi"; then - echo "skip $name — track-mode (.ai/ not gitignored)" + # Gitignore mode iff .ai/ is already ignored (either style). Otherwise + # track-mode: leave the .gitignore alone, but warn when tracked tooling can + # reach a non-cjennings.net remote — a track-mode repo on a public host (or + # behind an invisible server-side mirror) is the exposure the convention + # exists to prevent. + if [ ! -f "$gi" ] || ! has_ignore '.ai/' "$gi"; then + tracked_tooling=() + for pat in "${IGNORE_SET[@]}"; do + path="${pat%/}" + if git -C "$project" ls-files --error-unmatch "$path" >/dev/null 2>&1; then + tracked_tooling+=("$path") + fi + done + public_remote="$(git -C "$project" remote -v 2>/dev/null \ + | awk '{print $2}' | grep -v 'cjennings\.net' | sort -u | head -1 || true)" + if [ "${#tracked_tooling[@]}" -gt 0 ] && [ -n "$public_remote" ]; then + echo "skip $name — track-mode (.ai/ not gitignored)" + echo " WARN $name: tracked tooling (${tracked_tooling[*]}) is publicly reachable via $public_remote — gitignore the set and 'git -C $project rm --cached -r <path>' unless this is a deliberate team-shared config" + else + echo "skip $name — track-mode (.ai/ not gitignored)" + fi skipped=$((skipped + 1)) continue fi + # Append in the style the file already uses: anchored if its .ai/ marker + # line is the anchored form. + prefix="" + grep -qFx '/.ai/' "$gi" && prefix="/" + needed=() for pat in "${IGNORE_SET[@]}"; do - grep -qFx "$pat" "$gi" || needed+=("$pat") + has_ignore "$pat" "$gi" || needed+=("${prefix}${pat}") done if [ "${#needed[@]}" -eq 0 ]; then @@ -103,9 +140,11 @@ for project in "${projects[@]}"; do swept=$((swept + 1)) # Warn on any newly-ignored path that's already tracked — the ignore won't - # untrack it. + # untrack it. Strip the anchored prefix before asking git: the pattern + # `/CLAUDE.md` is the repo-relative path `CLAUDE.md`. for pat in "${needed[@]}"; do - path="${pat%/}" + path="${pat#/}" + path="${path%/}" if git -C "$project" ls-files --error-unmatch "$path" >/dev/null 2>&1; then echo " WARN $name: $path is currently tracked — 'git -C $project rm --cached -r $path' to untrack" fi diff --git a/scripts/tests/sweep-gitignore-tooling.bats b/scripts/tests/sweep-gitignore-tooling.bats index a28087e..6dc46ee 100644 --- a/scripts/tests/sweep-gitignore-tooling.bats +++ b/scripts/tests/sweep-gitignore-tooling.bats @@ -109,3 +109,70 @@ make_project() { [ "$status" -eq 0 ] [[ "$output" == *"not a git checkout"* ]] } + +@test "sweep: anchored /.ai/ is recognized as gitignore-mode, appends anchored" { + make_project anchored $'/.ai/\n' + + run bash "$SWEEP" "$ROOT" + + [ "$status" -eq 0 ] + [[ "$output" != *"anchored — track-mode"* ]] + grep -qFx "/.claude/" "$ROOT/anchored/.gitignore" + grep -qFx "/CLAUDE.md" "$ROOT/anchored/.gitignore" + grep -qFx "/AGENTS.md" "$ROOT/anchored/.gitignore" +} + +@test "sweep: anchored partial project gets only the missing lines" { + make_project anchoredpartial $'/.ai/\n/.claude/\n' + + run bash "$SWEEP" "$ROOT" + + [ "$status" -eq 0 ] + # /.claude/ already present in anchored form — not re-added in either form. + [ "$(grep -cFx '/.claude/' "$ROOT/anchoredpartial/.gitignore")" -eq 1 ] + ! grep -qFx ".claude/" "$ROOT/anchoredpartial/.gitignore" + grep -qFx "/CLAUDE.md" "$ROOT/anchoredpartial/.gitignore" + grep -qFx "/AGENTS.md" "$ROOT/anchoredpartial/.gitignore" +} + +@test "sweep: anchored gitignore-mode is idempotent" { + make_project anchored2 $'/.ai/\n' + bash "$SWEEP" "$ROOT" >/dev/null + + run bash "$SWEEP" "$ROOT" + + [ "$status" -eq 0 ] + [[ "$output" == *"already complete"* ]] + [ "$(grep -cFx '/.claude/' "$ROOT/anchored2/.gitignore")" -eq 1 ] +} + +@test "sweep: track-mode with tracked tooling and a non-cjennings.net remote warns" { + make_project publictrack $'out/\n' + echo "# project rules" > "$ROOT/publictrack/CLAUDE.md" + (cd "$ROOT/publictrack" \ + && git add CLAUDE.md \ + && git -c user.email=t@t -c user.name=t commit -qm seed \ + && git remote add origin git@github.com:someone/publictrack.git) + + run bash "$SWEEP" "$ROOT" + + [ "$status" -eq 0 ] + [[ "$output" == *"WARN"* ]] + [[ "$output" == *"publicly reachable"* ]] + # Still track-mode: nothing written to its .gitignore. + ! grep -qFx ".claude/" "$ROOT/publictrack/.gitignore" +} + +@test "sweep: track-mode with tracked tooling on a cjennings.net remote stays quiet" { + make_project privatetrack $'out/\n' + echo "# project rules" > "$ROOT/privatetrack/CLAUDE.md" + (cd "$ROOT/privatetrack" \ + && git add CLAUDE.md \ + && git -c user.email=t@t -c user.name=t commit -qm seed \ + && git remote add origin git@cjennings.net:privatetrack.git) + + run bash "$SWEEP" "$ROOT" + + [ "$status" -eq 0 ] + [[ "$output" != *"publicly reachable"* ]] +} |
