hooks: warn at push time when skill/ changes are not yet mirrored

Editing skill/ here is only half the job: skillset mirrors it at
skills/pi-extensions/, and that copy is what most consumers actually read --
each Mac's host-side ~/.agents/skills/pi-extensions symlinks into it, and a
workstation with only skillset cloned has no other copy. The refresh is a manual
cp in another repo and it has now been forgotten on two consecutive edits, so
the mirror drifted 4890 B behind, then 9579 B.

hooks/pre-push compares the PUSHED content (git show <sha>:skill/...) against the
mirror on disk and prints the exact cp/sed/commit sequence when they differ.
Pushed content rather than the worktree: uncommitted local edits are not what
this push publishes.

It warns and exits 0 rather than blocking, for two reasons that are not
squeamishness: the direction rule is "edit upstream, THEN refresh", so the
refresh legitimately comes after this push, and it is a commit in a different
repo that cannot be made from here. Enforcement belongs downstream and already
exists -- skillset's pre-commit gate refuses a commit that leaves the mirror
stale. This hook only shortens time-to-detection from "next skillset commit" to
"seconds, to the person who caused it".

Silent when the mirror already matches, when the push does not touch skill/, on
branch deletions, and when no skillset clone is on disk -- a reminder that cannot
be acted on is noise that trains people to skim hook output.

install.sh activates it (core.hooksPath=hooks, per-clone config that cannot be
tracked), preserves a foreign hooksPath rather than clobbering it, and does NOT
undo the activation on --uninstall: removing a safety gate as a side effect of
uninstalling extensions would be a surprise in the wrong direction.

Verified: all three activate_hooks branches in a throwaway repo, and five
pre-push scenarios driven through the real stdin protocol (stale -> warns,
in-sync -> silent, non-skill push -> silent, branch deletion -> silent, no
skillset clone -> silent).
This commit is contained in:
2026-09-08 23:01:00 +02:00
parent c64c122dd3
commit 2610545c83
3 changed files with 146 additions and 0 deletions
+28
View File
@@ -176,10 +176,38 @@ do_install() {
ok "Linked ${name} → ${src}"
done
echo
activate_hooks
echo
ok "Done. Reload pi with /reload or restart to pick up new extensions."
}
# ── commit/push hooks ────────────────────────────────
# core.hooksPath is per-clone git CONFIG and cannot be tracked, so a fresh clone
# has no hooks until something sets it. This is that something.
#
# Deliberately NOT undone by --uninstall: uninstall means "stop managing my pi
# extensions", and silently removing a safety gate as a side effect of that would
# be a surprise in the wrong direction. Unset it by hand if you want it gone.
activate_hooks() {
[[ -d "${SCRIPT_DIR}/hooks" ]] || return 0
local cur
cur="$(git -C "$SCRIPT_DIR" config --local core.hooksPath 2>/dev/null || true)"
if [[ -z "$cur" ]]; then
git -C "$SCRIPT_DIR" config core.hooksPath hooks
ok "Activated repo hooks (core.hooksPath=hooks)"
ok " pre-push warns when a skill/ change needs mirroring into skillset"
elif [[ "$cur" == "hooks" ]]; then
ok "Repo hooks already active (core.hooksPath=hooks)"
else
warn "core.hooksPath is '$cur', leaving it alone -- the skill-mirror"
warn "pre-push reminder will not run. Copy hooks/pre-push into '$cur' to keep it."
fi
}
# ── uninstall ────────────────────────────────────────
do_uninstall() {
echo