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:
@@ -30,6 +30,19 @@ mounted). `skill/evaluate-extension-usage.py` is referenced by the skill and
|
||||
must stay alongside it. `install.sh` does not deploy the skill — skill
|
||||
deployment remains the `skillset` repo's job on a normal workstation.
|
||||
|
||||
**Editing `skill/` obliges you to refresh the mirror.** `skillset` keeps a copy at
|
||||
`skills/pi-extensions/`, and that copy — not this one — is what most consumers
|
||||
read: every Mac's host-side `~/.agents/skills/pi-extensions` symlinks into it, and
|
||||
a workstation with only `skillset` cloned has no other copy. The refresh has been
|
||||
forgotten on two consecutive edits (the mirror drifted 4,890 B behind, then
|
||||
9,579 B), so `hooks/pre-push` now warns at push time when a pushed `skill/` change
|
||||
is not yet mirrored, printing the exact `cp`. It **warns rather than blocks** —
|
||||
the refresh is a commit in another repo and chronologically comes after this push.
|
||||
Enforcement lives downstream: `skillset`'s own pre-commit gate refuses a commit
|
||||
that leaves the mirror stale. `./install.sh` activates the hook
|
||||
(`core.hooksPath=hooks`, per-clone config that cannot be tracked); it is silent
|
||||
when no `skillset` clone is on disk, since a reminder you cannot act on is noise.
|
||||
|
||||
**Install a subset:**
|
||||
|
||||
```bash
|
||||
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
#!/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
|
||||
+28
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user