feat(agents-vault): back up curated agent memory to a host-scoped vault repo #126

Merged
rootiest merged 19 commits from feat/agent-memory-vault into main 2026-09-03 23:09:57 +00:00
5 changed files with 341 additions and 4 deletions
Showing only changes of commit 2c185f7e23 - Show all commits
+19
View File
@@ -54,6 +54,25 @@ Example: to increase the scrollback history limit:
set -gx SCROLLBACK_HISTORY_MAX_FILES 200
## Agent Memory Vault
__fish_agent_vault_dir
Overrides the agent memory vault location. Defaults to
$XDG_DATA_HOME/agent-vault (or ~/.local/share/agent-vault).
__fish_agent_vault_autopush
When set to 1, agents-vault also pushes on wrapper launch. Defaults to
off: the vault commits locally on every launch and pushes from the
Claude Code SessionEnd hook or an explicit agents-vault --push.
NOTE:
With autopush off and no SessionEnd hook installed, backups accumulate
locally and never reach the remote. Run agents-vault --status to check
how far ahead the vault is.
## Fish Universal Variables
Some settings (fzf colors, theme) are stored in fish_variables via
+7 -4
View File
@@ -10,8 +10,10 @@
# refreshing them when the shipped agents-tools-version: marker is newer
# than the installed copy. Files are made executable. Idempotent: prints
# nothing when the installed tooling is already current, or a short summary
# line when it installed or updated the tooling. Shared by agents-init and
# agents-vault.
# line when it installed or updated the tooling, naming <repo_dir>'s own
# basename rather than a hardcoded caller (e.g. "AGENTS/.agents-tools/" for
# agents-init, "agent-vault/.agents-tools/" for agents-vault). Shared by
# agents-init and agents-vault.
#
# ARGUMENTS
# repo_dir Absolute path to the git repo root to install tooling into
@@ -41,9 +43,10 @@ function _agents_repo_install_tools --argument-names repo_dir
command cp "$src/hooks/prepare-commit-msg" "$dest/hooks/prepare-commit-msg"; or return 1
chmod +x "$dest/version-bump" "$dest/hooks/pre-commit" "$dest/hooks/prepare-commit-msg"; or return 1
set -l label (path basename -- "$repo_dir")
if test -z "$have"
echo "→ Installed AGENTS/.agents-tools/ (version-bump v$want)"
echo "→ Installed $label/.agents-tools/ (version-bump v$want)"
else
echo "→ Updated AGENTS/.agents-tools/ (v$have → v$want)"
echo "→ Updated $label/.agents-tools/ (v$have → v$want)"
end
end
+32
View File
@@ -0,0 +1,32 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _agents_vault_dir
#
# DESCRIPTION
# Prints the agent memory vault root. Honors the universal variable
# __fish_agent_vault_dir when set, otherwise
# ${XDG_DATA_HOME:-$HOME/.local/share}/agent-vault.
#
# The vault holds agy state as well as Claude state, so it is not nested
# under either tool's directory; it is backed-up state rather than
# configuration, hence XDG_DATA_HOME rather than XDG_CONFIG_HOME.
#
# EXIT STATUS
# 0 Always
#
# RETURNS
# The vault root path, one line on stdout.
#
# EXAMPLE
# set -l vault (_agents_vault_dir)
function _agents_vault_dir
if set -q __fish_agent_vault_dir; and test -n "$__fish_agent_vault_dir"
printf '%s\n' "$__fish_agent_vault_dir"
return 0
end
set -l base $XDG_DATA_HOME
test -n "$base"; or set base "$HOME/.local/share"
printf '%s\n' "$base/agent-vault"
end
+242
View File
@@ -0,0 +1,242 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CATEGORY
# 12-ai-and-developer-tools
#
# DEPENDENCIES
# _agents_vault_dir, _agents_repo_slug, _agents_repo_ensure_symlink,
# _agents_repo_sync, _agents_repo_install_tools, git
#
# SYNOPSIS
# agents-vault [--link] [--push] [--restore] [--status]
# [--adopt=SLUG] [--remote=URL]
# [-v | --verbose] [-q | --quiet] [-s | --silent]
# [-h | --help]
#
# DESCRIPTION
# Tracks curated agent memory in a host-scoped git repository so it
# survives losing a machine. Complements agents-init, which scaffolds the
# per-project AGENTS/ repo: that holds the shareable agent specification,
# while this holds the personal memory an agent accumulates.
#
# Memory does not live in any project tree. Claude keeps it under
# ~/.claude/projects/<mangled-path>/memory/ and agy keeps its knowledge
# store under ~/.gemini/antigravity-cli/, both outside every repository.
#
# Entries are keyed by normalized git remote URL rather than by path, so
# the key survives a machine change or a directory rename. The live
# memory directory becomes a symlink into the vault, which makes backup
# and restore the same operation: on a new machine, clone the vault once
# and the first agents-vault run in any project relinks its memory
# automatically. No manifest and no batch restore step are involved.
#
# Only curated memory is tracked. Session transcripts are excluded (tens
# of megabytes per project, growing per session). Paths are allowlisted,
# never denylisted, so nothing new upstream adds can leak in.
#
# ARGUMENTS
# --link Ensure this project's memory link only; do not commit
# --push Commit and push to the vault remote
# --restore Walk the vault, relink what is possible, report the rest
# --status Show entries, link health, remote state, and orphans
# --adopt=SLUG Bind the current project to an existing vault entry
# --remote=URL Set the vault remote
# -v, --verbose Print all per-step output (default)
# -q, --quiet Print one summary line only if changes were made
# -s, --silent Suppress all output; errors only
# -h, --help Show this help message and exit
#
# EXIT STATUS
# 0 Completed successfully
# 1 Fatal error (vault unavailable, git failure, ambiguous migration)
#
# EXAMPLE
# agents-vault
# agents-vault --status
# agents-vault --remote=https://git.rootiest.dev/rootiest/agent-vault.git
# agents-vault --push
#
# NOTES
# Set __fish_agent_vault_dir to relocate the vault. Set
# __fish_agent_vault_autopush to 1 to also push on wrapper launch;
# it defaults to off so a backgrounded push can never hang or prompt
# invisibly underneath a starting agent.
function agents-vault --description 'track curated agent memory in a host-scoped vault repo'
set -l c_head (set_color --bold cyan)
set -l c_cmd (set_color --bold)
set -l c_flag (set_color yellow)
set -l c_ok (set_color green)
set -l c_warn (set_color yellow)
set -l c_dim (set_color brblack)
set -l c_err (set_color red)
set -l c_reset (set_color normal)
argparse h/help link push restore status 'adopt=' 'remote=' \
v/verbose q/quiet s/silent -- $argv
or return 1
if set -q _flag_help
echo "$c_head""Usage:$c_reset $c_cmd""agents-vault$c_reset $c_flag""[--link] [--push] [--restore] [--status] [--adopt=SLUG] [--remote=URL] [-v] [-q] [-s] [-h]$c_reset"
echo
echo " Track curated agent memory in a host-scoped vault repository."
echo
echo "$c_head""Options:$c_reset"
echo " $c_flag-h$c_reset, $c_flag--help$c_reset Show this help message"
echo " $c_flag--link$c_reset Ensure this project's memory link only"
echo " $c_flag--push$c_reset Commit and push to the vault remote"
echo " $c_flag--restore$c_reset Relink everything possible, report the rest"
echo " $c_flag--status$c_reset Show entries, link health, remote, orphans"
echo " $c_flag--adopt$c_reset=SLUG Bind this project to an existing vault entry"
echo " $c_flag--remote$c_reset=URL Set the vault remote"
echo " $c_flag-v$c_reset, $c_flag--verbose$c_reset Print all per-step output (default)"
echo " $c_flag-q$c_reset, $c_flag--quiet$c_reset Print one summary line only if changed"
echo " $c_flag-s$c_reset, $c_flag--silent$c_reset Suppress all output; errors only"
return 0
end
set -l verbose 1
set -l quiet 0
if set -q _flag_silent
set verbose 0
else if set -q _flag_quiet
set verbose 0
set quiet 1
end
if not type -q git
echo "$c_err""agents-vault: git is required$c_reset" >&2
return 1
end
set -l vault (_agents_vault_dir)
set -l changed 0
set -l did_init 0
# ────────────────────── ensure the vault repo ──────────────────────
if not test -d "$vault"
if not mkdir -p "$vault"
echo "$c_err""agents-vault: could not create $vault$c_reset" >&2
return 1
end
set changed 1
set did_init 1
end
if not test -d "$vault/.git"
git -C "$vault" init -q
or begin
echo "$c_err""agents-vault: git init failed in $vault$c_reset" >&2
return 1
end
set changed 1
set did_init 1
test $verbose -eq 1; and echo "$c_ok→ Initialized vault repo at $vault$c_reset"
end
test -f "$vault/.version"; or echo 1.0.0 >"$vault/.version"
if not test -f "$vault/.gitignore"
printf '%s\n' \
'# SQLite sidecars are never safe to commit mid-write.' \
'*.db-wal' \
'*.db-shm' >"$vault/.gitignore"
set changed 1
end
if not test -f "$vault/README.md"
printf '%s\n' \
'# Agent Memory Vault' \
'' \
'Curated agent memory, tracked so it survives losing a machine.' \
'Managed by `agents-vault` from rootiest/fish-config.' \
'' \
'## Restore' \
'' \
'Clone this repository to the path `agents-vault` resolves to' \
'(`$XDG_DATA_HOME/agent-vault`, or `$__fish_agent_vault_dir`).' \
'Then simply run `claude` in any project: the wrapper derives that' \
"project's slug, finds its entry here, and relinks the live memory" \
'directory automatically. There is no separate restore step.' \
'' \
'Entries are keyed by normalized git remote URL. A `local-*` key' \
'belongs to a project with no remote and is machine-specific;' \
'rebind one with `agents-vault --adopt=SLUG`.' >"$vault/README.md"
set changed 1
end
set -l tools_msg (_agents_repo_install_tools "$vault")
if test -n "$tools_msg"
set changed 1
test $verbose -eq 1; and echo "$c_ok$tools_msg$c_reset"
end
set -l hp (git -C "$vault" config --local core.hooksPath 2>/dev/null)
if test "$hp" != .agents-tools/hooks
git -C "$vault" config --local core.hooksPath .agents-tools/hooks
set changed 1
end
# ─────────────────────── unimplemented modes ───────────────────────
for f in _flag_push _flag_restore _flag_status _flag_adopt _flag_remote
if set -q $f
echo "$c_err""agents-vault: that mode is not implemented yet$c_reset" >&2
return 1
end
end
# ──────────────────── link the current project ─────────────────────
set -l root (git rev-parse --show-toplevel 2>/dev/null)
if test -n "$root"
set -l slug (_agents_repo_slug "$root")
set -l entry "$vault/projects/$slug"
set -l vmem "$entry/claude/memory"
if not test -d "$vmem"
mkdir -p "$vmem"; or return 1
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"
if test -d "$claude_root/$mangled"; or test -d "$live"
set -l link_msg (_agents_repo_ensure_symlink "$live" "$vmem")
if test -n "$link_msg"
set changed 1
test $verbose -eq 1; and echo "$c_ok$link_msg$c_reset"
end
end
if not test -f "$entry/origin"
set -l url (git -C "$root" remote get-url origin 2>/dev/null)
test -n "$url"; or set url "(none)"
printf 'remote: %s\npath: %s\nhost: %s\n' \
"$url" "$root" (hostname 2>/dev/null) >"$entry/origin"
set changed 1
end
end
# ───────────────────────────── commit ──────────────────────────────
if not set -q _flag_link
set -l msg "chore: sync agent memory vault"
test $did_init -eq 1; and set msg "chore: initialize agent memory vault"
set -l sync_out (_agents_repo_sync "$vault" "$msg")
set -l sync_rc $status
if test $sync_rc -eq 2
echo "$c_warn""agents-vault: unresolved rebase conflict in the vault; nothing committed$c_reset" >&2
else if test -n "$sync_out"
set changed 1
test $verbose -eq 1; and echo "$c_ok$sync_out$c_reset"
end
end
if test $quiet -eq 1; and test $changed -eq 1
if test $did_init -eq 1
echo "$c_ok→ Initialized agent memory vault$c_reset"
else
echo "$c_ok→ Synced agent memory vault$c_reset"
end
end
end
+41
View File
@@ -202,6 +202,47 @@ set -l hrc (_agents_repo_sync $h "chore: blocked" 2>/dev/null; echo $status)
check "commit hook rejection returns 1" 1 "$hrc"
check "commit hook rejection commits nothing" $before_hook_count (git -C $h rev-list --count HEAD)
# ──────────────────────── vault scaffold + link ────────────────────────
echo ""
echo "== agents-vault (scaffold + link) =="
set -l vroot (mktemp -d); set -ga TMPDIRS $vroot
set -l croot (mktemp -d); set -ga TMPDIRS $croot
set -g __fish_agent_vault_dir $vroot/agent-vault
set -g __fish_agent_vault_claude_root $croot
set -l proj (new_repo https://git.rootiest.dev/rootiest/fish-config.git)
set -l pslug git.rootiest.dev-rootiest-fish-config
# Seed a live memory directory the way Claude would.
set -l mangled (string replace -a '/' '-' -- $proj | string replace -a '.' '-')
mkdir -p $croot/$mangled/memory
echo "a memory" >$croot/$mangled/memory/thing.md
pushd $proj >/dev/null
agents-vault --silent
popd >/dev/null
check "vault repo created" true (test -d $vroot/agent-vault/.git; and echo true; or echo false)
check "vault version seeded" 1.0.0 (cat $vroot/agent-vault/.version 2>/dev/null)
check "entry created for slug" true (test -d $vroot/agent-vault/projects/$pslug/claude/memory; and echo true; or echo false)
check "live memory is now a link" true (test -L $croot/$mangled/memory; and echo true; or echo false)
check "memory content preserved" "a memory" (cat $croot/$mangled/memory/thing.md)
check "content lives in the vault" "a memory" (cat $vroot/agent-vault/projects/$pslug/claude/memory/thing.md)
check "origin file written" true (test -f $vroot/agent-vault/projects/$pslug/origin; and echo true; or echo false)
check "vault committed" true (test (git -C $vroot/agent-vault rev-list --count HEAD) -ge 1; and echo true; or echo false)
# Idempotence: a second run prints nothing and adds no commit.
set -l before (git -C $vroot/agent-vault rev-list --count HEAD)
pushd $proj >/dev/null
set -l again (agents-vault --verbose)
popd >/dev/null
check "second run is silent" "" "$again"
check "second run adds no commit" $before (git -C $vroot/agent-vault rev-list --count HEAD)
set -e __fish_agent_vault_dir
set -e __fish_agent_vault_claude_root
cleanup
echo ""
echo (math $TESTS_RUN - $TESTS_FAILED)"/$TESTS_RUN passed"