2610545c83
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).
106 lines
4.6 KiB
Bash
Executable File
106 lines
4.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Push-time reminder: skill/ changed, so the skillset MIRROR needs refreshing.
|
|
#
|
|
# WHY THIS EXISTS
|
|
# skill/ is canonical for the pi-extensions agent skill, but it is not the copy
|
|
# most consumers read. skillset mirrors it at skills/pi-extensions/, and every
|
|
# Mac's host-side ~/.agents/skills/pi-extensions symlinks into that mirror. So a
|
|
# skill/ edit that is pushed without refreshing the mirror is invisible to the
|
|
# people it was written for.
|
|
#
|
|
# That has now happened on two consecutive edits: the mirror sat 4890 B behind,
|
|
# then 9579 B behind. Both times the edit was correct and the omission was the
|
|
# follow-up step in another repo.
|
|
#
|
|
# WHAT IT DOES
|
|
# On push, if the pushed commits touch skill/, compare the PUSHED content
|
|
# against the mirror on disk. If they differ, print the refresh commands.
|
|
#
|
|
# It WARNS, it does not block. The direction rule is "edit upstream, THEN
|
|
# refresh" -- the refresh is chronologically after this push, and it is a commit
|
|
# in a different repo, so refusing this push would be both wrong-ordered and
|
|
# unfixable from here. Enforcement lives downstream, in skillset's own
|
|
# pre-commit gate, which refuses a commit that leaves the mirror stale.
|
|
#
|
|
# Silent when the mirror already matches, and silent when no skillset clone is
|
|
# on disk -- a machine without one cannot act on the reminder, and a warning it
|
|
# cannot act on is noise that trains people to skim hook output.
|
|
#
|
|
# ACTIVATION (per clone, cannot be tracked in git)
|
|
# ./install.sh -- or -- git config core.hooksPath hooks
|
|
#
|
|
# ESCAPE HATCH
|
|
# git push --no-verify
|
|
set -euo pipefail
|
|
|
|
tag="[mirror]"
|
|
MIRROR_SUBPATH="skills/pi-extensions"
|
|
ZERO="0000000000000000000000000000000000000000"
|
|
|
|
# Locate the skillset clone. Explicit override, then the layouts this fleet has.
|
|
skillset=""
|
|
repo_root="$(git rev-parse --show-toplevel)"
|
|
for cand in "${PI_EXTENSIONS_SKILLSET:-}" "$repo_root/../skillset" "/workspace/skillset"; do
|
|
[ -n "$cand" ] || continue
|
|
if [ -d "$cand/$MIRROR_SUBPATH" ]; then skillset="$(cd "$cand" && pwd)"; break; fi
|
|
done
|
|
[ -n "$skillset" ] || exit 0 # nothing actionable on this machine
|
|
|
|
# pre-push feeds ref updates on stdin: <local ref> <local sha> <remote ref> <remote sha>
|
|
touched=""
|
|
pushed_sha=""
|
|
while read -r _local_ref local_sha _remote_ref remote_sha; do
|
|
[ -n "${local_sha:-}" ] || continue
|
|
[ "$local_sha" = "$ZERO" ] && continue # branch deletion
|
|
|
|
if [ "$remote_sha" = "$ZERO" ]; then
|
|
# New ref on the remote: consider commits not already on any remote, so a
|
|
# first push of a branch does not diff against the whole of history.
|
|
range_args=("$local_sha" "--not" "--remotes=origin")
|
|
else
|
|
range_args=("$remote_sha..$local_sha")
|
|
fi
|
|
|
|
if git log --format= --name-only "${range_args[@]}" -- skill/ 2>/dev/null | grep -q .; then
|
|
touched="yes"
|
|
pushed_sha="$local_sha"
|
|
fi
|
|
done
|
|
|
|
[ -n "$touched" ] || exit 0
|
|
|
|
# Compare what is being PUSHED (committed content at that sha) against the mirror
|
|
# on disk -- the file consumers actually read. Not the worktree: uncommitted local
|
|
# edits are not what this push publishes.
|
|
differ=""
|
|
while read -r path; do
|
|
base="$(basename "$path")"
|
|
mirror_file="$skillset/$MIRROR_SUBPATH/$base"
|
|
if [ ! -f "$mirror_file" ]; then
|
|
differ="${differ}${base} (missing from mirror)"$'\n'
|
|
continue
|
|
fi
|
|
if ! git show "$pushed_sha:$path" 2>/dev/null | diff -q - "$mirror_file" >/dev/null 2>&1; then
|
|
differ="${differ}${base}"$'\n'
|
|
fi
|
|
done < <(git ls-tree -r --name-only "$pushed_sha" skill/)
|
|
|
|
[ -n "$differ" ] || exit 0 # mirror already refreshed; nothing to say
|
|
|
|
short="$(git rev-parse --short "$pushed_sha")"
|
|
echo "$tag this push changes skill/, and skillset's mirror does not match it yet." >&2
|
|
echo "$tag" >&2
|
|
printf '%s' "$differ" | sed 's/^/'"$tag"' differs: /' >&2
|
|
echo "$tag" >&2
|
|
echo "$tag Consumers read the MIRROR, not this repo: each Mac's host-side" >&2
|
|
echo "$tag ~/.agents/skills/pi-extensions symlinks into it, and a workstation with" >&2
|
|
echo "$tag only skillset cloned has no other copy. Refresh it:" >&2
|
|
echo "$tag" >&2
|
|
echo "$tag cp $repo_root/skill/SKILL.md $repo_root/skill/evaluate-extension-usage.py $skillset/$MIRROR_SUBPATH/" >&2
|
|
echo "$tag sed -i 's/^# Last refreshed from: .*/# Last refreshed from: pi-extensions $short/' $skillset/$MIRROR_SUBPATH/.skill-source" >&2
|
|
echo "$tag git -C $skillset commit -m 'skills(pi-extensions): refresh mirror from upstream $short' $MIRROR_SUBPATH" >&2
|
|
echo "$tag" >&2
|
|
echo "$tag Not blocking this push -- the refresh comes after it. skillset's own" >&2
|
|
echo "$tag pre-commit gate will refuse a commit that leaves the mirror stale." >&2
|
|
exit 0
|