feat(agents-vault): back up curated agent memory to a host-scoped vault repo #126
@@ -11,7 +11,10 @@ complete -c agents-vault -l restore -d 'Relink everything possible, report the r
|
|||||||
complete -c agents-vault -l status -d 'Show entries, link health, remote, orphans'
|
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
|
# --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.
|
# 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'
|
# -A because a dot-led slug is legitimate (a relative-path remote keys as
|
||||||
|
# ..-mirror), and an entry that cannot be completed reads as one that is
|
||||||
|
# not there.
|
||||||
|
complete -c agents-vault -l adopt -r -a '(command ls -1A (_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 -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 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 q -l quiet -d 'Print one summary line only if changed'
|
||||||
|
|||||||
@@ -120,6 +120,13 @@
|
|||||||
# 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.
|
||||||
#
|
#
|
||||||
|
# --adopt rebinds an entry; it does not pin its name. The slug is
|
||||||
|
# re-derived from the project on every run, so the next ordinary run
|
||||||
|
# migrates the adopted entry straight back to the canonical key, carrying
|
||||||
|
# the memory and the live link with it. That is the point rather than a
|
||||||
|
# wart: adopting is how a mismatched or ambiguous binding is repaired, not
|
||||||
|
# how an entry is given a permanent name of its own.
|
||||||
|
#
|
||||||
# An entry's origin file records the project path once, when the entry is
|
# 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
|
# created, and is never refreshed. A project that later moves on disk
|
||||||
# therefore keeps a stale path there and --restore degrades to reporting
|
# therefore keeps a stale path there and --restore degrades to reporting
|
||||||
@@ -237,10 +244,21 @@ function agents-vault --description 'track curated agent memory in a host-scoped
|
|||||||
echo ""
|
echo ""
|
||||||
echo "$c_head""Entries:$c_reset"
|
echo "$c_head""Entries:$c_reset"
|
||||||
set -l seen 0
|
set -l seen 0
|
||||||
for entry in "$vault"/projects/*
|
# Listed rather than globbed: fish's * skips dot-led names, and a
|
||||||
|
# dot-led slug is both reachable and sanctioned. A relative-path
|
||||||
|
# remote -- the sibling-bare-mirror idiom, `git remote add origin
|
||||||
|
# ../mirror.git` -- keys as ..-mirror, a dot-led host keys as
|
||||||
|
# .hidden.example.com-o-r, and --adopt accepts a leading dot on
|
||||||
|
# purpose. Such an entry is scaffolded, linked and committed like any
|
||||||
|
# other, so a glob here would under-report the vault without ever
|
||||||
|
# saying that it had. The 2>/dev/null is load-bearing: a vault with
|
||||||
|
# no projects/ is ordinary (running agents-vault outside any git
|
||||||
|
# repo makes exactly that one) and a bare ls would spew.
|
||||||
|
for name in (command ls -A "$vault/projects" 2>/dev/null)
|
||||||
|
set -l entry "$vault/projects/$name"
|
||||||
test -d "$entry"; or continue
|
test -d "$entry"; or continue
|
||||||
set seen 1
|
set seen 1
|
||||||
set -l eslug (path basename "$entry")
|
set -l eslug $name
|
||||||
set -l count (command ls -A "$entry/claude/memory" 2>/dev/null | count)
|
set -l count (command ls -A "$entry/claude/memory" 2>/dev/null | count)
|
||||||
set -l want (path resolve "$entry/claude/memory")
|
set -l want (path resolve "$entry/claude/memory")
|
||||||
set -l linked 0
|
set -l linked 0
|
||||||
@@ -290,7 +308,12 @@ function agents-vault --description 'track curated agent memory in a host-scoped
|
|||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'# SQLite sidecars are never safe to commit mid-write.' \
|
'# SQLite sidecars are never safe to commit mid-write.' \
|
||||||
'*.db-wal' \
|
'*.db-wal' \
|
||||||
'*.db-shm' >"$vault/.gitignore"
|
'*.db-shm' \
|
||||||
|
'' \
|
||||||
|
'# --adopt stashes the entry it is about to overwrite inside .git/,' \
|
||||||
|
'# out of reach of `git add -A`. This covers the fallback location' \
|
||||||
|
'# it uses when .git is not a directory.' \
|
||||||
|
'/.adopt-stash' >"$vault/.gitignore"
|
||||||
set changed 1
|
set changed 1
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -423,17 +446,37 @@ function agents-vault --description 'track curated agent memory in a host-scoped
|
|||||||
# So a contentless target entry is moved aside rather than deleted,
|
# So a contentless target entry is moved aside rather than deleted,
|
||||||
# and the origin note is appended only once the relink has
|
# and the origin note is appended only once the relink has
|
||||||
# succeeded. Both are undone below if it does not. The stash lives
|
# succeeded. Both are undone below if it does not. The stash lives
|
||||||
# at the vault root, not under projects/, so a crash between the
|
# inside .git/: it is on the same filesystem, so the rename stays a
|
||||||
# two renames cannot leave something that reads as an entry.
|
# rename; it is not under projects/, so a crash between the two moves
|
||||||
|
# cannot leave something that reads as an entry; and `git add -A`
|
||||||
|
# never descends there, so a crash cannot leave junk that the next
|
||||||
|
# ordinary run commits either. A vault whose .git is not a directory
|
||||||
|
# is not one this tool made, so that case falls back to the vault
|
||||||
|
# root, which the scaffolded .gitignore covers.
|
||||||
set -l stash ""
|
set -l stash ""
|
||||||
if test -d "$to"
|
if test -d "$to"
|
||||||
set stash "$vault/.adopt-stash"
|
if test -d "$vault/.git"
|
||||||
|
set stash "$vault/.git/agents-vault-adopt-stash"
|
||||||
|
else
|
||||||
|
set stash "$vault/.adopt-stash"
|
||||||
|
end
|
||||||
rm -rf "$stash"
|
rm -rf "$stash"
|
||||||
command mv "$to" "$stash"; or return 1
|
command mv "$to" "$stash"; or return 1
|
||||||
end
|
end
|
||||||
if not git -C "$vault" mv "projects/$cur" "projects/$_flag_adopt" 2>/dev/null
|
if not git -C "$vault" mv "projects/$cur" "projects/$_flag_adopt" 2>/dev/null
|
||||||
if not command mv "$from" "$to"
|
if not command mv "$from" "$to"
|
||||||
test -n "$stash"; and command mv "$stash" "$to"
|
test -n "$stash"; and command mv "$stash" "$to"
|
||||||
|
# Putting the worktree back is only half a rollback. Every
|
||||||
|
# move here is a plain rename as far as git is concerned, so
|
||||||
|
# the index still describes the half-applied state, and a
|
||||||
|
# hand `git commit` in that window would record it -- the
|
||||||
|
# next ordinary run's `git add -A` heals it, but only later.
|
||||||
|
# projects/ is re-read whole rather than the two entries
|
||||||
|
# named: git add refuses a pathspec that matches nothing --
|
||||||
|
# which one of the two always is, once it has been moved
|
||||||
|
# back -- and then stages none of them, not even the one
|
||||||
|
# that did match.
|
||||||
|
git -C "$vault" add -A -- projects 2>/dev/null
|
||||||
return 1
|
return 1
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -450,6 +493,13 @@ function agents-vault --description 'track curated agent memory in a host-scoped
|
|||||||
command mv "$to" "$from"
|
command mv "$to" "$from"
|
||||||
end
|
end
|
||||||
test -n "$stash"; and command mv "$stash" "$to"
|
test -n "$stash"; and command mv "$stash" "$to"
|
||||||
|
# Only now, with the worktree whole again -- the stash restore
|
||||||
|
# included -- is the index worth re-reading. Unconditional
|
||||||
|
# because even the git-native rollback leaves it out of line:
|
||||||
|
# the forward `git mv` overwrote the stashed entry's index
|
||||||
|
# entries, and moving the files back does not bring them back.
|
||||||
|
# See the same call above for why projects/ is named whole.
|
||||||
|
git -C "$vault" add -A -- projects 2>/dev/null
|
||||||
echo "$c_err""agents-vault: could not relink $claude_root/$mangled/memory; $cur was left as it was$c_reset" >&2
|
echo "$c_err""agents-vault: could not relink $claude_root/$mangled/memory; $cur was left as it was$c_reset" >&2
|
||||||
return 1
|
return 1
|
||||||
end
|
end
|
||||||
@@ -473,9 +523,14 @@ function agents-vault --description 'track curated agent memory in a host-scoped
|
|||||||
set -l claude_root $__fish_agent_vault_claude_root
|
set -l claude_root $__fish_agent_vault_claude_root
|
||||||
test -n "$claude_root"; or set claude_root "$HOME/.claude/projects"
|
test -n "$claude_root"; or set claude_root "$HOME/.claude/projects"
|
||||||
set -l restore_failed 0
|
set -l restore_failed 0
|
||||||
for entry in "$vault"/projects/*
|
# Listed rather than globbed, for the reason spelled out at --status:
|
||||||
|
# fish's * skips a dot-led slug, which is a legitimate key, and a
|
||||||
|
# batch restore that silently walks past an entry is worse here than
|
||||||
|
# in a report -- that project is simply left unlinked.
|
||||||
|
for name in (command ls -A "$vault/projects" 2>/dev/null)
|
||||||
|
set -l entry "$vault/projects/$name"
|
||||||
test -d "$entry/claude/memory"; or continue
|
test -d "$entry/claude/memory"; or continue
|
||||||
set -l eslug (path basename "$entry")
|
set -l eslug $name
|
||||||
set -l opath ""
|
set -l opath ""
|
||||||
if test -f "$entry/origin"
|
if test -f "$entry/origin"
|
||||||
set -l line (command grep -m1 '^path:' "$entry/origin" 2>/dev/null)
|
set -l line (command grep -m1 '^path:' "$entry/origin" 2>/dev/null)
|
||||||
@@ -734,7 +789,7 @@ function agents-vault --description 'track curated agent memory in a host-scoped
|
|||||||
# locally and the next push will carry it -- but nothing
|
# locally and the next push will carry it -- but nothing
|
||||||
# left this machine, which is the whole point of pushing,
|
# left this machine, which is the whole point of pushing,
|
||||||
# so this is a failure and not a warning to walk past.
|
# so this is a failure and not a warning to walk past.
|
||||||
echo "$c_warn""agents-vault: push failed; the vault is committed locally but not backed up off this machine$c_reset" >&2
|
echo "$c_err""agents-vault: push failed; the vault is committed locally but not backed up off this machine$c_reset" >&2
|
||||||
set failed 1
|
set failed 1
|
||||||
end
|
end
|
||||||
else if set -q _flag_push
|
else if set -q _flag_push
|
||||||
|
|||||||
@@ -860,6 +860,104 @@ popd >/dev/null
|
|||||||
check "atomic adopt: retry succeeds" 0 "$t2rc"
|
check "atomic adopt: retry succeeds" 0 "$t2rc"
|
||||||
check "atomic adopt: retry moved the memory" atomic-precious (cat $vroot9/agent-vault/projects/atomic-target/claude/memory/keep.md 2>/dev/null)
|
check "atomic adopt: retry moved the memory" atomic-precious (cat $vroot9/agent-vault/projects/atomic-target/claude/memory/keep.md 2>/dev/null)
|
||||||
|
|
||||||
|
# The cases above all adopt onto a slug with no entry at all. The other
|
||||||
|
# half of --adopt is a target that already exists but holds no memory -- a
|
||||||
|
# scaffolded entry, or one a departed project left behind. That one is
|
||||||
|
# stashed rather than deleted, so a failed relink can put it back exactly
|
||||||
|
# as it was.
|
||||||
|
set -l sa (new_repo https://git.rootiest.dev/rootiest/stash-src.git)
|
||||||
|
set -l saslug git.rootiest.dev-rootiest-stash-src
|
||||||
|
set -l samang (string replace -a '/' '-' -- $sa | string replace -a '.' '-')
|
||||||
|
mkdir -p $croot9/$samang/memory
|
||||||
|
echo stash-precious >$croot9/$samang/memory/keep.md
|
||||||
|
pushd $sa >/dev/null
|
||||||
|
agents-vault --silent
|
||||||
|
popd >/dev/null
|
||||||
|
|
||||||
|
# The target must be *tracked* for this to bite: stashing it takes files
|
||||||
|
# git knows about out from under the index, which is the whole hazard.
|
||||||
|
mkdir -p $vroot9/agent-vault/projects/stash-target/claude/memory
|
||||||
|
printf 'remote: (none)\npath: %s\nhost: t\n' /nowhere \
|
||||||
|
>$vroot9/agent-vault/projects/stash-target/origin
|
||||||
|
pushd $sa >/dev/null
|
||||||
|
agents-vault --silent
|
||||||
|
popd >/dev/null
|
||||||
|
check "stash adopt: target entry is tracked" true (git -C $vroot9/agent-vault ls-files --error-unmatch projects/stash-target/origin >/dev/null 2>&1; and echo true; or echo false)
|
||||||
|
check "stash adopt: vault clean before the adopt" "" (git -C $vroot9/agent-vault status --porcelain | string join ',')
|
||||||
|
|
||||||
|
# Where the stash lives is load-bearing rather than cosmetic: at the vault
|
||||||
|
# root, a crash between the two moves leaves it for the next ordinary run's
|
||||||
|
# `git add -A` to commit as permanent junk. Inside .git/ it is out of reach
|
||||||
|
# of both the entry walk and `git add -A`. Pinned by making the vault root
|
||||||
|
# itself unwritable for the duration -- adopt writes nothing there, so only
|
||||||
|
# a stash at the root would need it.
|
||||||
|
chmod 500 $vroot9/agent-vault
|
||||||
|
pushd $sa >/dev/null
|
||||||
|
set -l sarc (agents-vault --adopt=stash-target --silent; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
chmod 700 $vroot9/agent-vault
|
||||||
|
check "stash adopt: succeeds over a contentless target" 0 "$sarc"
|
||||||
|
check "stash adopt: memory moved to the target slug" stash-precious (cat $vroot9/agent-vault/projects/stash-target/claude/memory/keep.md 2>/dev/null)
|
||||||
|
check "stash adopt: source entry removed" false (test -d $vroot9/agent-vault/projects/$saslug; and echo true; or echo false)
|
||||||
|
check "stash adopt: link repinned onto the target" (path resolve $vroot9/agent-vault/projects/stash-target/claude/memory) (path resolve $croot9/$samang/memory)
|
||||||
|
check "stash adopt: stash cleaned up" false (test -e $vroot9/agent-vault/.git/agents-vault-adopt-stash; and echo true; or echo false)
|
||||||
|
check "stash adopt: nothing stashed at the vault root" false (test -e $vroot9/agent-vault/.adopt-stash; and echo true; or echo false)
|
||||||
|
check "stash adopt: the root fallback location is gitignored" true (git -C $vroot9/agent-vault check-ignore -q .adopt-stash; and echo true; or echo false)
|
||||||
|
check "stash adopt: vault clean afterwards" "" (git -C $vroot9/agent-vault status --porcelain | string join ',')
|
||||||
|
|
||||||
|
# ... and when the relink fails, the stash must come back and the *index*
|
||||||
|
# must come back with it. Restoring the worktree alone leaves git
|
||||||
|
# describing a half-applied rename -- the files are right, git status is
|
||||||
|
# not -- and a hand commit in that window records it.
|
||||||
|
set -l sf (new_repo https://git.rootiest.dev/rootiest/stash-fail.git)
|
||||||
|
set -l sfslug git.rootiest.dev-rootiest-stash-fail
|
||||||
|
set -l sfmang (string replace -a '/' '-' -- $sf | string replace -a '.' '-')
|
||||||
|
mkdir -p $croot9/$sfmang/memory
|
||||||
|
echo stashfail-precious >$croot9/$sfmang/memory/keep.md
|
||||||
|
pushd $sf >/dev/null
|
||||||
|
agents-vault --silent
|
||||||
|
popd >/dev/null
|
||||||
|
|
||||||
|
mkdir -p $vroot9/agent-vault/projects/stashfail-target/claude/memory
|
||||||
|
printf 'remote: (none)\npath: %s\nhost: t\n' /nowhere-else \
|
||||||
|
>$vroot9/agent-vault/projects/stashfail-target/origin
|
||||||
|
pushd $sf >/dev/null
|
||||||
|
agents-vault --silent
|
||||||
|
popd >/dev/null
|
||||||
|
|
||||||
|
set -l sf_entries (command ls -A $vroot9/agent-vault/projects | sort | string join ',')
|
||||||
|
set -l sf_head (git -C $vroot9/agent-vault rev-list --count HEAD)
|
||||||
|
set -l sf_porcelain (git -C $vroot9/agent-vault status --porcelain | string join ',')
|
||||||
|
set -l sf_link (path resolve $croot9/$sfmang/memory)
|
||||||
|
check "stash adopt rollback: vault clean before the adopt" "" "$sf_porcelain"
|
||||||
|
|
||||||
|
set -l sferr (mktemp); set -ga TMPDIRS $sferr
|
||||||
|
chmod 500 $croot9/$sfmang
|
||||||
|
pushd $sf >/dev/null
|
||||||
|
set -l sfrc (agents-vault --adopt=stashfail-target --silent 2>$sferr; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
chmod 700 $croot9/$sfmang
|
||||||
|
|
||||||
|
check "stash adopt rollback: failed relink returns 1" 1 "$sfrc"
|
||||||
|
check "stash adopt rollback: says the entry was left alone" true (string match -q "*$sfslug*left as it was*" -- (cat $sferr); and echo true; or echo false)
|
||||||
|
check "stash adopt rollback: entries unchanged" "$sf_entries" (command ls -A $vroot9/agent-vault/projects | sort | string join ',')
|
||||||
|
check "stash adopt rollback: source entry intact" stashfail-precious (cat $vroot9/agent-vault/projects/$sfslug/claude/memory/keep.md 2>/dev/null)
|
||||||
|
check "stash adopt rollback: stashed target came back" true (string match -q '*nowhere-else*' -- (cat $vroot9/agent-vault/projects/stashfail-target/origin 2>/dev/null); and echo true; or echo false)
|
||||||
|
check "stash adopt rollback: target memory still contentless" 0 (command ls -A $vroot9/agent-vault/projects/stashfail-target/claude/memory 2>/dev/null | count)
|
||||||
|
check "stash adopt rollback: nothing committed" "$sf_head" (git -C $vroot9/agent-vault rev-list --count HEAD)
|
||||||
|
check "stash adopt rollback: index and worktree unchanged" "$sf_porcelain" (git -C $vroot9/agent-vault status --porcelain | string join ',')
|
||||||
|
check "stash adopt rollback: no stash left behind" false (test -e $vroot9/agent-vault/.git/agents-vault-adopt-stash -o -e $vroot9/agent-vault/.adopt-stash; and echo true; or echo false)
|
||||||
|
check "stash adopt rollback: live link never removed" "$sf_link" (path resolve $croot9/$sfmang/memory)
|
||||||
|
check "stash adopt rollback: memory still reachable live" stashfail-precious (cat $croot9/$sfmang/memory/keep.md 2>/dev/null)
|
||||||
|
|
||||||
|
# The point of rolling back: the ordinary run afterwards finds the original
|
||||||
|
# entry instead of fabricating a fresh, history-less one.
|
||||||
|
pushd $sf >/dev/null
|
||||||
|
agents-vault --silent
|
||||||
|
popd >/dev/null
|
||||||
|
check "stash adopt rollback: ordinary run fabricated no entry" "$sf_entries" (command ls -A $vroot9/agent-vault/projects | sort | string join ',')
|
||||||
|
check "stash adopt rollback: ordinary run kept the memory reachable" stashfail-precious (cat $croot9/$sfmang/memory/keep.md 2>/dev/null)
|
||||||
|
|
||||||
set -e __fish_agent_vault_dir
|
set -e __fish_agent_vault_dir
|
||||||
set -e __fish_agent_vault_claude_root
|
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
|
||||||
@@ -908,6 +1006,28 @@ set -l r2rc (agents-vault --restore >$r2out; echo $status)
|
|||||||
check "restore: exits 0 with an unplaceable entry" 0 "$r2rc"
|
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)
|
check "restore: reports the unplaceable entry" true (string match -q '*ghost-entry*' -- (cat $r2out); and echo true; or echo false)
|
||||||
|
|
||||||
|
# A dot-led slug is a real key, not a curiosity: the sibling-bare-mirror
|
||||||
|
# idiom (`git remote add origin ../mirror.git`) keys as ..-mirror, a
|
||||||
|
# dot-led host keys as .hidden.example.com-o-r, and --adopt accepts a
|
||||||
|
# leading dot on purpose. Fish's * skips such a name, so both walks list
|
||||||
|
# instead of globbing -- otherwise --status under-reports the entry and
|
||||||
|
# --restore leaves that project unlinked, both without saying a word.
|
||||||
|
set -l dotp (new_repo https://git.rootiest.dev/rootiest/dotted.git)
|
||||||
|
set -l dotmang (string replace -a '/' '-' -- $dotp | string replace -a '.' '-')
|
||||||
|
mkdir -p $vroot10/agent-vault/projects/.dot-entry/claude/memory
|
||||||
|
echo dot-precious >$vroot10/agent-vault/projects/.dot-entry/claude/memory/keep.md
|
||||||
|
printf 'remote: (none)\npath: %s\nhost: t\n' $dotp \
|
||||||
|
>$vroot10/agent-vault/projects/.dot-entry/origin
|
||||||
|
check "dot-led entry: a glob really does skip it" false (string match -q '*.dot-entry*' -- (echo $vroot10/agent-vault/projects/*); and echo true; or echo false)
|
||||||
|
|
||||||
|
set -l dotreport (agents-vault --status)
|
||||||
|
check "status lists a dot-led entry" true (string match -q '*.dot-entry*' -- "$dotreport"; and echo true; or echo false)
|
||||||
|
|
||||||
|
set -l dotout (agents-vault --restore)
|
||||||
|
check "restore: relinks a dot-led entry" dot-precious (cat $croot10/$dotmang/memory/keep.md 2>/dev/null)
|
||||||
|
check "restore: the dot-led live path is a link" true (test -L $croot10/$dotmang/memory; and echo true; or echo false)
|
||||||
|
check "restore: names the dot-led entry it restored" true (string match -q '*.dot-entry*' -- "$dotout"; and echo true; or echo false)
|
||||||
|
|
||||||
# An entry that *could* be placed but could not be relinked is a failure,
|
# An entry that *could* be placed but could not be relinked is a failure,
|
||||||
# not a note in passing -- the same branchless-`if` false zero as the
|
# not a note in passing -- the same branchless-`if` false zero as the
|
||||||
# commit and push paths. An entry with no live project is not a failure.
|
# commit and push paths. An entry with no live project is not a failure.
|
||||||
@@ -1010,6 +1130,114 @@ 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
|
||||||
|
|
||||||
|
# ────────────── a failed vault commit is fatal, not a note ─────────────
|
||||||
|
# _agents_repo_sync's own rejection path is covered further up, but
|
||||||
|
# agents-vault has to *propagate* it. The function otherwise ends on a
|
||||||
|
# branchless `if`, which fish resolves to 0, and a backup tool that reports
|
||||||
|
# success while nothing was recorded recreates the exact loss the vault
|
||||||
|
# exists to prevent. That false zero has already bitten this project three
|
||||||
|
# times, so both ways a sync can fail are pinned here rather than one.
|
||||||
|
echo ""
|
||||||
|
echo "== agents-vault (a failed vault commit is fatal) =="
|
||||||
|
|
||||||
|
set -l vroot13 (mktemp -d); set -ga TMPDIRS $vroot13
|
||||||
|
set -l croot13 (mktemp -d); set -ga TMPDIRS $croot13
|
||||||
|
set -l chome13 (mktemp -d); set -ga TMPDIRS $chome13
|
||||||
|
set -l agy13 (mktemp -d); set -ga TMPDIRS $agy13
|
||||||
|
set -g __fish_agent_vault_dir $vroot13/agent-vault
|
||||||
|
set -g __fish_agent_vault_claude_root $croot13
|
||||||
|
set -g __fish_agent_vault_claude_home $chome13
|
||||||
|
set -g __fish_agent_vault_agy_root $agy13
|
||||||
|
|
||||||
|
set -l hp13 (new_repo https://git.rootiest.dev/rootiest/hookfail.git)
|
||||||
|
set -l hmang13 (string replace -a '/' '-' -- $hp13 | string replace -a '.' '-')
|
||||||
|
mkdir -p $croot13/$hmang13/memory
|
||||||
|
echo hook-precious >$croot13/$hmang13/memory/keep.md
|
||||||
|
pushd $hp13 >/dev/null
|
||||||
|
agents-vault --silent
|
||||||
|
popd >/dev/null
|
||||||
|
|
||||||
|
# The vault runs its own hooks out of .agents-tools/hooks -- agents-vault
|
||||||
|
# points core.hooksPath there itself -- so a rejecting pre-commit in that
|
||||||
|
# directory is exactly the shape of a real secret scanner blocking the
|
||||||
|
# vault commit. _agents_repo_install_tools only refreshes the shims when
|
||||||
|
# the version marker moves, so the replacement survives the run under test.
|
||||||
|
printf '#!/bin/sh\nexit 1\n' >$vroot13/agent-vault/.agents-tools/hooks/pre-commit
|
||||||
|
chmod +x $vroot13/agent-vault/.agents-tools/hooks/pre-commit
|
||||||
|
echo hook-more >$croot13/$hmang13/memory/keep2.md
|
||||||
|
set -l hhead13 (git -C $vroot13/agent-vault rev-list --count HEAD)
|
||||||
|
set -l herr13 (mktemp); set -ga TMPDIRS $herr13
|
||||||
|
pushd $hp13 >/dev/null
|
||||||
|
set -l hrc13 (agents-vault --silent 2>$herr13; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "rejected vault commit returns non-zero" 1 "$hrc13"
|
||||||
|
check "rejected vault commit says nothing was recorded" true (string match -q '*nothing recorded*' -- (cat $herr13); and echo true; or echo false)
|
||||||
|
check "rejected vault commit really recorded nothing" "$hhead13" (git -C $vroot13/agent-vault rev-list --count HEAD)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# The other way a sync fails: a rebase conflict in the vault, which
|
||||||
|
# _agents_repo_sync aborts (exit 2) rather than committing conflict markers
|
||||||
|
# under a routine-looking message. That is a backup that did not happen
|
||||||
|
# too, and must not exit 0 either.
|
||||||
|
set -l vroot14 (mktemp -d); set -ga TMPDIRS $vroot14
|
||||||
|
set -l croot14 (mktemp -d); set -ga TMPDIRS $croot14
|
||||||
|
set -l chome14 (mktemp -d); set -ga TMPDIRS $chome14
|
||||||
|
set -l agy14 (mktemp -d); set -ga TMPDIRS $agy14
|
||||||
|
set -g __fish_agent_vault_dir $vroot14/agent-vault
|
||||||
|
set -g __fish_agent_vault_claude_root $croot14
|
||||||
|
set -g __fish_agent_vault_claude_home $chome14
|
||||||
|
set -g __fish_agent_vault_agy_root $agy14
|
||||||
|
|
||||||
|
set -l bare14 (mktemp -d); set -ga TMPDIRS $bare14
|
||||||
|
git init -q --bare $bare14
|
||||||
|
|
||||||
|
set -l cp14 (new_repo https://git.rootiest.dev/rootiest/conflict.git)
|
||||||
|
set -l cslug14 git.rootiest.dev-rootiest-conflict
|
||||||
|
set -l cmang14 (string replace -a '/' '-' -- $cp14 | string replace -a '.' '-')
|
||||||
|
mkdir -p $croot14/$cmang14/memory
|
||||||
|
echo base >$croot14/$cmang14/memory/keep.md
|
||||||
|
pushd $cp14 >/dev/null
|
||||||
|
agents-vault --silent
|
||||||
|
popd >/dev/null
|
||||||
|
agents-vault --remote=$bare14 --silent
|
||||||
|
git -C $vroot14/agent-vault push -q -u origin HEAD:refs/heads/main
|
||||||
|
|
||||||
|
# Another machine records a conflicting change to the same line...
|
||||||
|
set -l cclone14 (mktemp -d); set -ga TMPDIRS $cclone14
|
||||||
|
git clone -q $bare14 $cclone14
|
||||||
|
git -C $cclone14 config user.email t@t
|
||||||
|
git -C $cclone14 config user.name t
|
||||||
|
git -C $cclone14 config commit.gpgsign false
|
||||||
|
git -C $cclone14 config core.hooksPath /dev/null
|
||||||
|
echo theirs >$cclone14/projects/$cslug14/claude/memory/keep.md
|
||||||
|
git -C $cclone14 commit -qam theirs
|
||||||
|
git -C $cclone14 push -q origin HEAD:main
|
||||||
|
|
||||||
|
# ... while this one has a conflicting commit of its own waiting to be
|
||||||
|
# replayed on top. It has to be committed: an uncommitted change is merely
|
||||||
|
# autostashed, and the rebase then fast-forwards instead of conflicting.
|
||||||
|
echo ours >$croot14/$cmang14/memory/keep.md
|
||||||
|
git -C $vroot14/agent-vault -c user.email=t@t -c user.name=t \
|
||||||
|
-c commit.gpgsign=false -c core.hooksPath=/dev/null commit -qam ours
|
||||||
|
|
||||||
|
set -l cerr14 (mktemp); set -ga TMPDIRS $cerr14
|
||||||
|
pushd $cp14 >/dev/null
|
||||||
|
set -l crc14 (agents-vault --silent 2>$cerr14; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "vault rebase conflict returns non-zero" 1 "$crc14"
|
||||||
|
check "vault rebase conflict says nothing was committed" true (string match -q '*nothing committed*' -- (cat $cerr14); and echo true; or echo false)
|
||||||
|
check "vault rebase conflict left no rebase in progress" false (test -d $vroot14/agent-vault/.git/rebase-merge -o -d $vroot14/agent-vault/.git/rebase-apply; and echo true; or echo false)
|
||||||
|
check "vault rebase conflict kept the local memory" ours (cat $croot14/$cmang14/memory/keep.md)
|
||||||
|
|
||||||
|
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 ──────────────
|
# ────────────── a dangling global memory link is repinned ──────────────
|
||||||
# The one state from the real incident the suite never pinned down. For a
|
# 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
|
# *broken* symlink both -d and -e are false, so only the `-L` disjunct in
|
||||||
|
|||||||
Reference in New Issue
Block a user