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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user