feat(agents-vault): back up curated agent memory to a host-scoped vault repo #126
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user