From 19126316a7f2609f5b733048b5bdc491e061476f Mon Sep 17 00:00:00 2001 From: Rootiest Date: Wed, 2 Sep 2026 21:55:24 -0400 Subject: [PATCH] fix(agents-vault): always link the current project's memory The guard around the symlink step only linked when the live Claude project directory already existed, which is exactly backwards for the clone-onto-a-new-machine restore case: a freshly cloned vault entry would be silently left unlinked and a starting agent would write fresh, history-less memory instead. _agents_repo_ensure_symlink already makes its own parent directories and is idempotent, so nothing depended on the guard; it is removed and the link is now attempted unconditionally. Also stop swallowing a refused or failed link as success: the helper's exit status is now checked, and agents-vault reports its own error and exits 1 instead of silently continuing with no link in place. Smaller fixes from the same review pass: - check the exit status of _agents_repo_install_tools and the core.hooksPath git config write, instead of discarding both - give the vmem mkdir failure a stderr message like every other fatal in the function - guard hostname with type -q and add it to DEPENDENCIES - .version creation now sets changed, so --link (which skips the commit step) reports it in --quiet mode - reword --link's help/doc text: it still scaffolds the vault and links memory, it only skips the final commit - drop the unused c_dim color variable - move the __fish_agent_vault_dir / __fish_agent_vault_autopush documentation below Opinionated Components so its NOTE: callout (now flush-left so it actually renders as a Starlight Aside, per review) doesn't become the first Note aside in the page and steal the existing test's assertions about the original 4-bullet one Adds two tests: pre-seeded vault entry with no live directory at all (the restore path the guard was breaking), and a forced link failure asserting agents-vault now exits 1 instead of 0. --- docs/manual/07-customization.md | 38 +++++++++---------- functions/agents-vault.fish | 55 ++++++++++++++++++++------- tests/test-agents-vault.fish | 67 +++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 32 deletions(-) diff --git a/docs/manual/07-customization.md b/docs/manual/07-customization.md index f787f27..e5f1a1a 100644 --- a/docs/manual/07-customization.md +++ b/docs/manual/07-customization.md @@ -54,25 +54,6 @@ 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 @@ -178,6 +159,25 @@ interactively. See [Components Reference](/08-components-reference/) for the full sub-category breakdown of every category. +## 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. + + ## Prompt and Theme ### Starship diff --git a/functions/agents-vault.fish b/functions/agents-vault.fish index 99f6638..1971682 100644 --- a/functions/agents-vault.fish +++ b/functions/agents-vault.fish @@ -6,7 +6,7 @@ # # DEPENDENCIES # _agents_vault_dir, _agents_repo_slug, _agents_repo_ensure_symlink, -# _agents_repo_sync, _agents_repo_install_tools, git +# _agents_repo_sync, _agents_repo_install_tools, git, hostname # # SYNOPSIS # agents-vault [--link] [--push] [--restore] [--status] @@ -36,7 +36,8 @@ # never denylisted, so nothing new upstream adds can leak in. # # ARGUMENTS -# --link Ensure this project's memory link only; do not commit +# --link Scaffold the vault and link this project's memory; skip +# the final 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 @@ -68,7 +69,6 @@ function agents-vault --description 'track curated agent memory in a host-scoped 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) @@ -83,7 +83,7 @@ function agents-vault --description 'track curated agent memory in a host-scoped 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--link$c_reset Scaffold + link this project; skip the commit" 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" @@ -133,7 +133,10 @@ function agents-vault --description 'track curated agent memory in a host-scoped 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/.version" + echo 1.0.0 >"$vault/.version" + set changed 1 + end if not test -f "$vault/.gitignore" printf '%s\n' \ @@ -165,6 +168,10 @@ function agents-vault --description 'track curated agent memory in a host-scoped end set -l tools_msg (_agents_repo_install_tools "$vault") + or begin + echo "$c_err""agents-vault: could not install .agents-tools/ into $vault$c_reset" >&2 + return 1 + end if test -n "$tools_msg" set changed 1 test $verbose -eq 1; and echo "$c_ok$tools_msg$c_reset" @@ -173,6 +180,10 @@ function agents-vault --description 'track curated agent memory in a host-scoped 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 + or begin + echo "$c_err""agents-vault: could not set core.hooksPath in $vault$c_reset" >&2 + return 1 + end set changed 1 end @@ -192,7 +203,10 @@ function agents-vault --description 'track curated agent memory in a host-scoped set -l vmem "$entry/claude/memory" if not test -d "$vmem" - mkdir -p "$vmem"; or return 1 + if not mkdir -p "$vmem" + echo "$c_err""agents-vault: could not create $vmem$c_reset" >&2 + return 1 + end set changed 1 end @@ -201,19 +215,34 @@ function agents-vault --description 'track curated agent memory in a host-scoped 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 + # 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 + # leave the clone's memory unlinked and let a starting agent write + # fresh, history-less memory instead. _agents_repo_ensure_symlink is + # idempotent and makes its own parent directories, so there is + # nothing this guard would protect that the helper does not already + # handle on its own. + set -l link_msg (_agents_repo_ensure_symlink "$live" "$vmem") + set -l link_rc $status + if test $link_rc -ne 0 + echo "$c_err""agents-vault: could not link $live$c_reset" >&2 + return 1 + end + if test -n "$link_msg" + set changed 1 + test $verbose -eq 1; and echo "$c_ok$link_msg$c_reset" 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)" + set -l host "" + if type -q hostname + set host (hostname 2>/dev/null) + end printf 'remote: %s\npath: %s\nhost: %s\n' \ - "$url" "$root" (hostname 2>/dev/null) >"$entry/origin" + "$url" "$root" "$host" >"$entry/origin" set changed 1 end end diff --git a/tests/test-agents-vault.fish b/tests/test-agents-vault.fish index 15f5753..289c587 100644 --- a/tests/test-agents-vault.fish +++ b/tests/test-agents-vault.fish @@ -243,6 +243,73 @@ check "second run adds no commit" $before (git -C $vroot/agent-vault rev-list -- set -e __fish_agent_vault_dir set -e __fish_agent_vault_claude_root +# ─────────────────── emergent restore (vault -> live) ────────────────── +# The reverse of the adoption case above: a vault entry already has curated +# memory (as if cloned from a remote onto a fresh machine) but the live +# Claude project directory does not exist at all yet. agents-vault must +# still create the link so the pre-existing memory is what a starting +# agent sees, rather than silently skipping the link because there was +# nothing local to notice yet. +echo "" +echo "== agents-vault (emergent restore) ==" + +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 + +set -l proj2 (new_repo https://git.rootiest.dev/rootiest/agent-vault.git) +set -l pslug2 git.rootiest.dev-rootiest-agent-vault +set -l mangled2 (string replace -a '/' '-' -- $proj2 | string replace -a '.' '-') + +# Pre-seed the vault entry the way a cloned vault would already have it. +# Deliberately no $croot2/$mangled2 directory at all -- not even the +# project's own entry, let alone a memory/ subdirectory -- so the fix under +# test is exercised: linking must not depend on the live side existing. +mkdir -p $vroot2/agent-vault/projects/$pslug2/claude/memory +echo "restored memory" >$vroot2/agent-vault/projects/$pslug2/claude/memory/old.md + +pushd $proj2 >/dev/null +agents-vault --silent +popd >/dev/null + +check "restore: live memory link created with no prior live dir" true (test -L $croot2/$mangled2/memory; and echo true; or echo false) +check "restore: pre-existing vault content readable through the link" "restored memory" (cat $croot2/$mangled2/memory/old.md 2>/dev/null) + +set -e __fish_agent_vault_dir +set -e __fish_agent_vault_claude_root + +# ─────────────────── a refused link is reported as failure ───────────── +# _agents_repo_ensure_symlink refuses (exit 1, no stdout) when it cannot +# make the link -- e.g. its mkdir -p of the link's parent directory fails. +# agents-vault must surface that as its own exit 1, not swallow it and +# report success because stdout happened to be empty either way. +echo "" +echo "== agents-vault (link failure surfaces as exit 1) ==" + +set -l vroot3 (mktemp -d); set -ga TMPDIRS $vroot3 +set -l croot3 (mktemp -d); set -ga TMPDIRS $croot3 +set -g __fish_agent_vault_dir $vroot3/agent-vault +set -g __fish_agent_vault_claude_root $croot3 + +set -l proj3 (new_repo https://git.rootiest.dev/rootiest/link-fail-test.git) +set -l mangled3 (string replace -a '/' '-' -- $proj3 | string replace -a '.' '-') + +# Make the mangled project path a plain FILE. _agents_repo_ensure_symlink's +# own `mkdir -p (path dirname $link)` then fails outright (ENOTDIR), which +# is exactly the class of failure (permissions, ENOSPC, ...) this guards. +touch $croot3/$mangled3 + +pushd $proj3 >/dev/null +set -l rc (agents-vault --silent 2>/dev/null; echo $status) +popd >/dev/null + +check "link failure: agents-vault exits 1" 1 "$rc" +check "link failure: no link was left behind" false (test -L $croot3/$mangled3/memory; and echo true; or echo false) + +set -e __fish_agent_vault_dir +set -e __fish_agent_vault_claude_root + cleanup echo "" echo (math $TESTS_RUN - $TESTS_FAILED)"/$TESTS_RUN passed"