forked from diegoecab/oci-deal-accelerator
Diego asked the right question: what if someone edits the Codex side directly? Until now the sync was unidirectional (root → .agents/) but the hook only fired when root SKILL.md was staged — isolated .agents/ edits sneaked past the hook locally and only got caught by CI's --check. A future ``make sync-skill`` would then silently destroy them. Hardened both layers to reject the asymmetric case at the moment of the commit/PR: - Pre-commit hook now classifies the staged set and REJECTS commits that stage .agents/skills/.../SKILL.md in isolation, with a step-by-step message: move the edit into root SKILL.md and re-stage. The hook still auto-syncs when root SKILL.md is staged. - CI gate adds a second step (PR-only) that diffs the PR's base against head; if .agents/ moved but root SKILL.md didn't, fails with an actionable error before the build proceeds. - README documents the direction explicitly: edits land in root SKILL.md; .agents/ is auto-generated; Codex-specific instructions belong in AGENTS.md, not in the SKILL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
84 lines
3.1 KiB
Bash
84 lines
3.1 KiB
Bash
#!/usr/bin/env bash
|
|
# Pre-commit hook: keep .agents/skills/oci-deal-accelerator/SKILL.md
|
|
# byte-aligned with the root SKILL.md (the source of truth that
|
|
# CLAUDE.md / Claude Code reads). The Codex copy under .agents/
|
|
# follows the same content with an auto-generated banner — they MUST
|
|
# stay in lock-step or Claude Code and Codex will give the user
|
|
# different instructions.
|
|
#
|
|
# Behavior:
|
|
# 1. Only the .agents/ copy staged (no root SKILL.md change):
|
|
# REJECT — the .agents/ file is auto-generated, edits MUST land
|
|
# in root SKILL.md. Otherwise the next sync silently destroys
|
|
# the change.
|
|
# 2. Root SKILL.md staged: auto-run scripts/sync-skill.py and
|
|
# stage the regenerated .agents/ copy in the same commit.
|
|
# 3. Neither: nothing to do.
|
|
# • If the script can't run (no python, etc.), abort with a clear
|
|
# message rather than committing a desync silently.
|
|
#
|
|
# Install once per clone: make install-hooks (sets core.hooksPath)
|
|
|
|
set -euo pipefail
|
|
|
|
ROOT="$(git rev-parse --show-toplevel)"
|
|
SOURCE="SKILL.md"
|
|
TARGET=".agents/skills/oci-deal-accelerator/SKILL.md"
|
|
SCRIPT="scripts/sync-skill.py"
|
|
|
|
STAGED="$(git diff --cached --name-only)"
|
|
SOURCE_STAGED=0
|
|
TARGET_STAGED=0
|
|
echo "$STAGED" | grep -qx "$SOURCE" && SOURCE_STAGED=1 || true
|
|
echo "$STAGED" | grep -qx "$TARGET" && TARGET_STAGED=1 || true
|
|
|
|
# Case 1: only the auto-generated .agents/ copy is staged. Reject —
|
|
# the source of truth is root SKILL.md and a future sync will
|
|
# silently overwrite this change.
|
|
if [ "$TARGET_STAGED" = "1" ] && [ "$SOURCE_STAGED" = "0" ]; then
|
|
echo "" >&2
|
|
echo "pre-commit: REJECTED — you staged $TARGET in isolation." >&2
|
|
echo " This file is auto-generated from $SOURCE and the next" >&2
|
|
echo " ``make sync-skill`` (or this pre-commit hook firing on a" >&2
|
|
echo " later commit) will overwrite your changes silently." >&2
|
|
echo "" >&2
|
|
echo " Workflow:" >&2
|
|
echo " 1. Move your edits into $SOURCE." >&2
|
|
echo " 2. ``git restore --staged --worktree $TARGET``" >&2
|
|
echo " 3. ``git add $SOURCE`` and re-commit — the hook will" >&2
|
|
echo " regenerate the .agents/ copy automatically." >&2
|
|
echo "" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Nothing else to do unless root SKILL.md changed.
|
|
if [ "$SOURCE_STAGED" = "0" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
if [ ! -f "$ROOT/$SCRIPT" ]; then
|
|
echo "pre-commit: $SCRIPT missing — cannot verify SKILL.md sync. Aborting." >&2
|
|
exit 1
|
|
fi
|
|
|
|
PYTHON="$(command -v python3 || command -v python || true)"
|
|
if [ -z "$PYTHON" ]; then
|
|
echo "pre-commit: no python found in PATH — cannot run $SCRIPT. Aborting." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Run the full sync, then stage the regenerated target so it travels
|
|
# in the same commit. (Idempotent — if it was already in sync, the
|
|
# file write is a no-op.)
|
|
"$PYTHON" "$ROOT/$SCRIPT" >&2
|
|
git add "$ROOT/$TARGET"
|
|
|
|
# Final check — if for any reason the result still drifts, fail loud.
|
|
if ! "$PYTHON" "$ROOT/$SCRIPT" --check >/dev/null 2>&1; then
|
|
echo "pre-commit: SKILL.md and $TARGET are still out of sync after auto-run." >&2
|
|
echo " Investigate scripts/sync-skill.py before committing." >&2
|
|
exit 1
|
|
fi
|
|
|
|
exit 0
|