diff --git a/docs/manual/07-customization.md b/docs/manual/07-customization.md index abfd609..f787f27 100644 --- a/docs/manual/07-customization.md +++ b/docs/manual/07-customization.md @@ -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 diff --git a/functions/_agents_repo_install_tools.fish b/functions/_agents_repo_install_tools.fish index 70f7e77..95aeb83 100644 --- a/functions/_agents_repo_install_tools.fish +++ b/functions/_agents_repo_install_tools.fish @@ -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 '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 diff --git a/functions/_agents_vault_dir.fish b/functions/_agents_vault_dir.fish new file mode 100644 index 0000000..e2de353 --- /dev/null +++ b/functions/_agents_vault_dir.fish @@ -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 diff --git a/functions/agents-vault.fish b/functions/agents-vault.fish new file mode 100644 index 0000000..99f6638 --- /dev/null +++ b/functions/agents-vault.fish @@ -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//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 diff --git a/tests/test-agents-vault.fish b/tests/test-agents-vault.fish index d1827dc..15f5753 100644 --- a/tests/test-agents-vault.fish +++ b/tests/test-agents-vault.fish @@ -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"