feat(agents-cleanup): archive, materialize links, remove AGENTS/, strip .gitignore

This commit is contained in:
2026-09-30 17:01:47 -04:00
parent 0f9e9e81fa
commit beb6518ab9
2 changed files with 268 additions and 0 deletions
+166
View File
@@ -272,6 +272,172 @@ function agents-cleanup --description 'undo agents-init: restore real files, arc
test $verbose -eq 1; and echo "$c_ok→ Wrote .agents-disabled$c_reset" test $verbose -eq 1; and echo "$c_ok→ Wrote .agents-disabled$c_reset"
end end
# ────────────────────────── Phase 3: archive ─────────────────────────
# Nothing is moved until the history is safely out of AGENTS/.
if test $has_repo -eq 1
# 2>/dev/null: the helper's own message would leak past --silent (a
# command substitution's stderr ignores the caller's redirect); the
# error below says the same thing.
set -l sync_out (_agents_repo_sync "$agents_dir" "chore: final sync before agents-cleanup" 2>/dev/null)
if test $status -ne 0
echo "$c_err""Error: could not commit pending AGENTS/ changes; nothing moved or removed$c_reset" >&2
return 1
end
if test -n "$sync_out"
set changed 1
test $verbose -eq 1; and echo "$c_ok$sync_out$c_reset"
end
if git -C "$agents_dir" rev-parse -q --verify HEAD >/dev/null
if not mkdir -p (path dirname "$bundle")
echo "$c_err""Error: could not create "(path dirname "$bundle")"; nothing moved or removed$c_reset" >&2
return 1
end
if not git -C "$agents_dir" bundle create -q "$bundle" --all 2>/dev/null
echo "$c_err""Error: could not write $bundle; nothing moved or removed$c_reset" >&2
return 1
end
if not git -C "$agents_dir" bundle verify -q "$bundle" >/dev/null 2>&1
echo "$c_err""Error: $bundle failed verification; nothing moved or removed$c_reset" >&2
return 1
end
set changed 1
set archived "$bundle"
if test $verbose -eq 1
echo "$c_ok→ Archived AGENTS/ history to $bundle$c_reset"
echo "$c_dim restore with: git clone $bundle AGENTS$c_reset"
end
else
test $verbose -eq 1; and echo "$c_dim→ AGENTS/ has no commits; nothing to archive$c_reset"
end
else if test $has_agents -eq 1
test $verbose -eq 1; and echo "$c_dim→ AGENTS/ is not a git repository; nothing to archive$c_reset"
end
# ──────────────────────── Phase 4: materialize ───────────────────────
set -l directive 'SYSTEM DIRECTIVE FOR AI AGENTS: FILE EDITING'
for i in (seq (count $keep))
set -l link $keep[$i]
set -l rel (string replace -- "$root/" "" "$link")
if not rm -f "$link"; or not command mv "$keep_tgt[$i]" "$link"
echo "$c_err""Error: could not restore $rel from AGENTS/; re-run to resume$c_reset" >&2
return 1
end
set changed 1
test $verbose -eq 1; and echo "$c_ok→ Restored $rel$c_reset"
if test -d "$link"
rm -f "$link/.gitkeep"
else if test (path basename "$link") = AGENTS.md
if _agents_init_stub | cmp -s - "$link"
rm -f "$link"
test $verbose -eq 1; and echo "$c_ok→ Removed $rel (agents-init stub, no user content)$c_reset"
else if grep -qF -- "$directive" "$link"
# Drop the first run of '>' lines carrying the directive, and
# the blank lines after it. Not "the first lines": real files
# put a heading above it.
set -l body (awk -v d="$directive" '
{ line[NR] = $0 }
END {
s = 0
for (i = 1; i <= NR; i++) {
if (line[i] !~ /^>/) continue
j = i; hit = 0
while (j <= NR && line[j] ~ /^>/) { if (index(line[j], d)) hit = 1; j++ }
if (hit) { s = i; e = j; break }
i = j
}
if (!s) { for (i = 1; i <= NR; i++) print line[i]; exit }
while (e <= NR && line[e] ~ /^[ \t]*$/) e++
for (i = 1; i < s; i++) print line[i]
for (i = e; i <= NR; i++) print line[i]
}' "$link")
if test $status -ne 0
echo "$c_err""Error: could not rewrite $rel$c_reset" >&2
return 1
end
if set -q body[1]; and string match -qr -- '\S' $body
printf '%s\n' $body >"$link"
test $verbose -eq 1; and echo "$c_ok→ Removed the AGENTS/ directive from $rel$c_reset"
else
rm -f "$link"
test $verbose -eq 1; and echo "$c_ok→ Removed $rel (only the directive, no user content)$c_reset"
end
end
end
end
for link in $drop
set -l rel (string replace -- "$root/" "" "$link")
if not rm -f "$link"
echo "$c_err""Error: could not remove link $rel$c_reset" >&2
return 1
end
set changed 1
test $verbose -eq 1; and echo "$c_ok→ Removed link $rel$c_reset"
end
# ─────────────────────── Phase 5: remove AGENTS/ ─────────────────────
if test $has_agents -eq 1
if not rm -rf "$agents_dir"
echo "$c_err""Error: could not remove AGENTS/$c_reset" >&2
return 1
end
set changed 1
test $verbose -eq 1; and echo "$c_ok→ Removed AGENTS/$c_reset"
# agents-init creates these; drop them only if nothing else lives there.
for d in "$root/docs/superpowers" "$root/docs"
if test -d "$d"; and rmdir "$d" 2>/dev/null
test $verbose -eq 1; and echo "$c_ok→ Removed empty "(string replace -- "$root/" "" "$d")"/$c_reset"
end
end
end
# ───────────────────────── Phase 6: .gitignore ───────────────────────
# Matched by pattern, not the exact strings _agents_init_ensure_gitignore
# writes, so blocks from older header variants go too. A header with no
# footer before the next header is left in place (and reported).
if test -f "$gitignore"; and grep -q 'Added by agents-init' "$gitignore"
set -l kept (awk '
{ line[NR] = $0 }
END {
n = 0
for (i = 1; i <= NR; i++) {
if (line[i] ~ /^#.*Added by agents-init/) {
for (j = i + 1; j <= NR; j++) {
if (line[j] ~ /Added by agents-init/) { j = NR + 1; break }
if (line[j] ~ /^#[ \t]*(─)+[ \t]*$/) break
}
if (j <= NR) {
if (n > 0 && out[n] ~ /^[ \t]*$/) n--
i = j
continue
}
print "agents-cleanup: unterminated agents-init block in .gitignore left in place" > "/dev/stderr"
}
out[++n] = line[i]
}
for (i = 1; i <= n; i++) print out[i]
}' "$gitignore")
if test $status -ne 0
echo "$c_err""Error: could not rewrite .gitignore$c_reset" >&2
return 1
end
if set -q kept[1]; and string match -qr -- '\S' $kept
printf '%s\n' $kept >"$gitignore"
else if test $in_git -eq 1; and git -C "$root" ls-files --error-unmatch -- .gitignore >/dev/null 2>&1
# Tracked: someone committed it, so it is not agents-init's to
# delete. Leave it empty; the change shows in git status.
true >"$gitignore"
else
# Untracked and nothing but agents-init's blocks: agents-init
# created it.
rm -f "$gitignore"
end
set changed 1
test $verbose -eq 1; and echo "$c_ok→ Removed agents-init blocks from .gitignore$c_reset"
end
# ────────────────────────────── Summary ─────────────────────────────── # ────────────────────────────── Summary ───────────────────────────────
if test $changed -eq 0 if test $changed -eq 0
test $verbose -eq 1; and echo "$c_dim→ Nothing to clean up; agents-init is already disabled here$c_reset" test $verbose -eq 1; and echo "$c_dim→ Nothing to clean up; agents-init is already disabled here$c_reset"
+102
View File
@@ -191,5 +191,107 @@ check "glob path: dry-run exits 0" 0 "$grc"
check "glob path: plan removes AGENTS/" true (string match -q -- '*remove AGENTS/*' "$gout"; and echo true; or echo false) check "glob path: plan removes AGENTS/" true (string match -q -- '*remove AGENTS/*' "$gout"; and echo true; or echo false)
check "glob path: plan never lists .git/" false (string match -q -- '*.git/*' "$gout"; and echo true; or echo false) check "glob path: plan never lists .git/" false (string match -q -- '*.git/*' "$gout"; and echo true; or echo false)
section "agents-cleanup: full cleanup"
fresh_state
set -l c1 (scaffolded_repo)
pushd $c1 >/dev/null
set -l crc (agents-cleanup --silent 2>/dev/null; echo $status)
popd >/dev/null
check "full: exits 0" 0 "$crc"
check "full: AGENTS/ removed" false (test -e $c1/AGENTS; and echo true; or echo false)
check "full: root AGENTS.md is a real file" false (test -L $c1/AGENTS.md; and echo true; or echo false)
check "full: root content kept" user-root (string collect <$c1/AGENTS.md)
check "full: subdir AGENTS.md real, content kept" user-scoped (test -L $c1/functions/AGENTS.md; or string collect <$c1/functions/AGENTS.md)
check "full: docs/plans restored as a real dir" a-plan (test -L $c1/docs/plans; or string collect <$c1/docs/plans/p.md)
check "full: no .gitkeep in docs/plans" false (test -e $c1/docs/plans/.gitkeep; and echo true; or echo false)
check "full: docs/superpowers removed" false (test -e $c1/docs/superpowers -o -L $c1/docs/superpowers; and echo true; or echo false)
check "full: git key set" true (git -C $c1 config --type=bool --get agents-init.disabled)
check "full: .gitignore keeps only user lines" keep-me (string collect <$c1/.gitignore)
check "full: nothing committed to the outer repo" "" (git -C $c1 rev-list --all 2>/dev/null)
set -l cb $XDG_STATE_HOME/agents-cleanup/*.bundle
check "full: one bundle written" 1 (count $cb)
set -l restored (mktemp -d)
set -a TMPDIRS $restored
git clone -q $cb[1] $restored/AGENTS 2>/dev/null
check "full: bundle restores the history" user-root (string collect <$restored/AGENTS/AGENTS.md)
pushd $c1 >/dev/null
agents-init --silent 2>/dev/null
popd >/dev/null
check "full: agents-init no longer scaffolds" false (test -e $c1/AGENTS; and echo true; or echo false)
pushd $c1 >/dev/null
set -l c2out (agents-cleanup --quiet 2>&1)
set -l c2rc $status
popd >/dev/null
check "rerun: exits 0" 0 "$c2rc"
check "rerun: quiet prints nothing" "" "$c2out"
section "agents-cleanup: stub AGENTS.md"
fresh_state
set -l s1 (new_repo)
pushd $s1 >/dev/null
agents-init --silent 2>/dev/null
agents-cleanup --silent 2>/dev/null
popd >/dev/null
check "stub: deleted" false (test -e $s1/AGENTS.md -o -L $s1/AGENTS.md; and echo true; or echo false)
check "stub: untracked blocks-only .gitignore deleted" false (test -e $s1/.gitignore; and echo true; or echo false)
section "agents-cleanup: tracked blocks-only .gitignore is emptied, not deleted"
fresh_state
set -l g1 (new_repo)
pushd $g1 >/dev/null
agents-init --silent 2>/dev/null
git add .gitignore
git commit -qm gi
agents-cleanup --silent 2>/dev/null
popd >/dev/null
check "tracked .gitignore: still exists" true (test -f $g1/.gitignore; and echo true; or echo false)
check "tracked .gitignore: emptied" "" (string collect <$g1/.gitignore)
check "tracked .gitignore: index untouched (unstaged change only)" " M .gitignore" (git -C $g1 status --porcelain -- .gitignore)
section "agents-cleanup: directive stripped from a user AGENTS.md"
fresh_state
set -l t1 (new_repo)
printf '%s\n' '# Title' '' '> ⚠️ **SYSTEM DIRECTIVE FOR AI AGENTS: FILE EDITING**' '> write to AGENTS/AGENTS.md' '' body >$t1/AGENTS.md
pushd $t1 >/dev/null
agents-init --silent 2>/dev/null
agents-cleanup --silent 2>/dev/null
popd >/dev/null
check "directive: blockquote and its trailing blank line removed" (printf '%s\n' '# Title' '' body | string collect) (string collect <$t1/AGENTS.md)
section "agents-cleanup: --drop-extras"
fresh_state
set -l x2 (scaffolded_repo)
echo notes >$x2/AGENTS/notes.md
pushd $x2 >/dev/null
set -l x2rc (agents-cleanup --drop-extras --silent 2>/dev/null; echo $status)
popd >/dev/null
check "drop-extras: exits 0" 0 "$x2rc"
check "drop-extras: AGENTS/ removed" false (test -e $x2/AGENTS; and echo true; or echo false)
set -l xb $XDG_STATE_HOME/agents-cleanup/*.bundle
set -l xr (mktemp -d)
set -a TMPDIRS $xr
git clone -q $xb[1] $xr/AGENTS 2>/dev/null
check "drop-extras: dropped file is in the bundle" notes (string collect <$xr/AGENTS/notes.md)
section "agents-cleanup: rejected final commit"
fresh_state
set -l f1 (scaffolded_repo)
set -l hooks (mktemp -d)
set -a TMPDIRS $hooks
printf '%s\n' '#!/bin/sh' 'exit 1' >$hooks/pre-commit
chmod +x $hooks/pre-commit
git -C $f1/AGENTS config core.hooksPath $hooks
echo change >>$f1/AGENTS/AGENTS.md
pushd $f1 >/dev/null
set -l frc (agents-cleanup --silent 2>/dev/null; echo $status)
popd >/dev/null
check "rejected commit: exits 1" 1 "$frc"
check "rejected commit: AGENTS/ kept" true (test -d $f1/AGENTS/.git; and echo true; or echo false)
check "rejected commit: root link untouched" AGENTS/AGENTS.md (readlink $f1/AGENTS.md)
set -l fb $XDG_STATE_HOME/agents-cleanup/*.bundle
check "rejected commit: no bundle" 0 (count $fb)
cleanup cleanup
report report