feat(agents-vault): migrate entries when a project's slug changes

Adding a remote to a previously remote-less project changes its slug. Left
unhandled, the link step repinned the live memory symlink to a fresh empty
entry and orphaned the real memory.

The previous slug is read from the live symlink target rather than guessed,
which covers a remote being added, rewritten, or removed. When both the old
and new entries hold content the migration is ambiguous, so nothing moves
and the user is directed to --adopt.
This commit is contained in:
2026-09-03 18:56:45 -04:00
parent 2d8db42b12
commit b7ff4e0981
2 changed files with 116 additions and 5 deletions
+58 -5
View File
@@ -35,6 +35,14 @@
# of megabytes per project, growing per session). Paths are allowlisted,
# never denylisted, so nothing new upstream adds can leak in.
#
# Because the slug is derived from the remote, gaining, losing, or
# rewriting a project's origin changes it. Each run detects this by
# reading the previous slug straight off the live memory symlink's
# target (no guessing) and migrates that entry to the new slug before
# relinking, so memory accumulated under the old key is never orphaned.
# If both the old and new entries already hold content the migration is
# ambiguous and is refused; resolve it with --adopt=SLUG.
#
# ARGUMENTS
# --link Scaffold the vault and link this project's memory; skip
# the final commit
@@ -202,6 +210,56 @@ function agents-vault --description 'track curated agent memory in a host-scoped
set -l entry "$vault/projects/$slug"
set -l vmem "$entry/claude/memory"
set -l claude_root $__fish_agent_vault_claude_root
test -n "$claude_root"; or set claude_root "$HOME/.claude/projects"
set -l mangled (string replace -a '/' '-' -- "$root" | string replace -a '.' '-')
set -l live "$claude_root/$mangled/memory"
# ── Slug migration (spec 4.5) ─────────────────────────────────────
# The previous slug never has to be guessed: the live symlink still
# points at the old entry. This covers a remote being added, a
# remote URL being rewritten, and a remote being removed.
set -l prev_slug ""
if test -L "$live"
set -l tgt (path resolve "$live")
set -l pdir (path resolve "$vault/projects")
if string match -q "$pdir/*" -- "$tgt"
set -l rest (string replace "$pdir/" "" -- "$tgt")
set prev_slug (string split -f1 '/' -- $rest)
end
end
if test -z "$prev_slug"
# No link yet (fresh machine): try the path-derived candidate.
set -l cand_rp (path resolve "$root")
set -l cand_base (string lower -- (path basename "$cand_rp"))
set -l cand_digest (printf '%s' "$cand_rp" | sha256sum | string split -f1 ' ')
set -l cand "local-$cand_base-"(string sub -l 8 -- "$cand_digest")
if test "$cand" != "$slug"; and test -d "$vault/projects/$cand"
set prev_slug $cand
end
end
if test -n "$prev_slug"; and test "$prev_slug" != "$slug"
set -l prev_mem "$vault/projects/$prev_slug/claude/memory"
set -l cur_content
test -d "$vmem"; and set cur_content (command ls -A "$vmem" 2>/dev/null)
if test (count $cur_content) -gt 0
echo "$c_err""agents-vault: cannot migrate $prev_slug$slug; both entries hold content.$c_reset" >&2
echo "$c_err"" Resolve with: agents-vault --adopt=SLUG$c_reset" >&2
return 1
end
test -d "$vmem"; and rm -rf "$vault/projects/$slug"
mkdir -p (path dirname "$vault/projects/$slug")
if not git -C "$vault" mv "projects/$prev_slug" "projects/$slug" 2>/dev/null
command mv "$vault/projects/$prev_slug" "$vault/projects/$slug"; or return 1
end
printf 'renamed: %s → %s (%s)\n' "$prev_slug" "$slug" (date -I) \
>>"$vault/projects/$slug/origin"
rm -f "$live"
set changed 1
test $verbose -eq 1; and echo "$c_ok→ Migrated vault entry $prev_slug$slug$c_reset"
end
if not test -d "$vmem"
if not mkdir -p "$vmem"
echo "$c_err""agents-vault: could not create $vmem$c_reset" >&2
@@ -210,11 +268,6 @@ function agents-vault --description 'track curated agent memory in a host-scoped
set changed 1
end
set -l claude_root $__fish_agent_vault_claude_root
test -n "$claude_root"; or set claude_root "$HOME/.claude/projects"
set -l mangled (string replace -a '/' '-' -- "$root" | string replace -a '.' '-')
set -l live "$claude_root/$mangled/memory"
# Unconditional: this is the emergent-restore path. On a freshly
# cloned vault, $vmem already exists (populated from the clone) and
# $live does not exist yet -- skipping the link here would silently
+58
View File
@@ -310,6 +310,64 @@ check "link failure: no link was left behind" false (test -L $croot3/$mangled3/m
set -e __fish_agent_vault_dir
set -e __fish_agent_vault_claude_root
# ────────────────────────── slug migration ─────────────────────────────
echo ""
echo "== agents-vault (slug migration) =="
set -l vroot2 (mktemp -d); set -ga TMPDIRS $vroot2
set -l croot2 (mktemp -d); set -ga TMPDIRS $croot2
set -g __fish_agent_vault_dir $vroot2/agent-vault
set -g __fish_agent_vault_claude_root $croot2
# Start with no remote, so the project is keyed local-*.
set -l mp (new_repo)
set -l mmangled (string replace -a '/' '-' -- $mp | string replace -a '.' '-')
mkdir -p $croot2/$mmangled/memory
echo "precious" >$croot2/$mmangled/memory/keep.md
pushd $mp >/dev/null
agents-vault --silent
set -l local_slug (_agents_repo_slug $mp)
popd >/dev/null
check "local entry populated" precious (cat $vroot2/agent-vault/projects/$local_slug/claude/memory/keep.md)
# Now add a remote: the slug changes and the entry must migrate.
git -C $mp remote add origin https://git.rootiest.dev/rootiest/later.git
pushd $mp >/dev/null
agents-vault --silent
popd >/dev/null
set -l new_slug git.rootiest.dev-rootiest-later
check "migrated to remote slug" precious (cat $vroot2/agent-vault/projects/$new_slug/claude/memory/keep.md)
check "old entry removed" false (test -d $vroot2/agent-vault/projects/$local_slug; and echo true; or echo false)
check "memory still reachable live" precious (cat $croot2/$mmangled/memory/keep.md)
check "link repinned to new entry" (path resolve $vroot2/agent-vault/projects/$new_slug/claude/memory) (path resolve $croot2/$mmangled/memory)
check "rename recorded in origin" true (grep -q "$local_slug" $vroot2/agent-vault/projects/$new_slug/origin; and echo true; or echo false)
# Ambiguous migration: both entries hold content. Nothing may move.
set -l amb (new_repo)
set -l amangled (string replace -a '/' '-' -- $amb | string replace -a '.' '-')
mkdir -p $croot2/$amangled/memory
echo old >$croot2/$amangled/memory/x.md
pushd $amb >/dev/null
agents-vault --silent
set -l aslug (_agents_repo_slug $amb)
popd >/dev/null
git -C $amb remote add origin https://git.rootiest.dev/rootiest/clash.git
mkdir -p $vroot2/agent-vault/projects/git.rootiest.dev-rootiest-clash/claude/memory
echo new >$vroot2/agent-vault/projects/git.rootiest.dev-rootiest-clash/claude/memory/y.md
pushd $amb >/dev/null
set -l arc (agents-vault --silent 2>/dev/null; echo $status)
popd >/dev/null
check "ambiguous migration fails" 1 "$arc"
check "ambiguous leaves old entry" old (cat $vroot2/agent-vault/projects/$aslug/claude/memory/x.md)
check "ambiguous leaves new entry" new (cat $vroot2/agent-vault/projects/git.rootiest.dev-rootiest-clash/claude/memory/y.md)
set -e __fish_agent_vault_dir
set -e __fish_agent_vault_claude_root
cleanup
echo ""
echo (math $TESTS_RUN - $TESTS_FAILED)"/$TESTS_RUN passed"