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:
@@ -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
|
# If both the old and new entries already hold content the migration is
|
||||||
# ambiguous and is refused; resolve it with --adopt=SLUG.
|
# 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
|
# ARGUMENTS
|
||||||
# --link Scaffold the vault and link this project's memory; skip
|
# --link Scaffold the vault and link this project's memory; skip
|
||||||
# the final commit
|
# the final commit
|
||||||
@@ -70,13 +93,16 @@
|
|||||||
#
|
#
|
||||||
# EXIT STATUS
|
# EXIT STATUS
|
||||||
# 0 Completed successfully
|
# 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
|
# EXAMPLE
|
||||||
# agents-vault
|
# agents-vault
|
||||||
# agents-vault --status
|
# agents-vault --status
|
||||||
# agents-vault --remote=https://git.rootiest.dev/rootiest/agent-vault.git
|
# agents-vault --remote=https://git.rootiest.dev/rootiest/agent-vault.git
|
||||||
# agents-vault --push
|
# agents-vault --push
|
||||||
|
# agents-vault --adopt=git.rootiest.dev-rootiest-fish-config
|
||||||
|
# agents-vault --restore
|
||||||
#
|
#
|
||||||
# NOTES
|
# NOTES
|
||||||
# Set __fish_agent_vault_dir to relocate the vault. Set
|
# 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
|
# it defaults to off so a backgrounded push can never hang or prompt
|
||||||
# invisibly underneath a starting agent.
|
# 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
|
# 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
|
# 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
|
# 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_ok (set_color green)
|
||||||
set -l c_warn (set_color yellow)
|
set -l c_warn (set_color yellow)
|
||||||
set -l c_err (set_color red)
|
set -l c_err (set_color red)
|
||||||
|
set -l c_dim (set_color brblack)
|
||||||
set -l c_reset (set_color normal)
|
set -l c_reset (set_color normal)
|
||||||
|
|
||||||
argparse h/help link push restore status 'adopt=' 'remote=' \
|
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 changed 0
|
||||||
set -l did_init 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 ──────────────────────
|
# ────────────────────── ensure the vault repo ──────────────────────
|
||||||
if not test -d "$vault"
|
if not test -d "$vault"
|
||||||
if not mkdir -p "$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
|
set changed 1
|
||||||
end
|
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 ───────────────────────────
|
# ────────────────────────── global state ───────────────────────────
|
||||||
# Allowlist, never a denylist. The agy root and ~/.claude also hold
|
# Allowlist, never a denylist. The agy root and ~/.claude also hold
|
||||||
# .credentials.json, history.jsonl, sessions/, session-env/,
|
# .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
|
||||||
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 ─────────────────────
|
# ──────────────────── link the current project ─────────────────────
|
||||||
set -l root (git rev-parse --show-toplevel 2>/dev/null)
|
set -l root (git rev-parse --show-toplevel 2>/dev/null)
|
||||||
if test -n "$root"
|
if test -n "$root"
|
||||||
@@ -441,6 +655,33 @@ function agents-vault --description 'track curated agent memory in a host-scoped
|
|||||||
end
|
end
|
||||||
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 $quiet -eq 1; and test $changed -eq 1
|
||||||
if test $did_init -eq 1
|
if test $did_init -eq 1
|
||||||
echo "$c_ok→ Initialized agent memory vault$c_reset"
|
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_claude_home $HERMETIC_HOME/claude
|
||||||
set -g __fish_agent_vault_agy_root $HERMETIC_HOME/agy
|
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 ────────────────────────
|
# ──────────────────────── hermeticity assertion ────────────────────────
|
||||||
# The whole suite must never have touched the real global agent state. The
|
# 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
|
# failure this guards is specific: a global-memory sync with no test
|
||||||
|
|||||||
Reference in New Issue
Block a user