Six findings from the whole-branch review, all of which end in the same place: a backup tool reporting success while nothing was backed up. Slug migration nested the old entry inside the new one. The clear before the rename was gated on the destination's claude/memory subdirectory rather than on the destination itself, so an entry that exists without one survived, `git mv A B` moved A *inside* B, and the mkdir below fabricated a fresh empty memory directory for the live link to point at. The real memory ended up one level deeper than --status and --restore ever look, and the run returned 0. That shape is not exotic: git cannot track an empty directory, so an entry committed while its memory was empty comes back from a clone as projects/<slug>/origin and nothing else -- and cloning the vault is this feature's own recovery path. The destination is now moved aside the way --adopt already does it rather than deleted (widening the rm -rf would have destroyed the clone's origin log), its provenance is folded into the migrated entry, and every failure path rolls back and reports. The launch path pulled over the network. Both wrappers call agents-vault synchronously before starting an agent, and the pull in the shared sync helper was unguarded once an upstream existed: against a blackholed remote it blocked the launch indefinitely and then aborted the commit, so an offline laptop silently stopped being backed up at all. Committing never needed a remote, so the pull moved to the push path, which was already opt-in for exactly this reason. A failure there now distinguishes a real rebase conflict (rebase-merge/ or rebase-apply/ present) from an unreachable remote instead of calling both a conflict, and both network calls set GIT_TERMINAL_PROMPT=0 and GIT_ASKPASS so they fail fast rather than prompt with nobody watching. The helper still refuses to commit a rebase in progress, and leaves it standing rather than aborting one it did not start. This also restores agents-init's pre-refactor ability to commit while offline. The agy knowledge copy was unfiltered. The allowlist held at the agy root and nowhere below it, so a planted .credentials.json inside knowledge/ was committed verbatim while the documentation promised nothing new upstream added could leak in. Only *.md and *.json are copied now -- which is what the store actually holds -- so lock files, transcripts and conversation databases are excluded by having no business in a backup rather than by being known about. The scaffolded .gitignore also ignored only the SQLite sidecars and not the databases, which is worse than ignoring neither: a torn database landed in history with the write-ahead log that would have completed it deliberately excluded. Both changes are template-only, on a feature that has never shipped. agents-init reported success when nothing was committed. It ended on a branchless `if` with no arm for a failed commit, which fish resolves to 0 -- the same false zero already fixed in agents-vault, left in the function the refactor was rewriting. It now has the arm and an explicit final status. The --adopt forward-failure path with no stash left a raw coreutils `mv:` line and no statement that the adopt had been abandoned cleanly; it is branded like every other error exit in the function. Tests: the suite now clones a vault with git and runs agents-vault against the clone, instead of trusting hand-built fixtures to have shapes git can actually produce -- that blind spot shipped both of the merge blockers. The "present but empty" migration fixture is rebuilt as the origin-only directory a clone leaves behind, with the hand-built shape kept as a separate case. Reverting each fix drops the suite from 285 to 279 (migration), 261 (network), 275 (knowledge allowlist) and 283 (agents-init status).
74 lines
3.1 KiB
Fish
74 lines
3.1 KiB
Fish
# Copyright (C) 2026 Rootiest
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
# SYNOPSIS
|
|
# _agents_repo_sync <dir> <message>
|
|
#
|
|
# DESCRIPTION
|
|
# Stages everything in <dir> and commits it with <message>. Shared by
|
|
# agents-init and agents-vault.
|
|
#
|
|
# It never touches the network, and that is the point rather than an
|
|
# omission. Both callers run on every agent launch, synchronously, ahead
|
|
# of the agent itself, and a fetch there blocks the launch for as long as
|
|
# an unreachable remote takes to time out and can prompt for credentials
|
|
# invisibly underneath a starting agent. Committing needs no remote at
|
|
# all -- only pushing does -- so the pull lives on agents-vault's push
|
|
# path, which is already opt-in for exactly this reason. An offline
|
|
# laptop therefore still gets a complete local backup, which is the whole
|
|
# point of keeping one.
|
|
#
|
|
# A rebase already in progress is refused rather than committed: the
|
|
# worktree then holds conflict markers, and recording those under a
|
|
# routine-looking message buries the conflict in the history instead of
|
|
# reporting it. The rebase is left exactly as it stands -- this function
|
|
# did not start it, so it is not this function's to abort -- and the
|
|
# caller says so.
|
|
#
|
|
# Commits are made with commit.gpgsign=false so a pinentry prompt can
|
|
# never block a shell or an agent launch. If a pre-commit or commit-msg
|
|
# hook rejects the commit (e.g. a secret scanner), that failure is
|
|
# surfaced too: nothing is committed and a diagnostic goes to stderr.
|
|
#
|
|
# 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, arguments were missing, or the commit
|
|
# itself failed (e.g. a pre-commit/commit-msg hook rejected it)
|
|
# 2 A rebase is in progress; nothing committed, nothing touched
|
|
#
|
|
# 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
|
|
|
|
# The guard above proved .git is a directory, so these are the same two
|
|
# paths `agents-vault --status` reports an unresolved rebase from.
|
|
if test -d "$dir/.git/rebase-merge"; or test -d "$dir/.git/rebase-apply"
|
|
echo "_agents_repo_sync: unresolved rebase in $dir; nothing committed" >&2
|
|
return 2
|
|
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"
|
|
return 0
|
|
else
|
|
echo "_agents_repo_sync: commit failed in $dir (hook rejected it?); nothing committed" >&2
|
|
return 1
|
|
end
|
|
end
|