feat(agents-vault): add status, restore, adopt, remote, and push

--status reports link health, orphaned entries, and how far the vault is
ahead of its remote, which is how an unpushed backup gets noticed. --adopt
rebinds a machine-specific local-* entry by hand. --push is explicit;
autopush stays opt-in via __fish_agent_vault_autopush.

Three corrections to the planned shape:

--status is dispatched ahead of the scaffold instead of behind it. As
planned it sat after the tool install, the agy knowledge copy, and the
global memory link, so asking for a report would first sync global state
and claim ~/.claude/memory. It is now read-only and reports a missing
vault rather than creating one. The global-state block moved below the
mode dispatch so it runs only on a default or --link run; the mutating
modes still need the vault repo, so they sit between the scaffold and it.

--adopt validates its slug before using it. It is interpolated into
"$vault/projects/$slug" and handed to `git mv`, so --adopt=../../../etc
walked straight out of the vault. Only the charset the slug formula emits
is accepted, with no slash and no leading dot.

--remote captures the git exit status explicitly rather than chaining an
`or` off the block terminator. That construct does work in fish, but it
reads as the silent-false-success shape that a hook-rejected commit once
produced here, and it stops working the moment the `else` goes away.

Also pins the dangling-global-symlink case the suite never covered: for a
broken ~/.claude/memory link both -d and -e are false, so the -L disjunct
in the global-memory guard is the only thing that notices it. That is the
state a buggy earlier run left on a real machine; the test asserts it is
detected, repinned into the vault, and exits 0.
This commit is contained in:
2026-09-03 18:56:46 -04:00
parent 4c7334c46e
commit 45f0fb9688
3 changed files with 534 additions and 9 deletions
+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