feat(agents-vault): back up curated agent memory to a host-scoped vault repo #126
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user