SKILL.md sync: reject isolated edits to the .agents/ copy
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>
This commit is contained in:
@@ -47,3 +47,21 @@ jobs:
|
|||||||
- name: Verify SKILL.md is in sync with the Codex copy
|
- name: Verify SKILL.md is in sync with the Codex copy
|
||||||
run: |
|
run: |
|
||||||
python3 scripts/sync-skill.py --check
|
python3 scripts/sync-skill.py --check
|
||||||
|
|
||||||
|
- name: Reject .agents/ edits without a matching root SKILL.md change
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
run: |
|
||||||
|
# If only .agents/skills/.../SKILL.md is in the diff (no
|
||||||
|
# change to root SKILL.md), someone edited the auto-generated
|
||||||
|
# copy directly. That edit will be silently destroyed by the
|
||||||
|
# next sync. Block the PR with a clear message.
|
||||||
|
base="${{ github.event.pull_request.base.sha }}"
|
||||||
|
head="${{ github.event.pull_request.head.sha }}"
|
||||||
|
changed=$(git diff --name-only "$base" "$head" || true)
|
||||||
|
if echo "$changed" | grep -qx ".agents/skills/oci-deal-accelerator/SKILL.md"; then
|
||||||
|
if ! echo "$changed" | grep -qx "SKILL.md"; then
|
||||||
|
echo "::error::PR edits .agents/skills/.../SKILL.md without touching root SKILL.md."
|
||||||
|
echo "::error::That file is auto-generated. Move the edit to SKILL.md and run 'make sync-skill'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|||||||
@@ -7,9 +7,13 @@
|
|||||||
# different instructions.
|
# different instructions.
|
||||||
#
|
#
|
||||||
# Behavior:
|
# Behavior:
|
||||||
# • If SKILL.md is staged but the .agents/ copy is not synced,
|
# 1. Only the .agents/ copy staged (no root SKILL.md change):
|
||||||
# auto-run scripts/sync-skill.py and stage the result so the
|
# REJECT — the .agents/ file is auto-generated, edits MUST land
|
||||||
# commit ships both files together.
|
# 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
|
# • If the script can't run (no python, etc.), abort with a clear
|
||||||
# message rather than committing a desync silently.
|
# message rather than committing a desync silently.
|
||||||
#
|
#
|
||||||
@@ -22,8 +26,33 @@ SOURCE="SKILL.md"
|
|||||||
TARGET=".agents/skills/oci-deal-accelerator/SKILL.md"
|
TARGET=".agents/skills/oci-deal-accelerator/SKILL.md"
|
||||||
SCRIPT="scripts/sync-skill.py"
|
SCRIPT="scripts/sync-skill.py"
|
||||||
|
|
||||||
# Only act when SKILL.md is part of the staged change set.
|
STAGED="$(git diff --cached --name-only)"
|
||||||
if ! git diff --cached --name-only | grep -qx "$SOURCE"; then
|
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
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -472,6 +472,8 @@ Three layers of defense, run from least to most enforcement:
|
|||||||
|
|
||||||
3. **CI gate (`.gitea/workflows/skill-sync.yaml`):** every push/PR runs `python scripts/sync-skill.py --check` and fails the build if the two files have drifted. Catches anyone who skipped the hook.
|
3. **CI gate (`.gitea/workflows/skill-sync.yaml`):** every push/PR runs `python scripts/sync-skill.py --check` and fails the build if the two files have drifted. Catches anyone who skipped the hook.
|
||||||
|
|
||||||
|
**Direction is always root → `.agents/`.** The `.agents/` copy is auto-generated; do NOT edit it directly — the next sync will silently overwrite your changes. Both the local pre-commit hook and the CI gate REJECT a commit/PR that stages `.agents/skills/oci-deal-accelerator/SKILL.md` without a matching root `SKILL.md` change. If you find yourself wanting to tweak the Codex side specifically, that signal belongs in `AGENTS.md` (Codex-only project instructions) — `SKILL.md` content stays unified across both agents.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
### ECAL Completeness (see `docs/ecal-gaps-backlog.md` for full list)
|
### ECAL Completeness (see `docs/ecal-gaps-backlog.md` for full list)
|
||||||
|
|||||||
Reference in New Issue
Block a user