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

Merged
rootiest merged 19 commits from feat/agent-memory-vault into main 2026-09-03 23:09:57 +00:00
3 changed files with 296 additions and 10 deletions
Showing only changes of commit 11f4551fa5 - Show all commits
+4 -1
View File
@@ -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'
+64 -9
View File
@@ -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
+228
View File
@@ -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