From cc72aa635f733da36010567c8718b1ede7622c52 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Wed, 10 Jun 2026 01:14:46 -0500 Subject: feat(install-ai): gitignore the full personal-tooling set, add backfill sweep A gitignore-mode project only ignored .ai/. CLAUDE.md was left untracked but not ignored, so an accidental git add or a codify run could still commit a personal CLAUDE.md, the private rule copies under .claude/, or an AGENTS.md. install-ai now ignores the whole set (.ai/, .claude/, CLAUDE.md, AGENTS.md) at bootstrap, line-idempotent so an existing .gitignore isn't duplicated. .claude/ goes in the set because it's rulesets-owned (copies of claude-rules/*.md plus the language bundle's rules, hooks, and settings), re-synced from rulesets every startup, so git isn't how it travels. Ignoring it also keeps those private rule copies out of the repo, which ignoring CLAUDE.md alone would miss. The gate is unchanged: track-mode projects (personal/doc repos, team repos sharing config) keep tracking the set. sweep-gitignore-tooling.sh backfills the set across existing gitignore-mode projects, idempotent and skipping track-mode by design. It warns when a now-ignored path is already tracked, since the ignore won't untrack it. protocols.org states the policy once. --- scripts/tests/install-ai.bats | 21 ++++++ scripts/tests/sweep-gitignore-tooling.bats | 111 +++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 scripts/tests/sweep-gitignore-tooling.bats (limited to 'scripts/tests') diff --git a/scripts/tests/install-ai.bats b/scripts/tests/install-ai.bats index dca70ea..8e91770 100644 --- a/scripts/tests/install-ai.bats +++ b/scripts/tests/install-ai.bats @@ -36,12 +36,33 @@ teardown() { [ -d "$TEST_HOME/code/fresh/.ai/retrospectives" ] [ -f "$TEST_HOME/code/fresh/.ai/protocols.org" ] [ -f "$TEST_HOME/code/fresh/.ai/notes.org" ] + # Gitignore mode ignores the whole personal-tooling set, not just .ai/. grep -qFx ".ai/" "$TEST_HOME/code/fresh/.gitignore" + grep -qFx ".claude/" "$TEST_HOME/code/fresh/.gitignore" + grep -qFx "CLAUDE.md" "$TEST_HOME/code/fresh/.gitignore" + grep -qFx "AGENTS.md" "$TEST_HOME/code/fresh/.gitignore" # Top-level inbox/ is created so the project is an inbox-send target. [ -d "$TEST_HOME/code/fresh/inbox" ] [ -f "$TEST_HOME/code/fresh/inbox/.gitkeep" ] } +@test "install-ai --gitignore: appends only the missing tooling lines" { + mkdir -p "$TEST_HOME/code/partial" + (cd "$TEST_HOME/code/partial" && git init -q) + # Project already ignores .ai/ and CLAUDE.md from a prior convention. + printf '# Personal tooling\n.ai/\nCLAUDE.md\n' > "$TEST_HOME/code/partial/.gitignore" + + run bash "$INSTALL_AI" --gitignore "$TEST_HOME/code/partial" + + [ "$status" -eq 0 ] + # The two already-present lines are not duplicated. + [ "$(grep -cFx '.ai/' "$TEST_HOME/code/partial/.gitignore")" -eq 1 ] + [ "$(grep -cFx 'CLAUDE.md' "$TEST_HOME/code/partial/.gitignore")" -eq 1 ] + # The two missing lines were added. + grep -qFx ".claude/" "$TEST_HOME/code/partial/.gitignore" + grep -qFx "AGENTS.md" "$TEST_HOME/code/partial/.gitignore" +} + @test "install-ai: creates top-level inbox/ in --track mode too" { mkdir -p "$TEST_HOME/code/inbox-track" (cd "$TEST_HOME/code/inbox-track" && git init -q) diff --git a/scripts/tests/sweep-gitignore-tooling.bats b/scripts/tests/sweep-gitignore-tooling.bats new file mode 100644 index 0000000..a28087e --- /dev/null +++ b/scripts/tests/sweep-gitignore-tooling.bats @@ -0,0 +1,111 @@ +#!/usr/bin/env bats +# +# Tests for scripts/sweep-gitignore-tooling.sh — backfill the personal-tooling +# gitignore set across existing gitignore-mode AI projects. +# +# Strategy: scaffold fake AI projects (a .ai/protocols.org marker + a git repo) +# under a temp root, run the sweep against that root, and assert on each +# project's .gitignore. + +REAL_REPO="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)" +SWEEP="$REAL_REPO/scripts/sweep-gitignore-tooling.sh" + +setup() { + ROOT="$(mktemp -d -t sweep-bats.XXXXXX)" +} + +teardown() { + rm -rf "$ROOT" +} + +# Scaffold a project: $1 = name, $2 = initial .gitignore content (may be empty), +# $3 = "git" to init a repo (default), "nogit" to skip. +make_project() { + local name="$1" gitignore="${2-}" git="${3-git}" + mkdir -p "$ROOT/$name/.ai" + echo "marker" > "$ROOT/$name/.ai/protocols.org" + [ -n "$gitignore" ] && printf '%s' "$gitignore" > "$ROOT/$name/.gitignore" + [ "$git" = "git" ] && (cd "$ROOT/$name" && git init -q) + return 0 +} + +@test "sweep: gitignore-mode project missing all three gets them appended" { + make_project gimode $'# tooling\n.ai/\n' + + run bash "$SWEEP" "$ROOT" + + [ "$status" -eq 0 ] + grep -qFx ".claude/" "$ROOT/gimode/.gitignore" + grep -qFx "CLAUDE.md" "$ROOT/gimode/.gitignore" + grep -qFx "AGENTS.md" "$ROOT/gimode/.gitignore" +} + +@test "sweep: is idempotent — a second run adds nothing" { + make_project gimode $'.ai/\n' + bash "$SWEEP" "$ROOT" >/dev/null + + run bash "$SWEEP" "$ROOT" + + [ "$status" -eq 0 ] + [[ "$output" == *"already complete"* ]] + # No line is duplicated. + [ "$(grep -cFx '.claude/' "$ROOT/gimode/.gitignore")" -eq 1 ] + [ "$(grep -cFx 'CLAUDE.md' "$ROOT/gimode/.gitignore")" -eq 1 ] + [ "$(grep -cFx 'AGENTS.md' "$ROOT/gimode/.gitignore")" -eq 1 ] +} + +@test "sweep: track-mode project (.ai/ not ignored) is skipped untouched" { + make_project trackmode $'# build\nout/\n' + + run bash "$SWEEP" "$ROOT" + + [ "$status" -eq 0 ] + [[ "$output" == *"track-mode"* ]] + # .gitignore is untouched — none of the tooling lines were added. + ! grep -qFx ".claude/" "$ROOT/trackmode/.gitignore" + ! grep -qFx "CLAUDE.md" "$ROOT/trackmode/.gitignore" +} + +@test "sweep: partial project gets only the missing lines" { + make_project partial $'.ai/\n.claude/\n' + + run bash "$SWEEP" "$ROOT" + + [ "$status" -eq 0 ] + [ "$(grep -cFx '.claude/' "$ROOT/partial/.gitignore")" -eq 1 ] + grep -qFx "CLAUDE.md" "$ROOT/partial/.gitignore" + grep -qFx "AGENTS.md" "$ROOT/partial/.gitignore" +} + +@test "sweep: --dry-run reports without writing" { + make_project gimode $'.ai/\n' + + run bash "$SWEEP" --dry-run "$ROOT" + + [ "$status" -eq 0 ] + [[ "$output" == *"would add"* ]] + [[ "$output" == *"no files written"* ]] + # Nothing was actually written. + ! grep -qFx ".claude/" "$ROOT/gimode/.gitignore" +} + +@test "sweep: warns when a now-ignored path is already tracked" { + make_project tracked $'.ai/\n' + echo "# project rules" > "$ROOT/tracked/CLAUDE.md" + (cd "$ROOT/tracked" && git add CLAUDE.md && git -c user.email=t@t -c user.name=t commit -qm seed) + + run bash "$SWEEP" "$ROOT" + + [ "$status" -eq 0 ] + [[ "$output" == *"WARN"* ]] + [[ "$output" == *"CLAUDE.md is currently tracked"* ]] +} + +@test "sweep: non-git project with .ai/ marker is skipped" { + make_project nogit $'.ai/\n' "" nogit + + run bash "$SWEEP" "$ROOT" + + [ "$status" -eq 0 ] + [[ "$output" == *"not a git checkout"* ]] +} -- cgit v1.2.3