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
2 changed files with 102 additions and 0 deletions
Showing only changes of commit 51543cb7ca - Show all commits
+55
View File
@@ -0,0 +1,55 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# SYNOPSIS
# _agents_repo_sync <dir> <message>
#
# DESCRIPTION
# Pulls (when an upstream is configured), stages everything, and commits
# with <message>. Shared by agents-init and agents-vault.
#
# A failed rebase is aborted and nothing is committed. Committing blindly
# after a failed pull would stage conflict markers and record them under a
# routine-looking message, so the failure is surfaced instead: the repo is
# left clean at local HEAD for the user to resolve by hand.
#
# Commits are made with commit.gpgsign=false so a pinentry prompt can
# never block a shell or an agent launch.
#
# ARGUMENTS
# dir Absolute path to the git repository
# message Commit subject used when there is something to commit
#
# EXIT STATUS
# 0 Committed, or nothing needed committing
# 1 <dir> is not a git repository
# 2 Rebase conflict; aborted, nothing committed
#
# RETURNS
# A single "→ Committed (<sha>) <subject>" line on stdout when it
# commits; nothing when there was nothing to do.
#
# EXAMPLE
# _agents_repo_sync /path/to/AGENTS "chore: sync AGENTS repository"
function _agents_repo_sync --argument-names dir msg
test -n "$dir" -a -n "$msg"; or return 1
test -d "$dir/.git"; or return 1
if git -C "$dir" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1
if not git -C "$dir" pull --rebase --autostash -q >/dev/null 2>/dev/null
git -C "$dir" rebase --abort >/dev/null 2>/dev/null
echo "_agents_repo_sync: rebase conflict in $dir; aborted, left at local HEAD" >&2
return 2
end
end
git -C "$dir" add -A 2>/dev/null
set -l status_out (git -C "$dir" status --porcelain 2>/dev/null)
test -n "$status_out"; or return 0
if git -C "$dir" -c commit.gpgsign=false commit -q -m "$msg" 2>/dev/null
set -l sha (git -C "$dir" rev-parse --short HEAD 2>/dev/null)
set -l subject (git -C "$dir" log -1 --pretty=%s 2>/dev/null)
echo "→ Committed ($sha) $subject"
end
end
+47
View File
@@ -136,6 +136,53 @@ ln -s $p/one $p/link
_agents_repo_ensure_symlink $p/link $p/two >/dev/null
check "repins a wrong link" (path resolve $p/two) (path resolve $p/link)
# ─────────────────────────── sync policy ───────────────────────────────
echo ""
echo "== _agents_repo_sync =="
set -l s (new_repo)
echo hello >$s/a.md
_agents_repo_sync $s "chore: test" >/dev/null
check "commits new content" 1 (git -C $s rev-list --count HEAD)
# Nothing to do: no second commit, no output.
set -l out (_agents_repo_sync $s "chore: test")
check "idempotent, no new commit" 1 (git -C $s rev-list --count HEAD)
check "idempotent, silent" "" "$out"
# Conflict: two clones diverge with conflicting commits on the same line.
# (Merely leaving "ours" uncommitted in the worktree isn't enough to force
# a rebase conflict -- with nothing local to replay, --autostash's rebase
# step fast-forwards cleanly and only the stash *pop* would conflict,
# leaving "theirs" committed on HEAD with "ours" stranded in the stash.
# Committing "ours" locally first means the rebase itself must replay a
# real commit over "theirs" on the same line, which is where the intended
# conflict-and-abort path actually lives.)
set -l origin (mktemp -d); set -ga TMPDIRS $origin
git -C $origin init -q --bare
git -C $s remote add origin $origin
git -C $s push -q -u origin HEAD:refs/heads/main 2>/dev/null
set -l clone (mktemp -d); set -ga TMPDIRS $clone
git clone -q $origin $clone
git -C $clone config user.email t@t
git -C $clone config user.name t
git -C $clone config commit.gpgsign false
git -C $clone config core.hooksPath /dev/null
echo theirs >$clone/a.md
git -C $clone commit -qam theirs
git -C $clone push -q origin HEAD:main
echo ours >$s/a.md
git -C $s commit -qam ours
set -l before_count (git -C $s rev-list --count HEAD)
set -l rc (_agents_repo_sync $s "chore: test" 2>/dev/null; echo $status)
check "conflict returns 2" 2 "$rc"
check "conflict leaves no rebase in progress" false (test -d $s/.git/rebase-merge -o -d $s/.git/rebase-apply; and echo true; or echo false)
check "conflict commits nothing" $before_count (git -C $s rev-list --count HEAD)
check "conflict content survives" ours (cat $s/a.md)
check "conflict left no markers" false (grep -q '<<<<<<<' $s/a.md; and echo true; or echo false)
cleanup
echo ""
echo (math $TESTS_RUN - $TESTS_FAILED)"/$TESTS_RUN passed"