feat(agents-vault): scaffold the vault and link project memory

Creates the vault repo on demand, reusing the AGENTS version bumper and
hook shims, then links the current project's live memory directory into
its slug-keyed entry and commits.

Because the live directory becomes a symlink into the vault, backup and
restore are the same operation: a cloned vault relinks itself on the next
run in each project, with no manifest and no batch restore step.

Also fixes _agents_repo_install_tools' progress messages, which hardcoded
the literal "AGENTS/.agents-tools/" even for callers writing elsewhere:
they now name repo_dir's own basename, so agents-vault reports its own
directory instead of a false AGENTS/ path.
This commit is contained in:
2026-09-03 18:56:45 -04:00
parent b0585d00ad
commit 2c185f7e23
5 changed files with 341 additions and 4 deletions
+19
View File
@@ -54,6 +54,25 @@ Example: to increase the scrollback history limit:
set -gx SCROLLBACK_HISTORY_MAX_FILES 200 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 ## Fish Universal Variables
Some settings (fzf colors, theme) are stored in fish_variables via 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 # refreshing them when the shipped agents-tools-version: marker is newer
# than the installed copy. Files are made executable. Idempotent: prints # than the installed copy. Files are made executable. Idempotent: prints
# nothing when the installed tooling is already current, or a short summary # 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 # line when it installed or updated the tooling, naming <repo_dir>'s own
# agents-vault. # 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 # ARGUMENTS
# repo_dir Absolute path to the git repo root to install tooling into # 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 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 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" if test -z "$have"
echo "→ Installed AGENTS/.agents-tools/ (version-bump v$want)" echo "→ Installed $label/.agents-tools/ (version-bump v$want)"
else else
echo "→ Updated AGENTS/.agents-tools/ (v$have → v$want)" echo "→ Updated $label/.agents-tools/ (v$have → v$want)"
end end
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 returns 1" 1 "$hrc"
check "commit hook rejection commits nothing" $before_hook_count (git -C $h rev-list --count HEAD) 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 cleanup
echo "" echo ""
echo (math $TESTS_RUN - $TESTS_FAILED)"/$TESTS_RUN passed" echo (math $TESTS_RUN - $TESTS_FAILED)"/$TESTS_RUN passed"