feat(agents-vault): back up curated agent memory to a host-scoped vault repo #126

Merged
rootiest merged 19 commits from feat/agent-memory-vault into main 2026-09-03 23:09:57 +00:00
3 changed files with 534 additions and 9 deletions
Showing only changes of commit 45f0fb9688 - Show all commits
+18
View File
@@ -0,0 +1,18 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Completions for agents-vault.
complete -c agents-vault -f
complete -c agents-vault -s h -l help -d 'Show help message'
complete -c agents-vault -l link -d "Ensure this project's memory link only"
complete -c agents-vault -l push -d 'Commit and push to the vault remote'
complete -c agents-vault -l restore -d 'Relink everything possible, report the rest'
complete -c agents-vault -l status -d 'Show entries, link health, remote, orphans'
# --adopt takes an existing vault slug, so offer the entries that are
# actually there; the vault may not exist yet, in which case this is empty.
complete -c agents-vault -l adopt -r -a '(command ls -1 (_agents_vault_dir)/projects 2>/dev/null)' -d 'Bind this project to an existing vault entry'
complete -c agents-vault -l remote -r -d 'Set the vault remote URL'
complete -c agents-vault -s v -l verbose -d 'Print all per-step output (default)'
complete -c agents-vault -s q -l quiet -d 'Print one summary line only if changed'
complete -c agents-vault -s s -l silent -d 'Suppress all output; errors only'
+250 -9
View File
@@ -55,6 +55,29 @@
# If both the old and new entries already hold content the migration is
# ambiguous and is refused; resolve it with --adopt=SLUG.
#
# Run with no flags, the command scaffolds the vault, syncs global state,
# links the current project, and commits. The other modes are exclusive
# and each returns as soon as it is done:
#
# --status is a report and mutates nothing at all. It is answered before
# the vault is even scaffolded, so asking what the vault looks like never
# creates it, never copies agy state into it, and never claims
# ~/.claude/memory. A missing vault is reported rather than built.
#
# --restore walks every vault entry and relinks the live memory directory
# of each one whose recorded origin path still exists, naming the rest so
# they can be rebound by hand. It is a convenience: the ordinary per-
# project run restores a cloned vault's memory on its own.
#
# --adopt=SLUG rebinds the current project's entry to SLUG, which is how
# a machine-specific local-* key or an ambiguous migration is resolved.
# SLUG must match [a-z0-9._-]+ with no slash and no leading dot -- the
# charset the slug formula itself emits -- since it is interpolated into
# a vault path and handed to git mv.
#
# --remote=URL points the vault at a remote; --push commits and then
# pushes there.
#
# ARGUMENTS
# --link Scaffold the vault and link this project's memory; skip
# the final commit
@@ -70,13 +93,16 @@
#
# EXIT STATUS
# 0 Completed successfully
# 1 Fatal error (vault unavailable, git failure, ambiguous migration)
# 1 Fatal error (vault unavailable, git failure, ambiguous migration,
# invalid --adopt slug, or --push with no remote configured)
#
# EXAMPLE
# agents-vault
# agents-vault --status
# agents-vault --remote=https://git.rootiest.dev/rootiest/agent-vault.git
# agents-vault --push
# agents-vault --adopt=git.rootiest.dev-rootiest-fish-config
# agents-vault --restore
#
# NOTES
# Set __fish_agent_vault_dir to relocate the vault. Set
@@ -84,6 +110,12 @@
# it defaults to off so a backgrounded push can never hang or prompt
# invisibly underneath a starting agent.
#
# An entry's origin file records the project path once, when the entry is
# created, and is never refreshed. A project that later moves on disk
# therefore keeps a stale path there and --restore degrades to reporting
# it as unplaceable rather than relinking the wrong directory. Rebind
# such an entry from the project itself with --adopt=SLUG.
#
# 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
@@ -112,6 +144,7 @@ function agents-vault --description 'track curated agent memory in a host-scoped
set -l c_ok (set_color green)
set -l c_warn (set_color yellow)
set -l c_err (set_color red)
set -l c_dim (set_color brblack)
set -l c_reset (set_color normal)
argparse h/help link push restore status 'adopt=' 'remote=' \
@@ -155,6 +188,69 @@ function agents-vault --description 'track curated agent memory in a host-scoped
set -l changed 0
set -l did_init 0
# ─────────────────────────── --status ──────────────────────────────
# A report, and nothing but a report. This is dispatched here -- ahead
# of the scaffold, the tool install, and the global-state sync -- on
# purpose: asking what the vault looks like must never be the thing
# that creates it, copies the agy knowledge store into it, or claims
# ~/.claude/memory. A status command that mutates cannot be trusted to
# diagnose the thing it just changed.
if set -q _flag_status
echo "$c_head""Vault:$c_reset $vault"
if not test -d "$vault"
echo " $c_warn""no vault yet — run agents-vault inside a project to create one$c_reset"
return 0
end
set -l url (git -C "$vault" remote get-url origin 2>/dev/null)
if test -z "$url"
echo "$c_head""Remote:$c_reset $c_warn""no remote configured — nothing is backed up off this machine$c_reset"
else
echo "$c_head""Remote:$c_reset $url"
if git -C "$vault" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1
set -l ahead (git -C "$vault" rev-list --count '@{u}..HEAD' 2>/dev/null)
if test -n "$ahead"; and test "$ahead" != 0
echo " $c_warn$ahead commit(s) not yet pushed$c_reset"
end
else
echo " $c_warn""no upstream branch — never pushed$c_reset"
end
end
if test -d "$vault/.git/rebase-merge"; or test -d "$vault/.git/rebase-apply"
echo " $c_err""unresolved rebase in progress — resolve it before syncing$c_reset"
end
set -l claude_root $__fish_agent_vault_claude_root
test -n "$claude_root"; or set claude_root "$HOME/.claude/projects"
echo ""
echo "$c_head""Entries:$c_reset"
set -l seen 0
for entry in "$vault"/projects/*
test -d "$entry"; or continue
set seen 1
set -l eslug (path basename "$entry")
set -l count (command ls -A "$entry/claude/memory" 2>/dev/null | count)
set -l want (path resolve "$entry/claude/memory")
set -l linked 0
for cand in "$claude_root"/*/memory
test -L "$cand"; or continue
if test (path resolve "$cand") = "$want"
set linked 1
break
end
end
if test $linked -eq 1
echo " $c_ok""linked$c_reset $eslug $c_dim($count file(s))$c_reset"
else
echo " $c_warn""orphan$c_reset $eslug $c_dim($count file(s)) — no live project links here$c_reset"
end
end
test $seen -eq 0; and echo " $c_dim(none)$c_reset"
return 0
end
# ────────────────────── ensure the vault repo ──────────────────────
if not test -d "$vault"
if not mkdir -p "$vault"
@@ -229,6 +325,132 @@ function agents-vault --description 'track curated agent memory in a host-scoped
set changed 1
end
# ─────────────────────────── --remote ──────────────────────────────
# Mutating modes are dispatched here: after the vault repo exists (they
# all need one) but before the global-state sync below, which belongs
# to a default backup run and has no business running as a side effect
# of rebinding an entry or setting a URL.
if set -q _flag_remote
if test -z "$_flag_remote"
echo "$c_err""agents-vault: --remote needs a URL$c_reset" >&2
return 1
end
# The result is captured explicitly rather than chained off the
# block terminator with `or`. `end` does carry the taken branch's
# status in fish, but only when a branch was taken at all: the same
# construct one `else` away silently reports success, which is
# exactly how a hook-rejected commit once passed for a good one.
set -l rc 0
if git -C "$vault" remote get-url origin >/dev/null 2>&1
git -C "$vault" remote set-url origin "$_flag_remote"
set rc $status
else
git -C "$vault" remote add origin "$_flag_remote"
set rc $status
end
if test $rc -ne 0
echo "$c_err""agents-vault: could not set the vault remote to $_flag_remote$c_reset" >&2
return 1
end
test $verbose -eq 1; and echo "$c_ok→ Vault remote set to $_flag_remote$c_reset"
return 0
end
# ─────────────────────────── --adopt ───────────────────────────────
if set -q _flag_adopt
# The requested slug is interpolated into a vault path and handed
# to `git mv`, so it is validated before it is used anywhere:
# --adopt=../../../etc would otherwise walk straight out of the
# vault. Only the charset the slug formula itself emits is
# accepted, and a leading dot is refused as well, which also rules
# out the bare "." and ".." entries.
if not string match -qr '^[a-z0-9_-][a-z0-9._-]*$' -- "$_flag_adopt"
echo "$c_err""agents-vault: invalid slug '$_flag_adopt'$c_reset" >&2
echo "$c_err"" A slug is [a-z0-9._-]+ with no slash and no leading dot.$c_reset" >&2
return 1
end
set -l root (git rev-parse --show-toplevel 2>/dev/null)
if test -z "$root"
echo "$c_err""agents-vault: --adopt must run inside a project$c_reset" >&2
return 1
end
set -l cur (_agents_repo_slug "$root")
set -l from "$vault/projects/$cur"
set -l to "$vault/projects/$_flag_adopt"
if test "$cur" = "$_flag_adopt"
test $verbose -eq 1; and echo "$c_ok→ Already bound to $_flag_adopt$c_reset"
return 0
end
if not test -d "$from"
echo "$c_err""agents-vault: no vault entry for this project ($cur)$c_reset" >&2
return 1
end
set -l to_content
test -d "$to/claude/memory"; and set to_content (command ls -A "$to/claude/memory" 2>/dev/null)
if test (count $to_content) -gt 0
echo "$c_err""agents-vault: $_flag_adopt already holds content; refusing to overwrite$c_reset" >&2
return 1
end
test -d "$to"; and rm -rf "$to"
if not git -C "$vault" mv "projects/$cur" "projects/$_flag_adopt" 2>/dev/null
command mv "$from" "$to"; or return 1
end
printf 'adopted: %s → %s (%s)\n' "$cur" "$_flag_adopt" (date -I) >>"$to/origin"
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 '.' '-')
# The live path still points at the old entry, which no longer
# exists; drop it so ensure_symlink is not asked to resolve a
# broken link before repinning it.
test -L "$claude_root/$mangled/memory"; and rm -f "$claude_root/$mangled/memory"
if not _agents_repo_ensure_symlink "$claude_root/$mangled/memory" "$to/claude/memory" >/dev/null
echo "$c_err""agents-vault: adopted $_flag_adopt but could not relink $claude_root/$mangled/memory$c_reset" >&2
return 1
end
_agents_repo_sync "$vault" "chore: adopt $cur as $_flag_adopt" >/dev/null
test $verbose -eq 1; and echo "$c_ok→ Adopted $cur as $_flag_adopt$c_reset"
return 0
end
# ────────────────────────── --restore ──────────────────────────────
# The batch counterpart of the emergent per-project restore. Each entry
# records the path it was created at; where that path still exists the
# live memory directory is relinked, and where it does not the entry is
# named so it can be rebound with --adopt. The origin file is written
# once and never refreshed, so a project that has since moved on disk
# simply degrades to "cannot place" rather than relinking the wrong
# directory.
if set -q _flag_restore
set -l claude_root $__fish_agent_vault_claude_root
test -n "$claude_root"; or set claude_root "$HOME/.claude/projects"
for entry in "$vault"/projects/*
test -d "$entry/claude/memory"; or continue
set -l eslug (path basename "$entry")
set -l opath ""
if test -f "$entry/origin"
set -l line (command grep -m1 '^path:' "$entry/origin" 2>/dev/null)
test -n "$line"; and set opath (string replace -r '^path:\s+' '' -- "$line")
end
if test -n "$opath"; and test -d "$opath"
set -l m (string replace -a '/' '-' -- "$opath" | string replace -a '.' '-')
set -l msg (_agents_repo_ensure_symlink "$claude_root/$m/memory" "$entry/claude/memory")
if test $status -ne 0
echo "$c_err""agents-vault: could not relink $eslug$c_reset" >&2
else if test -n "$msg"
test $verbose -eq 1; and echo "$c_ok→ Restored $eslug$c_reset"
end
else
test $verbose -eq 1
and echo "$c_warn→ Cannot place $eslug: no live project found; use --adopt from the project$c_reset"
end
end
return 0
end
# ────────────────────────── global state ───────────────────────────
# Allowlist, never a denylist. The agy root and ~/.claude also hold
# .credentials.json, history.jsonl, sessions/, session-env/,
@@ -327,14 +549,6 @@ function agents-vault --description 'track curated agent memory in a host-scoped
end
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"
@@ -441,6 +655,33 @@ function agents-vault --description 'track curated agent memory in a host-scoped
end
end
# ────────────────────────────── push ───────────────────────────────
# Pushing is explicit. Autopush exists but is opt-in, because this runs
# on every agent launch and a network operation there can hang or
# prompt for credentials invisibly underneath a starting agent.
set -l do_push 0
set -q _flag_push; and set do_push 1
if set -q __fish_agent_vault_autopush; and test "$__fish_agent_vault_autopush" = 1
set do_push 1
end
if test $do_push -eq 1
if git -C "$vault" remote get-url origin >/dev/null 2>&1
if git -C "$vault" push -q origin HEAD
test $verbose -eq 1; and echo "$c_ok→ Pushed the vault to origin$c_reset"
else
# Not fatal: the commit above already happened, so the
# memory is safe locally and the next push will carry it.
echo "$c_warn""agents-vault: push failed; the vault is committed locally$c_reset" >&2
end
else if set -q _flag_push
# An explicit --push that pushed nowhere must not read as a
# successful backup. Autopush stays quiet: it is a background
# convenience on a vault that may deliberately have no remote.
echo "$c_err""agents-vault: no remote configured; set one with --remote=URL$c_reset" >&2
return 1
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"
+266
View File
@@ -679,6 +679,272 @@ 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
# ──────────────────── status / adopt / remote / push ───────────────────
# The four report-and-rebind modes. --status is a *report*: the checks
# below pin that it never mutates, because it is dispatched ahead of the
# scaffold rather than behind it.
echo ""
echo "== agents-vault (status, remote, adopt) =="
set -l vroot9 (mktemp -d); set -ga TMPDIRS $vroot9
set -l croot9 (mktemp -d); set -ga TMPDIRS $croot9
set -l chome9 (mktemp -d); set -ga TMPDIRS $chome9
set -l agy9 (mktemp -d); set -ga TMPDIRS $agy9
set -g __fish_agent_vault_dir $vroot9/agent-vault
set -g __fish_agent_vault_claude_root $croot9
set -g __fish_agent_vault_claude_home $chome9
set -g __fish_agent_vault_agy_root $agy9
# A report asked for before the vault exists must say so, not scaffold one.
set -l s0out (mktemp); set -ga TMPDIRS $s0out
set -l s0rc (agents-vault --status >$s0out; echo $status)
check "status without a vault exits 0" 0 "$s0rc"
check "status without a vault says so" true (string match -q '*no vault*' -- (cat $s0out); and echo true; or echo false)
check "status without a vault scaffolds nothing" false (test -e $vroot9/agent-vault; and echo true; or echo false)
set -l sp (new_repo https://git.rootiest.dev/rootiest/statusrepo.git)
set -l smangled (string replace -a '/' '-' -- $sp | string replace -a '.' '-')
mkdir -p $croot9/$smangled/memory
echo m >$croot9/$smangled/memory/m.md
pushd $sp >/dev/null
agents-vault --silent
set -l report (agents-vault --status)
popd >/dev/null
check "status names the slug" true (string match -q '*git.rootiest.dev-rootiest-statusrepo*' -- "$report"; and echo true; or echo false)
check "status reports no remote" true (string match -q '*no remote*' -- "$report"; and echo true; or echo false)
check "status reports the link as healthy" true (string match -q '*linked*' -- "$report"; and echo true; or echo false)
# --status must not mutate. Global state that a default run *would* sync is
# staged here and must still be untouched afterwards: a report that first
# copies the agy knowledge store and claims ~/.claude/memory is not a
# report. This is what dispatching --status ahead of the scaffold buys.
mkdir -p $agy9/knowledge
echo learned >$agy9/knowledge/fact.md
mkdir -p $chome9/memory
echo global >$chome9/memory/g.md
set -l head_before (git -C $vroot9/agent-vault rev-list --count HEAD)
set -l porcelain_before (git -C $vroot9/agent-vault status --porcelain | string join ',')
agents-vault --status >/dev/null
check "status did not copy agy state" false (test -e $vroot9/agent-vault/global/agy; and echo true; or echo false)
check "status did not claim the global memory path" false (test -L $chome9/memory; and echo true; or echo false)
check "status made no commit" "$head_before" (git -C $vroot9/agent-vault rev-list --count HEAD)
check "status left the vault worktree as it found it" "$porcelain_before" (git -C $vroot9/agent-vault status --porcelain | string join ',')
# An entry no live project links to is surfaced as an orphan.
mkdir -p $vroot9/agent-vault/projects/ghost-entry/claude/memory
echo x >$vroot9/agent-vault/projects/ghost-entry/claude/memory/x.md
set -l oreport (agents-vault --status)
check "status lists the orphan" true (string match -q '*orphan*ghost-entry*' -- "$oreport"; and echo true; or echo false)
rm -rf $vroot9/agent-vault/projects/ghost-entry
# --remote sets origin on the vault.
agents-vault --remote=https://git.rootiest.dev/rootiest/agent-vault.git --silent
check "remote set" https://git.rootiest.dev/rootiest/agent-vault.git (git -C $vroot9/agent-vault remote get-url origin)
check "status reports the remote" true (string match -q '*rootiest/agent-vault.git*' -- (agents-vault --status); and echo true; or echo false)
# A *failed* remote update must return non-zero. Reporting success after a
# git command that did not run is the same silent-false-success shape that
# a hook-rejected commit produced earlier in this project.
set -l rerr (mktemp); set -ga TMPDIRS $rerr
chmod 500 $vroot9/agent-vault/.git
set -l rrc (agents-vault --remote=https://git.rootiest.dev/rootiest/other.git --silent 2>$rerr; echo $status)
chmod 700 $vroot9/agent-vault/.git
check "failing --remote returns 1" 1 "$rrc"
check "failing --remote reports on stderr" true (string match -q '*could not set*remote*' -- (cat $rerr); and echo true; or echo false)
check "failing --remote left the old remote in place" https://git.rootiest.dev/rootiest/agent-vault.git (git -C $vroot9/agent-vault remote get-url origin)
# --adopt renames the current project's entry.
set -l ap (new_repo)
set -l amang (string replace -a '/' '-' -- $ap | string replace -a '.' '-')
mkdir -p $croot9/$amang/memory
echo adopted >$croot9/$amang/memory/a.md
pushd $ap >/dev/null
agents-vault --silent
set -l aslug (_agents_repo_slug $ap)
agents-vault --adopt=my-chosen-slug --silent
popd >/dev/null
check "adopt renamed the entry" adopted (cat $vroot9/agent-vault/projects/my-chosen-slug/claude/memory/a.md)
check "adopt repinned the link" (path resolve $vroot9/agent-vault/projects/my-chosen-slug/claude/memory) (path resolve $croot9/$amang/memory)
check "adopt removed the old entry" false (test -d $vroot9/agent-vault/projects/$aslug; and echo true; or echo false)
check "adopt recorded the rebind" true (string match -q "*$aslug*my-chosen-slug*" -- (cat $vroot9/agent-vault/projects/my-chosen-slug/origin); and echo true; or echo false)
# An unvalidated --adopt slug is a path-traversal primitive: it lands in
# "$vault/projects/$slug" and in `git mv`. Only the charset the slug
# formula itself emits is accepted, and a leading dot is refused too.
set -l bp (new_repo)
set -l bmang (string replace -a '/' '-' -- $bp | string replace -a '.' '-')
mkdir -p $croot9/$bmang/memory
echo bad >$croot9/$bmang/memory/b.md
pushd $bp >/dev/null
agents-vault --silent
popd >/dev/null
set -l projects_before (command ls -A $vroot9/agent-vault/projects | sort | string join ',')
set -l bad_slugs ../escape .hidden has/slash . .. 'UPPER' 'sp ace' ''
pushd $bp >/dev/null
for bad in $bad_slugs
set -l berr (mktemp); set -ga TMPDIRS $berr
set -l brc (agents-vault --adopt=$bad --silent 2>$berr; echo $status)
check "adopt refuses '$bad'" 1 "$brc"
check "adopt refuses '$bad' out loud" true (string match -q '*invalid*' -- (cat $berr); and echo true; or echo false)
end
popd >/dev/null
check "refused adopts moved nothing" "$projects_before" (command ls -A $vroot9/agent-vault/projects | sort | string join ',')
check "refused adopts escaped nothing above projects/" false (test -e $vroot9/agent-vault/escape; and echo true; or echo false)
check "refused adopts created no dotted entry" false (test -e $vroot9/agent-vault/projects/.hidden; 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
# ─────────────────────────────── restore ───────────────────────────────
# The batch counterpart of the emergent per-project restore: walk the vault
# and relink every entry whose recorded origin path still exists.
echo ""
echo "== agents-vault (restore) =="
set -l vroot10 (mktemp -d); set -ga TMPDIRS $vroot10
set -l croot10 (mktemp -d); set -ga TMPDIRS $croot10
set -l chome10 (mktemp -d); set -ga TMPDIRS $chome10
set -l agy10 (mktemp -d); set -ga TMPDIRS $agy10
set -g __fish_agent_vault_dir $vroot10/agent-vault
set -g __fish_agent_vault_claude_root $croot10
set -g __fish_agent_vault_claude_home $chome10
set -g __fish_agent_vault_agy_root $agy10
set -l rp (new_repo https://git.rootiest.dev/rootiest/restoreme.git)
set -l rmang (string replace -a '/' '-' -- $rp | string replace -a '.' '-')
mkdir -p $croot10/$rmang/memory
echo restore-precious >$croot10/$rmang/memory/keep.md
pushd $rp >/dev/null
agents-vault --silent
popd >/dev/null
# Lose the live link the way a reinstalled machine would.
rm -f $croot10/$rmang/memory
check "restore: link gone to begin with" false (test -e $croot10/$rmang/memory; and echo true; or echo false)
set -l rout (agents-vault --restore)
check "restore: relinked from the origin file" restore-precious (cat $croot10/$rmang/memory/keep.md 2>/dev/null)
check "restore: the live path is a link" true (test -L $croot10/$rmang/memory; and echo true; or echo false)
check "restore: names what it restored" true (string match -q '*restoreme*' -- "$rout"; and echo true; or echo false)
# An entry whose recorded path is gone cannot be placed; the run still
# succeeds and says which entry needs --adopt.
mkdir -p $vroot10/agent-vault/projects/ghost-entry/claude/memory
echo x >$vroot10/agent-vault/projects/ghost-entry/claude/memory/x.md
printf 'remote: (none)\npath: %s\nhost: t\n' $vroot10/gone-forever \
>$vroot10/agent-vault/projects/ghost-entry/origin
set -l r2out (mktemp); set -ga TMPDIRS $r2out
set -l r2rc (agents-vault --restore >$r2out; echo $status)
check "restore: exits 0 with an unplaceable entry" 0 "$r2rc"
check "restore: reports the unplaceable entry" true (string match -q '*ghost-entry*' -- (cat $r2out); 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
# ──────────────────────────────── push ─────────────────────────────────
echo ""
echo "== agents-vault (push) =="
set -l vroot11 (mktemp -d); set -ga TMPDIRS $vroot11
set -l croot11 (mktemp -d); set -ga TMPDIRS $croot11
set -l chome11 (mktemp -d); set -ga TMPDIRS $chome11
set -l agy11 (mktemp -d); set -ga TMPDIRS $agy11
set -l bare (mktemp -d); set -ga TMPDIRS $bare
git init -q --bare $bare
set -g __fish_agent_vault_dir $vroot11/agent-vault
set -g __fish_agent_vault_claude_root $croot11
set -g __fish_agent_vault_claude_home $chome11
set -g __fish_agent_vault_agy_root $agy11
set -l pp (new_repo https://git.rootiest.dev/rootiest/pushme.git)
set -l pslug git.rootiest.dev-rootiest-pushme
set -l pmang (string replace -a '/' '-' -- $pp | string replace -a '.' '-')
mkdir -p $croot11/$pmang/memory
echo pushed >$croot11/$pmang/memory/p.md
# --push with no remote must fail loudly. The commit still happened, so
# silently returning 0 would read as "backed up off this machine".
set -l perr (mktemp); set -ga TMPDIRS $perr
pushd $pp >/dev/null
set -l prc0 (agents-vault --push --silent 2>$perr; echo $status)
popd >/dev/null
check "push without a remote returns 1" 1 "$prc0"
check "push without a remote says so" true (string match -q '*no remote*' -- (cat $perr); and echo true; or echo false)
check "push without a remote still committed locally" true (git -C $vroot11/agent-vault ls-files --error-unmatch projects/$pslug/claude/memory/p.md >/dev/null 2>&1; and echo true; or echo false)
agents-vault --remote=$bare --silent
set -l vbranch (git -C $vroot11/agent-vault rev-parse --abbrev-ref HEAD)
pushd $pp >/dev/null
set -l prc (agents-vault --push --silent; echo $status)
popd >/dev/null
check "push exits 0" 0 "$prc"
check "push landed in the remote" pushed (git -C $bare show $vbranch:projects/$pslug/claude/memory/p.md 2>/dev/null)
# Autopush is opt-in and off by default: a plain run must not push.
echo pushed-later >$croot11/$pmang/memory/p2.md
pushd $pp >/dev/null
agents-vault --silent
popd >/dev/null
check "no autopush by default" false (git -C $bare cat-file -e $vbranch:projects/$pslug/claude/memory/p2.md 2>/dev/null; and echo true; or echo false)
set -g __fish_agent_vault_autopush 1
echo pushed-auto >$croot11/$pmang/memory/p3.md
pushd $pp >/dev/null
agents-vault --silent
popd >/dev/null
set -e __fish_agent_vault_autopush
check "autopush pushes when enabled" pushed-auto (git -C $bare show $vbranch:projects/$pslug/claude/memory/p3.md 2>/dev/null)
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
# ────────────── a dangling global memory link is repinned ──────────────
# The one state from the real incident the suite never pinned down. For a
# *broken* symlink both -d and -e are false, so only the `-L` disjunct in
# the global-memory guard can notice it; the vault side is deliberately
# left empty so no other disjunct can stand in and pass this by accident.
echo ""
echo "== agents-vault (dangling global memory link) =="
set -l vroot12 (mktemp -d); set -ga TMPDIRS $vroot12
set -l croot12 (mktemp -d); set -ga TMPDIRS $croot12
set -l chome12 (mktemp -d); set -ga TMPDIRS $chome12
set -l agy12 (mktemp -d); set -ga TMPDIRS $agy12
set -g __fish_agent_vault_dir $vroot12/agent-vault
set -g __fish_agent_vault_claude_root $croot12
set -g __fish_agent_vault_claude_home $chome12
set -g __fish_agent_vault_agy_root $agy12
ln -s $vroot12/vanished-vault/global/claude/memory $chome12/memory
check "dangling: -e is false for the broken link" false (test -e $chome12/memory; and echo true; or echo false)
check "dangling: -L is the only signal" true (test -L $chome12/memory; and echo true; or echo false)
check "dangling: the vault side is empty" false (test -e $vroot12/agent-vault/global/claude/memory; and echo true; or echo false)
set -l dp (new_repo https://git.rootiest.dev/rootiest/dangling.git)
set -l derr (mktemp); set -ga TMPDIRS $derr
pushd $dp >/dev/null
set -l drc (agents-vault --silent 2>$derr; echo $status)
popd >/dev/null
check "dangling: exits 0" 0 "$drc"
check "dangling: warns about nothing" "" (cat $derr)
check "dangling: repinned into the vault" (path resolve $vroot12/agent-vault/global/claude/memory) (path resolve $chome12/memory)
check "dangling: the link resolves again" true (test -d $chome12/memory; 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