diff --git a/functions/agents-vault.fish b/functions/agents-vault.fish index 6cdd3ea..baebb21 100644 --- a/functions/agents-vault.fish +++ b/functions/agents-vault.fish @@ -84,6 +84,12 @@ # it defaults to off so a backgrounded push can never hang or prompt # invisibly underneath a starting agent. # +# The agy knowledge copy is merge-only. Files are copied into the vault +# but are never removed from it, so a fact deleted upstream from agy's +# knowledge store persists in the vault indefinitely, and a restore or a +# fresh clone brings it back. Prune such an entry from the vault by hand +# if it must really be gone. +# # Three further variables exist only so the test suite can run against # throwaway directories instead of the real home, and are not meant for # everyday use. __fish_agent_vault_claude_root overrides Claude's @@ -288,20 +294,36 @@ function agents-vault --description 'track curated agent memory in a host-scoped # it). Never out of thin air -- ~/.claude/memory does not exist by # default, and fabricating it would invent state Claude never asked # for and permanently claim the path. - if test -d "$glive"; or test -L "$glive"; or test (count $gvault_content) -gt 0 + # -e rather than -d on the live side so a *broken* global path (a stray + # regular file where the directory belongs) is noticed and reported on + # every run, instead of being silently skipped and mistaken for the + # absent-by-default case. + if test -e "$glive"; or test -L "$glive"; or test (count $gvault_content) -gt 0 + # Best-effort, exactly like the agy copy above. Global memory is + # optional and frequently absent, and this block runs before the + # per-project link, so a fault here must warn and continue rather + # than return: aborting would let a stray file or a permission + # problem at ~/.claude/memory kill the per-project memory backup + # and its commit for every project, on every agent launch. The + # secondary feature must not take the primary one down with it. + # + # Continuing is safe: _agents_repo_ensure_symlink validates and + # refuses before it mutates anything, so its failure window is the + # same whether the caller returns or carries on. $changed stays + # untouched unless the link actually succeeded, and nothing is + # recorded to make a later run believe the global memory is linked + # when it is not -- the next run re-enters this block and retries. if not mkdir -p "$gvault" - echo "$c_err""agents-vault: could not create $gvault$c_reset" >&2 - return 1 - end - set -l gmsg (_agents_repo_ensure_symlink "$glive" "$gvault") - set -l grc $status - if test $grc -ne 0 - echo "$c_err""agents-vault: could not link $glive$c_reset" >&2 - return 1 - end - if test -n "$gmsg" - set changed 1 - test $verbose -eq 1; and echo "$c_ok$gmsg$c_reset" + echo "$c_warn""agents-vault: could not create $gvault; skipping global memory$c_reset" >&2 + else + set -l gmsg (_agents_repo_ensure_symlink "$glive" "$gvault") + set -l grc $status + if test $grc -ne 0 + echo "$c_warn""agents-vault: could not link $glive; global memory not backed up$c_reset" >&2 + else if test -n "$gmsg" + set changed 1 + test $verbose -eq 1; and echo "$c_ok$gmsg$c_reset" + end end end diff --git a/tests/test-agents-vault.fish b/tests/test-agents-vault.fish index 1f5a400..33fbdec 100644 --- a/tests/test-agents-vault.fish +++ b/tests/test-agents-vault.fish @@ -620,6 +620,65 @@ set -e __fish_agent_vault_claude_root set -g __fish_agent_vault_claude_home $HERMETIC_HOME/claude set -g __fish_agent_vault_agy_root $HERMETIC_HOME/agy +# ────────────── a failed global link must not abort the run ──────────── +# The global block runs before the per-project link and before the commit, +# and global memory is optional and frequently absent. A fault there must +# never take the primary per-project backup down with it: otherwise a +# stray file or a permission problem at ~/.claude/memory would break +# memory backup for every project, on every agent launch. +echo "" +echo "== agents-vault (global link failure is non-fatal) ==" + +set -l vroot8 (mktemp -d); set -ga TMPDIRS $vroot8 +set -l croot8 (mktemp -d); set -ga TMPDIRS $croot8 +set -l chome8 (mktemp -d); set -ga TMPDIRS $chome8 +set -l agy8 (mktemp -d); set -ga TMPDIRS $agy8 +set -g __fish_agent_vault_dir $vroot8/agent-vault +set -g __fish_agent_vault_claude_root $croot8 +set -g __fish_agent_vault_claude_home $chome8 +set -g __fish_agent_vault_agy_root $agy8 + +# A regular FILE where the global memory directory belongs. +# _agents_repo_ensure_symlink refuses to replace a non-directory, so the +# global link cannot succeed here. +echo not-a-directory >$chome8/memory + +# ... while this project has perfectly good memory waiting to be backed up. +set -l fp (new_repo https://git.rootiest.dev/rootiest/globalfail.git) +set -l fslug git.rootiest.dev-rootiest-globalfail +set -l fmangled (string replace -a '/' '-' -- $fp | string replace -a '.' '-') +mkdir -p $croot8/$fmangled/memory +echo project-memory >$croot8/$fmangled/memory/p.md + +set -l ferr (mktemp); set -ga TMPDIRS $ferr +pushd $fp >/dev/null +set -l frc (agents-vault --silent 2>$ferr; echo $status) +popd >/dev/null +set -l fwarn (cat $ferr) + +check "global link failure: exits 0" 0 "$frc" +check "global link failure: warns on stderr" true (string match -q "*$chome8/memory*" -- "$fwarn"; and echo true; or echo false) +check "global link failure: live file left alone" not-a-directory (cat $chome8/memory) + +# The finding itself: the primary, per-project backup must still happen. +check "global link failure: per-project link still created" true (test -L $croot8/$fmangled/memory; and echo true; or echo false) +check "global link failure: per-project memory reached the vault" project-memory (cat $vroot8/agent-vault/projects/$fslug/claude/memory/p.md) +check "global link failure: vault still committed" true (test (git -C $vroot8/agent-vault rev-list --count HEAD) -ge 1; and echo true; or echo false) +check "global link failure: per-project memory is committed" true (git -C $vroot8/agent-vault ls-files --error-unmatch projects/$fslug/claude/memory/p.md >/dev/null 2>&1; and echo true; or echo false) + +# A failed global link must not be recorded as done: the next run has to +# re-enter the block and warn again, not treat the vault as correct. +pushd $fp >/dev/null +set -l ferr2 (mktemp); set -ga TMPDIRS $ferr2 +agents-vault --silent 2>$ferr2 +popd >/dev/null +check "global link failure: retried on the next run" true (string match -q "*$chome8/memory*" -- (cat $ferr2); and echo true; or echo false) + +set -e __fish_agent_vault_dir +set -e __fish_agent_vault_claude_root +set -g __fish_agent_vault_claude_home $HERMETIC_HOME/claude +set -g __fish_agent_vault_agy_root $HERMETIC_HOME/agy + # ──────────────────────── hermeticity assertion ──────────────────────── # The whole suite must never have touched the real global agent state. The # failure this guards is specific: a global-memory sync with no test