Files
fish-config/functions/_agents_repo_sync.fish
T
rootiest 16ea31289d fix(agents-vault): repair slug migration, keep the network off the launch path
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).
2026-09-03 18:56:46 -04:00

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