Replace monolithic ci.yml with per-platform workflows so each README badge tracks one job. Scan commit messages for tool-agnostic AI attribution in CI, sync local hooks to the canon, and refresh README status plus git-discipline Rule 8.
109 lines
4.3 KiB
Bash
Executable File
109 lines
4.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Scan commit MESSAGES (and author/committer identity) in a range for
|
|
# AI-attribution tells. Tool-agnostic.
|
|
#
|
|
# Complements scripts/check-style.sh, which scans tracked FILE CONTENT via
|
|
# `git ls-files` and therefore never sees commit messages. An attribution
|
|
# trailer (e.g. "Co-authored-by: Cursor") lives in the message, not a file, so
|
|
# without this scan a poisoned trailer reaches the remote with no CI backstop.
|
|
#
|
|
# This is the merge-time backstop for github-discipline.md Rule 5.1; the local
|
|
# .githooks/commit-msg hook is the write-time backstop. Some assistants re-add a
|
|
# trailer on every commit/amend even when told not to, so neither the per-tool
|
|
# setting nor discipline alone is enough.
|
|
#
|
|
# Usage:
|
|
# scripts/check-commit-attribution.sh [<range>]
|
|
# Range resolution (first that applies):
|
|
# 1) explicit "<range>" argument (e.g. origin/main..HEAD)
|
|
# 2) BASE_SHA and HEAD_SHA environment variables -> BASE_SHA..HEAD_SHA
|
|
# 3) fallback: just HEAD (single commit)
|
|
#
|
|
# Portable: BSD-compatible grep, bash 3.2 (macOS) and bash 4+ (CI).
|
|
|
|
set -u
|
|
|
|
RANGE="${1:-}"
|
|
if [ -z "$RANGE" ]; then
|
|
if [ -n "${BASE_SHA:-}" ] && [ -n "${HEAD_SHA:-}" ]; then
|
|
RANGE="${BASE_SHA}..${HEAD_SHA}"
|
|
else
|
|
RANGE="HEAD~0..HEAD"
|
|
fi
|
|
fi
|
|
|
|
# Resolve the commit list. If the range is unusable (shallow clone, unknown
|
|
# base), fall back to HEAD so we always scan at least the tip.
|
|
COMMITS=$(git rev-list "$RANGE" 2>/dev/null || true)
|
|
[ -z "$COMMITS" ] && COMMITS=$(git rev-parse HEAD)
|
|
|
|
# Tool-agnostic AI-attribution detection (attribution-context only, so prose
|
|
# that merely contains a tool word is not a false positive). Mirrors
|
|
# .githooks/commit-msg and scripts/check-style.sh.
|
|
AI_TOOLS='Claude|Anthropic|Codex|OpenAI|ChatGPT|GPT-[0-9]|Cursor|Copilot|Gemini|Google AI'
|
|
ATTRIB_REGEX="^(Co-Authored-By|Co-authored-with|Generated (with|by)|Created (with|by)|Powered by|with help from|written by|authored by)[: ].*(${AI_TOOLS})"
|
|
|
|
GENERIC_PATTERNS=(
|
|
'as an AI'
|
|
'this commit was generated'
|
|
'this change was generated'
|
|
)
|
|
|
|
# AI-tell signature emojis, kept as bytes so this file carries no glyphs.
|
|
EMOJI_TELLS=$(printf '\xf0\x9f\xa4\x96\n\xe2\x9c\xa8\n\xf0\x9f\xaa\x84\n\xf0\x9f\xa7\xa0\n\xf0\x9f\xa6\xbe\n\xf0\x9f\xa4\x9d')
|
|
EMDASH=$(printf '\xe2\x80\x94')
|
|
|
|
# AI-agent author/committer identity. Conservative on names (a human may be
|
|
# named Claude or Gemini); matches agent email domains and the Cursor agent
|
|
# identity, which injects itself in practice.
|
|
IDENT_REGEX='@cursor\.com|cursoragent|(^|[^[:alpha:]])[Cc]ursor [Aa]gent([^[:alpha:]]|$)'
|
|
|
|
FAIL=0
|
|
for sha in $COMMITS; do
|
|
MSG=$(git log -1 --format='%B' "$sha")
|
|
IDENT=$(git log -1 --format='%an <%ae>%n%cn <%ce>' "$sha")
|
|
short=$(git rev-parse --short "$sha")
|
|
|
|
# Attribution trailer/phrase, anchored to line start so a body sentence that
|
|
# describes the trailer (documentation) is not flagged, only a real trailer.
|
|
if printf '%s\n' "$MSG" | grep -qiE -- "$ATTRIB_REGEX"; then
|
|
printf 'commit-attribution: %s names an AI tool/vendor in attribution context.\n' "$short" >&2
|
|
printf '%s\n' "$MSG" | grep -niE -- "$ATTRIB_REGEX" | sed 's/^/ /' >&2
|
|
FAIL=1
|
|
fi
|
|
|
|
for p in "${GENERIC_PATTERNS[@]}"; do
|
|
if printf '%s' "$MSG" | grep -qiF -- "$p"; then
|
|
printf 'commit-attribution: %s contains forbidden phrase: %s\n' "$short" "$p" >&2
|
|
FAIL=1
|
|
fi
|
|
done
|
|
|
|
if LC_ALL=C printf '%s' "$MSG" | grep -qF -- "$EMDASH"; then
|
|
printf 'commit-attribution: %s contains an em dash (U+2014).\n' "$short" >&2
|
|
FAIL=1
|
|
fi
|
|
|
|
while IFS= read -r emoji; do
|
|
[ -z "$emoji" ] && continue
|
|
if LC_ALL=C printf '%s' "$MSG" | grep -qF -- "$emoji"; then
|
|
printf 'commit-attribution: %s contains an AI-signature emoji.\n' "$short" >&2
|
|
FAIL=1
|
|
break
|
|
fi
|
|
done <<<"$EMOJI_TELLS"
|
|
|
|
if printf '%s' "$IDENT" | grep -qiE -- "$IDENT_REGEX"; then
|
|
printf 'commit-attribution: %s has an AI-agent author/committer identity:\n' "$short" >&2
|
|
printf '%s\n' "$IDENT" | grep -niE -- "$IDENT_REGEX" | sed 's/^/ /' >&2
|
|
FAIL=1
|
|
fi
|
|
done
|
|
|
|
if [ "$FAIL" -ne 0 ]; then
|
|
printf 'commit-attribution: gate failed. Rule: github-discipline.md Rule 5.1 / 5.2 (tool-agnostic).\n' >&2
|
|
printf ' Rebuild the offending commit with a clean message (git commit-tree bypasses\n' >&2
|
|
printf ' any assistant trailer injection), then re-push.\n' >&2
|
|
fi
|
|
exit "$FAIL"
|